feat(daemon): the runtime owns its state and never the process
TH
1h ago+1275+1275+0123456789012345678901234567890123456789-550-550-012345678901234567890123456789
Summary
Part of #3747, stacked on #3763. Closes #3752.
A daemon runtime no longer assumes it owns the process.
- Process ownership:
startDaemonRuntimenever callsprocess.exitand installs noprocess.on/oncehandler.src/daemon.tsowns signals, fatal errors and the exit code. A startup refusal rejects withdetails.exitCode(daemonStartupExitCode). - Shutdown:
close()is idempotent: concurrent calls share one promise that resolves to aShutdownReport(provider releases, runners, sessions, plugins).closedsettles whoever closes (idle reap). Provider runtimes dispose exactly once; a startup that fails rolls back what it acquired.daemon stop --jsonstill reportsproviderReleases.pending. - Per-runtime state: provider admission, interactor resolution, artifact/upload tracking, resumable uploads, materialized paths, session-open locks and request cancellation, plus the Apple runner's sessions, locks, recycle ledger and owner state, live in the runtime. Accepted sockets and HTTP handlers keep the runtime explicitly (
AsyncResource.bind). The PNG worker is held by every runtime that uses it and stops with the last. - Not moved: the Apple runner host binding is a stateless function table (rebinding the same reference is a no-op).
Validation
pnpm check:affected --run --head 0b83711eb1 passed (exact commit). New tests: two runtimes in one process serve, close and keep state independently; idempotent close() and rollback; runner state per runtime; PNG worker refcount.
Live on iPhone 17e simulator (clean daemon, /private/tmp state dir): open, snapshot, close; graceful daemon stop handed the runner off (ios_runner_session_detached, runner kept alive) and a new daemon adopted it with no second xcodebuild; daemon stop --clean and device status left no claims or runners.
🤖 Generated with Claude Code