Skip to content

feat(consolidate): propose curated candidates from capture Records (#368) - #376

Merged
entelecheia merged 3 commits into
mainfrom
feat/368-record-candidates
Nov 11, 2026
Merged

entelecheia merged 3 commits into
mainfrom
feat/368-record-candidates

Conversation

@entelecheia

@entelecheia entelecheia commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Context

amem consolidate read only curated notes. A decision or fix an agent recorded in its final answer stayed in capture until a person retyped it, which is the last gap left by #313 item 7 and #369 (the Record rows now exist; nothing proposed from them).

Closes #368. Refs #313, agent-hub#375.

Change

  • amem consolidate also proposes candidates from the newest capture Record rows of the project. The window is the brief one — readBriefRecords() at its 32-row ceiling, main-session, project-bound, newest first and index-backed — so a proposal costs no more than the SessionStart read already pays. Its partial flag is reported as records.truncated, with a ponytail: note naming a dedicated bounded Record scan as the upgrade path.
  • A row is eligible only when its stored excerpt re-parses unchanged as exactly one canonical amem: line. Anchors come from that line, not from the row's paths copy, which is suffix-truncated at 400 bytes. Subagent rows, rewritten lines, malformed lines and screened history are counted as records.excluded.
  • Grouping is sha256(kind, anchors); a decision/contract with - anchors has no anchor set and groups by sha256(kind, text) instead, so two different empty-anchor Records stay two candidates.
  • Kind mapping adds no curated kind: decision/contract/finding become a memory candidate with that note kind, fix a memory candidate with note kind finding titled fix: ..., and fail a fail candidate whose symptom is the recorded left group and whose retryWhen is the recorded right group.
  • CandidateInput gains optional about and record fields, and CandidateAcceptance an optional recordSource. canonical() drops undefined, so every existing candidate id, hash and payload is unchanged, and no index or format version moves.
  • A group key already pending or accepting is skipped as records.alreadyQueued, and an already accepted or rejected one as records.alreadyReviewed, so a repeat run over the same window writes nothing. Each source row and each built input take the existing guarded policy screen and check() fence, refusals counted as records.refused.
  • Acceptance proves the quoted rows before beginAcceptance and again inside the append section, point-reading the cited turns themselves rather than the brief window, so a candidate can outlive 32 newer Records and still be accepted. A row that changed or was pruned refuses with a named next step.
  • The CLI opens the capture read-only (recordWindow, recordCaptureLookup) and passes nothing when the home has no capture shard, so a home without capture consolidates exactly as before.

Acceptance criteria

  • AC1: one candidate per eligible Record, none for subagent, malformed or policy-refused rows. test/record-candidates.test.ts "one candidate per eligible Record; subagent, malformed and rewritten rows yield none" (8 rows, 2 eligible, excluded: 6), plus records.refused for a row the proposal-time screen rejects.
  • AC2: two sessions with the same kind and anchors yield one grouped candidate citing both source rows. "two sessions with the same kind and anchors become one candidate citing both rows" asserts grouped: 1 and both at values in record.sources; the consolidate test asserts it end to end.
  • AC3: accepting a fix-derived candidate writes the decided kind and a fail-derived one writes a valid fail note. "accepting a fix and a fail Record candidate writes the decided kind and a labelled fail note" checks kind: finding plus the fix: title for the fix, and all five labelled lines with Symptom:, Evidence: and Retry when: filled for the fail.
  • AC4: acceptance refuses with a named next step when the source row changed or was pruned. "acceptance refuses with a named next step when the quoted row changed or was pruned" rewrites one row (changed) and deletes another (missing), asserting both messages name the next step.
  • AC5: hooks and SessionStart output unchanged. No file on that path is touched: src/hooks.ts, src/hook-*.ts, src/session-brief.ts, src/brief-capture.ts are all unchanged in this branch, and amem consolidate is a CLI-only command that no hook calls.

Review round 1 (2026-11-11)

All three findings are fixed at f2b0d42, each with a red proof.

  • I1, acceptance could deadlock. The recheck read the same 32-row brief window amem consolidate reads, so once 32 newer Records existed every cited row answered missing — and the next step the refusal named (rerun amem consolidate) reads that window too, so nothing could clear it. CaptureStore.readRecordTurns now point-reads the cited turns themselves: 1..8 turns, a 9-row cap each, lookups on the at index, no scan, no brief window. Only a row pruned or forgotten from capture answers missing, and the refusal no longer blames the window. The unit test pins that the check asks for the cited turns and never for a window; the volume test pins the end-to-end path.
  • I2, the fail mapping dropped the recorded retry condition. The grammar is <symptom/cause> -> <fix/retry-when>, so the right group is exactly retryWhen and is no longer discarded; only approach and cause stay empty for the reviewer, which keeps the empty-field relaxation to the two fields a Record cannot source.
  • I3, missing CHANGELOG entry. Added under Unreleased with Refs #368, naming the records report fields and the additive about/record inputs, and stating that existing candidate ids are unchanged.
  • Requested fixture: "a realistic volume proposes across sessions and acceptance outlives the brief window" stages 41 Records over five sessions — 30 of them one path each, plus an empty-anchor decision and a fail with a recorded retry condition — then confirms the queue, the fail mapping (symptom/retryWhen filled, approach/cause empty) and the empty-anchor candidate, pushes 33 newer Records so the cited row leaves the 32-row brief window, and accepts it: alreadyAccepted: false, the note body carries the quoted lesson.

Red proofs, run locally at this head:

  • With recordCaptureLookup restored to the brief-window read, "a realistic volume …" fails at record source row is missing (1 cited, …) and every other test stays green.
  • With retryWhen restored to "", "a fail Record fills the symptom and the retry condition it recorded" fails on retryWhen: "" vs "retry after the queue drains" (plus the three tests that assert the accepted fail body), and the rest stay green.

Decisions to review

Two points go past the agreed sketch; each is stated here rather than buried.

  • about on the candidate input. The sketch gave a Record's anchors to a fail note through evidenceRefs only, which leaves an accepted memory candidate with no path: edges at all, so path recall, briefs and sharedPaths clustering could never find it. The optional about field carries the anchors as path: plus symbol:<name>@<path> (the convention extractNodes already uses) and acceptance merges it into the note. Optional, so no existing id changes.
  • A source is identified by session, time and stored-line hash. A turn may emit two Records with the same timestamp, so distinctness on session + at alone would have rejected a legitimate two-row group. The acceptance recheck matches the triple, and a row at the cited turn whose line no longer parses reads as changed.

Rulings honored

  • No new curated kind, no index or format bump, no format floor write.
  • Grouping only when the anchor set is non-empty; - anchors key by kind and text.
  • readBriefRecords(32) stays the proposal window and its ceiling is reported as truncated.
  • note-add's failFields stays strict; only a candidate carrying the record binding may leave a fail field empty.

Verification

Hidden remote runner (dgx02), focused scope only, no heavy run on the Mac.

  • f2b0d42 (tree 4ec72a2, review round 1): install, tsc --noEmit clean, boundary 174 source files, 13 files 164 pass / 0 fail / 2007 assertions (record-candidates, consolidate, consolidate-cli, candidates, consolidation-acceptance, candidates-cli, mcp-candidate-review, review-pending, candidate-tools, capture, capture-record-index, capture-brief-readonly, capture-brief-index), check-docs OK with the 6 stale documents, npm pack --dry-run 184 files, exit 0.
  • a5534c6 (first head, tree a71384a): the same pipeline over 11 files, 141 pass / 0 fail / 1793 assertions, exit 0.
  • scripts/check-docs.ts reports OK (6 stale documents). Four were stale before this branch; README.md and docs/contracts.md are the two edited here, and their verifiedAgainst stamps are left for the tag step to recheck against origin/main as the release rule requires.
  • Hosted CI covers the full suite at the pushed head.

`amem consolidate` read only curated notes, so a decision or fix an agent
recorded in its final answer stayed in capture until someone retyped it.
It now also proposes candidates from the newest capture `Record` rows of
the project.

- The source window is `readBriefRecords()` at its 32-row ceiling, so a
  proposal costs no more than the SessionStart read already pays. Its
  `partial` flag is reported as `records.truncated`, with a `ponytail:`
  note naming a dedicated bounded Record scan as the upgrade path.
- A row is eligible only when its stored line re-parses unchanged as one
  canonical `amem:` line. Anchors come from that line, not from the row's
  400-byte suffix-truncated `paths` copy.
- Grouping uses `sha256(kind, anchors)`, or `sha256(kind, text)` when the
  Record carried `-` anchors, so two different empty-anchor Records stay
  two candidates and one turn's two Records are two distinct sources.
- Kind mapping adds no curated kind: decision/contract/finding become a
  `memory` candidate with that note kind, fix a `finding` titled `fix:`,
  and fail a `fail` candidate whose symptom is the recorded left group.
- `CandidateInput` gains optional `about` and `record` fields. `canonical()`
  drops undefined, so every existing candidate id, hash and payload is
  unchanged and no index or format version moves.
- A group key already pending or accepting is skipped as `alreadyQueued`
  and an already decided one as `alreadyReviewed`, so a repeat run writes
  nothing. Each source row and each built input take the existing guarded
  policy screen and `check()` fence.
- Acceptance proves the quoted rows through
  `CandidateAcceptance.recordSource`, before `beginAcceptance` and again
  inside the append section, and refuses with a named next step when a row
  changed or left the window.

No capture write, no format floor and no SessionStart change.
Names the #368 window, grouping key, kind mapping, counters and
acceptance recheck in `docs/contracts.md`, and adds the README sentence a
person reads before running `amem consolidate`.
Copilot AI balanced review requested due to automatic review settings November 11, 2026 09:45
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-11T09:45:15.989282Z
We couldn’t complete this review. Comment @codex review to try again.
a5534c6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

…retry condition

Review round 1 (claude, task 224): two behaviour findings and a missing
CHANGELOG entry.

- I1, acceptance could deadlock. The recheck read the same 32-row brief
  window `amem consolidate` reads, so once 32 newer Records existed every
  cited row answered "missing" — and the next step the refusal named
  (rerun consolidate) reads that window too, so nothing could clear it.
  `CaptureStore.readRecordTurns` now point-reads the cited turns
  themselves: 1..8 turns, a 9-row cap each, point lookups on the `at`
  index, no scan and no brief window. Only a row pruned or forgotten from
  capture answers missing, and the refusal no longer claims the window as
  the cause.
- I2, the fail mapping dropped the recorded retry condition. The grammar
  is `<symptom/cause> -> <fix/retry-when>`, so the right group is exactly
  `retryWhen`; it is no longer discarded, and only `approach` and `cause`
  stay empty for the reviewer, which keeps the empty-field relaxation to
  the two fields a Record cannot source.
- I3, add the Unreleased CHANGELOG entry: Refs #368, naming the `records`
  report fields and the additive `about`/`record` candidate inputs, and
  stating that existing candidate ids are unchanged.

A volume fixture (41 Records over five sessions, then 33 newer ones) pins
I1 end to end: with the window-based recheck restored the test fails at
the missing refusal, and with the retry condition dropped the mapping test
fails on retryWhen.
@entelecheia
entelecheia merged commit 6647c22 into main Nov 11, 2026
9 checks passed
@entelecheia
entelecheia deleted the feat/368-record-candidates branch November 11, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(records): propose curated review candidates from Records (episodic to long-term)

2 participants