Repository navigation
[AI-818] feat(daemon): per-user service supervisor (install/uninstall/start/stop) - #137
Conversation
Per-user OS service integration so the daemon auto-restarts after an external SIGKILL (jetsam/OOM/crash) — the root cause traced on 2026-06-10: exit 137 with zero death-rattle output = uncatchable kill, reproduced under detached tmux during a memory-pressure jetsam storm. launchd LaunchAgent (macOS), systemd --user unit (Linux), Task Scheduler logon task (Windows), behind an IServiceManager seam with pure unit-text generators for testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revise the daemon-service-supervisor spec for six review findings: - systemd: per-instance kcap-daemon-<id>.service files instead of one shared @.service template (per-instance args + safe single-unit uninstall) - pin profile via KCAP_PROFILE + capture PATH/KCAP_* into the unit instead of baking --server-url (which nulls profile resolution and drops claude/codex paths; supervised jobs don't inherit interactive PATH) - sanitize the daemon name into a portable service id and XML/systemd-escape every interpolated value in generated unit text - add ListInstalled()/ServiceStatus.BinaryPath so installed-but-stopped services show in status and doctor can validate the baked binary path - define idempotent install (replace existing unit, end running) - add ServiceManagerFactory.ForPlatform so all generators are testable on one OS Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task Scheduler's Exec action has no Environment element (only Command/ Arguments/WorkingDirectory), so the captured PATH/KCAP_* can't be set in the task XML. Generate a daemon-service-<id>.cmd wrapper that `set`s the env and execs kcap-daemon.exe; the task runs `cmd /c <wrapper>`. install rewrites it, uninstall deletes it. Add cmd batch escaping to the escaping rules. Generalize the pure generator seam from GenerateUnit (single string) to GenerateFiles -> [(path, content)] so Windows's two artifacts fit. stop fallback: if /End triggers a relaunch, disable -> /End -> re-enable so the task stays enabled for next logon instead of being left disabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 TDD tasks: contracts+escaping, three pure unit generators (launchd/systemd/windows), thin managers + process runner + factory, env capture + profile pin, `kcap daemon service` dispatch, status/stop/doctor awareness, docs, AOT verify. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review by Qodo
1. PATH capture is case-sensitive
|
The default AddSimpleConsole provider queues writes on a background thread (ConsoleLoggerProcessor) that can drop messages enqueued during runtime teardown — so a ProcessExit / signal-handler log line may never reach the terminal even though the hook fired. Add a DeathRattle() helper that writes straight to Console.Error (flushed, best-effort) from every exit hook (lifetime stopping/stopped, AppDomain unhandled/ProcessExit, unobserved task, POSIX signals), guaranteeing at least one byte lands before the logging pipeline tears down. This can't catch an external SIGKILL (jetsam/OOM) — which is what the new `kcap daemon service` supervisor addresses — but it does surface every catchable exit reason. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- P1: `kcap uninstall` now deregisters installed OS services first (ListInstalled → Uninstall) before `daemon stop --yes`. Previously the stop-deferral for service-managed daemons left launchd/systemd/Scheduled Task units registered while the config dir was deleted. - P2: systemd Environment= values and ExecStart argv are now double-quoted (with \ and " escaped) when they contain whitespace — paths/PATH entries with spaces no longer split. - P2: launchd doctor reads ProgramArguments[0] (LaunchdUnit.BinaryFromPlist) instead of the document's first <string>, which was the Label — fixes a false "binary missing" for every valid launchd service. - P2: Windows doctor reads the kcap-daemon.exe path baked inside the .cmd wrapper (WindowsTaskUnit.BinaryFromWrapper) instead of the wrapper path, so a moved daemon binary is detected even when the wrapper still exists. +4 unit tests (binary parsers, systemd space-quoting). 1321 pass, AOT clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the systemd Environment=/ExecStart quoting: the doctor reader
still used Split(' ', 2), so a quoted `ExecStart="/opt/k cap/kcap-daemon" …`
(spaced path) parsed to `"/opt/k`, false-reporting a missing binary. Add a
quote-aware SystemdUnit.BinaryFromUnit (reverses the \\ / \" escaping) and use
it in SystemdServiceManager.Status. +1 test covering the whitespace path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds
kcap daemon service install|uninstall|start|stop|statusto run the daemon under a per-user OS service so it auto-restarts after an externalSIGKILL(macOS jetsam / Linux OOM / crash) — the root cause traced on 2026-06-10 (exit 137, zero death-rattle output = uncatchable kill, reproduced under detached tmux during a memory-pressure jetsam storm; tmux doesn't shield against kernel memory reclamation).--userone-file-per-id (Linux), logon Scheduled Task +.cmdwrapper (Windows), behind anIServiceManagerseam selected byServiceManagerFactory.LaunchdUnit/SystemdUnit/WindowsTaskUnit— fully unit-tested, includingXDocument-validated escaping) from a thin shell-out manager.SIGKILLbut not on a clean stop;stopkeeps the unit installed, onlyuninstallderegisters.installpins the active profile viaKCAP_PROFILEand capturesPATH/KCAP_*into the unit (a baked--server-urlwould null out profile resolution and dropclaude/codexpaths; supervised jobs don't inherit the interactivePATH).kcap daemon status/stop/doctorare now service-aware (list installed-but-stopped services, refuse to raw-kill a supervised daemon, flag a moved binary path).help-daemon.txt+README.md(getting-started + Daemon section).Design + plan:
docs/superpowers/specs/2026-06-10-daemon-service-supervisor-design.md,docs/superpowers/plans/2026-06-10-daemon-service-supervisor.md.Test Plan
dotnet run --project test/Capacitor.Cli.Tests.Unit— 1317 pass, 0 fail (25 new tests: escaping, three generators, factory, env capture)dotnet publish -c Release— no IL2026/IL3050 warnings; NativeAOT binary runskcap daemon service statuskcap daemon service install→ reboot/relogin → daemon comes back;service stop/start/uninstallsystemctl --userschtasks /End-vs-restart-on-failure behavior on a real host (spec documents the disable→end→re-enable fallback if/Endrelaunches)🤖 Generated with Claude Code