Skip to content

feat(daemon): cross-platform supervisor + bicameral-mcp daemon CLI (Phase 2c-3)#508

Merged
jinhongkuan merged 1 commit into
devfrom
feat/daemon-02c-3-supervisor
May 23, 2026
Merged

feat(daemon): cross-platform supervisor + bicameral-mcp daemon CLI (Phase 2c-3)#508
jinhongkuan merged 1 commit into
devfrom
feat/daemon-02c-3-supervisor

Conversation

@jinhongkuan

Copy link
Copy Markdown
Contributor

Summary

Fifth in the daemon-as-process arc (plan) — and the first PR where the daemon actually runs as a separate OS-level process. Through 2c-2d the protocol surface was real but the Supervisor only managed the Runtime in-process. This PR adds the subprocess spawn, signal handling, descriptor lifecycle, and the CLI plumbing so MCP can launch a real detached daemon.

End-to-end works locally (verified manually):

$ python server.py daemon start --socket /tmp/bm.sock --descriptor /tmp/bm.json
{"status": "started", "pid": 16051, "socket_path": "/tmp/bm.sock"}

$ python server.py daemon status --descriptor /tmp/bm.json
{"status": "running", "pid": 16051, "socket_path": "/tmp/bm.sock", ...}

$ python server.py daemon stop --descriptor /tmp/bm.json
{"status": "stopped"}

What ships

File Purpose
daemon/__main__.py python -m daemon serve entry — bootstraps asyncio + MCP adapters, installs SIGTERM/SIGINT handlers, graceful shutdown
daemon/process.py spawn() / stop() / status() / is_alive() — cross-platform via Popen with platform-conditional detach flags
cli/daemon_cli.py bicameral-mcp daemon argparse: start / stop / restart / status. Idempotent at the CLI layer
server.py Wires the daemon subparser + dispatch into the existing CLI
tests/test_daemon_lifecycle.py 9 sociable tests. Each spawns a real python -m daemon serve subprocess against a tmp_path socket + descriptor

Cross-platform model

Same observable behavior on macOS / Linux / Windows; only the OS-detach flags differ:

if sys.platform == "win32":
    creationflags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
    proc = subprocess.Popen(argv, creationflags=creationflags, ...)
else:
    proc = subprocess.Popen(argv, start_new_session=True, ...)

stop uses SIGTERM on POSIX, CTRL_BREAK_EVENT on Windows (matches the new process group). 10s grace, then SIGKILL escalation.

Test strategy

Per Fowler's 2026-05-22 advisory:

  • Each boundary test gets its own logical daemon — own socket, own descriptor, own tmp_path.
  • Subprocess-per-test for now — ~8s per test, 9 tests, 72s total. Measured, not speculated.
  • Per-test fixture hides lifecycle — when this batch starts costing measurable CI time, swap the fixture body for an ObjectPool of pre-warmed daemons. Tests don't change.

Out of scope (separate PRs)

  • macOS LaunchAgent auto-start install → 2c-3b
  • Linux systemd-user auto-start → 2c-3c (when demand arrives)
  • Windows Service install → 2c-3d (when demand arrives)

Until the platform-specific auto-start PRs land: daemon start survives until reboot or explicit daemon stop. For hosted-mode customers (the funnel-stage-1 focus) the hosting platform IS the supervisor and auto-start is irrelevant.

Test plan

  • pytest tests/test_daemon_lifecycle.py — 9 passing (72s)
  • Full protocol + daemon suite — 46 passing (no regressions vs dev)
  • ruff format --check . && ruff check . clean
  • mypy daemon/ cli/daemon_cli.py clean
  • Manual end-to-end verified: spawn → status → stop loop
  • CI green on dev

Known limitation

Bicameral preflight unavailable this session (MCP server disconnected mid-session). Will validate against the ledger before 2c-4.

Plan refs

🤖 Generated with Claude Code

…hase 2c-3)

Fifth in the daemon-as-process arc — and the first PR where the daemon
actually runs as a separate OS-level process. Through 2c-2d the protocol
surface was real but the supervisor only managed the Runtime in-process.
This PR adds the subprocess spawn + signal handling + descriptor lifecycle
so MCP can launch a real detached daemon.

What ships:

- daemon/__main__.py — ``python -m daemon serve`` entry point. Bootstraps
  asyncio + the MCP adapter shells (already from Phase 2b), installs
  SIGTERM/SIGINT handlers, runs the loop until stopped, gracefully
  shuts down the Supervisor.

- daemon/process.py — cross-platform spawn/stop/status helpers:
    spawn()    Popen with platform-conditional detach flags. POSIX uses
               start_new_session=True; Windows uses DETACHED_PROCESS |
               CREATE_NEW_PROCESS_GROUP. Same observable behavior on
               every OS: detached child, parent exits, descriptor at
               ~/.bicameral/daemon.json names the PID + socket.
    stop()     SIGTERM → 10s grace → SIGKILL escalation. Idempotent
               cleanup of stale descriptors (daemon crashed without
               removing its file).
    status()   {"status": "running" | "stopped" | "stale", pid, socket}.
               "stale" = descriptor exists but PID is dead.
    is_alive() os.kill(pid, 0) probe (POSIX semantics; Windows treats
               PermissionError as alive).

- cli/daemon_cli.py — argparse subcommands for ``bicameral-mcp daemon``:
  start / stop / restart / status. Idempotent at the CLI layer — already-
  running on start exits 0 with a warning; already-stopped on stop exits 0.

- server.py — wires the ``daemon`` subparser into the existing CLI.

- tests/test_daemon_lifecycle.py — 9 sociable tests. Each spawns a real
  ``python -m daemon serve`` subprocess against a tmp_path socket +
  descriptor, exercises the wire boundary, then tears down. ~72s total
  (~8s per test for the spawn). No mocks; the seam is the per-test temp
  dir. Per Fowler's "measure before optimizing" — when this batch starts
  costing measurable CI time, swap the fixture body for a pooled-warmed-
  daemon implementation without touching the tests.

Out of scope (each gets its own focused PR):

- macOS LaunchAgent auto-start install (2c-3b)
- Linux systemd-user auto-start (2c-3c, when demand arrives)
- Windows Service install (2c-3d, when demand arrives)

Bicameral preflight unavailable this session (MCP server disconnected
mid-session); will validate against the ledger before 2c-4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bf1040a-6e09-4c9b-9078-7020a3452e6f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/daemon-02c-3-supervisor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jinhongkuan jinhongkuan merged commit 38ee264 into dev May 23, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant