fix(annotations): bounded reads keep the newest entries, rows carry their address, MCP flat add writes
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) | before | after |
|---|---|---|
bare pm comments pm-7zs0 --json | indexes 1..28 | indexes 238..263, total_count: 263 |
following next_cursor | 29..56 (forward) | 211..237 (backward) |
--output-limit 2 | the 2 oldest | 262, 263 (same tail as --limit 2) |
PM_READ_OUTPUT_TAIL_RETENTION_POLICIESdeclares 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 asread_output_cursor_stale. learningsjoinsPM_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/--deleteaccept--expect-at <ts>(SDK/MCPexpectAt). If the entry at that index has a differentcreated_at, the write is refused inside the locked mutation with exit 4 andannotation_entry_mismatch. Nothing is written, anddetails.matching_indexesnames 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_notesandpm_learningswith a top-leveladdortextreturned an empty listing and wrote nothing. So didpm_files,pm_docsandpm_testwith a top-leveladd.- Hoisting for these six actions is now derived from
PM_TOOL_ACTION_PARAMETER_CONTRACTS, the same way as for the read actions.textbinds toadd, and disagreeing values are refused withannotation_text_source_conflict. - A census of the other actions found more drops, for example
pm_historyignoring a top-levellimit. 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 acceptexpectAt, andPmReadOutputCursorEnvelope.direction?: "backward"exists.sliceReadOutputRowCollectiontakes an optionalend. - Migrate: address entries by
row.index, follownext_cursorfor older entries, and acceptdirectionwhen decoding cursors. Thesdk/public-surface.jsonacknowledgment and pm-gh1657 carry the same line.
Also
- Files pm-gh1684 for #1684. It records that
null_columns/list_columnsare 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, andannotation-tool-flat-parameters(iterates every schema-declared key and drivestools/callfor each action). - Full
pnpm lintchain 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, andlearningskept the oldest rows; they now retain the newest tail, the surface contracts declareordered_suffixretention, and a continuation cursor whosedirectiondisagrees with the path's declared policy is refused asread_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-limitand backward replays refreshcount,returned_count, and notes'structured_event_count, recounted from the unprojected producer rows so--output-includeprojections cannot drop the counting evidence. learningsmutations paired with an output control are refused before dispatch, so nothing writes before a stale--output-cursorrefusal. Positional annotation text and the--text/--comment/--note/--learningaliases 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 onlycomments,notes, andlearningsare store-sorted.--edit/--deleteaccept--expect-at <ts>: a different entry at that index is refused before anything is written; the guard compares timestamps, not identity, a non-stringexpectAtis refused asannotation_expectation_invalid, andmatching_indexesreports candidate entries. - A top-level
add(ortext) inpm_comments,pm_notes,pm_learnings,pm_files,pm_docs, andpm_testpreviously fell through to a silent listing read; parameter hoisting for these six actions now derives from the strict tool contracts, disagreeing values are refused withannotation_text_source_conflict, and declared snake_case parameters are renamed to camelCase for all six actions so a nestedexpect_atoradd_globcannot 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 gainexpectAt, andPmReadOutputCursorEnvelopegains an optionaldirection. Address entries byrow.index, follownext_cursorfor older entries, and acceptdirectionwhen decoding cursors.