feat: detect and reap stale locks (lock_heartbeat, lock_reap) - #3
Open
warnes wants to merge 2 commits into
Open
Conversation
Not upstream-bound — this fork uses a worktree-per-PR workflow that the original repo doesn't need. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds dead-agent detection: lock_query/lock_check_conflict now compute a stale flag (and staleForSeconds) on every active lock, and two new tools let an agent act on it explicitly: - lock_heartbeat: bumps only a lock's `updated` timestamp, for long stretches of work that aren't naturally hitting lock_update often enough to keep the lock from reading as abandoned. - lock_reap: finishes (same mechanism as lock_finish) every currently- stale active lock, or one specific lock_id. Refuses (LockNotStaleError) to reap a lock_id that isn't actually stale, so it can't be used to force-finish someone else's live work. Supports dry_run. Staleness is a pure function of `now` and each lock's existing `updated` field (no new frontmatter, no separate heartbeat/pid field) — computed fresh on every read, never mutated as a side effect of reading. Threshold resolves from an explicit stale_minutes argument, then AGENT_LOCKS_STALE_MINUTES (read fresh every call, never cached, matching resolveLocksRoot's own philosophy), then a 60-minute default. Deliberately reuses `updated` rather than adding a PID-liveness signal: a PID check means something different across this tool's two interfaces (an MCP server subprocess plausibly represents "is this session still connected" for its lifetime; a CLI invocation's process exits within milliseconds of creating a lock, by design, while the claimed work may continue for hours) — trusting PID liveness would flag every CLI-created lock as abandoned almost immediately. A pure timestamp-since-activity check means the same thing regardless of which interface touched the lock. README's new "Staleness detection" section has the full reasoning, including a note on what a PID-aware follow-up could look like. 30 new/updated tests (10 new in the reused timestamp.test.ts for parseTimestamp, 20 new in staleness.test.ts, 1 new e2e round trip, plus the tool-count assertion updated from 5 to 7) — all pass, typecheck clean. README documents both new tools and the staleness design. This branch does not touch the CLI (src/cli.ts doesn't exist here — it branched before #2 merged); exposing lock_heartbeat/lock_reap as `agent-locks heartbeat`/`agent-locks reap` is a natural small follow-up once both this and #2 have landed, kept separate here to keep each PR independently reviewable regardless of merge order. Pre-existing note: `pnpm test` on macOS will show 1 unrelated failure in git.test.ts (same as #2 — not touched by this change, already fixed in #1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Jul 30, 2026
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.
Adds dead-agent detection:
lock_query/lock_check_conflictnow compute astaleflag (andstaleForSeconds) on every active lock, and two new tools let an agent act on it explicitly:lock_heartbeat— bumps only a lock'supdatedtimestamp, for long stretches of work that aren't naturally hittinglock_updateoften enough to keep the lock from reading as abandoned.lock_reap— finishes (same mechanism aslock_finish) every currently-stale active lock, or one specificlock_id. Refuses (LockNotStaleError) to reap alock_idthat isn't actually stale, so it can't be used to force-finish someone else's live work. Supportsdry_run.Staleness is a pure function of
nowand each lock's existingupdatedfield — no new frontmatter, no separate heartbeat/pid field. Computed fresh on every read, never mutated as a side effect of reading (same principle as the rest of this project — see "No database, no in-memory cache" in the README). Threshold resolves from an explicitstale_minutesargument, thenAGENT_LOCKS_STALE_MINUTES(read fresh every call, never cached, matchingresolveLocksRoot's own philosophy), then a 60-minute default.Deliberately does not use a PID-liveness signal, even though that was the original design idea I started from: a PID check means something different across this tool's two interfaces. An MCP server subprocess plausibly represents "is this session still connected" for its whole lifetime, but a CLI invocation's process exits within milliseconds of creating a lock, by design, while the claimed work may continue for hours — trusting PID liveness would flag every CLI-created lock as abandoned almost immediately. A pure timestamp-since-last-activity check means the same thing regardless of which interface touched the lock. README's new "Staleness detection" section has the full reasoning, plus a note on what a PID-aware follow-up could look like for the long-running-MCP-server case specifically.
30 new/updated tests (10 new in
timestamp.test.tsfor the newparseTimestamp— the inverse offormatTimestamp, needed for staleness math — 20 new instaleness.test.ts, 1 new e2e round trip, plus the tool-count assertion updated from 5 to 7). All pass, typecheck clean.One real bug I hit and fixed in my own tests, worth flagging: lock timestamps have whole-second precision (see #4 — filed separately, since it's a pre-existing design property, not something this PR introduces), so a naive short-sleep test is flaky — truncation alone can show up to ~1000ms of apparent staleness with zero real inactivity. Tests now sleep past a full second and use thresholds calibrated to sit comfortably above that noise floor;
staleness.test.tsdocuments the reasoning inline.This branch doesn't touch the CLI —
src/cli.tsdoesn't exist here since it branched before #2 merged. Exposingagent-locks heartbeat/agent-locks reapis a natural small follow-up once both land; kept separate here so each PR stays independently reviewable regardless of merge order.Pre-existing note:
pnpm teston macOS will show 1 unrelated failure ingit.test.ts(a symlink-canonicalization mismatch inresolveLocksRoot, not touched by this change) — already has a fix up in #1.