docs(v2): cross-mount invariants + diagrams; inline a2a routing

- session-manager.ts: shrink the cross-mount invariant header from 31
  lines to 12, keeping each invariant's cause and consequence inline.
- agent-runner/db/connection.ts: parallel cross-mount comment for the
  container-side reader (inbound.db must be journal_mode=DELETE).
- agent-runner/db/messages-out.ts: document that even/odd seq parity
  is load-bearing — seq is the agent-facing message ID returned by
  send_message and consumed by edit_message / add_reaction, looked
  up across both tables.
- v2-checklist.md: record the cross-mount invariants and seq parity
  under Core Architecture so future "simplifications" don't regress
  them.
- scripts/sanity-live-poll.ts: empirical validation harness for the
  three cross-mount invariants — flips each one and observes silent
  message loss / corruption.
- delivery.ts: inline routeAgentMessage at its single callsite (-17
  net lines). The wrapper added more boilerplate than it factored.
- docs/v2-architecture-diagram.{md,html}: rendered Mermaid diagrams
  of the v2 system, message flow, named destinations, entity model,
  and the two-DB split.
- channels/adapter.ts, chat-sdk-bridge.ts, credentials.ts,
  db/sessions.ts, db/db-v2.test.ts: prettier format pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-12 00:21:12 +03:00
parent c9fa5cdbed
commit 9dda75bb21
13 changed files with 788 additions and 86 deletions

View File

@@ -1,10 +1,14 @@
/**
* Session lifecycle management.
* Creates session folders + DBs, writes messages, manages container status.
* Session lifecycle: folders, DBs, messages, container status.
*
* Two-DB architecture: each session has inbound.db (host-owned) and outbound.db
* (container-owned). This eliminates SQLite write contention across the
* host-container mount boundary — each file has exactly one writer.
* Two-DB split — inbound.db (host writes) + outbound.db (container writes).
* Three cross-mount invariants are load-bearing:
* 1. journal_mode=DELETE — WAL's mmapped -shm doesn't refresh host→guest;
* the container would silently miss every new message.
* 2. Host opens-writes-CLOSES per op — close invalidates the container's
* page cache; a long-lived connection freezes its view at first read.
* 3. One writer per file — DELETE-mode journal-unlink isn't atomic across
* the mount; concurrent writers corrupt the DB.
*/
import Database from 'better-sqlite3';
import fs from 'fs';
@@ -260,7 +264,13 @@ export function writeDestinations(agentGroupId: string, sessionId: string): void
log.debug('Destination map written', { sessionId, count: resolved.length });
}
/** Write a message to a session's inbound DB (messages_in). Host-only. */
/**
* Write a message to a session's inbound DB (messages_in). Host-only.
*
* ⚠ Opens and closes the DB on every call. Do not refactor to reuse a
* long-lived connection — see the "Cross-mount visibility invariants" note
* at the top of this file.
*/
export function writeSessionMessage(
agentGroupId: string,
sessionId: string,
@@ -285,8 +295,13 @@ export function writeSessionMessage(
db.pragma('busy_timeout = 5000');
try {
// Host uses even seq numbers, container uses odd — prevents collisions
// across the two-DB boundary without cross-DB coordination.
// Host uses even seq, container uses odd. This is not just collision
// avoidance between the two DB files — the seq is the agent-facing
// message ID returned by send_message and accepted by edit_message /
// add_reaction, and those tools look up by seq across BOTH tables
// (see container/agent-runner/src/db/messages-out.ts:getMessageIdBySeq).
// So the {messages_in.seq, messages_out.seq} namespace MUST be disjoint,
// or the agent's "edit message #5" could resolve to the wrong row.
const maxSeq = (db.prepare('SELECT COALESCE(MAX(seq), 0) AS m FROM messages_in').get() as { m: number }).m;
const nextSeq = maxSeq < 2 ? 2 : maxSeq + 2 - (maxSeq % 2); // next even