mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
9dda75bb21
- 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>
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
/**
|
|
* Cross-mount visibility regression test for the two-DB session architecture.
|
|
*
|
|
* What this catches: any change that breaks host→container write propagation
|
|
* across the Docker bind mount. The v2 session DB design relies on three
|
|
* invariants working together:
|
|
*
|
|
* 1. journal_mode = DELETE on every session DB (not WAL)
|
|
* 2. Host opens-writes-closes the DB file on every write
|
|
* 3. One writer per file (inbound = host, outbound = container)
|
|
*
|
|
* This script exercises a long-lived container-side reader polling a DB
|
|
* while the host writes. If visibility is working, the reader sees each
|
|
* write within one poll period. If any of the invariants regresses, the
|
|
* reader either sees nothing, sees only the first write, or sees updates
|
|
* only after the host closes its connection for good.
|
|
*
|
|
* Expected passing output (DELETE mode, close-per-write):
|
|
* reader sees each seq within ~1s of it being written.
|
|
* Anything else is a regression — investigate BEFORE assuming it's flaky.
|
|
*
|
|
* Keep this around. It ran for ~20 minutes once to map the failure modes
|
|
* and it takes about 60s to run — cheap insurance.
|
|
*
|
|
* Requires: Docker Desktop running, nanoclaw-agent:latest image built.
|
|
*/
|
|
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { join } from "node:path";
|
|
import { mkdirSync, rmSync } from "node:fs";
|
|
import Database from "better-sqlite3";
|
|
|
|
const dbDir = join("/tmp", `nanoclaw-live-${Date.now()}`);
|
|
mkdirSync(dbDir, { recursive: true });
|
|
spawnSync("chmod", ["777", dbDir]);
|
|
const dbPath = join(dbDir, "live.db");
|
|
|
|
for (const journalMode of ["DELETE", "WAL"]) {
|
|
console.log(`\n=== ${journalMode} ===`);
|
|
rmSync(dbPath, { force: true });
|
|
rmSync(dbPath + "-wal", { force: true });
|
|
rmSync(dbPath + "-shm", { force: true });
|
|
rmSync(dbPath + "-journal", { force: true });
|
|
|
|
const db = new Database(dbPath);
|
|
db.pragma(`journal_mode = ${journalMode}`);
|
|
db.pragma("synchronous = FULL");
|
|
db.exec("CREATE TABLE msgs (seq INTEGER PRIMARY KEY, content TEXT)");
|
|
db.close();
|
|
|
|
// Start container poller in background
|
|
const contProc = spawn("docker", [
|
|
"run", "--rm", "-w", "/app",
|
|
"-v", `${dbDir}:/workspace`,
|
|
"--entrypoint", "node",
|
|
"nanoclaw-agent:latest",
|
|
"-e",
|
|
`const Database = require('better-sqlite3');
|
|
const db = new Database('/workspace/live.db', { readonly: true });
|
|
db.pragma('busy_timeout = 2000');
|
|
const stmt = db.prepare('SELECT COUNT(*) as n, MAX(seq) as hi FROM msgs');
|
|
let count = 0;
|
|
const timer = setInterval(() => {
|
|
const r = stmt.get();
|
|
console.log('poll t=' + (Date.now() % 100000) + ' count=' + r.n + ' max=' + r.hi);
|
|
if (++count >= 10) { clearInterval(timer); db.close(); }
|
|
}, 1000);`,
|
|
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
|
|
contProc.stdout.on("data", (d) => process.stdout.write(` [cont] ${d}`));
|
|
contProc.stderr.on("data", (d) => process.stderr.write(` [cont-err] ${d}`));
|
|
|
|
// Give container a moment to start
|
|
const waitUntil = Date.now() + 2000;
|
|
while (Date.now() < waitUntil) {}
|
|
|
|
// Host opens, writes, CLOSES each time (matches production session-manager pattern)
|
|
for (let i = 1; i <= 8; i++) {
|
|
const h = new Database(dbPath);
|
|
h.pragma(`journal_mode = ${journalMode}`);
|
|
h.pragma("synchronous = FULL");
|
|
h.prepare("INSERT INTO msgs (seq, content) VALUES (?, ?)").run(i, `msg-${i}`);
|
|
h.close();
|
|
console.log(` [host] wrote+closed seq=${i} t=${Date.now() % 100000}`);
|
|
const sleepUntil = Date.now() + 1000;
|
|
while (Date.now() < sleepUntil) {}
|
|
}
|
|
|
|
// Wait for container to finish
|
|
await new Promise<void>((res) => contProc.once("exit", () => res()));
|
|
}
|
|
|
|
rmSync(dbDir, { recursive: true, force: true });
|