Skip to content

feat(loop): decouple driver lifetime from daemon — transient systemd unit per driver, re-attach, halt helper (#119) - #122

Merged
robercano-ghbot merged 2 commits into
mainfrom
feat/issue-119-driver-lifetime-decouple
Jul 15, 2026
Merged

feat(loop): decouple driver lifetime from daemon — transient systemd unit per driver, re-attach, halt helper (#119)#122
robercano-ghbot merged 2 commits into
mainfrom
feat/issue-119-driver-lifetime-decouple

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

Closes #119.

What & why

Driver lifetime was coupled to the daemon: a daemon restart/crash tore down the whole cgroup (driver + orchestrator + agents), and setsid/timeout 90m died with it — an orphaned driver had no ceiling. This decouples the two.

Changes

  • Transient systemd unit per driver (loop-daemon.sh run_driver): spawns via systemd-run --user --wait --collect --unit=pr-loop-driver-<issueN|prN> with RuntimeMaxSec from LOOP_DRIVER_TIMEOUT as the hard ceiling (enforced by the user manager, which outlives the daemon). --wait is a disposable waiter that propagates the exit code; the existing ledger + issue Loop: driver one-shot contract needs enforcement + tests — background orchestration produced phantom-success drivers and wedged the queue #111 post-exit verify logic is reused unchanged.
  • Graceful fallback: a start-marker file distinguishes a real driver run from a systemd-run spawn/connect failure (e.g. no --user bus on the legacy-cron path); on failure run_driver falls through to the original setsid timeout spawn, running the driver exactly once. systemctl --user reset-failed guards against stale same-named failed units.
  • systemd-path timeout normalization: a RuntimeMaxSec kill relays 143, so after --wait the unit's Result=timeout is read back and rc normalized to 124 — systemd and fallback paths converge on identical ledger/verify handling.
  • Startup re-attach (main()): before the first tick, waits for any still-active pr-loop-driver-* unit instead of double-spawning, then runs Loop: driver one-shot contract needs enforcement + tests — background orchestration produced phantom-success drivers and wedged the queue #111 post-exit verification on its outcome.
  • Census guard (loop-census.sh): never advances an issue whose driver unit is active. No-op without systemd.
  • loop-halt.sh (new ops helper): stop one driver (issueN/prN/unit), all drivers (--drivers), or everything (--all, daemon + drivers). Degrades cleanly without systemd.
  • Docs (docs/USAGE.md): daemon + failure-contract sections flipped from "restart kills in-flight drivers" to "restart is driver-safe"; documents loop-halt.sh and the systemctl --user stop commands.
  • Tests: systemd-run/systemctl stubs on a curated (systemd-free) fixture PATH — unit naming (advance + feedback), RuntimeMaxSec passthrough, exit-code propagation, systemd-path timeout normalization, spawn-failure fallback, startup re-attach, census guard (active + no-systemd), and loop-halt.sh argument/degrade paths.

Gates

build / lint / test all green (GATES_FILE=.claude/self/gates.json). Suites: loop-daemon 74/74, loop-halt 18/18, loop-census 11/11, plus self-adapter checks + fan-out smoke.

Review

Correctness (opus) + tests (sonnet) lenses both APPROVE after one reject to fix cycle (rc-normalization, reachability fallback, and test host-independence were the fixes).

Known limitation

The systemctl show -p Result timeout read-back races --collect GC; if GC wins, a killed driver ledgers result=exit rc=143 unnormalized (rare, same class as the existing wait_for_driver_unit race — documented, not a blocker).

Complementary to #111 (this eliminates the restart-collateral kill class; #111 heals what a host reboot/sleep/wsl --shutdown still kills).

🤖 Generated with Claude Code

robercano and others added 2 commits July 15, 2026 19:40
…unit per driver (#119)

Spawns each PR-loop driver as a transient `systemd --user` unit
(pr-loop-driver-issue<N>/pr-loop-driver-pr<N>) via `systemd-run --user --wait
--collect` with RuntimeMaxSec as the wall-clock ceiling, so a daemon restart
or crash (KillMode=control-group) no longer kills an in-flight driver — only
the daemon's disposable systemd-run waiter dies. Falls back to the existing
setsid+timeout spawn when systemd-run isn't on PATH. Adds startup re-attach
(wait for an already-active driver unit instead of ticking, then run the
existing post-exit verify + ledger path), a census guard against advancing
an issue whose driver unit is active, and loop-halt.sh for stopping one/all
drivers or everything by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…est coverage (#119)

Post-review fixes for the driver-lifetime-decouple change:
- systemd-run --wait relays rc=143 for a RuntimeMaxSec kill (not GNU
  timeout's 124/137); run_driver now normalizes it via the unit's own
  Result property so timeout classification/verify-skip converge with
  the fallback path.
- run_driver now detects a systemd-run spawn/connect failure (via a
  start-marker the wrapped command touches first) and falls through to
  the setsid+timeout fallback instead of silently ledgering a phantom
  spawn-error.
- reset-failed guard before every systemd-run spawn to avoid colliding
  with a stale failed unit under the same deterministic name.
- loop-halt.test.sh scenario 2 and new loop-census.test.sh scenarios use
  a curated PATH (excluding systemctl) so the no-systemd degrade paths
  are deterministically exercised regardless of host.
- loop-daemon.test.sh gains scenarios 14/15 covering the rc-normalization
  and spawn-failure-fallback paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@robercano-ghbot
robercano-ghbot merged commit d0b4622 into main Jul 15, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-119-driver-lifetime-decouple branch July 15, 2026 22:23
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.

Loop: decouple driver lifetime from the daemon — transient systemd unit per driver, startup re-attach, halt helper

2 participants