fix(annotations): bounded reads keep the newest entries, rows carry their address, MCP flat add writes

Merged
UN
4d ago
+2948-214

Three defects on the comment, note and learning collection surface, fixed together because they share the read path, the row shape and the MCP transport.

pm-8s53lj — bounded annotation reads kept the oldest entries

Budget compaction and --output-limit always kept the first rows of a declared collection, but annotations are stored oldest first.

on this tracker (pm-7zs0, 263 comments)beforeafter
bare pm comments pm-7zs0 --jsonindexes 1..28indexes 238..263, total_count: 263
following next_cursor29..56 (forward)211..237 (backward)
--output-limit 2the 2 oldest262, 263 (same tail as --limit 2)
  • PM_READ_OUTPUT_TAIL_RETENTION_POLICIES declares comments, notes and learnings as newest-tail paths, as data.
  • The continuation cursor carries direction: "backward". A cursor whose direction disagrees with the path's declaration is refused as read_output_cursor_stale.
  • learnings joins PM_READ_OUTPUT_SURFACES, so its reads now have an output ceiling.

pm-hka7ep — a page count is not an entry address

  • Every listing and receipt row now leads with index, its absolute 1-based position. It is assigned before limits, pages and note event filters.
  • --edit/--delete accept --expect-at <ts> (SDK/MCP expectAt). If the entry at that index has a different created_at, the write is refused inside the locked mutation with exit 4 and annotation_entry_mismatch. Nothing is written, and details.matching_indexes names where the expected entry is.
  • Help budgets grow by exactly one flag (18 tokens per command). The first spelling, --expect-created-at, widened Commander's option column and cost about 130 tokens per command.

pm-wsqxwb — MCP flat add silently read

  • pm_comments, pm_notes and pm_learnings with a top-level add or text returned an empty listing and wrote nothing. So did pm_files, pm_docs and pm_test with a top-level add.
  • Hoisting for these six actions is now derived from PM_TOOL_ACTION_PARAMETER_CONTRACTS, the same way as for the read actions. text binds to add, and disagreeing values are refused with annotation_text_source_conflict.
  • A census of the other actions found more drops, for example pm_history ignoring a top-level limit. That is filed as pm-yjjo4l.

SDK contract (migration)

  • Before: rows carried only their stored fields, bounded reads kept the oldest rows, and cursors only continued forward.
  • After: rows carry index (IndexedAnnotationEntry), the options accept expectAt, and PmReadOutputCursorEnvelope.direction?: "backward" exists. sliceReadOutputRowCollection takes an optional end.
  • Migrate: address entries by row.index, follow next_cursor for older entries, and accept direction when decoding cursors. The sdk/public-surface.json acknowledgment and pm-gh1657 carry the same line.

Also

  • Files pm-gh1684 for #1684. It records that null_columns/list_columns are restoration data (GH-1500), so only the empty arrays and absent coordinates are lossless savings.
  • Bumps the managed pm-changelog to 2026.11.7, which regenerated CHANGELOG.md.

