$curl -o .claude/agents/memory-proposal-collector.md https://raw.githubusercontent.com/Kanevry/session-orchestrator/HEAD/agents/memory-proposal-collector.mdReference documentation (NOT a dispatchable agent) for the coordinator-direct AUQ rendering flow at session-end Phase 3.6.3. The coordinator collects proposals from .orchestrator/metrics/proposals.jsonl via collectProposals() and renders the multiSelect AUQ in batches of 4. A
| 1 | # Memory Proposal Collector (Reference Documentation) |
| 2 | |
| 3 | **NOT a dispatchable subagent.** This file documents the coordinator-direct AUQ rendering flow |
| 4 | that runs at session-end Phase 3.6.3. Do not attempt to dispatch `memory-proposal-collector` as |
| 5 | an agent — the coordinator will receive "agent type not found" and that is by design. See |
| 6 | [Why this is documentation, not an agent](#why-this-is-documentation-not-an-agent) for rationale. |
| 7 | |
| 8 | --- |
| 9 | |
| 10 | ## Overview |
| 11 | |
| 12 | At session-end Phase 3.6.3 the coordinator presents pending memory proposals to the user for |
| 13 | approval or rejection. Proposals are short learning candidates queued by the |
| 14 | `memory-propose.mjs` CLI during the session (typically from hook invocations, auto-generated |
| 15 | by subagents that detected a repeatable pattern worth recording). |
| 16 | |
| 17 | The flow runs **coordinator-direct**: the coordinator calls library functions, renders an |
| 18 | `AskUserQuestion` picker, and writes results — no subagent dispatch involved. |
| 19 | |
| 20 | --- |
| 21 | |
| 22 | ## Gate Conditions |
| 23 | |
| 24 | This flow runs only when ALL of the following are true: |
| 25 | |
| 26 | 1. **`persistence: true`** is set in Session Config (CLAUDE.md `## Session Config` block). |
| 27 | 2. **`memory.proposals.enabled: true`** is set in Session Config (default when the |
| 28 | `pre-bash-memory-propose-audit` hook is active). |
| 29 | 3. **`.orchestrator/metrics/proposals.jsonl`** exists and is non-empty (≥1 line). |
| 30 | |
| 31 | If any condition fails, the coordinator emits a single info-line |
| 32 | (`[3.6.3] Memory proposals: skip — <reason>`) and continues to Phase 3.6.5. |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## Coordinator Step-by-Step |
| 37 | |
| 38 | ### Step 1 — Load proposal queue |
| 39 | |
| 40 | ```js |
| 41 | import { collectProposals } from '../scripts/lib/memory-proposals/collector.mjs'; |
| 42 | const { queue, stats, perWaveSummaries } = await collectProposals({ repoRoot }); |
| 43 | ``` |
| 44 | |
| 45 | > **Note:** `sessionId` is supplied by the coordinator at the call site (e.g., read from |
| 46 | > STATE.md frontmatter); it is NOT returned by `collectProposals`. |
| 47 | |
| 48 | `collectProposals()` reads `.orchestrator/metrics/proposals.jsonl`, parses each line as a |
| 49 | `ProposalRecord`, and returns them in **FIFO order** (insertion order, not sorted by |
| 50 | confidence). This matches the D3 decision locked in Wave 1. |
| 51 | |
| 52 | ### Step 2 — Empty-queue short-circuit |
| 53 | |
| 54 | ```js |
| 55 | if (queue.length === 0) { |
| 56 | log.info('[3.6.3] Memory proposals: queue empty — skip'); |
| 57 | return; |
| 58 | } |
| 59 | ``` |
| 60 | |
| 61 | Silent skip. No user interaction. |
| 62 | |
| 63 | ### Step 3 — Determine batch layout |
| 64 | |
| 65 | | Queue size | Batches | AUQ calls | |
| 66 | |---|---|---| |
| 67 | | 1 – 4 | 1 batch (all proposals) | 1 | |
| 68 | | 5 – 8 | 2 batches of ≤4 | 2 | |
| 69 | | 9 – 12 | 3 batches of ≤4 | 3 | |
| 70 | | N | `ceil(N / 4)` batches | `ceil(N / 4)` | |
| 71 | |
| 72 | Batches are sequential, not parallel. The coordinator waits for the user's response to batch |
| 73 | N before presenting batch N+1. |
| 74 | |
| 75 | ### Step 4 — Render AUQ per batch |
| 76 | |
| 77 | For each batch of up to 4 proposals, call `AskUserQuestion` with the template documented in |
| 78 | [AUQ Question Template](#auq-question-template) below. |
| 79 | |
| 80 | ### Step 5 — Collect and persist results |
| 81 | |
| 82 | After all batches are resolved: |
| 83 | |
| 84 | ```js |
| 85 | import { promoteAndClear, archiveRejected } |
| 86 | from '../scripts/lib/memory-proposals/sink.mjs'; |
| 87 | |
| 88 | // sessionId is read by the coordinator from STATE.md frontmatter (e.g. session_id field), |
| 89 | // NOT returned by collectProposals — see Step 1 note above. |
| 90 | const sessionId = parseStateMd(repoRoot).session_id; |
| 91 | |
| 92 | const writeResult = await sink.promoteAndClear({ approved, sessionId, repoRoot }); |
| 93 | await sink.archiveRejected({ rejected, repoRoot, reason: 'user-declined' }); |
| 94 | ``` |
| 95 | |
| 96 | `promoteAndClear()` composes `writeApproved()` + `clearProposalsJsonl()` behind a single |
| 97 | mechanical guard (#797/#828): it calls `writeApproved({ approved, repoRoot, sessionId })` |
| 98 | first, computes `expected` from `approved.length`, and clears `proposals.jsonl` — via |
| 99 | `clearProposalsJsonl()` internally — ONLY when `written === expected && errors.length === 0`. |
| 100 | There is no separate write-then-clear call sequence for the coordinator to reorder or forget |
| 101 | to gate; the guard is now IN-CODE. The coordinator's job is to inspect the returne |