LearnNewsExamplesServices
Frontmatter
id11066
titlebridge-daemon launchd/systemd packaging (close operator's bridge terminal)
stateClosed
labels
enhancementaiarchitecturemodel-experience
assignees[]
createdAtMay 10, 2026, 12:00 AM
updatedAtMay 10, 2026, 12:16 AM
githubUrlhttps://github.com/neomjs/neo/issues/11066
authorneo-opus-4-7
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 10, 2026, 12:16 AM

bridge-daemon launchd/systemd packaging (close operator's bridge terminal)

Closedenhancementaiarchitecturemodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 10, 2026, 12:00 AM

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:

  1. Friction — operator can't close the laptop / restart / move to a different machine without losing the bridge daemon
  2. Inconsistent with sibling pattern — SwarmHeartbeatService got launchd/systemd packaging via #11058; bridge is identical-shape persistent-process daemon and deserves the same packaging
  3. 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:

  1. npm script for clean invocation
  2. launchd plist template matching com.neomjs.swarm-heartbeat.plist.template shape
  3. 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

  • AC1 — package.json adds ai:bridge npm script invoking node ./ai/scripts/bridge-daemon.mjs
  • AC2 — learn/agentos/wake-substrate/com.neomjs.bridge-daemon.plist.template exists; mirrors com.neomjs.swarm-heartbeat.plist.template shape
  • AC3 — learn/agentos/wake-substrate/PersistentProcessManagement.md adds "Bridge Daemon Installation" section with launchd + systemd procedures + troubleshooting + coexistence notes
  • AC4 — Smoke-test verification: npm run ai:bridge produces identical bridge-daemon behavior to manual node ai/scripts/bridge-daemon.mjs invocation (no logic change)
  • AC5 — No code changes to ai/scripts/bridge-daemon.mjs itself (the daemon code is already correct-shape; this PR is packaging only)
  • AC6 — Cross-family review per pull-request §6.1

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