Skip to content

feat: turn-span activity model, install modes, and daemon self-upgrade#1

Merged
tsilenzio merged 11 commits into
mainfrom
feat/turn-span-model
Jul 15, 2026
Merged

feat: turn-span activity model, install modes, and daemon self-upgrade#1
tsilenzio merged 11 commits into
mainfrom
feat/turn-span-model

Conversation

@tsilenzio

Copy link
Copy Markdown
Owner

Summary

  • Turn-span activity model (ADR-0013): a session is held from UserPromptSubmit until stop/death/interrupt/awaiting-input, replacing the 120s/300s staleness timeouts that released mid-turn during long hook-free gaps. Adds Notification as 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.
  • Battery guards retained as a veto over the hold decision: the floor releases an active turn on battery, and hold_since now accrues battery time only, so an AC hold no longer consumes the max-hold budget before an unplug.
  • Install modes (ADR-0014): detects cargo and Homebrew installs and wires hooks at their native stable paths, replaces the own-copy binary via atomic rename (in-place overwrite invalidated the Mach-O signature), and the daemon self-upgrades by watching its binary's (device, inode) and handing off to the new binary.
  • Reactive .disabled sentinel 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
  • Real-use validation on this branch surfaced and fixed two issues (idle_prompt overnight hold, battery max-hold counting AC time), both covered by unit tests
  • CI (macOS check/test/clippy/fmt, conventions)

tsilenzio added 11 commits July 13, 2026 13:38
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.
@tsilenzio
tsilenzio merged commit 2fda072 into main Jul 15, 2026
7 checks passed
@tsilenzio
tsilenzio deleted the feat/turn-span-model branch July 15, 2026 02:01
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.

1 participant