chore(agent-status): move STA-4119 evidence out of the repo root

The reproduction record, the plan, and the two captured last-status payloads
were committed at the repo root, which the root-directory guard rejects.
Evidence belongs on the PR; the content is preserved verbatim in a PR comment.
This commit is contained in:
Brennan Benson
2026-08-17 16:52:48 -07:00
parent bc6c53877c
commit 6465073125
4 changed files with 0 additions and 328 deletions
-90
View File
@@ -1,90 +0,0 @@
# STA-4119 plan — leftover non-agent inventory must not hold a finished lead at `working`
## Decision
A Claude lead turn that has ended (`Stop` / `StopFailure` with `lead.state === 'done'`) must publish **`done`** even when a background shell / monitor is still in `background_tasks`. Live **child agents** on the roster still hold the pane at `working`. Session crons stay on the existing cron gate (out of scope).
This is a write-path change in `resolveClaudePaneState`, not a PTY-liveness backstop. Lane STA-4612 owns dead-pane / exit-path recovery. This lane owns the live pane whose lead is over and whose leftover process inventory currently upgrades `done` back to `working`.
## Why this, not a refresh loop
`claudeRunningNonAgentTaskPaneKeys` is written from `background_tasks` on lead events (`updateClaudeRunningNonAgentTask` in `src/shared/agent-hook-listener.ts`). An idle parent never emits another inventory, so the set is a one-way latch until the next lead turn.
Two ways to “re-evaluate the gate”:
1. **Keep the latch and poll / wait for a later inventory.** That reconciles after the fact, needs a signal the idle parent does not emit, and still publishes `working` while the shell is actually alive. It fails the acceptance oracle in `STA-4119-REPRO.md` (card/tab must reach `done` **while the background shell is still alive**).
2. **Stop letting leftover non-agent inventory participate in pane `state`.** Then a finished lead cannot be represented as `working` just because a child process lives. The latch can still be recorded for interrupt inference; it must not be an input to `resolveClaudePaneState`.
(2) makes the reported state unrepresentable. (1) does not.
`UserPromptSubmit` / `PreToolUse` / other mid-turn events still report `working` before `resolveClaudePaneState` sees a done lead. The pane stays `working` for a genuine lead turn, including one that happens to have a shell. That is the regression the coordinator forbade.
## Code change (single decision, few call sites)
In `resolveClaudePaneState` (`src/shared/agent-hook-listener.ts`), when `lead.state === 'done'`, return `working` only if:
- the roster has a working child, or
- the lead is not interrupted **and** `claudeActiveSessionCronPaneKeys` has the pane.
Remove `claudeRunningNonAgentTaskPaneKeys` from that predicate.
Same leftover-shell predicate is inlined in two other places and must be aligned so a shell cannot keep looking like a confirmed working gate after the lead is done:
- SubagentStop `hasConfirmedDoneGate` (~2682). That flag only decides whether a restored-child drain is “confirmed leftover inventory” vs “unconfirmed restore.” After this change a leftover shell is no longer a confirmed *working* gate, so this check must not treat the shell set as confirmation either. Otherwise a start-less `SubagentStop` (the reporter’s last event) can still mark the pane unconfirmed. After the resolve change, the re-emit is `done` if the cached lead is `done` and no child is working.
- The restored-snapshot guard on a lead Stop (~3110) currently requires `!claudeRunningNonAgentTaskPaneKeys.has(paneKey)` before marking `claudeUnconfirmedRestoredStatusPaneKeys`. A leftover shell would then skip that mark. Align this guard with the new resolve predicate (shells do not count as confirmation). Cron can stay.
Do **not**:
- add a PTY / pid liveness join (STA-4612)
- register `SessionEnd`
- map TUI-idle to `done` while the lead turn is still `working`
- invent a `backgroundOnly` render marker (closed #14620). Acceptance is `state === 'done'` on the card/tab.
Keep writing `claudeRunningNonAgentTaskPaneKeys` on lead inventories. `server.ts` still consults it so Escape/Ctrl+C at an idle prompt does not infer “interrupted done” over a provider-owned shell. That is interrupt inference, not sidebar `state`.
`turnCompletedAt` is only stamped when a lead Stop is gated **up** to `working`. After this change a shell-only Stop publishes `done` and does not need that stamp; the working→done edge is the completion notification. Stamping remains for a Stop that is still gated by a **working child** (existing #14580 behavior, unchanged).
## Tests (red before the resolve change, green after)
Add in `src/shared/claude-background-task-status.test.ts` (or the existing Claude subagent listener file) a STA-4119 case that is **red on current main**:
1. `UserPromptSubmit` → `working`
2. Lead `Stop` with `background_tasks: [{ type: 'shell', status: 'running' }]` and no child → **`done`** (today: `working`)
3. Start-less `SubagentStop` (native unmatched child stop) → still **`done`**, last published row is not `working` (today: `SubagentStop` / `working` — the reporter’s `last-status.json`)
4. `UserPromptSubmit` again → **`working`** (lead is active; leftover shell must not be “force everything to done”)
5. `Stop` while a roster child is `working` → still **`working`** + `turnCompletedAt` (do not ignore real child work)
Update existing tests that currently require `Stop` + `RUNNING_SHELL` → `working` so they expect `done` when no child is working. Keep tests that require `Stop` + running **subagent** → `working`.
Notification tests in `agent-hook-completion-background-turn-notifications.test.ts`: the shell-only sequence should announce on the `done` Stop, not on a gated-working Stop. The subagent-gated sequence stays on `turnCompletedAt`.
No new hook opcode, no new persisted field. Publishing `done` instead of `working` for this inventory is the intended product change; mixed-version remotes already understand both states.
## Hibernation / keep-awake (accepted tradeoff)
`agent-hibernation-planner.ts` refuses any pane with `state !== 'done'`. After this change a finished lead with a live leftover shell is hibernation-eligible. That matches “the lead is idle.” Killing a long-lived `npm run dev` by sleeping the worktree is the same class of leftover-process risk the reporter already works around by putting servers in a dedicated tab. Not in scope to invent a third pane state.
## Precedent (local reference checkouts; names omitted from this file on purpose)
A peer agent-session tracker in the local reference tree uses this exact state machine for hook-driven session status:
- `userPromptSubmit` / `preToolUse` / `postToolUse` → working
- `stop` → **idle**
- `subagentStop` → **unchanged** (a child finishing does not make the parent working)
- process-exit is a backstop only for a session that has **ended**, not for leftover children of a live idle parent
That is the same split this plan uses: lead Stop is idle; leftover child processes are session inventory; dead-pane recovery is a different ticket.
I did not find a peer that treats “provider-owned background shell still running” as “the lead turn is still working.” If we kept that latch we would be the unusual one, and we would fail the acceptance oracle.
## Acceptance (from `STA-4119-REPRO.md`)
After implement, re-run the exact macOS repro:
- TUI idle, `1 shell still running`, PTY alive
- card/tab **`done`** (not `working`)
- last-status not stuck on `SubagentStop` / `working`
- control (no shell) still `Stop` / `done`
- a turn that is actually running tools still shows `working`
Do not implement until a Claude Fable review and a fresh Codex verify of this plan both return clean.
-162
View File
@@ -1,162 +0,0 @@
# STA-4119 reproduction
**Verdict: reproduced on both macOS and Windows.** The reported mechanism is real and is not a Windows-only artifact. It is also not the phantom-working bug fixed by #14375 / #15082.
The pane stays `working` after the lead turn ends because a live background shell (and a start-less `SubagentStop`) keeps the background-task / child gate up. The owning Claude PTY is still alive, sitting at an idle `❯` prompt. A hooks-independent PTY-liveness join (STA-4612 as specified) does **not** clear this case.
## Builds
| Surface | App | Contains #14375 / #15082 / #14580? | Claude Code |
| --- | --- | --- | --- |
| Local macOS Orca (authoritative "has the merged status PRs" build) | `1.4.185-adhoc.20260817215434` | Yes. Source worktree HEAD `8cd338357ee35e7348cb08d3a0522fee161b2e28` is a descendant of `d137bb93e1` (#14375), `60805f5c45` (#15082), and `5e9e38fa75` (#14580). The running adhoc binary stamps `turnCompletedAt` on the gated working row, which is the #14580 signal. | v2.1.233 |
| Windows high spec remote (`runtimeId` `70a74ef9-bd02-4f76-b08b-846e230d2dc5`) | `1.4.184` (runtime ready; desktop app not running) | No. 1.4.184 predates those merges. Confirmed in-product: the working row has **no** `turnCompletedAt`. | v2.1.234 |
| Windows low spec | Unreachable (`ok: false`) | n/a | n/a |
This lane did not cherry-pick anything. The macOS run is the "still broken after the two merged status PRs" result. The Windows run is the original-platform result on the shipped 1.4.184 the reporter's era used.
## Exact steps (what I actually ran)
Throwaway fixture, not the orca repo (no setup hooks, no sibling-worktree discovery):
1. Create `/tmp/sta-4119-bg-fixture` with `package.json` `dev` → `python3 serve.py` (sleeps 3600s). `orca repo add --path /tmp/sta-4119-bg-fixture`.
2. **macOS:** `orca worktree create --repo id:a2200db1-ead2-44b2-98ef-305f17644241 --name sta-4119-repro-macos --no-parent --agent claude --setup skip`
- handle `term_0a08d384-10a2-49ec-a5da-a6defc516cac`
- paneKey `b1a746f4-93d0-483a-9b56-841d3d38b3c5:45175c2f-d16d-4ff4-bc95-809fed804872`
3. **Windows:** `orca worktree create --environment "Windows high spec" --repo id:be84c937-38c9-426c-aa55-300e8f658a83 --name sta-4119-repro-win --no-parent --agent claude --setup skip`
- handle `term_ab5aff7b-d73e-4946-b445-dfdfc729b68f`
- paneKey `0eefb1da-3af1-40cb-ab8c-309eea05e5cb:d96df5f8-1870-42e2-b11b-a50bbc6c6985`
- path `C:/Users/neil/orca/workspaces/orca-setup-shell-repro/sta-4119-repro-win`
4. Accept Claude's workspace-trust prompt (`1. Yes, I trust this folder`).
5. Send the same lead prompt on both panes:
> Do exactly these steps, then end your turn. Do not start a subagent. Do not ask questions.
> 1. Start this long-lived process as a background/async Bash shell and do NOT wait for it to exit: `python3 -c "import time; print(\"sta-4119-bg-started\", flush=True); time.sleep(3600)"` (fallback `python` / `sleep 3600`).
> 2. Confirm the process is running in the background.
> 3. Reply with exactly `STA-4119-TURN-DONE`.
6. Wait until `orca terminal wait --for tui-idle` is satisfied and the TUI shows the final assistant line plus an idle `❯`.
7. Read `last-status.json` for that pane and `orca worktree ps` card/agent state. Re-check ~5 minutes later.
**Control (same macOS adhoc build, no background work):** worktree `sta-4119-control-macos`, prompt "Reply with exactly STA-4119-CONTROL-DONE and end your turn. Do not run any shell command."
## What I observed
### Both platforms (the reported bug)
After the lead printed `STA-4119-TURN-DONE`:
- Claude TUI sat at an idle `❯`. Footer: `1 shell` / `1 shell still running`. Status line also showed `← for agents` even though the prompt forbade a subagent.
- `orca terminal wait --for tui-idle` returned satisfied. The PTY was still running.
- Worktree card / agent row stayed `working`. Windows tab title kept the working glyph (`✳ STA-4119 background shell test`).
- Last hook Orca stored for the pane was **`SubagentStop` with `payload.state: "working"`**. No later hook arrived while the background shell lived.
- The `SubagentStop` carried a `toolAgentId` that never had a matching visible `SubagentStart` in the pane status. That matches the already-measured fact that start-less `SubagentStop` is native traffic.
Re-check at 16:37:44 local, ~5 minutes after the macOS `SubagentStop` (16:32:58) and the Windows one (`receivedAt` 1787009583714): **same last event, still `working`.** This is not a brief race.
Windows extra: Claude printed `Background command "Start long-lived background process" failed with exit code 49` and still showed `1 shell still running`. The inventory Orca consumed was still a live shell as far as Claude's own status line was concerned.
### macOS 1.4.185-adhoc (has #14375 / #15082 / #14580)
`~/Library/Application Support/orca/agent-hooks/last-status.json` pane entry (full dump: `sta-4119-macos-last-status-entry.json`):
```json
{
"hookEventName": "SubagentStop",
"toolAgentId": "ac4a0c329a0e18cd9",
"payload": {
"state": "working",
"lastAssistantMessage": "STA-4119-TURN-DONE",
"toolName": "Bash",
"turnCompletedAt": 1787009576866
},
"receivedAt": 1787009578408,
"stateStartedAt": 1787009567895
}
```
`orca worktree ps`: card `status=working`, agent `state=working`, last message `STA-4119-TURN-DONE`. Unchanged 5 minutes later.
`turnCompletedAt` is present. That is the #14580 stamp: lead `Stop` was gated back up to `working` by the background shell, and the listener recorded the turn-end time so the completion coordinator *can* announce without waiting for `done`.
### Windows 1.4.184 (original platform, pre-#14580)
`C:\Users\neil\AppData\Roaming\orca\agent-hooks\last-status.json` for pane `0eefb1da-…:d96df5f8-…` (fields: `sta-4119-windows-last-status-fields.json`; full entry on the host at `sta-4119-win-last-status.json` in that worktree):
- `hookEventName=SubagentStop`
- `state=working`
- `turnCompletedAt=None` — **not in `payloadKeys`**
- `lastAssistantMessage=STA-4119-TURN-DONE`
- `toolName=Read`
- `toolAgentId=a0dc6d9fd3ea4d8be`
- `receivedAt=1787009583714`
This is the reporter's evidence, reproduced on the reported OS, on 1.4.184.
### Control (macOS adhoc, no background shell)
```json
{
"hookEventName": "Stop",
"payload": {
"state": "done",
"lastAssistantMessage": "STA-4119-CONTROL-DONE"
}
}
```
Card `status=active`, agent `state=done`. Same build, same Claude major, no background inventory → the working→done edge happens.
## Did the completion notification fire?
| Build | Notification |
| --- | --- |
| Windows 1.4.184 | **No path for it to fire.** Last row is `working` with no `turnCompletedAt` and no later `done`. The old coordinator only announces on a working→done (or equivalent) edge. Matches the reporter ("no completion notification, no sound, no chip"). |
| macOS 1.4.185-adhoc | **Stamp present, banner not independently observed.** The gated working row carries `turnCompletedAt=1787009576866`, which is exactly the signal #14580 added so the coordinator can announce the finished turn while the pane stays `working`. I searched `~/Library/Application Support/orca/logs/main.trace.ndjson` and `daemon.log` for `agent-task-complete` / this paneKey / `STA-4119-TURN-DONE` and found no dispatch log. I did not see an OS banner from this session. I am **not** claiming the live banner fired; I am claiming the data the coordinator needs is on the row. |
#14580 (`fix(agent-status): announce Claude turn complete while background work runs`) is already on this source HEAD. #14620 (stop *rendering* the leftover gate as foreground work) was **closed unmerged**. So on current main the remaining user-visible half of STA-4119 is the spinner/card staying "Claude is working right now" while the TUI is idle.
## Mechanism (checked against this tree, not the handoff line numbers)
`resolveClaudePaneState` (`src/shared/agent-hook-listener.ts`) returns `working` when the lead is `done` if a roster child is working **or** `claudeRunningNonAgentTaskPaneKeys` / session-cron gates are set.
`updateClaudeRunningNonAgentTask` runs on lead events that carry `background_tasks`. A running `type: "shell"` is a non-agent task (`src/shared/claude-background-task-inventory.ts`). Folding the inventory into the roster happens on a **lead turn boundary** (`Stop` / `StopFailure`). An idle parent never emits another inventory.
A later start-less `SubagentStop` (native traffic; we saw one on both platforms after a prompt that forbade subagents) re-emits the cached lead as `working`. That is why `last-status.json` ends on `SubagentStop` / `working` — same as the reporter — rather than on the lead `Stop`.
This is **not** the #14375 phantom: the gate has positive evidence (Claude's own "1 shell still running", a `turnCompletedAt` stamp on the adhoc build, a live PTY). Working is what the current state machine intends given that inventory.
## STA-4612 subsumption verdict: **does not subsume this case**
STA-4612 proposes a hooks-independent backstop that is either:
1. process-exit of the owning agent, keyed by pid + start time, or
2. a read-time liveness join with "the owning session/PTY is still live"
so a *dead* pane cannot stay `working`.
**Here the pane is alive.** Claude is at an idle prompt. `orca terminal wait --for tui-idle` succeeds. The background shell is a **child of that still-live PTY**, not a replacement for it. Intersecting status with "PTY live" still yields live + `working`. Process-exit never fires because the Claude process has not exited.
STA-4612's write-up also lists "background-task gate never refreshed" for the *other* failure: a background task **ends** while the parent stays idle, so the inventory is stale and nothing clears the gate. That is a sibling, not this ticket. This ticket's reporter case is "the shell is still running, the turn is over, stop looking like foreground work / fire the finish notification." PTY liveness does not distinguish those.
A TUI-idle → `done` fallback (the reporter's first suggestion) would also fight keep-awake / hibernation, which currently treat `state !== 'done'` as live work. #14620's closed approach — keep `state=working` for those systems, but stop *drawing* it as foreground activity — is the product-shaped remaining fix if notification-on-`turnCompletedAt` is accepted as sufficient for the banner half.
## Post-fix (this worktree)
Plan: `STA-4119-PLAN.md`. Reviewed CLEAN by Claude Fable (`task_599c4da8f712`) then independently verified CLEAN by a fresh Codex worker (`task_fa35d597c48c`) before any production edit.
The reporter sequence is now a unit test (`does not hold a finished lead at working for a leftover background shell`). It was **red** on the pre-change listener (`Stop` + running shell published `working`) and is **green** after removing leftover shells from `resolveClaudePaneState` / the two sibling predicates.
147 related tests pass (`claude-background-task-status`, Claude subagent listener, background-turn notifications, last-status write/lead-boundary, hook ingest, interrupt inference, relay hook server).
Live UI re-run against this commit is **not** possible in the currently running desktop (`1.4.185-adhoc.20260817215434`): that binary does not load this worktree. The leftover `sta-4119-repro-macos` / `sta-4119-repro-win` panes will stay `working` until a build that contains this change processes a new hook (or the session is restarted on that build).
## What is left
- Merge the PR that implements `STA-4119-PLAN.md`.
- Re-run the live macOS repro on a desktop that includes this commit: card/tab must be `done` while `1 shell still running`.
- Repro worktrees left running on purpose: `sta-4119-repro-macos` (local) and `sta-4119-repro-win` (Windows high spec). Kill the background `sleep`/python if they should not keep a core parked.
## Evidence files
- `sta-4119-macos-last-status-entry.json` — full local last-status rows for repro + control
- `sta-4119-windows-last-status-fields.json` — extracted Windows last-status fields
-57
View File
@@ -1,57 +0,0 @@
{
"capturedAtLocal": "2026-08-17T16:37:44-07:00",
"localAppVersion": "1.4.185-adhoc.20260817215434",
"sourceWorktreeHead": "8cd338357ee35e7348cb08d3a0522fee161b2e28",
"reproPane": {
"paneKey": "b1a746f4-93d0-483a-9b56-841d3d38b3c5:45175c2f-d16d-4ff4-bc95-809fed804872",
"source": "claude",
"tabId": "b1a746f4-93d0-483a-9b56-841d3d38b3c5",
"worktreeId": "a2200db1-ead2-44b2-98ef-305f17644241::/Users/brennanbenson/orca/workspaces/sta-4119-bg-fixture/sta-4119-repro-macos",
"connectionId": null,
"hasExplicitPrompt": false,
"hookEventName": "SubagentStop",
"providerPromptId": "cd77def5-25b9-40b3-a721-b4a66b6c8004",
"toolAgentId": "ac4a0c329a0e18cd9",
"providerSession": {
"key": "session_id",
"id": "4598b6d9-e09d-4a1c-896a-df301d180f44",
"transcriptPath": "/Users/brennanbenson/.claude/projects/-Users-brennanbenson-orca-workspaces-sta-4119-bg-fixture-sta-4119-repro-macos/4598b6d9-e09d-4a1c-896a-df301d180f44.jsonl"
},
"payload": {
"state": "working",
"prompt": "Do exactly these steps, then end your turn. Do not start a subagent. Do not ask questions. 1. Start this long-lived process as a background/async Bash shell and do NOT wait for it to exit: python3 ",
"agentType": "claude",
"toolName": "Bash",
"toolInput": "cat /private/tmp/claude-501/-Users-brennanbenson-orca-workspaces-sta-4119-bg-fixture-sta-4119-repro-macos/4598b6d9-e09d-4a1c-896a-df301d180f44/tasks/b0icpu59e.o",
"lastAssistantMessage": "STA-4119-TURN-DONE",
"turnCompletedAt": 1787009576866
},
"receivedAt": 1787009578408,
"stateStartedAt": 1787009567895,
"launchTokenHash": "dee6f186298eb5a60bb0a854ae0a6ced88cebfa4dc3988bd5dc48d84cce6896c"
},
"controlPane": {
"paneKey": "032f97d0-4e98-4833-a2c9-b360e1ecd162:92bc0766-7177-4cd6-89dd-a45130e36e70",
"source": "claude",
"tabId": "032f97d0-4e98-4833-a2c9-b360e1ecd162",
"worktreeId": "a2200db1-ead2-44b2-98ef-305f17644241::/Users/brennanbenson/orca/workspaces/sta-4119-bg-fixture/sta-4119-control-macos",
"connectionId": null,
"hasExplicitPrompt": false,
"hookEventName": "Stop",
"providerPromptId": "982d1b47-9d7b-47d0-bfb3-ccfb217e850d",
"providerSession": {
"key": "session_id",
"id": "e8ddb7ac-d5ec-424f-bead-4b76ff64ce16",
"transcriptPath": "/Users/brennanbenson/.claude/projects/-Users-brennanbenson-orca-workspaces-sta-4119-bg-fixture-sta-4119-control-macos/e8ddb7ac-d5ec-424f-bead-4b76ff64ce16.jsonl"
},
"payload": {
"state": "done",
"prompt": "Reply with exactly STA-4119-CONTROL-DONE and end your turn. Do not run any shell command. Do not start a background process. Do not start a subagent.",
"agentType": "claude",
"lastAssistantMessage": "STA-4119-CONTROL-DONE"
},
"receivedAt": 1787009672621,
"stateStartedAt": 1787009672621,
"launchTokenHash": "e3a4bc871a4a496087b8f220b65468476673ab9a7d297e9b7da4f5c4a7d5956f"
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"capturedAtLocal": "2026-08-17T16:38:30-07:00",
"windowsEnvironment": "Windows high spec",
"windowsAppVersion": "1.4.184",
"windowsRuntimeId": "70a74ef9-bd02-4f76-b08b-846e230d2dc5",
"lastStatusPath": "C:\\Users\\neil\\AppData\\Roaming\\orca\\agent-hooks\\last-status.json",
"paneKey": "0eefb1da-3af1-40cb-ab8c-309eea05e5cb:d96df5f8-1870-42e2-b11b-a50bbc6c6985",
"hookEventName": "SubagentStop",
"state": "working",
"turnCompletedAt": null,
"lastAssistantMessage": "STA-4119-TURN-DONE",
"toolName": "Read",
"toolAgentId": "a0dc6d9fd3ea4d8be",
"receivedAt": 1787009583714,
"stateStartedAt": 1787009565488,
"payloadKeys": ["state", "prompt", "agentType", "toolName", "toolInput", "lastAssistantMessage"],
"subagents": null,
"note": "Fields extracted on the Windows host from last-status.json. Full entry also written on that host as C:/Users/neil/orca/workspaces/orca-setup-shell-repro/sta-4119-repro-win/sta-4119-win-last-status.json"
}