Context
Across this session-arc (2026-04-27, post-#10402 / post-#10404 / post-#10409 wake-substrate hardening), multiple wake-failure investigations have hit the same diagnostic boundary: bridge-daemon.mjs writes only to console.log/console.error (terminal stdout), with no disk persistence. Specific empirical anchors:
- Leonard demo wake-failure (2026-04-27 ~11:30 UTC) — Tobias reported wake didn't fire during the live screenshare. Without persisted stdout, root cause was undiagnosable in real-time; hypothesis space included
osascript silent error / focus race / macOS Accessibility permission / coalesce-window race — none confirmable.
- Bug 2 of #10410 (duplicate-wake delivery) — empirically reproduced multiple times this session (
WAKE_SUB:ca08d381 echoed twice for one inbound message; later WAKE_SUB:e5f96999 exhibited same pattern). Root cause undiagnosable from SQLite/process-list alone; the diagnostic split (daemon-side queue/coalesce vs osascript-paste layer) requires the daemon's actual console.log lines ([Bridge Daemon] Delivered ${subscription.id} via osascript to ${appName}) to discriminate.
- 3 missed-wake messages in 10:22-10:32 UTC window — daemon healthy (PID stable, lastSyncId advancing, GraphLog had SENT_TO edges) but no wake fired. Hypothesis: silent
osascript error caught by deliverDigest's try/catch, logged via console.error then lost when Tobi's terminal scrolled past.
Every wake-failure investigation in this session has been blind beyond what's queryable from SQLite. The daemon's existing console.log/console.error sites are exactly the audit trail we need — but only if they're persisted.
The Problem
ai/scripts/bridge-daemon.mjs writes diagnostic information to process.stdout and process.stderr only. Symptoms:
- Terminal scrollback is the only audit trail; rotates out as new lines arrive
- No structured search across delivery history
- No correlation with
lastSyncId advancement state at failure time
- No cross-session continuity — restart the daemon, lose all prior context
- Cannot diagnose post-hoc — investigation must happen DURING the failure window or rely on terminal-buffer luck
Existing log sites in the daemon (per current ai/scripts/bridge-daemon.mjs):
console.log(`[Bridge Daemon] Started. Tail-syncing from GraphLog ID: ${lastSyncId}`);
console.log(`[Bridge Daemon] Delivered ${subscription.id} via osascript to ${appName}`);
console.log(`[Bridge Daemon] Delivered ${subscription.id} via tmux to session ${tmuxSession}`);
console.error('[Bridge Daemon] Error in poll loop:', err);
console.error();Each line is a high-signal diagnostic anchor. Currently lost the moment the terminal scrolls.
The Architectural Reality
bridge-daemon.mjs is a standalone Node.js script (per AGENTS.md §23 "out-of-process polling script with raw substrate access" convention). It:
- Imports
fs-extra already (used for lastSyncId state file)
- Has well-defined log directory at
.neo-ai-data/wake-daemon/ (the existing lastSyncId location)
- Runs as a long-lived process; daemon restart resets terminal but NOT the disk-backed state
Substrate options:
- A. File-based rotating log (e.g.,
fs.appendFileSync('.neo-ai-data/wake-daemon/bridge.log', line) with daily rotation OR size-based rotation via lightweight library like pino-roll). Sibling-shape to typical Unix daemon logging. Read via view_file or tail -f for live monitoring.
- B. Route through Neo's
logger.mjs — keeps consistency with the Neural Link logger refactor (per #9977). But logger.mjs is for in-process Neo classes; bridge-daemon is a standalone script outside the framework.
- C. SQLite via shared memory-core graph — sibling to
nl_action_log table that RecorderService uses. Possible but heavier (locks, schema migration, query overhead) than the diagnostic use-case warrants.
Recommendation: Option A (file-based with simple rotation). Reasoning:
- Bridge-daemon is
ai/scripts/-convention (raw substrate) per AGENTS.md §23 — file logging fits the directory pattern
- Diagnostic use-case is read-rare, write-frequent — file is the lighter substrate
- Live terminal observability preserved (write to BOTH stdout and file)
- Trivial rotation logic (daily timestamp suffix, or simple max-size + rename) keeps disk usage bounded
The Fix
- Add a
writeLog(line) helper at the top of bridge-daemon.mjs that wraps both console.log AND fs.appendFileSync('.neo-ai-data/wake-daemon/bridge.log', line + '\n').
- Replace existing
console.log(...) and console.error(...) sites with writeLog(...) calls.
- Add a daily-timestamp rotation: at start of
pollLoop, check if today's date differs from the file's last modification day; if so, rename bridge.log → bridge.log.YYYY-MM-DD before appending.
- Document the log location in the daemon's header comment block.
Optionally:
- Include
lastSyncId value in each delivery log line (already-known context for correlation)
- Keep the rotation footprint bounded — auto-prune logs older than 30 days
Acceptance Criteria
Out of Scope
- SQLite-based logging (Option C) — heavier substrate than diagnostic use-case warrants; defer if/when query-across-deliveries becomes a real workflow need.
- Centralized agent-wide logger (
logger.mjs refactor) — bridge-daemon is a standalone script outside the Neo class system per AGENTS.md §23; pulling the framework logger in cross-cuts the convention.
- Bug 2 of #10410 root-cause investigation — this ticket creates the substrate; the ACTUAL diagnostic happens after the substrate ships AND a duplicate-wake is captured in the persisted log.
- Real-time alerting on log patterns — diagnostic substrate is read-rare; alerting is a Phase-2 concern if the failure mode justifies it.
Avoided Traps
- Reusing
logger.mjs — rejected. Bridge-daemon is ai/scripts/-convention (out-of-process), not Neo class system. Pulling the framework logger introduces cross-cutting dependency for a script that should stay light.
- SQLite for diagnostic logs — rejected. Adds locking + schema migration overhead; query-across-deliveries isn't a current workflow need. File grep /
view_file covers the realistic post-hoc audit path.
- Stdout-only with "just remember to scrollback" — rejected. Multiple empirical instances this session-arc demonstrate the audit trail evaporates the moment investigation is delayed.
- Per-line buffered async writes —
fs.appendFileSync is fine; daemon writes are infrequent (coalesce-window-bounded), and synchronous-write preserves audit-trail integrity even if daemon crashes mid-write.
Related
- Empirical anchor — Bug 2 of #10410 (duplicate-wake delivery) — diagnostic substrate this ticket provides will unblock root-cause investigation
- Empirical anchor — #10395 Leonard demo wake-failure — without persisted stdout, the failure was undiagnosable in real time
- Sibling pattern — #9977 routed Neural Link Bridge through
logger.mjs to fix stdout corruption (different substrate, related concern)
- Sibling persistence —
RecorderService.nl_action_log in memory-core.sqlite (Neural Link audit trail) — different scope but same architectural intent
- AGENTS.md §23 sibling-file-lift convention —
ai/scripts/ is the "out-of-process polling script with raw substrate access" directory; this ticket's fix stays within that convention
Origin Session ID: 825527f3-06f7-440e-9607-ab37d4a16eb5
Handoff Retrieval Hints
query_raw_memories(query='bridge daemon stdout console log persistence wake failure diagnostic audit trail')
query_raw_memories(query='Bug 2 duplicate-wake delivery bridge-daemon stdout content')
- File anchor:
ai/scripts/bridge-daemon.mjs (existing console.log/console.error call sites)
- File anchor:
.neo-ai-data/wake-daemon/lastSyncId (existing daemon-state file location; new bridge.log co-locates here)
Context
Across this session-arc (2026-04-27, post-#10402 / post-#10404 / post-#10409 wake-substrate hardening), multiple wake-failure investigations have hit the same diagnostic boundary:
bridge-daemon.mjswrites only toconsole.log/console.error(terminal stdout), with no disk persistence. Specific empirical anchors:osascriptsilent error / focus race / macOS Accessibility permission / coalesce-window race — none confirmable.WAKE_SUB:ca08d381echoed twice for one inbound message; laterWAKE_SUB:e5f96999exhibited same pattern). Root cause undiagnosable from SQLite/process-list alone; the diagnostic split (daemon-side queue/coalesce vs osascript-paste layer) requires the daemon's actualconsole.loglines ([Bridge Daemon] Delivered ${subscription.id} via osascript to ${appName}) to discriminate.osascripterror caught bydeliverDigest's try/catch, logged viaconsole.errorthen lost when Tobi's terminal scrolled past.Every wake-failure investigation in this session has been blind beyond what's queryable from SQLite. The daemon's existing
console.log/console.errorsites are exactly the audit trail we need — but only if they're persisted.The Problem
ai/scripts/bridge-daemon.mjswrites diagnostic information toprocess.stdoutandprocess.stderronly. Symptoms:lastSyncIdadvancement state at failure timeExisting log sites in the daemon (per current
ai/scripts/bridge-daemon.mjs):console.log(`[Bridge Daemon] Started. Tail-syncing from GraphLog ID: ${lastSyncId}`); console.log(`[Bridge Daemon] Delivered ${subscription.id} via osascript to ${appName}`); console.log(`[Bridge Daemon] Delivered ${subscription.id} via tmux to session ${tmuxSession}`); console.error('[Bridge Daemon] Error in poll loop:', err); console.error(/* deliverDigest catch path */);Each line is a high-signal diagnostic anchor. Currently lost the moment the terminal scrolls.
The Architectural Reality
bridge-daemon.mjsis a standalone Node.js script (per AGENTS.md §23 "out-of-process polling script with raw substrate access" convention). It:fs-extraalready (used forlastSyncIdstate file).neo-ai-data/wake-daemon/(the existinglastSyncIdlocation)Substrate options:
fs.appendFileSync('.neo-ai-data/wake-daemon/bridge.log', line)with daily rotation OR size-based rotation via lightweight library likepino-roll). Sibling-shape to typical Unix daemon logging. Read viaview_fileortail -ffor live monitoring.logger.mjs— keeps consistency with the Neural Link logger refactor (per #9977). Butlogger.mjsis for in-process Neo classes; bridge-daemon is a standalone script outside the framework.nl_action_logtable thatRecorderServiceuses. Possible but heavier (locks, schema migration, query overhead) than the diagnostic use-case warrants.Recommendation: Option A (file-based with simple rotation). Reasoning:
ai/scripts/-convention (raw substrate) per AGENTS.md §23 — file logging fits the directory patternThe Fix
writeLog(line)helper at the top ofbridge-daemon.mjsthat wraps bothconsole.logANDfs.appendFileSync('.neo-ai-data/wake-daemon/bridge.log', line + '\n').console.log(...)andconsole.error(...)sites withwriteLog(...)calls.pollLoop, check if today's date differs from the file's last modification day; if so, renamebridge.log→bridge.log.YYYY-MM-DDbefore appending.Optionally:
lastSyncIdvalue in each delivery log line (already-known context for correlation)Acceptance Criteria
bridge-daemon.mjswrites ALL existingconsole.log/console.errorsites to BOTH stdout (live terminal) AND.neo-ai-data/wake-daemon/bridge.log(persistent audit trail)bridge.log.2026-04-27) for greppabilitylastSyncIdat the time of the linebridge-daemon.mjsdocuments the log path and rotation policyOut of Scope
logger.mjsrefactor) — bridge-daemon is a standalone script outside the Neo class system per AGENTS.md §23; pulling the framework logger in cross-cuts the convention.Avoided Traps
logger.mjs— rejected. Bridge-daemon isai/scripts/-convention (out-of-process), not Neo class system. Pulling the framework logger introduces cross-cutting dependency for a script that should stay light.view_filecovers the realistic post-hoc audit path.fs.appendFileSyncis fine; daemon writes are infrequent (coalesce-window-bounded), and synchronous-write preserves audit-trail integrity even if daemon crashes mid-write.Related
logger.mjsto fix stdout corruption (different substrate, related concern)RecorderService.nl_action_login memory-core.sqlite (Neural Link audit trail) — different scope but same architectural intentai/scripts/is the "out-of-process polling script with raw substrate access" directory; this ticket's fix stays within that conventionOrigin Session ID:
825527f3-06f7-440e-9607-ab37d4a16eb5Handoff Retrieval Hints
query_raw_memories(query='bridge daemon stdout console log persistence wake failure diagnostic audit trail')query_raw_memories(query='Bug 2 duplicate-wake delivery bridge-daemon stdout content')ai/scripts/bridge-daemon.mjs(existingconsole.log/console.errorcall sites).neo-ai-data/wake-daemon/lastSyncId(existing daemon-state file location; newbridge.logco-locates here)