3.9 KiB
M3.1 — L2 synthesis pass
| Field | Value |
|---|---|
| Phase | M3 — L2 synthesis and retrieval |
| Size | M — 1–3 days |
| Status | ✅ Done |
| Flags | — |
| Spec | inlined below |
| Blocks | M1.5 |
Goal
Project-level memory across the per-query memories — using the same loop, with the exit gate switched on.
Facts (inlined — no spec read needed)
mem synthesize --project poimen
L2 is not new machinery. It is run_loop (M1.5) with:
| L1 | L2 | |
|---|---|---|
| input stream | Chunk from sources |
L1 MemoryNodes |
| question | per-query question | synthesis.question |
use_exit_gate |
false | true |
query_id |
set | NULL |
| parents | L0 evidence shas | L1 memory shas |
Why the exit gate flips on. At L1 the input is hundreds of chunks and the question is exhaustive ("what are all the X"), which is exactly the case paper §3.3 says to run without the gate. At L2 the input is a handful of memories and "enough evidence" is genuinely decidable, which is the case the gate was designed for and where the paper measures its 4× speedup.
If M1.5 was written correctly this task is mostly wiring. If it needs changes to
run_loop, the level was not really a parameter — and assertion a10 in M1.5
existed to prevent exactly that.
Ordering: L1 memories enter the stream in a stable order (query id, ascending), so synthesis is reproducible.
Steps
mem synthesize --project Preads the final L1 memory per standing query.- Wrap them as the loop's input stream, in sorted query-id order.
- Run
run_loopwithlevel = L2,use_exit_gate = true, the synthesis question,query_id = None. - Write to
log/<project>/_synthesis/<run-id>.jsonl. parentson the L2 memory are the L1 memory shas consumed up to that turn.- Refuse to run if any standing query has no completed L1 run — synthesizing over a partial set silently produces a partial picture.
Acceptance
- No change to
run_loopis required. - The exit gate fires and stops early on a real project.
- L2 parents are L1 shas, never L0.
- Sorted input order makes two runs consume memories in the same sequence.
Verify
Harness: scripted client, plus one live run.
Integration test — tests/it_l2.rs:
a1_reuses_run_loop— assertmem synthesizecalls the samerun_loopsymbol; a duplicated loop is a review failure, and a#[deny]-style test here is a grep assertingfn run_loopappears exactly once in the workspace.a2_exit_gate_on— scriptedendat turn 2 of 5; assert it stops at 2.a3_parents_are_l1— every L2 parent sha resolves to an L1 node.a4_query_id_null— the L2 record has noquery_id.a5_stable_input_order— two runs consume L1 memories in identical order.a6_refuses_partial— one query with no completed run; assert non-zero exit naming the query.a7_level_check_holds— runmem verify; invariant 6 (level consistency) passes.a8_live—#[ignore]; real project, assert an L2 memory is produced and print whether the exit gate fired and at which turn.
Command: cargo test -p mem-cli l2 (add -- --ignored for a8)
False pass:
- Copying
run_loopinto an L2-specific function. Everything passes, the two drift within a month, and the tier model quietly becomes two implementations. Assertion 1 is the guard. - Testing the exit gate with a script that never says
end. The gate's effect is invisible and ause_exit_gatethat is ignored passes.
Traps
- Feeding L0 evidence into L2 "for more detail". It blows the context budget and breaks the level invariant; L2 reads memories, and if they are inadequate the fix is at L1.
- Synthesizing over whatever L1 runs happen to exist. A missing query produces a confident summary of an incomplete project, which is worse than no summary.
Background: DESIGN.md — tier model · paper §3.3