feat: turn-span activity model, install modes, and daemon self-upgrade#1
Merged
Conversation
Records the decision to replace the timeout-driven release with a turn-span hold (UserPromptSubmit until Stop/interrupt/death/awaiting-input), motivated by the 2026-07-13 mid-turn display-sleep incident and the verification that no incremental signal survives a long thinking block. Supersedes ADR-0006 and dissolves the commit-aware timeout of ADR-0005. Adds TODO-002 with the phase-1 work breakdown and the battery-floor invariant.
Replace the timeout-driven release with a turn-span hold: a session is active while its log exists, its process is alive, its transcript is not interrupted, its newest line is not an awaiting-input Notification, and its age is under a long safety cap. Fixes the mid-turn display sleep where a >120s tool-free gap elapsed the idle timeout and released the hold. - Wire Notification as a seventh hook event; record notification_type and treat permission_prompt/agent_needs_input/elicitation_dialog as awaiting-input, active only through a 90s grace. - Remove STANDARD_TIMEOUT, COMMIT_TIMEOUT, GC_THRESHOLD, and commit detection; the commit hold now falls out of turn-span. - Add a stateless 12h safety cap (log age) as the missed-Stop backstop, and a scan-time interrupt-marker check for a marker written before daemon start. - Extract the battery cap into pure battery_cap_latch/hold_wanted functions with the floor-vetoes-active-hold invariant locked by a unit test. - status reports working / tool-in-flight / awaiting-input.
Records three mechanisms: atomic-rename install (removes the in-place-overwrite signature corruption behind the install --force SIGKILL), inode-change daemon self-upgrade, and a reactive .disabled flag. Notes Homebrew and cargo install behavior and coverage.
copy_self wrote the new binary over the destination in place with fs::copy, which rewrites the bytes of the inode a running daemon holds and invalidates the Mach-O signature, so macOS SIGKILLs the next exec (observed exit 137 on an install --force over a live daemon). Copy to a temp file beside the destination and rename over it: rename repoints the directory entry to a new inode, leaving the running daemon's inode intact. Verified by reproducing install --force over a running daemon and exec'ing the result.
Adds the install-mode model (own-copy default, native cargo and Homebrew when detected from current_exe) and reframes the self-upgrade watch to follow the front-door symlink so brew upgrades are caught too. Flags the Homebrew end-to-end as unverified until a tap exists.
vigil now classifies how it was obtained from current_exe: a cargo binary
(${CARGO_HOME:-~/.cargo}/bin/vigil) or a Homebrew Cellar path wires the hooks at
that package-managed path with no copy and no vigil symlink, and uninstall leaves
the binary for cargo/brew. Anything else (build artifact, --dir, VIGIL_INSTALL_DIR)
keeps the own-copy behavior. The Homebrew front-door derivation is pure string
surgery and unit-tested; its end-to-end is unverified until a tap exists.
The daemon records the (device, inode) of its watch path at startup (the Homebrew front door when running from a Cellar, else current_exe) and re-checks it on the power-poll cadence. When it changes (install --force or cargo install renames a new inode, or a brew upgrade repoints the front-door symlink) it stops its caffeinate, releases the lock, and spawns a fresh daemon from the watch path, which follows a brew symlink flip to the new version. Verified end-to-end: a daemon handed off to a new binary within the poll interval after an atomic rename over its watched path.
A sentinel file in the runtime dir disables the daemon: checked at startup so a hook-spawned daemon exits at once, and each loop pass so creating the file (which wakes the existing log-dir watch) releases within a wake. The daemon clean-exits, stopping its caffeinate, so unlike a pkill it leaves no orphan. status reports the disabled state. Verified end-to-end: creating the flag exits the daemon and stops its caffeinate with no orphan, and a fresh spawn is blocked while the flag exists.
The awaiting-input release set was missing idle_prompt (the user has gone idle at the prompt) and agent_completed (the turn finished). A session ending on an idle_prompt Notification stayed active under the 12h safety cap and held the display awake overnight (2026-07-13, 20:27 to 10:44 per pmset -g log) on an abandoned session. Both now release after the grace, so an idle session lets the display sleep ~90s after the prompt. auth_success and elicitation_complete/ _response still mean work is resuming and do not release.
The 3h BATTERY_MAX_HOLD guard measured now - hold_since, where hold_since marked the start of the continuous hold across AC and battery. Turn-span makes a hold span a whole turn, so a multi-hour AC hold consumed the battery budget before an unplug: a ~14h continuous hold (mostly on AC) tripped the guard the instant the machine was unplugged, releasing at 100% charge. Clear hold_since on AC and set it on the first battery tick (extracted into next_hold_since, unit-tested), so the guard counts battery time only and each unplug starts a fresh 3h budget, matching ADR-0009's intent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
UserPromptSubmituntil stop/death/interrupt/awaiting-input, replacing the 120s/300s staleness timeouts that released mid-turn during long hook-free gaps. AddsNotificationas a seventh hook event with an awaiting-input release set (90s grace) and a 12h safety cap. Removes the commit-detection apparatus, whose hold now falls out of turn-span.hold_sincenow accrues battery time only, so an AC hold no longer consumes the max-hold budget before an unplug.(device, inode)and handing off to the new binary..disabledsentinel stands the daemon down without signals, releasing within one wake via the existing log-dir watch.Test plan
cargo fmt --check,cargo clippy --all-targets --all-features -- -D warnings,cargo test(24 tests) green locally