R89.A execution packet — what changes, in what order

1. Source edits — src/domains/trail/repository.ts

All line numbers refer to the file at base commit fab4bf57.

1.1 Edit A — CreateThoughtRecordInput interface (~line 81-86)

Add an optional session_id field to the input interface. JSDoc receives a short note that the field is opt-in and NULL when absent — preserving the donor-era behaviour for callers that do not opt in.

1.2 Edit B — CreateThoughtRecordInputSchema Zod object (~line 114-119)

Add session_id: z.string().min(1).optional(). Min(1) so empty strings are rejected (consistent with task_id and agent_id non-empty constraints, prevents accidental “” passing through and matching WHERE session_id = ? against the empty-string).

1.3 Edit C — ThoughtRecordRow internal type (~line 143-152)

Add readonly session_id: string | null so the row-shape returned from better-sqlite3 after SELECT can carry the new column.

1.4 Edit D — rowToRecord mapper (~line 160-171)

Widen the function’s return signature and emit the new field. The canonical ThoughtRecord type in schema.ts stays 8-field; rowToRecord will return ThoughtRecord & { session_id: string | null } (a structural superset).

1.5 Edit E — INSERT statement column list (~line 226-230)

Append session_id to the 9-column list, making it 10 columns. Add a 10th positional placeholder to the VALUES clause.

1.6 Edit F — INSERT VALUES binding (~line 243-253)

Append a 10th positional argument: parsed.session_id ?? null. SQLite accepts JavaScript null and stores it as SQL NULL.

1.7 Edit G — createThoughtRecord return shape (~line 254-263)

Include session_id: parsed.session_id ?? null in the returned object. Function return signature widens correspondingly to ThoughtRecord & { session_id: string | null }.

1.8 Edit H — getThoughtRecord SELECT (~line 284-289)

Append , session_id to the SELECT column list so the row carries the new field. Return type widens as in Edit D.

1.9 Edit I — listThoughtRecords four SELECT statements (~line 318-344)

Each of the four prepared statements gains , session_id in its SELECT list. Function return type widens to (ThoughtRecord & { session_id: string | null })[].

1.10 Edit J — module-level type alias

Introduce a local helper type for readability:

type ThoughtRecordWithSession = ThoughtRecord & { session_id: string | null };

Use it as the return-type annotation on createThoughtRecord, getThoughtRecord, listThoughtRecords, and rowToRecord. Exported so tests can import it.

2. Read-path consistency

After the source edits, all three reader functions (createThoughtRecord, getThoughtRecord, listThoughtRecords) return the same widened shape. Three downstream consumers must continue to work:

Consumer Why it’s safe
verifyChain in verifier.ts:119 Reads only the 6-field hash subset + hash + prev_hash. Treats the extra field as opaque — TypeScript structural subtyping permits the widened input.
thought_record MCP tool handler in repository.ts:377 Returns the function’s output verbatim. α middleware wraps it in {ok:true, data:...}. Clients gain a new optional field; existing clients ignore it.
thought_record_list MCP tool handler in repository.ts:389-398 Same as above.

No edit to verifier.ts. No edit to the MCP tool handlers — only the inferred output shape widens.

3. Test additions

3.1 src/__tests__/domains/trail/repository.test.ts

New describe block immediately after the existing “createThoughtRecord — explicit parent via sequence” block (~line 232):

describe('createThoughtRecord — session_id binding (R89.A)', () => {
  it('persists session_id when supplied', () => {  SQL readback  });
  it('returns session_id on the created record', () => {  });
  it('writes NULL when session_id is omitted', () => {  });
  it('rejects empty-string session_id via Zod', () => {  });
  it('does NOT change the pinned hash (session_id is out of band)', () => {
    // Repeats the pinned-hash snapshot test BUT with session_id supplied.
    // Asserts the hash matches '6a2f9597…' — proves invariant I1 + I2.
  });
});

Plus a small inline addition to “getThoughtRecord” / “listThoughtRecords” describes to read back the session_id round-trip:

it('round-trips session_id through getThoughtRecord (R89.A)', () => {  });
it('round-trips session_id through listThoughtRecords (R89.A)', () => {  });

3.2 src/__tests__/tools/merkle.test.ts

New describe block placed after the existing “finalizeMerkleRoot — acceptance test (5 records)” section. The goal is the end-to-end chain that the dispatch named:

