Context
Sibling-fileable from PR #11058 (SwarmHeartbeatService class+wrapper split → launchd/systemd-packaged daemon). Operator surfaced 2026-05-09: end-state goal is closing the manually-kept-open bridge terminal + chroma terminal + running orchestrator instead. Bridge terminal is the easiest win.
Empirical state (2026-05-09):
node ai/scripts/bridge-daemon.mjs running PID 63662, started Fri 10PM (12 days uptime)
.neo-ai-data/wake-daemon/bridge.log.YYYY-MM-DD rotation series back to 2026-04-27 — operator has been keeping it alive in a terminal continuously
- No npm script
ai:bridge exists in package.json (verified via grep)
- No launchd plist template exists for bridge-daemon (only
com.neomjs.swarm-heartbeat.plist.template is sibling-precedent)
- Bridge daemon ALREADY has the right shape internally — it's a long-running process with proper PID-lock per #10422/#10423; just needs persistent-process packaging
Duplicate sweep before filing:
gh issue list --search "bridge-daemon launchd in:title,body" — no matches
gh issue list --search "bridge daemon plist systemd" — no matches
learn/agentos/wake-substrate/PersistentProcessManagement.md documents SwarmHeartbeatService packaging only
The Problem
Operator must keep a terminal open continuously to run node ai/scripts/bridge-daemon.mjs. This is:
- Friction — operator can't close the laptop / restart / move to a different machine without losing the bridge daemon
- Inconsistent with sibling pattern — SwarmHeartbeatService got launchd/systemd packaging via #11058; bridge is identical-shape persistent-process daemon and deserves the same packaging
- Blocks operator's terminal-consolidation goal — three terminals currently (bridge, chroma, optional orchestrator); reducing to zero-terminals is the end-state
The Architectural Reality
Sibling precedent: PR #11058 split SwarmHeartbeatService into:
ai/daemons/SwarmHeartbeatService.mjs (class only)
ai/scripts/swarm-heartbeat-daemon.mjs (entry-point wrapper)
learn/agentos/wake-substrate/com.neomjs.swarm-heartbeat.plist.template
- Documentation in
learn/agentos/wake-substrate/PersistentProcessManagement.md
Bridge-daemon doesn't need the class+wrapper split (it's already a single entry-point script, not a class). It just needs:
- npm script for clean invocation
- launchd plist template matching
com.neomjs.swarm-heartbeat.plist.template shape
- PersistentProcessManagement.md updated to document bridge-daemon packaging in parallel
The Fix
Step 1: Add ai:bridge npm script
"ai:orchestrator" : "node ./ai/scripts/orchestrator-daemon.mjs",
+ "ai:bridge" : "node ./ai/scripts/bridge-daemon.mjs",
Step 2: Create learn/agentos/wake-substrate/com.neomjs.bridge-daemon.plist.template
Lift com.neomjs.swarm-heartbeat.plist.template shape:
- Label:
com.neomjs.bridge-daemon
- ProgramArguments:
[/usr/bin/env, node, [OPERATOR_REPO_ROOT]/ai/scripts/bridge-daemon.mjs]
- WorkingDirectory:
[OPERATOR_REPO_ROOT]
- KeepAlive: true (long-running daemon)
- ThrottleInterval: 10
- StandardOutPath/StandardErrorPath:
[REPO_ROOT]/.neo-ai-data/wake-daemon/bridge.{stdout,stderr}.log
- EnvironmentVariables:
PATH + NEO_AI_DAEMON_DIR (optional; defaults to .neo-ai-data/wake-daemon)
Step 3: Update learn/agentos/wake-substrate/PersistentProcessManagement.md
Add parallel "Bridge Daemon Installation" section with:
- Empirical-prerequisites (verify node, check
.neo-ai-data/wake-daemon/ exists, manual one-shot test)
- macOS launchd installation (sed substitution +
launchctl bootstrap)
- Linux systemd .service template (mirroring SwarmHeartbeatService)
- Troubleshooting table (similar gotchas: PATH, KeepAlive thrashing, plist sync issues)
- Cross-coexistence note: bridge + swarm-heartbeat + orchestrator are independent daemons; can install/uninstall each separately
Step 4: Verify operator-runnable smoke-test
npm run ai:bridge should produce identical behavior to current manual node ai/scripts/bridge-daemon.mjs — no logic changes, only invocation path.
Acceptance Criteria
Out of Scope
- Bridge-daemon code refactoring — no class+wrapper split needed (single-file entry-point shape is correct for this daemon's role; #11058 split was specific to SwarmHeartbeatService's hybrid class+self-invoke pattern)
- Chroma terminal close — separate sibling work (chroma is a third-party process, not a Neo daemon; needs its own launchd packaging investigation)
- Orchestrator daemon launchd packaging — separate sibling; orchestrator boot has its own first-run validation needs (
.neo-ai-data/orchestrator-daemon/ directory creation, MC quietness check, etc.)
- Auto-installer / installer-script — operator can run the substitution + bootstrap manually per the existing PersistentProcessManagement.md pattern
Avoided Traps
- ❌ Bundling chroma + orchestrator launchd into this PR — three different daemons, three different first-run validation needs. One ticket = one PR per ticket-create §3.
- ❌ Refactoring bridge-daemon.mjs internals — it works (PID 63662 has 12-day uptime). Don't fix what isn't broken; just package it.
- ❌ Hardcoded PATH — must list operator-environment-specific dirs (Homebrew, nvm, etc.) per the SwarmHeartbeatService plist's gotcha note.
Provenance
- Operator end-state goal (2026-05-09): "the real goal would be that i close my bridge terminal, and the chroma terminal process, and run the new orchestrator instead"
- Empirical anchor (2026-05-09): PID 63662 bridge-daemon Fri 10PM uptime;
.neo-ai-data/wake-daemon/bridge.log.YYYY-MM-DD rotation series back to 2026-04-27
- Sibling precedent: PR #11058 (SwarmHeartbeatService class+wrapper split with launchd template)
- Bridge daemon source:
ai/scripts/bridge-daemon.mjs (#10319 / #10422 / #10423 PID-lock substrate)
Related
- PR #11058 (sibling: SwarmHeartbeatService launchd packaging — direct precedent shape)
- #10422, #10423 (bridge-daemon PID-lock singleton enforcement)
learn/agentos/wake-substrate/PersistentProcessManagement.md (the canonical operator-facing substrate to extend)
Self-Identification: @neo-opus-4-7 (Claude Opus 4.7, Claude Code) — claiming this lane immediately. Non-LLM/non-DB-load work; concrete operator-terminal-close win; sibling-pattern lift from #11058.
Origin Session ID: c2912891-b459-4a03-b2af-154d5e264df1
Context
Sibling-fileable from PR #11058 (SwarmHeartbeatService class+wrapper split → launchd/systemd-packaged daemon). Operator surfaced 2026-05-09: end-state goal is closing the manually-kept-open bridge terminal + chroma terminal + running orchestrator instead. Bridge terminal is the easiest win.
Empirical state (2026-05-09):
node ai/scripts/bridge-daemon.mjsrunning PID 63662, started Fri 10PM (12 days uptime).neo-ai-data/wake-daemon/bridge.log.YYYY-MM-DDrotation series back to 2026-04-27 — operator has been keeping it alive in a terminal continuouslyai:bridgeexists inpackage.json(verified via grep)com.neomjs.swarm-heartbeat.plist.templateis sibling-precedent)Duplicate sweep before filing:
gh issue list --search "bridge-daemon launchd in:title,body"— no matchesgh issue list --search "bridge daemon plist systemd"— no matcheslearn/agentos/wake-substrate/PersistentProcessManagement.mddocuments SwarmHeartbeatService packaging onlyThe Problem
Operator must keep a terminal open continuously to run
node ai/scripts/bridge-daemon.mjs. This is:The Architectural Reality
Sibling precedent: PR #11058 split SwarmHeartbeatService into:
ai/daemons/SwarmHeartbeatService.mjs(class only)ai/scripts/swarm-heartbeat-daemon.mjs(entry-point wrapper)learn/agentos/wake-substrate/com.neomjs.swarm-heartbeat.plist.templatelearn/agentos/wake-substrate/PersistentProcessManagement.mdBridge-daemon doesn't need the class+wrapper split (it's already a single entry-point script, not a class). It just needs:
com.neomjs.swarm-heartbeat.plist.templateshapeThe Fix
Step 1: Add
ai:bridgenpm script"ai:orchestrator" : "node ./ai/scripts/orchestrator-daemon.mjs", + "ai:bridge" : "node ./ai/scripts/bridge-daemon.mjs",Step 2: Create
learn/agentos/wake-substrate/com.neomjs.bridge-daemon.plist.templateLift
com.neomjs.swarm-heartbeat.plist.templateshape:com.neomjs.bridge-daemon[/usr/bin/env, node, [OPERATOR_REPO_ROOT]/ai/scripts/bridge-daemon.mjs][OPERATOR_REPO_ROOT][REPO_ROOT]/.neo-ai-data/wake-daemon/bridge.{stdout,stderr}.logPATH+NEO_AI_DAEMON_DIR(optional; defaults to.neo-ai-data/wake-daemon)Step 3: Update
learn/agentos/wake-substrate/PersistentProcessManagement.mdAdd parallel "Bridge Daemon Installation" section with:
.neo-ai-data/wake-daemon/exists, manual one-shot test)launchctl bootstrap)Step 4: Verify operator-runnable smoke-test
npm run ai:bridgeshould produce identical behavior to current manualnode ai/scripts/bridge-daemon.mjs— no logic changes, only invocation path.Acceptance Criteria
package.jsonaddsai:bridgenpm script invokingnode ./ai/scripts/bridge-daemon.mjslearn/agentos/wake-substrate/com.neomjs.bridge-daemon.plist.templateexists; mirrorscom.neomjs.swarm-heartbeat.plist.templateshapelearn/agentos/wake-substrate/PersistentProcessManagement.mdadds "Bridge Daemon Installation" section with launchd + systemd procedures + troubleshooting + coexistence notesnpm run ai:bridgeproduces identical bridge-daemon behavior to manualnode ai/scripts/bridge-daemon.mjsinvocation (no logic change)ai/scripts/bridge-daemon.mjsitself (the daemon code is already correct-shape; this PR is packaging only)pull-request §6.1Out of Scope
.neo-ai-data/orchestrator-daemon/directory creation, MC quietness check, etc.)Avoided Traps
Provenance
.neo-ai-data/wake-daemon/bridge.log.YYYY-MM-DDrotation series back to 2026-04-27ai/scripts/bridge-daemon.mjs(#10319 / #10422 / #10423 PID-lock substrate)Related
learn/agentos/wake-substrate/PersistentProcessManagement.md(the canonical operator-facing substrate to extend)Self-Identification: @neo-opus-4-7 (Claude Opus 4.7, Claude Code) — claiming this lane immediately. Non-LLM/non-DB-load work; concrete operator-terminal-close win; sibling-pattern lift from #11058.
Origin Session ID: c2912891-b459-4a03-b2af-154d5e264df1