Closes #10419. Diagnostic substrate that unblocks future wake-failure investigations including Bug 2 of #10410 and the Leonard demo failure.
Context
bridge-daemon.mjs previously emitted all diagnostic lines via console.log / console.error only — terminal scrollback was the sole audit trail. Multiple wake-failure investigations this session-arc hit the same diagnostic boundary: the daemon's log lines existed momentarily then evaporated. Specific empirical anchors per #10419's body:
- Leonard demo wake-failure — root cause undiagnosable in real-time; hypothesis space (
osascript silent error / focus race / macOS Accessibility permission / coalesce-window race) couldn't be confirmed.
- Bug 2 of #10410 — empirically reproduced multiple times; the diagnostic split (daemon-side queue/coalesce vs osascript-paste layer) requires the daemon's actual
[Bridge Daemon] Delivered ${subscription.id} via osascript to ${appName} lines to discriminate.
- 3 missed-wake messages in 10:22-10:32 UTC window (this session) — daemon healthy, lastSyncId advancing, GraphLog had SENT_TO edges, but no wake fired. Hypothesis: silent
osascript error caught by deliverDigest's try/catch then lost.
Every wake-failure investigation in this session has been blind beyond what's queryable from SQLite.
The Fix
New writeLog(level, message) helper writes to BOTH stdout (live terminal observability) AND .neo-ai-data/wake-daemon/bridge.log (persistent audit trail).
Line format: [ISO-timestamp] [PID:NNN] [LEVEL] message — greppable, per-line correlation with daemon process + chronology.
Daily rotation: previous-day file renamed to bridge.log.YYYY-MM-DD before appending today's first line. rotateLogIfNewDay runs on every writeLog call (cheap; just a stat + day-string compare).
Startup pruning: archive files older than LOG_RETENTION_DAYS (30) are unlinked at daemon boot. Best-effort; per-file failures don't gate startup.
Replaced sites: all existing console.log / console.error / console.warn calls in bridge-daemon.mjs now route through writeLog('INFO'|'ERROR', ...). The console-write inside writeLog itself is preserved (it IS the console-write path; preserves original terminal behavior).
Failure semantics: log writes are best-effort. File-write failures are silently swallowed — daemon liveness MUST NOT depend on log integrity. Console-write preserves the original behavior so terminal observability is unchanged even when file substrate fails.
The Architectural Reality
bridge-daemon.mjs is ai/scripts/-convention (per AGENTS.md §23 "out-of-process polling script with raw substrate access"). File-based logging fits that convention; SQLite-based logging would be heavier than the diagnostic use-case warrants; logger.mjs is for in-process Neo classes (the framework logger refactor in #9977 was specifically for Neural Link's in-process WebSocket bridge, not standalone scripts).
The substrate choice — file-based with appendFileSync + daily-suffix rotation + simple-prune — matches the canonical Unix daemon pattern. Read access is via view_file / tail -f for live monitoring; greppable for post-hoc audits.
Test Evidence
- Existing
bridge-daemon.spec.mjs test (which captures stdout delivery output) continues to pass — writeLog preserves the original console-write path. 4.6s, 1 passed.
- New assertions in the same test verify the persistent log file exists, contains the expected delivery line, and conforms to the documented format (ISO timestamp + PID + INFO/ERROR level prefix). All pass.
Acceptance Criteria
Out of Scope
- SQLite-based logging (Option C from ticket body) — heavier substrate than diagnostic use-case warrants
- Centralized agent-wide logger (
logger.mjs refactor) — different audience (in-process Neo classes vs standalone script)
- Bug 2 of #10410 root-cause investigation — this PR creates the diagnostic substrate; the actual investigation happens after this ships AND a duplicate-wake is captured in the persisted log
- Real-time alerting on log patterns — Phase-2 concern if the failure mode justifies it
Avoided Traps (per ticket body)
- Reusing
logger.mjs — rejected; bridge-daemon is ai/scripts/-convention (out-of-process), not Neo class system
- SQLite for diagnostic logs — rejected; adds locking + schema migration overhead for read-rare write-frequent use case
- Stdout-only with "just remember to scrollback" — rejected; multiple empirical instances this session-arc demonstrate the audit trail evaporates
- Per-line buffered async writes —
fs.appendFileSync chosen for sync-write-on-every-line audit-trail integrity even if daemon crashes
Compounds with Other PRs
- #10423 (Gemini's, focus-steal-bleed + PID-lock — Cycle 3 Approved, awaiting merge): once both land, daemon-restart events get logged with timestamp + old-PID + new-PID, making "wait, why did the daemon restart?" investigations recoverable from disk audit alone.
Cross-Family Review
Per #10208 mandate — requesting review from @neo-gemini-pro.
Closes #10419. Diagnostic substrate that unblocks future wake-failure investigations including Bug 2 of #10410 and the Leonard demo failure.
Context
bridge-daemon.mjspreviously emitted all diagnostic lines viaconsole.log/console.erroronly — terminal scrollback was the sole audit trail. Multiple wake-failure investigations this session-arc hit the same diagnostic boundary: the daemon's log lines existed momentarily then evaporated. Specific empirical anchors per #10419's body:osascriptsilent error / focus race / macOS Accessibility permission / coalesce-window race) couldn't be confirmed.[Bridge Daemon] Delivered ${subscription.id} via osascript to ${appName}lines to discriminate.osascripterror caught bydeliverDigest's try/catch then lost.Every wake-failure investigation in this session has been blind beyond what's queryable from SQLite.
The Fix
New
writeLog(level, message)helper writes to BOTH stdout (live terminal observability) AND.neo-ai-data/wake-daemon/bridge.log(persistent audit trail).Line format:
[ISO-timestamp] [PID:NNN] [LEVEL] message— greppable, per-line correlation with daemon process + chronology.Daily rotation: previous-day file renamed to
bridge.log.YYYY-MM-DDbefore appending today's first line.rotateLogIfNewDayruns on everywriteLogcall (cheap; just astat+ day-string compare).Startup pruning: archive files older than
LOG_RETENTION_DAYS(30) are unlinked at daemon boot. Best-effort; per-file failures don't gate startup.Replaced sites: all existing
console.log/console.error/console.warncalls inbridge-daemon.mjsnow route throughwriteLog('INFO'|'ERROR', ...). The console-write insidewriteLogitself is preserved (it IS the console-write path; preserves original terminal behavior).Failure semantics: log writes are best-effort. File-write failures are silently swallowed — daemon liveness MUST NOT depend on log integrity. Console-write preserves the original behavior so terminal observability is unchanged even when file substrate fails.
The Architectural Reality
bridge-daemon.mjsisai/scripts/-convention (per AGENTS.md §23 "out-of-process polling script with raw substrate access"). File-based logging fits that convention; SQLite-based logging would be heavier than the diagnostic use-case warrants;logger.mjsis for in-process Neo classes (the framework logger refactor in #9977 was specifically for Neural Link's in-process WebSocket bridge, not standalone scripts).The substrate choice — file-based with
appendFileSync+ daily-suffix rotation + simple-prune — matches the canonical Unix daemon pattern. Read access is viaview_file/tail -ffor live monitoring; greppable for post-hoc audits.Test Evidence
bridge-daemon.spec.mjstest (which captures stdout delivery output) continues to pass —writeLogpreserves the original console-write path. 4.6s, 1 passed.Acceptance Criteria
bridge-daemon.mjswrites ALL existingconsole.log/console.errorsites to BOTH stdout AND.neo-ai-data/wake-daemon/bridge.logbridge.log.YYYY-MM-DDarchive naming)bridge-daemon.mjsdocuments log path + rotation policyOut of Scope
logger.mjsrefactor) — different audience (in-process Neo classes vs standalone script)Avoided Traps (per ticket body)
logger.mjs— rejected; bridge-daemon isai/scripts/-convention (out-of-process), not Neo class systemfs.appendFileSyncchosen for sync-write-on-every-line audit-trail integrity even if daemon crashesCompounds with Other PRs
Cross-Family Review
Per #10208 mandate — requesting review from @neo-gemini-pro.