describe('finalizeMerkleRoot — end-to-end session_id binding (R89.A)', () => {
  it('audit_session_start → createThoughtRecord({session_id}) → finalizeMerkleRoot returns a root, no ERR_NO_RECORDS', () => {
    // Uses createThoughtRecord (NOT the SQL-bypass `insertRecord` helper).
    // Asserts: result.record_count === 3, result.root is 64-hex.
  });

  it('records bound to a different session_id are not counted', () => {
    // Creates 2 records on session A, 3 on session B, finalizes A.
    // Asserts: result.record_count === 2 for A; B remains unfinalized.
  });

  it('thought records created without session_id are NOT collected for any session', () => {
    // Creates 2 unbound records, finalize on an empty session ⇒ throws NoThoughtRecordsError.
  });
});

These tests do NOT bypass the repository. They prove the canonical chain returns a Merkle root.

4. Lines-of-code budget

Estimated additions:

File LOC
src/domains/trail/repository.ts ~25 (input, Zod, ThoughtRecordRow, rowToRecord, INSERT cols+vals, return shape, getThoughtRecord SELECT, 4× listThoughtRecords SELECTs, helper alias)
src/__tests__/domains/trail/repository.test.ts ~80 (new describe + small additions)
src/__tests__/tools/merkle.test.ts ~70 (new describe)
Source files total ~175 LOC additions

(The dispatch’s “~25 LOC across 3 files” referred to production code; tests are additive.)

5. Build / lint / test plan

  1. cd .worktrees/claude/r89-a-merkle-session-binding
  2. npm install — only if node_modules absent (worktree copies it).
  3. Implement edits A-J in repository.ts first.
  4. npm run build — must compile cleanly. TypeScript will surface any type-widening miss.
  5. Write the repository.test.ts new describe block + small additions.
  6. npm test src/__tests__/domains/trail/repository.test.ts — must pass, including the pinned-hash snapshot.
  7. Write the merkle.test.ts new describe block.
  8. npm test src/__tests__/tools/merkle.test.ts — must pass.
  9. npm test — full suite. Expected delta: starting count 2406 → 2414-2418. No pre-existing test should regress.
  10. npm run lint — must be clean.
  11. npm run build && npm run lint && npm test — final gate.

6. Risk register

Risk Probability Mitigation
.toEqual(rec) assertions at repository.test.ts:240, 288, 526 break because rec gains the session_id: null field while fetched doesn’t. Low after Edits H + I (both readers also include session_id). Edits H + I make all three accessors return the same widened shape.
Pinned hash snapshot regresses. None — computeHash is unchanged and only sees the 6-field subset. Test-asserted in §3.1.
verifyChain rejects the widened input. None — TypeScript structural subtyping accepts a superset; runtime reads only canonical fields. Existing tests prove this remains green.
MCP tool envelope shape becomes incompatible with existing clients. None — the new field is additive and optional. Tool docstring stays unchanged.
Migration ordering hazard (a fresh DB without 006_eta missing the column). None — both repository.test.ts and merkle.test.ts already apply 006_eta in test setup, and the live runtime runs migrations in order. Existing migration tests at merkle.test.ts:159-214 pin the column’s existence.

7. Commit sequence

Step File(s) Commit message
1 docs/audits/r89-a-merkle-session-binding-audit.md audit(r89-a-merkle-session-binding): inventory surface (shipped c2a6d124)
2 docs/contracts/r89-a-merkle-session-binding-contract.md contract(r89-a-merkle-session-binding): behavioral contract (shipped bba94a62)
3 docs/packets/r89-a-merkle-session-binding-packet.md packet(r89-a-merkle-session-binding): execution plan
4 src/domains/trail/repository.ts + test files feat(r89-a-merkle-session-binding): thread session_id through createThoughtRecord
5 docs/verification/r89-a-merkle-session-binding-verification.md verify(r89-a-merkle-session-binding): test evidence

8. Out-of-scope (intentionally not done in R89.A)

  • No backfill of NULL session_id rows. Out of scope.
  • No reverse-index from task_id to session_id. The two scopes remain independent.
  • No new MCP tool. thought_record keeps the same name and protocol.
  • No change to audit_verify_chain behaviour.
  • No new migration. The schema is already correct as of 006_eta.sql.
  • No update to colibri-verification SKILL.md to remove the “Common Verification Failures” row added in R88.B. R88.B’s documentation row remains correct as a historical artifact and a how-to recover note. A separate R89+ hygiene task may close it once R89.A lands and is observed green in a live MCP session.

Back to top

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

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