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:

  1. 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.
  2. 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.
  3. 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: string to ThoughtRecordToolInputSchema.
  • Add the column binding to the INSERT in createThoughtRecord.
  • 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_guard on thought_records BEFORE INSERT and BEFORE UPDATE OF session_id.
  • The trigger raises SQLITE_ABORT with message ERR_SESSION_FINALIZED when the row’s session_id matches a row in merkle_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 because merkle_roots.session_id is a one-to-one shadow of audit_sessions.session_id and the join is exact).
  • merkle_finalize updates this column inside its existing transaction: after building the tree and inserting merkle_roots, run UPDATE thought_records SET anchored_in_root = ? WHERE session_id = ?.
  • Add a Boolean composite index idx_trail_anchor on (anchored_in_root) for the reverse-lookup read pattern.
  • The MCP surface gains no new tool; readers query thought_record_list({task_id}) and read anchored_in_root per 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_list call 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_root write in merkle_finalize extends the transaction span by one UPDATE; benchmark needed for large sessions.
  • Existing ζ tests that pre-seeded thought_records rows directly without session_id continue 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_id exclusion from the hash subset (P0.7.1 §3) is unchanged. session_id likewise 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_at column on audit_sessions. Promote the implicit “finalized session” status to an explicit column. Use a CHECK constraint or trigger. Rejected: duplicates state already encoded in merkle_roots PK; the trigger over merkle_roots is 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_count integrity is already encoded in merkle_roots.record_count; a join table adds size without unlocking anything Phase 0 needs.

§Verification (≈250-350 words, prose checklist)

Sigma verifies:

  1. ADR file exists at docs/architecture/decisions/ADR-007-eta-session-lifecycle.md.
  2. Status is Proposed.
  3. The three §Decision subsections each name one chosen option with a one-sentence rationale.
  4. §Alternatives lists ≥ 3 options per gap.
  5. §Implementation names 007_post_finalize_guard.sql and 008_anchor_back_pointer.sql (or the chosen migration numbers, if the executor adjusts).
  6. Index file docs/architecture/decisions/index.md lists ADR-007.
  7. npm run build && npm run lint && npm test green 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-006colibri_code semantics (η stays colibri_code: partial after 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).


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.