Skip to content

fix(watch): ask for a person when a turn goes silent, not for nothing - #36

Merged
pfriedrix merged 5 commits into
masterfrom
fix/status-decay-asks-for-a-person
Aug 7, 2026
Merged

fix(watch): ask for a person when a turn goes silent, not for nothing#36
pfriedrix merged 5 commits into
masterfrom
fix/status-decay-asks-for-a-person

Conversation

@pfriedrix

Copy link
Copy Markdown
Owner
  • The 15-minute backstop resolved a silent active session to idle, which is a claim only a Stop hook earns. What it actually knows is that a turn was in flight and nothing has been reported since — wedged, or blocked on something whose notification never arrived. Both want a person, so it now resolves to waiting.
  • idle still never decays, for the same reason in the other direction: a session that really did report Stop is finished, and ageing it into waiting would claim attention for every row already dealt with.

Review notes

Found on a live row: a session sat mid-turn with an unconsumed queued message, aged out of active exactly at the window, and read ○ idle — the same dim circle as the sessions that had genuinely finished. status_at - last_activity_at was active_decay_seconds + 1, which is how the decay is told apart from a real Stop.

Running sessions keep the old behaviour until their daemon is replaced (lcc open already flags this as outdated_build).

The 15-minute backstop resolved a session that had stopped reporting to
`idle`, which is a positive claim only a `Stop` hook earns: the turn
finished, come back whenever. What the backstop actually knows is that a
turn was in flight and nothing has been said about it since — the turn
wedged, or it is blocked on something whose notification never reached
the daemon. Both want someone to open the session.

Measured on a real row: a session sat at a prompt nobody was told about,
aged out of `active`, and then read as the same dim circle as every
session that had genuinely finished — so the one row on the dashboard
worth opening was the one it said to ignore.

Silence now resolves to `waiting`. `idle` still never moves, for the same
reason in the other direction: a session that really did report `Stop` is
finished, and ageing it into `waiting` would claim attention for every
row already dealt with.
4,422 comment lines across build.zig and src/, stripped by a scan that
knows where a `//` is not a comment — inside a string, inside a char
literal, or on a `\\` multiline-string line, which is where all 21 URLs
in the tree live. All of them survived; test count is unchanged at 266,
so no module dropped out of the test root's import list.

CLAUDE.md's Style section said these comments were the only rationale
record in the repo, and it was right, so it now says where a "why" goes
instead: the commit message, its own Traps section, or README. The traps
worth losing sleep over — keychain's five narrow C headers, build.zig's
signing, the test-root import list — were already written down there.

YAML and Markdown are untouched; the Zig version pin still carries its
"must match README" note in ci.yml.
The daemon matched every report against the worktree path in the payload
and took the first session it found there. A worktree is not a unique
key — `lcc open` will start a second session in one that already has
another — so with four sessions in the same worktree every hook landed on
the same row.

Measured on PE-289, four sessions deep: the row the reports landed on had
already exited, `apply` keeps `exited` terminal, so nothing moved except
its `last_event_at`, which is why a dead session carried an activity
timestamp two minutes younger than its own death. The two live agents
under it were never updated at all — frozen at the `idle` their first byte
of output set, whatever they went on to do. The dashboard then merged the
worktree onto that same first row, so it reported a corpse for a worktree
with two agents working in it, and enter on that row could not attach.

Each session now gets its own settings file, `hooks-<id>.json`, with its
id baked into every hook command line beside the event — the same reason
the event is baked in rather than parsed out of a payload. Reports name
their session and route exactly. A report with no id still falls back to
the worktree, which is what a session started by an older daemon sends,
and both that fallback and the dashboard's merge now prefer a session that
is still alive over one that has exited.
fix(watch): route a hook report to the session that sent it
chore: remove every comment from the Zig sources
@pfriedrix
pfriedrix merged commit 97fedea into master Aug 7, 2026
1 check passed
@pfriedrix
pfriedrix deleted the fix/status-decay-asks-for-a-person branch August 7, 2026 14:25
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