Skip to content

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

Description

@robercano-ghbot

Evidence (2026-07-14 → 2026-07-15)

Since 07-14, six drivers spawned and only ONE got a ledger completion line (a 30s rc=1 run). The other five died ledger-less — the fingerprint of the DAEMON being killed mid-driver: the ledger write is the daemon's last act after a driver exits, so a missing line means the daemon died first and systemd's default KillMode=control-group took the whole cgroup — driver, orchestrator, implementers, reviewers — down with it. setsid does not help: cgroup membership ignores process groups.

Confirmed killers: the 07-14 09:33 CEST service restart (killed the #107 driver post-commit/pre-push → wedge, recovered manually as PR #117), overnight VM downtime 07-15 (01:18Z driver died before scoping), and a ~06:19Z daemon death (killed the #106 driver 13 min into implementation, leaving dirty-worktree debris). The 07-14 tick stream shows a restarted daemon ticking 2 minutes after a driver spawn that should have been blocking it.

Root problem: driver lifetime is coupled to daemon lifetime, and the daemon's environment (WSL2 host: sleep, reboot, wsl --shutdown, service restarts) is not always-on.

Design

  1. Spawn each driver as a transient systemd unit, not a daemon child:
    systemd-run --user --wait --collect --unit="pr-loop-driver-<issueN|prN>" \
      -p RuntimeMaxSec=$LOOP_DRIVER_TIMEOUT -- bash -c '<claude -p ...> > <out-file>'
    
    • The driver's parent becomes the user manager, so Restart=always bouncing the daemon (or the daemon crashing) no longer kills it; the daemon's systemd-run --wait is just a disposable waiter that blocks and propagates the exit code — run_driver's ledger logic is unchanged.
    • RuntimeMaxSec replaces the timeout wrapper as the hard ceiling: it is enforced by the user manager, which outlives the daemon. (Today's timeout 90m dies WITH the daemon, so an orphaned driver currently has NO ceiling — the current design is strictly worse.)
    • Keep the current setsid timeout ... spawn as the fallback when systemd-run is unavailable (legacy-cron path, non-systemd environments).
  2. Startup re-attach: before the first run_once, check systemctl --user list-units 'pr-loop-driver-*'. If a driver unit is still active, WAIT for it instead of ticking (no duplicate driver, no premature debris classification), then run Loop: driver one-shot contract needs enforcement + tests — background orchestration produced phantom-success drivers and wedged the queue #111's post-exit verification on its outcome. Census additionally must never advance an issue whose driver unit is active.
  3. Ops / halt helper: a daemon restart no longer stops drivers — document the new commands and add loop-halt.sh:
    • one driver: systemctl --user stop pr-loop-driver-issue106
    • all drivers: systemctl --user stop 'pr-loop-driver-*'
    • everything: loop-halt.sh → stops the daemon unit AND all driver units.
      Update USAGE.md's daemon + failure-contract sections accordingly (the "restart kills in-flight drivers" caveat documented there flips to "restart is driver-safe" once this lands).
  4. Tests (loop-daemon.test.sh, systemd-run stub on fixture PATH): unit naming derived from the verdict; RuntimeMaxSec passed through from LOOP_DRIVER_TIMEOUT; exit-code propagation into the ledger line; re-attach scenario (stubbed active unit → no new spawn, waits, then ledgers); fallback to setsid+timeout when the systemd-run stub is absent.

Interaction with #111 / #98

Complementary, not overlapping: this issue eliminates the restart-collateral kill class (daemon bounce no longer destroys work in flight). #111's post-exit/startup debris classifier heals what still dies — host reboot, sleep, wsl --shutdown kill everything regardless, and nothing can prevent that. The startup re-attach here is the natural place to invoke #111's verification for drivers that finished while no daemon was watching.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmodule:harnessOrchestrator machinery under .claudeplannedOwner-approved for the autonomous loop (assigned ONLY by the owner)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions