v2: split session DB into inbound/outbound for write isolation

Eliminates SQLite write contention across the host-container mount
boundary by splitting the single session.db into two files, each with
exactly one writer:

  inbound.db  — host writes (messages_in, delivered tracking)
  outbound.db — container writes (messages_out, processing_ack)

Key changes:
- Host uses even seq numbers, container uses odd (collision-free)
- Container heartbeat via file touch instead of DB UPDATE
- Scheduling MCP tools now emit system actions via messages_out
  (host applies them to inbound.db during delivery)
- Host sweep reads processing_ack + heartbeat file for stale detection
- OneCLI ensureAgent() call added (was missing from v2, caused
  applyContainerConfig to reject unknown agent identifiers)

Verified: tsc clean, 327 tests pass, real e2e through Docker works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-09 12:17:31 +03:00
parent 320176e7e8
commit 82cb363f84
19 changed files with 738 additions and 347 deletions

View File

@@ -69,16 +69,21 @@ CREATE TABLE pending_questions (
`;
/**
* Session DB schema — created fresh by the host for each session.
* Session DB schemassplit into two files so each has exactly one writer.
* This eliminates SQLite write contention across the host-container mount boundary.
*
* inbound.db — host writes, container reads (read-only mount or open read-only)
* outbound.db — container writes, host reads (read-only open)
*/
export const SESSION_SCHEMA = `
/** Host-owned: inbound messages + delivery tracking. */
export const INBOUND_SCHEMA = `
CREATE TABLE messages_in (
id TEXT PRIMARY KEY,
seq INTEGER UNIQUE,
kind TEXT NOT NULL,
timestamp TEXT NOT NULL,
status TEXT DEFAULT 'pending',
status_changed TEXT,
process_after TEXT,
recurrence TEXT,
tries INTEGER DEFAULT 0,
@@ -88,12 +93,21 @@ CREATE TABLE messages_in (
content TEXT NOT NULL
);
-- Host tracks which messages_out IDs have been delivered.
-- Avoids writing to outbound.db (container-owned).
CREATE TABLE delivered (
message_out_id TEXT PRIMARY KEY,
delivered_at TEXT NOT NULL
);
`;
/** Container-owned: outbound messages + processing acknowledgments. */
export const OUTBOUND_SCHEMA = `
CREATE TABLE messages_out (
id TEXT PRIMARY KEY,
seq INTEGER UNIQUE,
in_reply_to TEXT,
timestamp TEXT NOT NULL,
delivered INTEGER DEFAULT 0,
deliver_after TEXT,
recurrence TEXT,
kind TEXT NOT NULL,
@@ -102,4 +116,13 @@ CREATE TABLE messages_out (
thread_id TEXT,
content TEXT NOT NULL
);
-- Container tracks processing status here instead of updating messages_in.
-- Host reads this to know which messages have been processed.
-- On container startup, stale 'processing' entries are cleared (crash recovery).
CREATE TABLE processing_ack (
message_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
status_changed TEXT NOT NULL
);
`;