Commit Graph
1 Commits
Author SHA1 Message Date
Brennan Benson 1921ba2250 fix(agent-status): clear the pane when a Claude compact finishes (STA-2915, STA-4613) (#15202)
* fix(agent-status): clear the pane when a Claude compact finishes (STA-2915, STA-4613)

A manual /compact ends at an idle prompt without emitting Stop, so nothing in the
compact window could ever clear the pane. A worktree that entered the compact
`working` stayed `working` until the 30-minute stale sweep -- and the summarizer's
start-less SubagentStop kept republishing the row, resetting that clock each time.

The correlation added by #12332 was supposed to own this, but it could never run:
PreCompact and PostCompact were never added to CLAUDE_EVENTS, so they were never
registered with Claude. compactTrigger was always undefined, and the transition
guard, the ownership cache, the relay wire field and the ingest branch were all
unreachable. Five test files exercised the logic by injecting events past the
registration boundary, so the suite stayed green over code that could not execute.

Register PostCompact -- and deliberately NOT PreCompact. Measured on Claude Code
2.1.227, a successful manual compact emits PreCompact, a start-less SubagentStop,
SessionStart(source=compact), then PostCompact; an ABORTED compact ("Not enough
messages to compact") emits PreCompact ALONE. Mapping PreCompact to `working`
would strand the pane on every aborted compact, which is the bug being fixed, so
the abort guard is structural: Orca never subscribes to the pre-validation event.

PostCompact carries its own trigger, so no anchor is needed to tell manual from
auto and the correlation machinery is deleted rather than repaired. Manual becomes
a `done` with sessionBoundary set -- a finished compact is a session-shaped
boundary, not a completed turn, so completion notifications, unread counts and
automation-run evidence stay out of it. Auto claims nothing: it runs inside a turn
that resumes and emits its own Stop.

The source-blind early return that dropped compact events for EVERY provider
before its normalizer ran is narrowed to Claude, so it keeps failing closed on a
malformed payload without pre-empting other providers.

Ownership is kept where the deleted guard had it: a valid provider prompt id is
required, a completion clears a row but never creates one (a retired pane must not
be resurrected), and a hydrated row is matched on provider session only -- it
carries the previous session's connectionId, and older rows carry no session at
all, so a strict check would reject the restart case this fixes. A consumed
prompt id keeps relay duplicates from refreshing the row.

Mixed versions: no new wire field and no new opcode. An older relay normalizes
with its own shipped mapping and forwards the event, so ingest drops `auto`
envelopes and stamps the boundary on `manual` ones; its replay strips the trigger
entirely, so payload state stands in for it while ownership is still enforced. The
relay now caches a completion with its compact identity removed, so a client that
was offline during the compact still receives the clearing row on reconnect.

Tests go red before this change and green after: 6 of 12 in the new
registration-gated suite and 5 of 8 in the relay/ingest suite. The harness delivers
only events present in CLAUDE_EVENTS, so a fix that is never registered cannot
pass -- the failure mode that let the original correlation ship unreachable.

* test(agent-status): restate the compact reliability gate around the new invariant

The gate pinned a test file this change deletes, so the manifest check failed.
Repointing the path alone would have left the gate describing an invariant that
no longer exists: it required a manual PostCompact to match its exact PreCompact
generation, and PreCompact is no longer consumed at all.

Restate it. The invariant is now that PreCompact never moves a pane, that only a
manual PostCompact marks done and does so as a session boundary, that a
completion clears an existing row but never creates one, and that a relay
predating the contract has its automatic envelopes dropped and its trigger-
stripped replays classified by payload state under the same ownership checks.

Evidence runs are the real ones: the 105-test suite from this branch, and the
Claude Code 2.1.227 PTY capture that measured PreCompact arriving alone on an
aborted compact.

* fix(agent-status): clear the restart-stuck pane a compact was meant to clear

Review found the completion did not clear the pane STA-2915 actually reports, and
that republishing it was a strict regression.

- A manual completion now retires a subagent that exists only as a disk snapshot:
  a /compact only completes at an idle prompt, so a restored child is proof of
  nothing. Live evidence -- a child observed in this runtime, an unclassifiable
  running background task, a registered session cron -- still holds the pane.
- A completion that cannot clear now publishes nothing instead of restating the
  row, which was stripping restoredUnconfirmed off a hydrated row and restarting
  the staleness clock for work the compact never observed.
- The relay defers compact ownership to the client that owns pane identity, so a
  cold relay cache can no longer swallow the one event that clears a remote pane.
- claudeConsumedCompactPromptIdByPaneKey joins all three pane-scoped teardown
  routes, and an auto compact no longer spends the pane's consumed-compact slot.
- The promptless completion keeps the summarized turn's label with or without a
  trigger on the envelope.

Tests: the two restart cases now deliver the completion while the hydrated row is
still cached, so they exercise the restored-row branch instead of passing through
the strict one; the triggerless working replay is asserted from a FINISHED pane so
it can fail. Reverting the four source files turns 12 of 21 registration-gated and
8 of 12 relay/ingest tests red, and 18 of 18 targeted mutations are caught.

* fix(agent-hooks): preserve compact identity across relay replay

* docs(reliability): describe compact replay ownership
2026-08-24 13:06:44 -07:00