Parent / Discovered During
Surfaced while implementing #11948 (workspace-safety integration test, Sub-5 AC5 of #11837). The test pre-creates a minimal sqlite schema as a workaround so downstream assertions can fire; this ticket tracks the substrate fix.
The Problem
ai/daemons/bridge/queries.mjs::initializeDatabase opens with fileMustExist: true:
export function initializeDatabase(dbPath) {
try {
const db = new Database(dbPath, { fileMustExist: true });
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
return db;
} catch (err) {
console.error(`[Bridge Daemon] Failed to open database at ${dbPath}. Is the graph initialized?`);
process.exit(1);
}
}The Orchestrator daemon also consumes this primitive (ai/daemons/orchestrator/Orchestrator.mjs:476 — this.db = this.initializeDatabaseFn(this.dbPath);). In a true fresh npx-neo-app workspace where the user has not yet booted the Memory Core MCP server, the sqlite file does not exist; the orchestrator daemon hard-crashes with process.exit(1) before any of its downstream Neo-team-substrate-aware lane gates can run their degrade-with-log paths.
Three independent shape issues entangled in this single function:
- Misnamed for orchestrator consumption: the error prefix says
[Bridge Daemon] regardless of caller — surfaces as confusing log noise when the orchestrator triggers it.
- Hard
process.exit(1): removes the orchestrator's lane-isolated failure-recovery discipline. The orchestrator otherwise treats lane failures as per-task, recoverable, and log-visible; this primitive bypasses that.
fileMustExist: true is wrong for the orchestrator path: unlike the bridge daemon (which is a child task assumed to inherit a pre-initialized DB), the orchestrator IS one of the primary daemons an external user spawns in a fresh workspace.
The Architectural Reality
ai/graph/storage/SQLite.mjs::initAsync() already implements the right shape: ensureDir(dirname(dbPath)) + open without fileMustExist + initSchema() on connect. That path is what the MC MCP server uses on first boot; the orchestrator could either reuse that primitive directly OR have its own initializeDatabase mirror the same self-bootstrap discipline.
The Fix (suggestion, not prescription)
Option A (minimal): change orchestrator's initializeDatabaseFn default to (dbPath) => { fs.ensureDirSync(dirname(dbPath)); return new Database(dbPath); } plus a follow-up call to run the SQLite schema migrations — keeps the bridge daemon's stricter contract intact.
Option B (consolidating): retire bridge/queries.mjs::initializeDatabase in favor of a shared ai/graph/storage/SQLite::open(dbPath) primitive that both callers use, with the bridge daemon adding its own fileMustExist wrapper if it wants the stricter contract.
Option C (boundary): keep bridge/queries.mjs::initializeDatabase as-is for the bridge daemon child task; have the orchestrator use a different DB-open path that handles missing-file gracefully.
Operator + reviewer can weigh in on which shape best matches the substrate's intent.
Contract Ledger
Per ticket-intake-workflow.md §7 — backfilled per @neo-gpt intake-readiness handoff 2026-05-27T06:46Z. Documents operator-visible daemon-boot behavior + consumed Agent OS startup semantics before branch/code work.
Adapted from GPT's suggested matrix in issue intake comment with one additional row (schema-creation surface).
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Orchestrator.start() SQLite open path |
#12012 + #11948 workspace-safety follow-up |
Fresh workspace with absent sqlite path creates + initializes the graph sqlite (via shared self-bootstrap primitive or in-orchestrator equivalent) and reaches [Orchestrator] Started. log line |
Invalid/malformed dbPath fails with orchestrator-scoped log (not bridge-prefixed) + structured error; no process.exit(1) from the orchestrator path — let lane-isolated failure-recovery handle it |
JSDoc/comments on the chosen open helper (or split, if Option B taken) |
Integration test in fresh-workspace fixture proving absent-file boot creates schema + reaches Started. line + asserts dbPath exists post-boot |
bridge/queries.mjs::initializeDatabase() bridge-daemon path |
Existing bridge daemon wake-substrate contract (child task assumed inheriting pre-initialized DB) |
Existing behavior preserved when sqlite already exists: open with WAL + busy_timeout pragmas, return db handle |
If strict bridge contract retained: still fails loudly for missing inherited DB (matches current fileMustExist: true + process.exit(1) semantic). If shared helper introduced (Option B): bridge wraps with explicit fileMustExist enforcer |
JSDoc updated only if helper behavior changes (Option B path) |
Existing unit coverage showing bridge query consumers still open an existing DB without regression |
workspaceSafety.spec.mjs fixture workaround |
#11948 Sub-5 AC5 + #12012 AC4 |
After orchestrator self-bootstrap lands: initSqliteSchema() workaround function deleted from spec; test asserts the orchestrator's own bootstrap creates the schema |
Test must still isolate temp workspace state + terminate daemon cleanly; assert post-boot file/schema presence |
Test summary updated if probe scope changes (e.g., moves from pre-create to post-boot assertion) |
Integration test no longer pre-creates sqlite; asserts created sqlite + schema after boot completes |
Schema-creation surface (ai/graph/storage/SQLite.mjs::initSchema() or equivalent) |
Existing Memory Core MCP boot path (already invokes this primitive on first boot) |
Orchestrator boot path invokes the same schema-creation primitive Memory Core uses, ensuring schema-shape parity between MC-first-boot and orchestrator-first-boot workspaces |
If migration system exists post-#nnn (none known today), schema-creation MUST be idempotent — repeat invocation on existing schema must no-op safely |
Reference docstring on Orchestrator.start() citing SQLite.initSchema as the shared substrate |
Integration test proves orchestrator-bootstrapped schema is byte-equivalent (or semantically equivalent) to MC-bootstrapped schema — no divergence on day-2 workspace bootstrap |
Option-selection signal (operator/lead decision)
Three implementation shapes are open per the ticket body's "The Fix" section:
- Option A (minimal) — change orchestrator's
initializeDatabaseFn default + add follow-up schema-migration call. Smallest delta; preserves bridge contract; some code duplication between orchestrator-side + MC-side bootstrap.
- Option B (consolidating) — retire
bridge/queries.mjs::initializeDatabase in favor of shared ai/graph/storage/SQLite::open(dbPath); bridge daemon adds its own fileMustExist wrapper if needed. Cleaner long-term; small risk of bridge-daemon regression if wrapper subtly differs.
- Option C (boundary) — keep bridge as-is; orchestrator uses different DB-open path. Most-isolated; some duplication.
Implementation PR should pick one + cite which + document rationale; this ledger covers all three shapes since the Contract is about behavior not which file holds the implementation.
Intake disposition
Ticket is intake-ready with this ledger. Open for claim per @neo-gpt author-yield (he's over FAIR-band 18/30; under-band peers favored). I (the ticket author) am willing to claim implementation in a follow-up turn pending session-energy assessment; operator override welcome.
Acceptance Criteria
Empirical anchor
Smoke-tested 2026-05-26 against agent/11948-workspace-safety-integration-test branch: spawning node ai/daemons/orchestrator/daemon.mjs with NEO_AI_DB_PATH=/tmp/fresh-workspace/memory-core-graph.sqlite (file absent) produces [Bridge Daemon] Failed to open database at /tmp/fresh-workspace/memory-core-graph.sqlite. Is the graph initialized? on stderr followed by process.exit(1) within ~1.5s. Pre-creating the sqlite with new Database(path); db.exec('CREATE TABLE ...') lets the orchestrator boot cleanly past the open and reach [Orchestrator] Started. at ~2s.
Out of Scope
- Pre-#11948 substrate behavior (the bridge daemon's stricter contract works correctly for its actual child-task usage; only the orchestrator's reuse of the primitive is the issue).
- Generalizing this to all daemons (KB MCP server, etc.) — file separate tickets if those exhibit the same pattern.
Labels
enhancement, ai, daemon, workspace-safety
Authority
Surfaced during #11948 implementation per the parent ticket's "Out of Scope — file separate sub-tickets if discovered" clause.
Authored by: Claude Opus 4.7 (Claude Code).
Parent / Discovered During
Surfaced while implementing #11948 (workspace-safety integration test, Sub-5 AC5 of #11837). The test pre-creates a minimal sqlite schema as a workaround so downstream assertions can fire; this ticket tracks the substrate fix.
The Problem
ai/daemons/bridge/queries.mjs::initializeDatabaseopens withfileMustExist: true:// ai/daemons/bridge/queries.mjs:5-15 export function initializeDatabase(dbPath) { try { const db = new Database(dbPath, { fileMustExist: true }); db.pragma('journal_mode = WAL'); db.pragma('busy_timeout = 5000'); return db; } catch (err) { console.error(`[Bridge Daemon] Failed to open database at ${dbPath}. Is the graph initialized?`); process.exit(1); } }The Orchestrator daemon also consumes this primitive (
ai/daemons/orchestrator/Orchestrator.mjs:476—this.db = this.initializeDatabaseFn(this.dbPath);). In a true freshnpx-neo-appworkspace where the user has not yet booted the Memory Core MCP server, the sqlite file does not exist; the orchestrator daemon hard-crashes withprocess.exit(1)before any of its downstream Neo-team-substrate-aware lane gates can run their degrade-with-log paths.Three independent shape issues entangled in this single function:
[Bridge Daemon]regardless of caller — surfaces as confusing log noise when the orchestrator triggers it.process.exit(1): removes the orchestrator's lane-isolated failure-recovery discipline. The orchestrator otherwise treats lane failures as per-task, recoverable, and log-visible; this primitive bypasses that.fileMustExist: trueis wrong for the orchestrator path: unlike the bridge daemon (which is a child task assumed to inherit a pre-initialized DB), the orchestrator IS one of the primary daemons an external user spawns in a fresh workspace.The Architectural Reality
ai/graph/storage/SQLite.mjs::initAsync()already implements the right shape:ensureDir(dirname(dbPath))+ open withoutfileMustExist+initSchema()on connect. That path is what the MC MCP server uses on first boot; the orchestrator could either reuse that primitive directly OR have its owninitializeDatabasemirror the same self-bootstrap discipline.The Fix (suggestion, not prescription)
Option A (minimal): change orchestrator's
initializeDatabaseFndefault to(dbPath) => { fs.ensureDirSync(dirname(dbPath)); return new Database(dbPath); }plus a follow-up call to run the SQLite schema migrations — keeps the bridge daemon's stricter contract intact.Option B (consolidating): retire
bridge/queries.mjs::initializeDatabasein favor of a sharedai/graph/storage/SQLite::open(dbPath)primitive that both callers use, with the bridge daemon adding its ownfileMustExistwrapper if it wants the stricter contract.Option C (boundary): keep
bridge/queries.mjs::initializeDatabaseas-is for the bridge daemon child task; have the orchestrator use a different DB-open path that handles missing-file gracefully.Operator + reviewer can weigh in on which shape best matches the substrate's intent.
Contract Ledger
Per
ticket-intake-workflow.md §7— backfilled per @neo-gpt intake-readiness handoff 2026-05-27T06:46Z. Documents operator-visible daemon-boot behavior + consumed Agent OS startup semantics before branch/code work.Adapted from GPT's suggested matrix in issue intake comment with one additional row (schema-creation surface).
Orchestrator.start()SQLite open path[Orchestrator] Started.log lineprocess.exit(1)from the orchestrator path — let lane-isolated failure-recovery handle itStarted.line + asserts dbPath exists post-bootbridge/queries.mjs::initializeDatabase()bridge-daemon pathfileMustExist: true+process.exit(1)semantic). If shared helper introduced (Option B): bridge wraps with explicitfileMustExistenforcerworkspaceSafety.spec.mjsfixture workaroundinitSqliteSchema()workaround function deleted from spec; test asserts the orchestrator's own bootstrap creates the schemaai/graph/storage/SQLite.mjs::initSchema()or equivalent)Orchestrator.start()citingSQLite.initSchemaas the shared substrateOption-selection signal (operator/lead decision)
Three implementation shapes are open per the ticket body's "The Fix" section:
initializeDatabaseFndefault + add follow-up schema-migration call. Smallest delta; preserves bridge contract; some code duplication between orchestrator-side + MC-side bootstrap.bridge/queries.mjs::initializeDatabasein favor of sharedai/graph/storage/SQLite::open(dbPath); bridge daemon adds its ownfileMustExistwrapper if needed. Cleaner long-term; small risk of bridge-daemon regression if wrapper subtly differs.Implementation PR should pick one + cite which + document rationale; this ledger covers all three shapes since the Contract is about behavior not which file holds the implementation.
Intake disposition
Ticket is intake-ready with this ledger. Open for claim per @neo-gpt author-yield (he's over FAIR-band 18/30; under-band peers favored). I (the ticket author) am willing to claim implementation in a follow-up turn pending session-energy assessment; operator override welcome.
Acceptance Criteria
.neo-ai-data/sqlite/dir, no sqlite file) boots withoutprocess.exit(1).[Orchestrator] Started.log line AND the sqlite schema is created on disk (post-boot probe assertion).initSqliteSchemafixture helper intest/playwright/integration/ai/daemons/workspaceSafety.spec.mjscan be deleted once this ticket lands.initializeDatabaseFninjection seam, or extending the #11948 integration test by removing the fixture helper and asserting the daemon-created sqlite file exists).Empirical anchor
Smoke-tested 2026-05-26 against
agent/11948-workspace-safety-integration-testbranch: spawningnode ai/daemons/orchestrator/daemon.mjswithNEO_AI_DB_PATH=/tmp/fresh-workspace/memory-core-graph.sqlite(file absent) produces[Bridge Daemon] Failed to open database at /tmp/fresh-workspace/memory-core-graph.sqlite. Is the graph initialized?on stderr followed byprocess.exit(1)within ~1.5s. Pre-creating the sqlite withnew Database(path); db.exec('CREATE TABLE ...')lets the orchestrator boot cleanly past the open and reach[Orchestrator] Started.at ~2s.Out of Scope
Labels
enhancement,ai,daemon,workspace-safetyAuthority
Surfaced during #11948 implementation per the parent ticket's "Out of Scope — file separate sub-tickets if discovered" clause.
Authored by: Claude Opus 4.7 (Claude Code).