Verification

  • New specs: annotation-entry-addressing (includes a negative control: editing by page count is refused, editing by the row's index succeeds), tail-retention, and annotation-tool-flat-parameters (iterates every schema-declared key and drives tools/call for each action).
  • Full pnpm lint chain passes locally, including the flag lexicon, flag parity, surface replication, contracts, SDK surface, token budget, graph-composition and mutation (98.0) gates.
  • Every added source line is covered.
  • The local full coverage run only failed timing-sensitive tests on a host with load average 20-36. Each of those specs passes when re-run alone.

Summary by Sourcery

Make annotation reads preserve the newest entries, expose stable row addresses, guard mutations against stale data, and ensure flat MCP writes are dispatched correctly.

New Features:

  • Add absolute indexes to annotation rows and expose timestamp-guarded edit and delete operations across CLI, SDK, and MCP.
  • Support newest-tail retention and backward continuation for bounded comment, note, and learning reads.
  • Make flat MCP add and text parameters write correctly for annotation, linked-artifact, and linked-test tools.

Bug Fixes:

  • Prevent bounded annotation reads from returning the oldest entries and preserve complete collection totals and delivered-row counts across truncation and continuation.
  • Prevent stale annotation edits and deletes by refusing writes when the addressed row no longer matches its expected creation timestamp.
  • Prevent conflicting annotation text sources and silently ignored flat MCP parameters.

Enhancements:

  • Extend the SDK contracts with indexed annotation entries, expectAt options, backward cursor directions, and learnings read-output controls.
  • Handle malformed annotation entries defensively during indexing and note event filtering.

Build:

  • Exclude the generated refusal-closure census from the npm package to preserve artifact size headroom.

CI:

  • Allow surface-replication waivers to be scoped to individual checks.

Documentation:

  • Document indexed annotation addressing, timestamp-guarded mutations, tail-retained reads, backward cursors, and MCP flat-parameter behavior.

Tests:

  • Add coverage for annotation addressing, tail retention, cursor direction, mutation guards, positional output controls, and flat MCP parameters.

Chores:

  • Regenerate the changelog and update managed project tracking records.

Summary by cubic

Fixes three defects on the comment, note, and learning collection surface: bounded reads keep the newest entries, rows carry their absolute address, and MCP tools with a flat add now write instead of silently listing. All three record their escape class and gate evidence. A pre-existing crash on malformed stored annotation entries is filed separately as pm-30tjoh.

Bug Fixes

  • Bounded reads of comments, notes, and learnings kept the oldest rows; they now retain the newest tail, the surface contracts declare ordered_suffix retention, and a continuation cursor whose direction disagrees with the path's declared policy is refused as read_output_cursor_stale.
  • A withheld tail keeps the producer's total_count (or the cursor snapshot) instead of replacing it with the page total; --output-limit and backward replays refresh count, returned_count, and notes' structured_event_count, recounted from the unprojected producer rows so --output-include projections cannot drop the counting evidence.
  • learnings mutations paired with an output control are refused before dispatch, so nothing writes before a stale --output-cursor refusal. Positional annotation text and the --text/--comment/--note/--learning aliases are also refused, even from callers that bypass the canonicalization layer; the untyped refusal is filed as pm-rrwh2o.
  • Listing and receipt rows now lead with index, their absolute 1-based position, numbered after the latest-N limit so only delivered rows carry one. Add and edit receipts follow the written entry through the store's stable sort with an index tie-break, so twin entries written in the same millisecond report exactly the occurrence written; malformed entries pass through unnumbered and the notes event filters read fields defensively. Custom SDK collections keep write-order addresses, since only comments, notes, and learnings are store-sorted. --edit/--delete accept --expect-at <ts>: a different entry at that index is refused before anything is written; the guard compares timestamps, not identity, a non-string expectAt is refused as annotation_expectation_invalid, and matching_indexes reports candidate entries.
  • A top-level add (or text) in pm_comments, pm_notes, pm_learnings, pm_files, pm_docs, and pm_test previously fell through to a silent listing read; parameter hoisting for these six actions now derives from the strict tool contracts, disagreeing values are refused with annotation_text_source_conflict, and declared snake_case parameters are renamed to camelCase for all six actions so a nested expect_at or add_glob cannot bypass the guard.
  • Keeps the packed npm artifact under its 20 MB gate by excluding the generated refusal-closure census from the tarball (pm-dwy8kb); packaged docs link it by canonical GitHub URL, and release-gate waivers can scope to one surface check.

Migration

  • SDK rows gain index (IndexedAnnotationEntry, now exported from ./sdk), annotation options gain expectAt, and PmReadOutputCursorEnvelope gains an optional direction. Address entries by row.index, follow next_cursor for older entries, and accept direction when decoding cursors.