Mint memory confirms only on evidence
Epic 6659: Project memory, open the pipe, honest trust. This is child C3 (improvement 3, PRD S3). It also owns PRD S5-4's demotion test, which the Architect moved here. It builds on C0's plan gate.
Sources: PRD (Task 6660) §3.3 and §3.5 S5-4, and Architecture (Task 6661) §1 C3, §1 C5 ("Unused demotion moves to C3") and §2.3.
## Original requirement (verbatim)
"It is important that all memory related mechanism stays behind the feature flag on the billing plan." … "Each later subtask ships its change inside this gate and adds a test proving the mechanism is inert with the term off."
"3. `memory_used` becomes an evidence field. `record_settle_votes` mints a confirm only when the task has a `read`/`followed`/non-provisional `cited` event for the unit, or its `mem-N` appears in the summary or PR body; other listed units are recorded as \"seen\" (no vote; still counts against unused demotion). Reword the prompt line (`dispatch_compose.py` ~3430) to \"list a unit only if you read it, ran its command, or cite it\". Done means: a settle listing only prompt ids mints zero confirm votes; one listing a searched-and-read unit mints one."
Also from improvement 5, verbatim: "count a unit surfaced in 20 sessions and never read as unused, the same demotion the prompt channel uses."
## Acceptance criteria (verbatim, PRD)
- **S3-1** `record_settle_votes` mints a confirm for a `memory_used` token only when that task has a read, followed or non-provisional cited event for the unit, or the unit's `mem-N` appears in the settle summary or the task's PR body. Every other listed token is recorded as **seen**: no vote, and it does not count as use. `use_pairs` and unused demotion must not count a seen record, or a confirm minted without evidence, as use. `memory_wrong` behaviour is unchanged.
- **S3-2** The prompt line in `_PROJECT_MEMORY_GUIDANCE` says, in substance, "list a unit in `memory_used` only if you read it, ran its command, or cite it in your summary". It stays one line and keeps the `memory_wrong` clause.
- **S3-3** The Project › Memory "Used" column counts only real use, never seen records.
- **S3-4 Inert** (S0-7).
- **S5-4 Hook-only units can be demoted.** A unit surfaced by the hook in 20 distinct sessions/tasks and never read, followed or cited is demoted as unused (`injectable=false`, reason "unused after 20 injections"), like the prompt channel.
## Approach (Architect §1 C3; settled)
- **Signatures.** `routes/agent.py:5517` `_record_settle_memory_votes` and `routes/agent_memory.py:318` `record_settle_votes` both gain `summary`. The call site `routes/agent.py:5833` passes `body.summary`. Run `lsp_find_references` first.
- **Evidence per used token** is any one of:
- (a) a `read` or `followed` event for (task, unit);
- (b) a non-provisional `cited` event;
- (c) the token in `summary`, matched with `\bmem-\d+\b`. For (c), first record a non-provisional `cited` event on channel `summary` via `events.record_events`, then vote.
- With evidence, cast a `confirm` (`source="settle"`). Without it, cast no vote and count the token as `seen`. The function returns and logs `{"confirmed": n, "seen": m, "flagged": k}`. No seen row is persisted, and no new event kind is added.
- **PR body.** A PR-body `mem-N` arrives from the node as a provisional `cited/pr` event, which becomes non-provisional at merge. Nightly `promote_citations` then mints the `citation`-source confirm. Do not fetch PR bodies from the forge at settle.
- **`use_pairs`** (`events.py:102-117`): the vote branch keeps only `confirm` votes with `source in ('tool','citation')`. Settle confirms count as use only through their evidence events.
- **"Used" column.** Verify the console count query reads `use_pairs`/`used_tasks`, not `confirms`, and fix it if not.
- **Prompt line.** `dispatch_compose.py:3430-3431` becomes: "on `task_settle`, list a unit in `memory_used` only if you read it, ran its command, or cite it in your summary; list wrong ones in `memory_wrong`". It stays one line.
- Pinned seam with C2 (Architect §2.3): every C3 confirm has a use event, so it weighs 1.0 under C2's `vote_weight_clause`. C2 owns weighting; C3 owns `use_pairs`.
Shared edit points (rebase expected): `routes/agent_memory.py` (C2 and C4 also edit it), `events.use_pairs`, `dispatch_compose.py` guidance, and `nightly.demote_unused` callers.
Credentials & reach (verbatim rows from Architecture §4):
- All unit/API/web tests, `./dc check full`, and the "local stack" oracles (S3-1 local) | Nothing beyond the worktree and its `./dc` stack's Postgres | verified: worktree present, gate runs in-slot. **Decided:** the "local stack" oracles run as API integration tests. They drive the real agent routes through the FastAPI test client against the slot DB, with a test-minted node caller. `jaah-call` never reaches the `./dc` stack.
- Dev console checks (S3-2/3, S5-4), operator sign-in on `https://app.dev.jaah.ai` | `jaah/debug/main/operator-login` | verified: on the roster, `task_credential_get` succeeded, and the dev health probe returned 200.
- PR merge and Jaah Deploy watch (E1) | `jaah-forge` (authenticated per task) | verified.
Out of scope:
- From the epic, verbatim: "configuring the production memory model credential (an operator action in Settings › Runners › Project memory, not code); retiring mem-2/mem-3 and purging mem-7834 (operator does it in the console); any change to the OMC notepad/project-memory tier; new embedding providers."
- Do NOT change `memory_wrong` handling.
- Do NOT change vote weights or trust formula (that is C2).
- Do NOT fetch PR bodies from a forge.
- Make no migration and no new event kind.
- Do NOT change hook-tier matching (that is C5).
Done means:
- S3-1 — an API test against the slot DB through the real settle route shows:
- (a) a settle listing only prompt-injected ids mints 0 confirms and N seen;
- (b) a settle listing a unit the task searched and then read via `memory_read` mints exactly 1 confirm;
- (c) a unit cited by `mem-N` in the summary mints 1 confirm and a non-provisional `cited/summary` event;
- `memory_wrong` still flags.
- S3-2 — the dispatch_compose test asserts the new one-line text. On dev, open a newly dispatched memory-ON runner's prompt (Task › session transcript) and read the line.
- S3-3 — a web/API test asserts Used counts only real use. On dev, a unit listed only reflexively shows no Used increment.
- S5-4 — a nightly test seeds 20 hook-channel `injected` events plus reflexive `settle` confirms with no use event, and asserts the unit is demoted with reason "unused after 20 injections". The test fails before the `use_pairs` change and passes after it.
- S3-4 — an inert test using `plan_off_project` shows the settle mints no vote and records nothing. Name it in the PR body.
- E1 — the PR is merged to develop through auto-merge with `./dc check full` green, and the Jaah Deploy run succeeded (or was superseded).
Outcome
Settle confirms memory only on use evidence (read/followed/cited or mem-N in summary); others seen. use_pairs ignores settle confirms, fixing Used and hook demotion. PR #5612 merged, deploy OK. Full gate green bar 1 unrelated flake. Dev runner-prompt/Used legs unverified (tests cover). Draft #6711.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)