Packet — ADR-007 execution plan
This packet outlines the section-by-section content of docs/architecture/decisions/ADR-007-eta-session-lifecycle.md. It is the gate between the contract (what the ADR must contain) and the implementation (writing the prose).
Approach
Write the ADR top-to-bottom in the order: frontmatter → status block → Context → Decision → Consequences → Implementation → Alternatives Considered → Verification → References. This matches the ADR-005 / ADR-006 structure most readers expect; in ADR-005 the order is Context → Decision → Consequences → Implementation → Alternatives → Verification → References, and ADR-007 mirrors it.
Each gap (C1, #6, #7) gets a parallel treatment in §Decision (one paragraph and a one-line summary), in §Alternatives Considered (its own subsection), and in §Implementation (named migration / column / trigger / test class). This keeps the three intertwined decisions readable while preserving the cohesion the audit identified.
Section outline
Frontmatter
---
title: "ADR-007: η Session Lifecycle — Wiring, Post-Finalize Guard, ζ↔η Coupling"
description: "How Phase 0 closes three η/ζ session-lifecycle gaps surfaced by post-R83 review: the broken thought_record→merkle_finalize wiring (C1), the absence of a post-finalize insert guard (#6), and the missing record↔root back-pointer (#7)."
tags: [adr, decision, eta, zeta, proof-store, session-lifecycle]
type: adr
round: R84-candidate
status: proposed
parent: Architecture Decision Records
updated: 2026-05-06
nav_order: 70
---
nav_order: 70 slots after ADR-006’s 60. round: R84-candidate because R83 is the most-recently-merged round; this ADR is drafted between rounds and the round it ships in is undetermined at draft time.
Title + status block
# ADR-007: η Session Lifecycle — Wiring, Post-Finalize Guard, ζ↔η Coupling
**Status:** Proposed
**Date:** 2026-05-06
**Round:** R84-candidate (drafted between rounds)
**Supersedes:** None
**Superseded by:** None
§Context (≈400-550 words)
Three paragraphs:
- Setup. Describe the proof-grade chain documented in CLAUDE.md §7 (
audit_session_start → thought_record × N → merkle_finalize → merkle_root). Position η as the legitimacy axis’s Phase 0 slice (per CLAUDE.md §10). Note that a whole-system code review at post-R83 surfaced three gaps in this chain. - Gap-by-gap. One paragraph per gap, copy-restating the audit findings with the load-bearing code citations (
src/domains/trail/repository.ts:114-119,src/domains/trail/repository.ts:226-230,src/db/migrations/006_eta.sql:54,src/tools/merkle.ts:296-300). Stay tight: one symptom + one root cause per gap. - Why one ADR. State the interaction explicitly — the C1 fix shape determines what the #6 trigger reads and what #7 back-pointer references. Cite the audit’s “Gap interaction” section.
§Decision (≈350-450 words, prose-first then bulleted summary)
Open with one paragraph framing the cohesive triple: opt-in session_id, schema-level guard, schema-level back-pointer. Each subsection then states the chosen option.
§§Decision for C1 — wire session_id through thought_record:
- Add optional
session_id: stringtoThoughtRecordToolInputSchema. - Add the column binding to the
INSERTincreateThoughtRecord. - Existing callers that omit it continue to insert with
session_id = NULL. Existing tests stay green. - Rationale: minimum-viable; non-breaking; opt-in η anchoring.
§§Decision for #6 — schema-level post-finalize guard:
- Add a SQLite trigger
enforce_post_finalize_guardonthought_recordsBEFORE INSERT and BEFORE UPDATE OFsession_id. - The trigger raises
SQLITE_ABORTwith messageERR_SESSION_FINALIZEDwhen the row’ssession_idmatches a row inmerkle_roots. - Migration
007_post_finalize_guard.sql. - The trigger explicitly allows
session_id = NULL(preserves opt-in semantics). - Rationale: enforcement at the layer that owns the invariant; cannot be bypassed by direct SQL or by future MCP tools.
§§Decision for #7 — schema-level record↔root back-pointer:
- Add column
thought_records.anchored_in_root TEXT(nullable, no FK becausemerkle_roots.session_idis a one-to-one shadow ofaudit_sessions.session_idand the join is exact). merkle_finalizeupdates this column inside its existing transaction: after building the tree and insertingmerkle_roots, runUPDATE thought_records SET anchored_in_root = ? WHERE session_id = ?.- Add a Boolean composite index
idx_trail_anchoron(anchored_in_root)for the reverse-lookup read pattern. - The MCP surface gains no new tool; readers query
thought_record_list({task_id})and readanchored_in_rootper record, or query the DB directly for “all records anchored in root X”. - Rationale: preserves the ADR-004 14-tool lock; bidirectional lookup is now a SELECT, not a new tool.
Close with a sentence calling out the Phase 1+ headroom (k-of-n threshold roots; multi-anchor support; optional merkle_anchored_records MCP tool if the SELECT path proves clumsy).
§Consequences (≈300-400 words)
Three subsections:
Positive:
- The proof-grade chain works end-to-end (C1 closed).
- Post-finalize anchoring guarantee restored (#6 closed).
- Record→root reachable from any
thought_record_listcall without a tool addition (#7 closed). - ADR-004 14-tool lock preserved.
Negative:
- Schema migration adds a column and a trigger; downgrade is not seamless (mitigation: the column is nullable, the trigger can be DROPPED).
- The
anchored_in_rootwrite inmerkle_finalizeextends the transaction span by one UPDATE; benchmark needed for large sessions. - Existing ζ tests that pre-seeded
thought_recordsrows directly withoutsession_idcontinue to work but no longer exercise the η happy path; new tests must cover the wired-through case.
Neutral:
- The η axis is still 3 MCP tools (audit_session_start, merkle_finalize, merkle_root). Tool count unchanged.
- Nothing new is registered in the runtime mode capability table.
- The
agent_idexclusion from the hash subset (P0.7.1 §3) is unchanged.session_idlikewise stays out of the hash — adding it would invalidate every existing record.
§Implementation (≈350-500 words, structured as a future-task breakdown)
Carve into three implementable executor slices. For each:
| Sub-task | Files touched | Migration | Tests |
|---|---|---|---|
C1.1 — wire session_id through thought_record |
src/domains/trail/repository.ts (Zod schema + INSERT) |
None (column exists since 006) | New: thought_record.test.ts “MCP path inserts session_id”; new: merkle_finalize.test.ts “end-to-end via MCP succeeds with N records” |
| #6.1 — post-finalize guard trigger | None in src/ |
New: src/db/migrations/007_post_finalize_guard.sql (trigger DDL only) |
New: merkle_finalize.test.ts “post-finalize INSERT throws ERR_SESSION_FINALIZED” |
| #7.1 — anchor back-pointer | src/tools/merkle.ts (one UPDATE inside the existing transaction); src/domains/trail/repository.ts (extend ThoughtRecord type + rowToRecord mapper if the column should be exposed in reads) |
New: src/db/migrations/008_anchor_back_pointer.sql (column + index DDL) |
New: merkle_finalize.test.ts “anchored_in_root populated after finalize”; new: thought_record.test.ts “anchored_in_root null until finalize” |
Three parallel executor slices possible. The order C1.1 → #6.1 → #7.1 is natural but they can be dispatched independently because the migrations are additive (007 trigger does not depend on 008 column; the column is referenced only by 007’s INSERT-OR-UPDATE arm if we choose to also UPDATE anchored_in_root past-finalize, which we explicitly do not).
Migration numbering reservation: 007 is the next free number (006 ships; 007 not taken). 008 reserved for the back-pointer.
Document the canonical order: C1.1 first (so the trigger has something meaningful to guard), then #6.1, then #7.1. Mention this is a guideline, not a hard dependency, because trigger and column can ship independently of the wiring.
§Alternatives Considered (≈350-450 words, three subsections, three options each)
§§C1 alternatives:
- A. Optional
session_id(chosen). Non-breaking, opt-in, minimal change. - B. Required
session_id. Breaking change for every existing caller. Forces every ζ record to anchor, even those not interested in proof grading. Rejected: the ζ axis is independently useful for non-proof callers. - C. Server-side session inference. Auto-resolve “the most recent open session for this task_id” inside the tool handler. Rejected: introduces global state, complicates testing, breaks for tasks with overlapping concurrent sessions.
§§#6 alternatives:
- A. SQLite trigger (chosen). Schema-level enforcement.
- B. Application-level guard in
createThoughtRecord. Same SELECT inside the existing transaction. Rejected (less preferred): bypassed by direct SQL, future maintainers may inadvertently route around it. Trigger is belt-and-braces. - C.
finalized_atcolumn onaudit_sessions. Promote the implicit “finalized session” status to an explicit column. Use a CHECK constraint or trigger. Rejected: duplicates state already encoded inmerkle_rootsPK; the trigger overmerkle_rootsis exact and avoids drift.
§§#7 alternatives:
- A. Schema back-pointer column (chosen).
thought_records.anchored_in_root; preserves 14-tool lock. - B. New MCP tool
merkle_anchored_records. Direct, expressive. Rejected: breaches ADR-004 14-tool lock without a 14→15 amendment. - C. Join table
merkle_anchors(root, record_id). Many-to-many capable (Phase 1+ k-of-n). Rejected as over-engineering for Phase 0 —record_countintegrity is already encoded inmerkle_roots.record_count; a join table adds size without unlocking anything Phase 0 needs.
§Verification (≈250-350 words, prose checklist)
Sigma verifies:
- ADR file exists at
docs/architecture/decisions/ADR-007-eta-session-lifecycle.md. - Status is
Proposed. - The three §Decision subsections each name one chosen option with a one-sentence rationale.
- §Alternatives lists ≥ 3 options per gap.
- §Implementation names
007_post_finalize_guard.sqland008_anchor_back_pointer.sql(or the chosen migration numbers, if the executor adjusts). - Index file
docs/architecture/decisions/index.mdlists ADR-007. npm run build && npm run lint && npm testgreen on the worktree.
§References
- ADR-004 — tool surface lock (the 14-tool count constrains #7’s solution).
- ADR-005 — Phase 0 / Phase 1.5 boundary template (this ADR mirrors §Decision shape).
- ADR-006 —
colibri_codesemantics (η stayscolibri_code: partialafter this lands). docs/agents/writeback-protocol.md— the writeback ordering that breaks under C1.docs/audits/adr-007-eta-session-lifecycle-audit.md— the audit this ADR responds to.docs/audits/merkle-tools-audit.md— original P0.8.3 audit; useful context.
Index update
Add to docs/architecture/decisions/index.md after the ADR-006 line:
- [ADR-007 — η Session Lifecycle: Wiring, Post-Finalize Guard, ζ↔η Coupling](ADR-007-eta-session-lifecycle.md)
Update updated: field in the index frontmatter to 2026-05-06.
Risks / open questions
- Word count overshoot. ADR-005 is ~1900 words; ADR-007 covers three gaps. The 1800-2700 budget is generous; if drafting overruns, the §Alternatives subsections are the first place to compress.
- Status: Proposed vs Accepted. Per dispatch deliverable spec point 3, the ADR ships as
Proposed. This is a draft for human/PM review. - Migration numbering claim. The packet asserts 007 and 008 are free. This is true at branch base
86e430fb(R83 merge); a re-check is part of the verify step.
Build / lint / test gate
No source changes. npm run build && npm run lint && npm test projected at 1357 tests passing (R83 post-merge baseline).