feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714) by Jinwoo-H · Pull Request #15261 · stablyai/orca · GitHub
Skip to content

feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714) - #15261

Merged
Jinwoo-H merged 7 commits into
mainfrom
next-p2-sta-4513-3714-input-wait-claude
Aug 18, 2026
Merged

Jinwoo-H merged 7 commits into
mainfrom
next-p2-sta-4513-3714-input-wait-claude

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #14854 (STA-4513) and #13209 (STA-3714).

What was wrong

A worker parked on an interactive prompt was indistinguishable from one that was thinking or inside a long tool call. Measured on origin/main @ 640e8a4, driving a real cursor-agent 2026.08.11-e8db854 through Orca on macOS:

coordinator surface control: running sleep 60 case: awaiting a command approval
worktree ps agents[].state working working
worktree ps toolName / toolInput Shell / sleep 60 && echo done Shell / curl -s … | head -3
terminal show / terminal list no such field no such field
terminal show .title ⠋ Cursor Agent ⠇ Cursor Agent
terminal wait --for tui-idle satisfied: true satisfied: true
orchestration worker-show no agent state no agent state
orchestration check mailbox only mailbox only

Byte-identical, and tui-idle actively reports the blocked lane as ready.

For a hook-capable agent the picture differs: a Claude pane on a permission prompt does report state: waiting on worktree ps. But that is worktree-scoped — a coordinator holding a dispatch id has no per-lane call that surfaces it, which is exactly what STA-3714 asked for.

Root cause, and why it is one fix

getTerminalAgentStatus already fuses hook state, OSC title, and matched prompt text into a permission verdict — the exact "blocked on a human" answer both issues want. Two things kept it from a coordinator:

  1. Exposure. The verdict is reachable only from the renderer. RuntimeTerminalSummary has no field for it, so terminal show, terminal list, and worker-show's terminal all drop it.
  2. Detection. It is blind to cursor-agent approval menus, so for Cursor there was no verdict to expose in the first place.

Same boundary, so one change rather than two overlapping ones.

Exposure

OrcaRuntimeService.getTerminalInteractiveWait(handle) publishes that same fusion minus the async foreground probe, and it lands as agentWait on:

  • terminal show (--json, and a text line above the preview)
  • orchestration worker-show → observation.agentWait, plus a Waiting on a human: … line in text output
  • the federated equivalents (federationShow), so a remote lane reports the same thing
"observation": {
  "status": "live",
  "exactWorker": true,
  "agentWait": { "source": "prompt-text", "reason": "agent-approval-prompt", "since": 1787035218627 }
}

source names the evidence — hook, prompt-text, or title — so a coordinator can weigh a hook-reported wait differently from a text match. Three properties are deliberate:

  • Never inferred from silence. Every branch requires positive evidence.
  • null means no proof of a wait; a missing field means the host predates it. Absence is never read as "not waiting" (the CLI prints unknown (host predates the field)).
  • A waiting worker is healthy. It carries no failure semantics, no strike, no timeout.

Detection

cursor-agent's hook set — enumerated from the shipped binary — is beforeSubmitPrompt, sessionStart, preToolUse, postToolUse, postToolUseFailure, beforeShellExecution, beforeMCPExecution, afterAgentResponse, stop, sessionEnd. There is no approval event, and beforeShellExecution fires identically for auto-allowed commands, which is why normalizeCursorEvent maps it to working. The rendered menu is the only authority left, so this is the narrow case where text is the boundary rather than a shortcut past one.

It matches the menu's key-bound choices rather than its prose (run (once), to allowlist?, run everything, skip & tell the agent), requires at least two of them, and clears itself when cursor's follow-up input line reappears after the menu. Cursor's live spinner title is exempted from the staleness rule that clears startup modals, because cursor keeps spinning while it waits — for the startup modals that rule is untouched and pinned by a test.

Falls out of it

Routing through the shared verdict means orchestration dispatch --inject into a cursor pane on an approval now refuses with agent_prompt_blocked instead of typing the coordinator's preamble into the dialog — the STA-2631 / #10666 shape, observed live on the built runtime.

Evidence

Deterministic — src/main/runtime/terminal-interactive-wait-visibility.test.ts, 21 cases over fixtures captured verbatim from a real cursor-agent driven through Orca: approval pending, the same agent inside a long tool call, idle after answering, an answered menu in scrollback, a menu followed by any later output, a footer under a live dialog, a partial menu with no decision keys, a non-local (connectionId) pane, a startup trust screen, a live title clearing a stale startup prompt, a dialog restored from terminal history, a startup modal restored from history, a hook-reported waiting, a hook-reported working, a retained permission row after a shell takes the pane, an exited pane, and the guard refusing both assertTerminalAgentSendable and sendTerminalAgentPrompt while allowing a working lane through.

Same oracle, fix reverted — all 14 new assertions fail. The substantive one:

 refuses to call the lane idle while the approval is unanswered
-   "blockedReason": "agent-approval-prompt",
-   "satisfied": false,
+   "satisfied": true,

Live, against a built runtime (isolated serve profile, real cursor-agent, headless — so this exercises the PTY-record branch the unit tests do not):

terminal show   → agentWait: agent-approval-prompt (via prompt-text)   [approval pending]
terminal show   → agentWait: none                                      [running sleep 45]
terminal wait   → satisfied: false, blockedReason: agent-approval-prompt
worker-show     → Waiting on a human: agent-approval-prompt (via prompt-text)
worker-show     → observation.agentWait: null                          [same lane, working]

After review: replayed through real PTYs on a rebuilt runtime, the case matrix reads approval → agent-approval-prompt, long tool call → none, answered menu still in scrollback → none (a false positive before 695beae71b).

pnpm lint, pnpm run typecheck, the changed-code quality and React Doctor gates, and the src/main/runtime, src/shared, and src/cli suites are green (10,542 tests). CI is green apart from the typecheck job, which fails on src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx:51 — a file this branch does not touch, failing identically on unrelated PR #15259.

Independent review (round 1)

Two fresh gpt-5.6-luna Codex reviewers ran against the first commit with different lenses. Their confirmed findings are fixed in 695beae71b; each was reproduced before acting on it and one was declined.

Fixed — stale approval menus read as live waits (correctness). The detector trusted a single dismissal string, so any later output that did not contain cursor's follow-up line left an answered menu reading as a live wait. Reproduced over a real PTY: the real menu plus two lines of ordinary output returned agent-approval-prompt, which fails tui-idle and refuses prompt injection on a healthy lane. Replaced with the structural property the string was standing in for — a live dialog owns the bottom of the screen, so the last choice may sit at most one line above the end of the tail. That tolerates a footer or a mid-redraw partial line, drops the vendor prose, and also subsumes a second reviewer finding about the follow-up heuristic misfiring.

Fixed — a restored lane went quiet for good. Being bottom-of-screen is also the dating this reason needed, so it no longer requires waitBlockedAt. A tail restored from terminal history carries none, and a lane parked on a prompt emits no bytes — so an Orca restart silenced exactly the lane both issues are about. The startup modals keep the timestamp rule: their text lingers in scrollback with nothing to say whether it was answered. Both behaviors are now pinned by tests.

Fixed — the tests did not test the shipping code. The worker-show test mocked getTerminalInteractiveWait, so it would have passed with detection permanently returning null. It now drives a real runtime, a real PTY tail, and the real detector, and asserts that observation.agentWait equals the terminal payload it is derived from. The guard claim is likewise asserted against the guard: a blocked pane now rejects both assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane still passes.

Fixed — duplicate tail scans. worker-show and federationShow reuse the verdict showTerminal already computed instead of rescanning (measured at ~0.11 ms per scan over a full 256 KiB tail), so the two can no longer disagree either.

Declined. A reviewer read the missing SSH/WSL/Windows coverage as a gap; the verdict is synchronous and derived from retained tail and title state with no shell, path, or subprocess work, so a non-local pane case was added rather than a platform matrix. The same reviewer confirmed no old-peer, old-client, or platform break, and walked every blockedReason consumer.

Independent review (round 2)

Two more gpt-5.6-luna reviewers looked at the design and at whether the issues are even still valid. Both independently confirmed the premise on origin/main and that neither issue should be closed as stale, with the same nuance: worktree ps --json already carries hook-derived agents[].state, so STA-3714's "zero signal" wording is too broad — but the dispatch-scoped surface it actually asks for has nothing, and worktree ps's own text formatter hides those rows.

Fixed — a hook wait could outlive its agent (correctness). The hook branch proved agent ownership from the pane title alone, while getTerminalAgentStatus, the verdict it claimed to reuse, also probes the foreground process. A shell that takes a pane back usually sets something like user@host: ~/repo, which no title rule recognizes, and a hook row stays fresh for AGENT_STATUS_STALE_AFTER_MS — half an hour of reporting a dead agent as waiting on a human. Hook evidence now goes through that shared verdict; the two prompt branches skip it because a matched prompt is on the pane's screen now and proves itself. The regression test fails against the previous commit.

Fixed — no coverage for a dead pane. The menu sits at the bottom of an exited pane's tail forever, and a worker whose process is gone needs intervention, not an answer. The snapshot already refused an exited pane and worker-show already gates on proven identity; both are now pinned.

Fixed — comments over the repo's concise-comment rule. Trimmed.

Corrected, not declined — my own claim about STA-4694. The first version of this description said STA-4694 owns the aggregate. Reading it, that is wrong: it lists "not PTY-output inference as a fallback" under Not asked for, so it covers only the hook-derived half and deliberately excludes exactly the Cursor evidence this PR adds. Nobody owned "one call, every lane, whatever the evidence", so STA-4740 now does, related to STA-4694. The two reviewers disagreed here and the ticket text settled it.

Noted, not fixed. The hook branch's probe means a fleet of per-lane worker-show calls costs one foreground read each — that is the price of not lying for half an hour, and it is why the prompt branches avoid it. And an agent that exits without its shell emitting a single byte would keep a menu at the bottom of a live PTY; shells print a prompt, so this stayed a note rather than a heuristic.

Independent review (rounds 4 and 5)

Round 4 found three P1s, all mine. The Cursor menu matcher used an independent lastIndexOf per choice marker, so text outside the menu could carry the anchor — the agent narrating "next time I'll suggest Run Everything" after a menu was answered revived it. The match is now confined to the last lines of the tail, and a choice is a line ending in the key that picks it. The one line of slack under the dialog went with it: it was a guess, every capture of a live dialog ends on its last choice, and one line is exactly enough room for that narration. Routing hook evidence through getTerminalAgentStatus had also put an unbounded foreground probe on showTerminal, a path that never probed before — a wedged remote host stalled every caller. It is bounded now, and a timeout leaves the wait unevaluated. And the null-vs-unknown defect above existed one level deeper: getTerminalInteractiveWait itself turned an unreadable pane into null, so showTerminal published "looked, nobody waiting" for a pane it could not read.

Round 5 found no correctness defects in shipped behaviour. Two robustness holes: the bounded probe abandoned the wait but not the request, so a wedged host accrued one live probe per poll (single-flighted per PTY now, matching the existing leaf-absence probe); and the trailing-key rule was written as a character class, so any lowercase run satisfied it and (as before) passed — spelled out as key names, which also admits the glyph forms. The contract wording was corrected everywhere it appeared: an absent agentWait also covers an unreadable pane and a probe that did not answer, not just an old host, and docs/reference/remote-wire-compatibility.md now carries an entry for a field whose absent and null states mean different things.

Two round-4 findings were rejected as stale diffs — origin/main moved mid-review, so git diff origin/main..HEAD showed main's own commits inverted.

Scope left out, on purpose

  • terminal list and terminal read keep no agentWait. Each row would need its own full-tail scan, which is real cost on a list call; the per-lane surfaces are where a coordinator asks about one worker. The aggregate is STA-4740, filed off this review because STA-4694 covers only the hook-derived half.
  • Three other vendors have the same blind spot and are not fixed here. Auditing every per-vendor normalizer for whether it can emit blocked/waiting: gemini, amp, and command-code all map every hook event to working or done, exactly like cursor did. Gemini's Allow execution / Do you want to proceed? dialogs match no existing rule either. Every rule in this PR was validated against bytes captured from a real agent driven through Orca and replayed through a real PTY; of those three only Gemini is installable here, so adding unvalidated rules would be the brittleness this PR is trying to avoid. Filed as STA-4778.
  • The codex- prefix on the existing blocked reasons is a misnomer — those prompts are matched by shape and already fire for Claude and Cursor (a Claude trust screen reports codex-trust-workspace). Renaming them would break paired hosts mid-skew, so the new reason is vendor-neutral (agent-approval-prompt) and the union carries a comment explaining the legacy names.

Wire compatibility

Rule 1 throughout: new optional fields on existing RPC results, and a new member on an already-optional blockedReason string. No consumer switches exhaustively on it. A new client against an old host sees the field absent and reports it as unknown rather than as "not waiting".

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: df1a0cc6-6383-4083-bcbb-36b104c58869

📥 Commits

Reviewing files that changed from the base of the PR and between d6ca1c7 and eed18e4.

📒 Files selected for processing (1)
  • src/main/runtime/orca-runtime.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/runtime/orca-runtime.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The runtime detects interactive waits from prompt text, live titles, hook status, lifecycle data, and Cursor approval menus. Terminal state and worker observations expose agentWait metadata with a reason and evidence source. Federation responses propagate the field across live, exited, and unverifiable attachments. CLI output displays active waits and terminal wait status. Tests and fixtures cover approval prompts, tool calls, idle sessions, trust prompts, hook states, stale evidence, and unavailable terminals.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the problem, solution, scope, linked issues, testing, compatibility, and review findings, but omits required template sections and visual proof. Add the required ELI5, Visual Proof or N/A, checklist, review, platform-impact, and other missing template sections.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: reporting workers blocked on human prompts through orchestration.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6aa730de-368b-49c3-bb33-d9c516b2593d

📥 Commits

Reviewing files that changed from the base of the PR and between 545b3fb and 53224a0.

📒 Files selected for processing (13)
  • src/cli/handlers/orchestration.ts
  • src/cli/specs/orchestration-worker-specs.ts
  • src/cli/terminal-format.ts
  • src/main/runtime/__fixtures__/cursor-agent-approval-prompt.txt
  • src/main/runtime/__fixtures__/cursor-agent-idle-after-approval.txt
  • src/main/runtime/__fixtures__/cursor-agent-long-tool-call.txt
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-interactive-wait.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/terminal-interactive-wait-visibility.test.ts
  • src/shared/runtime-types.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 2 remain after this review.

Comment thread src/main/runtime/orca-runtime.ts
Comment thread src/main/runtime/rpc/methods/orchestration-federation-control.ts Outdated
Comment thread src/main/runtime/terminal-interactive-wait-visibility.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/runtime/terminal-interactive-wait-visibility.test.ts (1)

350-361: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the ticket-coded pane title with a descriptive value.

'sta4513-claude' encodes a ticket number in test data. A descriptive title keeps the fixture readable when the ticket is closed.

♻️ Proposed rename
-      paneTitle: 'sta4513-claude',
+      paneTitle: 'claude',

Confirm that the wait detector does not require the title to differ from the process name before applying this change.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb562e88-8566-4d2d-85d9-7acbbc1e9120

📥 Commits

Reviewing files that changed from the base of the PR and between 53224a0 and 695beae.

📒 Files selected for processing (5)
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-interactive-wait.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/terminal-interactive-wait-visibility.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/orchestration-worker-interactive-wait.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 3 remain after this review.

…513, STA-3714)

A lane parked on an approval, trust, or permission prompt looked exactly like a
lane that was thinking or inside a long tool call. On origin/main, driving a real
cursor-agent through Orca:

  surface                        running `sleep 60`   awaiting approval
  worktree ps agents[].state     working              working
  terminal show / list           no such field        no such field
  terminal wait --for tui-idle   satisfied: true      satisfied: true
  worker-show                    no agent state       no agent state

The runtime already fuses hook state, OSC title, and matched prompt text into a
`permission` verdict inside getTerminalAgentStatus — it was reachable only from the
renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary.

Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async
foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s
observation. It carries the evidence that proved the wait (hook, prompt-text, or
title) so a coordinator can weigh it. Null means no proof; a missing field means
the host predates it — absence is never read as "not waiting".

Detection: cursor-agent's hook set has no approval event and beforeShellExecution
fires identically for auto-allowed commands, so its rendered menu is the only
authority. Matched on the key-bound choices rather than the prose, requiring two,
and self-clearing when the follow-up input line returns. Its live spinner title is
exempted from the staleness rule that clears startup modals, because cursor keeps
spinning while it waits.

Falls out of routing it through the shared verdict: `dispatch --inject` into a
cursor pane on an approval now refuses with agent_prompt_blocked instead of typing
the preamble into the dialog.

Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through
Orca; the same case matrix was replayed live against a built runtime.

terminal list stays untouched: its rows would each need a full tail scan, and
STA-4694 owns the one-call-per-run aggregate.
…e screen

Independent review found the approval detector trusted one dismissal string, so
any later output that did not contain cursor's follow-up line left the menu
reading as a live wait. Reproduced: a tail of the real menu followed by two lines
of ordinary output returned agent-approval-prompt, which fails tui-idle and
refuses prompt injection on a healthy lane.

Replaced with the structural property the string was standing in for: a live
dialog owns the bottom of the screen, so the last choice may sit at most one line
above the end of the retained tail. That tolerates a status footer or a partial
line mid-redraw without admitting scrollback, and it drops the vendor prose.

Being bottom-of-screen is also the dating this reason needed, so it no longer
requires waitBlockedAt. A tail restored from terminal history carries none, and a
lane parked on a prompt emits no bytes — so before this, an Orca restart made
exactly the lane both issues are about go quiet for good. The startup modals keep
the timestamp rule: their text lingers in scrollback with nothing to say whether
it was answered.

Also from review:
- worker-show and federationShow reuse the verdict showTerminal already computed
  rather than rescanning the tail, so the two can no longer disagree.
- The worker-show test now drives a real runtime, real PTY tail, and the real
  detector; it previously mocked getTerminalInteractiveWait, so it would have
  passed with detection permanently returning null.
- The guard claim is now asserted against the guard: a blocked pane rejects both
  assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane
  still passes.
- Added a non-local (connectionId) pane case, since the verdict is derived from
  retained tail and title state on every host.
A third reviewer caught that the hook branch proved agent ownership from the pane
title alone, while the shared verdict it claimed to reuse also probes the
foreground process. A shell that takes a pane back usually sets something like
`user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh
for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on
a human for half an hour.

Hook evidence now goes through getTerminalAgentStatus, which is the only thing
that can answer whether an agent still owns this PTY. The two prompt branches skip
it: a matched prompt is on the pane's screen now, so it proves itself. That makes
the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait
async, which only showTerminal had to absorb.

Also trims the comments the same reviewer flagged as longer than the repo's rule.
A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail
forever, and that no test covered process exit with no trailing output. The
snapshot already refuses an exited pane, and worker-show gates agentWait on proven
identity — this pins both so neither can drift into reporting a worker that needs
intervention as one that needs an answer.
@Jinwoo-H
Jinwoo-H force-pushed the next-p2-sta-4513-3714-input-wait-claude branch from d6ca1c7 to eed18e4 Compare August 18, 2026 18:57
Automated review caught that the three worker paths which return before the wait
is ever evaluated — unattached, missing, and identity_changed — then had their
undefined coerced to null by the emitters. A worker whose process was replaced was
reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting"
when Orca never looked. That is the false negative this field exists to remove.

The field is now emitted only when it was evaluated, so a present null is a claim
about the pane and an absent one means nobody looked — because the host predates
the field, or the worker's identity could not be verified. The CLI and the
worker-show note say that rather than blaming an old host.

Covered on the context-only path, where the regression test fails against the
previous behavior; the supervised and federated emitters take the identical
one-line change.

Also trims the two test-file headers to one statement of purpose.
… unknowns

Fourth review round, three findings, each reproduced before acting.

Matching each choice marker with an independent lastIndexOf let text outside the
menu carry the anchor. An agent narrating "next time I'll suggest Run Everything"
after the menu was answered pulled the match down to the bottom of the screen and
revived it. The match is now confined to the last lines of the tail, and a choice
is a line that ends in the key that picks it — prose writes the same words but not
the same shape.

The one line of slack under the dialog went with it. It was a guess; every capture
of a live dialog ends on its last choice, and one line is exactly enough room for
that narration. A redraw caught mid-flight now reads as no wait until the next
poll, which is the safe way to be wrong.

The hook branch awaited a foreground probe that reaches a PTY controller which may
be a remote host, so a wedged probe stalled every caller of showTerminal — a path
that never probed before. It is bounded now, and a timeout leaves the wait
unevaluated rather than claiming there is none.

Which is the same distinction the previous commit only fixed one level up:
getTerminalInteractiveWait itself turned an unreadable pane into `null`, so
showTerminal published "looked, nobody waiting" for a pane it could not read. It
returns undefined there, showTerminal omits the key, and worker-show's text output
prints unknown rather than rendering it the same as none.
…ose keys

Fifth review round. No correctness defects in the shipped behaviour this time; two
robustness holes and the documentation of the contract.

The bounded probe abandoned the wait but not the request, so a coordinator watching
a wedged remote host added one live probe on every poll. It is single-flighted per
PTY now, the way the leaf-absence probe already is.

The trailing-key rule that separates a menu row from the agent narrating a choice
was written as a character class, and any lowercase run up to twelve characters
satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key
names instead, which also lets the glyph forms of those keys through.

The contract wording said an absent agentWait meant an old host or an unverifiable
identity. It also covers an unreadable pane and a probe that did not answer, and a
reader diagnosing an old peer from that would be wrong. Corrected on the type, the
worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no
entry for a field whose absent and null states mean different things.

Also strengthens the worker-show agreement test, which compared the terminal and
observation payloads without asserting either held the expected wait, so it passed
when both were absent.
@Jinwoo-H
Jinwoo-H merged commit 79be5b7 into main Aug 18, 2026
52 checks passed
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 21, 2026
…513, STA-3714) (stablyai#15261)

* feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714)

A lane parked on an approval, trust, or permission prompt looked exactly like a
lane that was thinking or inside a long tool call. On origin/main, driving a real
cursor-agent through Orca:

  surface                        running `sleep 60`   awaiting approval
  worktree ps agents[].state     working              working
  terminal show / list           no such field        no such field
  terminal wait --for tui-idle   satisfied: true      satisfied: true
  worker-show                    no agent state       no agent state

The runtime already fuses hook state, OSC title, and matched prompt text into a
`permission` verdict inside getTerminalAgentStatus — it was reachable only from the
renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary.

Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async
foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s
observation. It carries the evidence that proved the wait (hook, prompt-text, or
title) so a coordinator can weigh it. Null means no proof; a missing field means
the host predates it — absence is never read as "not waiting".

Detection: cursor-agent's hook set has no approval event and beforeShellExecution
fires identically for auto-allowed commands, so its rendered menu is the only
authority. Matched on the key-bound choices rather than the prose, requiring two,
and self-clearing when the follow-up input line returns. Its live spinner title is
exempted from the staleness rule that clears startup modals, because cursor keeps
spinning while it waits.

Falls out of routing it through the shared verdict: `dispatch --inject` into a
cursor pane on an approval now refuses with agent_prompt_blocked instead of typing
the preamble into the dialog.

Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through
Orca; the same case matrix was replayed live against a built runtime.

terminal list stays untouched: its rows would each need a full tail scan, and
STA-4694 owns the one-call-per-run aggregate.

* fix(orchestration): only call a Cursor approval live while it owns the screen

Independent review found the approval detector trusted one dismissal string, so
any later output that did not contain cursor's follow-up line left the menu
reading as a live wait. Reproduced: a tail of the real menu followed by two lines
of ordinary output returned agent-approval-prompt, which fails tui-idle and
refuses prompt injection on a healthy lane.

Replaced with the structural property the string was standing in for: a live
dialog owns the bottom of the screen, so the last choice may sit at most one line
above the end of the retained tail. That tolerates a status footer or a partial
line mid-redraw without admitting scrollback, and it drops the vendor prose.

Being bottom-of-screen is also the dating this reason needed, so it no longer
requires waitBlockedAt. A tail restored from terminal history carries none, and a
lane parked on a prompt emits no bytes — so before this, an Orca restart made
exactly the lane both issues are about go quiet for good. The startup modals keep
the timestamp rule: their text lingers in scrollback with nothing to say whether
it was answered.

Also from review:
- worker-show and federationShow reuse the verdict showTerminal already computed
  rather than rescanning the tail, so the two can no longer disagree.
- The worker-show test now drives a real runtime, real PTY tail, and the real
  detector; it previously mocked getTerminalInteractiveWait, so it would have
  passed with detection permanently returning null.
- The guard claim is now asserted against the guard: a blocked pane rejects both
  assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane
  still passes.
- Added a non-local (connectionId) pane case, since the verdict is derived from
  retained tail and title state on every host.

* fix(agent-status): stop a hook wait from outliving its agent

A third reviewer caught that the hook branch proved agent ownership from the pane
title alone, while the shared verdict it claimed to reuse also probes the
foreground process. A shell that takes a pane back usually sets something like
`user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh
for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on
a human for half an hour.

Hook evidence now goes through getTerminalAgentStatus, which is the only thing
that can answer whether an agent still owns this PTY. The two prompt branches skip
it: a matched prompt is on the pane's screen now, so it proves itself. That makes
the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait
async, which only showTerminal had to absorb.

Also trims the comments the same reviewer flagged as longer than the repo's rule.

* test(agent-status): pin that a dead pane stops reporting a human wait

A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail
forever, and that no test covered process exit with no trailing output. The
snapshot already refuses an exited pane, and worker-show gates agentWait on proven
identity — this pins both so neither can drift into reporting a worker that needs
intervention as one that needs an answer.

* fix(orchestration): never report an unchecked worker as not waiting

Automated review caught that the three worker paths which return before the wait
is ever evaluated — unattached, missing, and identity_changed — then had their
undefined coerced to null by the emitters. A worker whose process was replaced was
reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting"
when Orca never looked. That is the false negative this field exists to remove.

The field is now emitted only when it was evaluated, so a present null is a claim
about the pane and an absent one means nobody looked — because the host predates
the field, or the worker's identity could not be verified. The CLI and the
worker-show note say that rather than blaming an old host.

Covered on the context-only path, where the regression test fails against the
previous behavior; the supervised and federated emitters take the identical
one-line change.

Also trims the two test-file headers to one statement of purpose.

* fix(agent-status): tighten the Cursor menu match and stop guessing on unknowns

Fourth review round, three findings, each reproduced before acting.

Matching each choice marker with an independent lastIndexOf let text outside the
menu carry the anchor. An agent narrating "next time I'll suggest Run Everything"
after the menu was answered pulled the match down to the bottom of the screen and
revived it. The match is now confined to the last lines of the tail, and a choice
is a line that ends in the key that picks it — prose writes the same words but not
the same shape.

The one line of slack under the dialog went with it. It was a guess; every capture
of a live dialog ends on its last choice, and one line is exactly enough room for
that narration. A redraw caught mid-flight now reads as no wait until the next
poll, which is the safe way to be wrong.

The hook branch awaited a foreground probe that reaches a PTY controller which may
be a remote host, so a wedged probe stalled every caller of showTerminal — a path
that never probed before. It is bounded now, and a timeout leaves the wait
unevaluated rather than claiming there is none.

Which is the same distinction the previous commit only fixed one level up:
getTerminalInteractiveWait itself turned an unreadable pane into `null`, so
showTerminal published "looked, nobody waiting" for a pane it could not read. It
returns undefined there, showTerminal omits the key, and worker-show's text output
prints unknown rather than rendering it the same as none.

* fix(agent-status): bound the wedged probe's cost and stop matching prose keys

Fifth review round. No correctness defects in the shipped behaviour this time; two
robustness holes and the documentation of the contract.

The bounded probe abandoned the wait but not the request, so a coordinator watching
a wedged remote host added one live probe on every poll. It is single-flighted per
PTY now, the way the leaf-absence probe already is.

The trailing-key rule that separates a menu row from the agent narrating a choice
was written as a character class, and any lowercase run up to twelve characters
satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key
names instead, which also lets the glyph forms of those keys through.

The contract wording said an absent agentWait meant an old host or an unverifiable
identity. It also covers an unreadable pane and a probe that did not answer, and a
reader diagnosing an old peer from that would be wrong. Corrected on the type, the
worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no
entry for a field whose absent and null states mean different things.

Also strengthens the worker-show agreement test, which compared the terminal and
observation payloads without asserting either held the expected wait, so it passed
when both were absent.
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
…513, STA-3714) (stablyai#15261)

* feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714)

A lane parked on an approval, trust, or permission prompt looked exactly like a
lane that was thinking or inside a long tool call. On origin/main, driving a real
cursor-agent through Orca:

  surface                        running `sleep 60`   awaiting approval
  worktree ps agents[].state     working              working
  terminal show / list           no such field        no such field
  terminal wait --for tui-idle   satisfied: true      satisfied: true
  worker-show                    no agent state       no agent state

The runtime already fuses hook state, OSC title, and matched prompt text into a
`permission` verdict inside getTerminalAgentStatus — it was reachable only from the
renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary.

Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async
foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s
observation. It carries the evidence that proved the wait (hook, prompt-text, or
title) so a coordinator can weigh it. Null means no proof; a missing field means
the host predates it — absence is never read as "not waiting".

Detection: cursor-agent's hook set has no approval event and beforeShellExecution
fires identically for auto-allowed commands, so its rendered menu is the only
authority. Matched on the key-bound choices rather than the prose, requiring two,
and self-clearing when the follow-up input line returns. Its live spinner title is
exempted from the staleness rule that clears startup modals, because cursor keeps
spinning while it waits.

Falls out of routing it through the shared verdict: `dispatch --inject` into a
cursor pane on an approval now refuses with agent_prompt_blocked instead of typing
the preamble into the dialog.

Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through
Orca; the same case matrix was replayed live against a built runtime.

terminal list stays untouched: its rows would each need a full tail scan, and
STA-4694 owns the one-call-per-run aggregate.

* fix(orchestration): only call a Cursor approval live while it owns the screen

Independent review found the approval detector trusted one dismissal string, so
any later output that did not contain cursor's follow-up line left the menu
reading as a live wait. Reproduced: a tail of the real menu followed by two lines
of ordinary output returned agent-approval-prompt, which fails tui-idle and
refuses prompt injection on a healthy lane.

Replaced with the structural property the string was standing in for: a live
dialog owns the bottom of the screen, so the last choice may sit at most one line
above the end of the retained tail. That tolerates a status footer or a partial
line mid-redraw without admitting scrollback, and it drops the vendor prose.

Being bottom-of-screen is also the dating this reason needed, so it no longer
requires waitBlockedAt. A tail restored from terminal history carries none, and a
lane parked on a prompt emits no bytes — so before this, an Orca restart made
exactly the lane both issues are about go quiet for good. The startup modals keep
the timestamp rule: their text lingers in scrollback with nothing to say whether
it was answered.

Also from review:
- worker-show and federationShow reuse the verdict showTerminal already computed
  rather than rescanning the tail, so the two can no longer disagree.
- The worker-show test now drives a real runtime, real PTY tail, and the real
  detector; it previously mocked getTerminalInteractiveWait, so it would have
  passed with detection permanently returning null.
- The guard claim is now asserted against the guard: a blocked pane rejects both
  assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane
  still passes.
- Added a non-local (connectionId) pane case, since the verdict is derived from
  retained tail and title state on every host.

* fix(agent-status): stop a hook wait from outliving its agent

A third reviewer caught that the hook branch proved agent ownership from the pane
title alone, while the shared verdict it claimed to reuse also probes the
foreground process. A shell that takes a pane back usually sets something like
`user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh
for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on
a human for half an hour.

Hook evidence now goes through getTerminalAgentStatus, which is the only thing
that can answer whether an agent still owns this PTY. The two prompt branches skip
it: a matched prompt is on the pane's screen now, so it proves itself. That makes
the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait
async, which only showTerminal had to absorb.

Also trims the comments the same reviewer flagged as longer than the repo's rule.

* test(agent-status): pin that a dead pane stops reporting a human wait

A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail
forever, and that no test covered process exit with no trailing output. The
snapshot already refuses an exited pane, and worker-show gates agentWait on proven
identity — this pins both so neither can drift into reporting a worker that needs
intervention as one that needs an answer.

* fix(orchestration): never report an unchecked worker as not waiting

Automated review caught that the three worker paths which return before the wait
is ever evaluated — unattached, missing, and identity_changed — then had their
undefined coerced to null by the emitters. A worker whose process was replaced was
reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting"
when Orca never looked. That is the false negative this field exists to remove.

The field is now emitted only when it was evaluated, so a present null is a claim
about the pane and an absent one means nobody looked — because the host predates
the field, or the worker's identity could not be verified. The CLI and the
worker-show note say that rather than blaming an old host.

Covered on the context-only path, where the regression test fails against the
previous behavior; the supervised and federated emitters take the identical
one-line change.

Also trims the two test-file headers to one statement of purpose.

* fix(agent-status): tighten the Cursor menu match and stop guessing on unknowns

Fourth review round, three findings, each reproduced before acting.

Matching each choice marker with an independent lastIndexOf let text outside the
menu carry the anchor. An agent narrating "next time I'll suggest Run Everything"
after the menu was answered pulled the match down to the bottom of the screen and
revived it. The match is now confined to the last lines of the tail, and a choice
is a line that ends in the key that picks it — prose writes the same words but not
the same shape.

The one line of slack under the dialog went with it. It was a guess; every capture
of a live dialog ends on its last choice, and one line is exactly enough room for
that narration. A redraw caught mid-flight now reads as no wait until the next
poll, which is the safe way to be wrong.

The hook branch awaited a foreground probe that reaches a PTY controller which may
be a remote host, so a wedged probe stalled every caller of showTerminal — a path
that never probed before. It is bounded now, and a timeout leaves the wait
unevaluated rather than claiming there is none.

Which is the same distinction the previous commit only fixed one level up:
getTerminalInteractiveWait itself turned an unreadable pane into `null`, so
showTerminal published "looked, nobody waiting" for a pane it could not read. It
returns undefined there, showTerminal omits the key, and worker-show's text output
prints unknown rather than rendering it the same as none.

* fix(agent-status): bound the wedged probe's cost and stop matching prose keys

Fifth review round. No correctness defects in the shipped behaviour this time; two
robustness holes and the documentation of the contract.

The bounded probe abandoned the wait but not the request, so a coordinator watching
a wedged remote host added one live probe on every poll. It is single-flighted per
PTY now, the way the leaf-absence probe already is.

The trailing-key rule that separates a menu row from the agent narrating a choice
was written as a character class, and any lowercase run up to twelve characters
satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key
names instead, which also lets the glyph forms of those keys through.

The contract wording said an absent agentWait meant an old host or an unverifiable
identity. It also covers an unreadable pane and a probe that did not answer, and a
reader diagnosing an old peer from that would be wrong. Corrected on the type, the
worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no
entry for a field whose absent and null states mean different things.

Also strengthens the worker-show agreement test, which compared the terminal and
observation payloads without asserting either held the expected wait, so it passed
when both were absent.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…513, STA-3714) (stablyai#15261)

* feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714)

A lane parked on an approval, trust, or permission prompt looked exactly like a
lane that was thinking or inside a long tool call. On origin/main, driving a real
cursor-agent through Orca:

  surface                        running `sleep 60`   awaiting approval
  worktree ps agents[].state     working              working
  terminal show / list           no such field        no such field
  terminal wait --for tui-idle   satisfied: true      satisfied: true
  worker-show                    no agent state       no agent state

The runtime already fuses hook state, OSC title, and matched prompt text into a
`permission` verdict inside getTerminalAgentStatus — it was reachable only from the
renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary.

Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async
foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s
observation. It carries the evidence that proved the wait (hook, prompt-text, or
title) so a coordinator can weigh it. Null means no proof; a missing field means
the host predates it — absence is never read as "not waiting".

Detection: cursor-agent's hook set has no approval event and beforeShellExecution
fires identically for auto-allowed commands, so its rendered menu is the only
authority. Matched on the key-bound choices rather than the prose, requiring two,
and self-clearing when the follow-up input line returns. Its live spinner title is
exempted from the staleness rule that clears startup modals, because cursor keeps
spinning while it waits.

Falls out of routing it through the shared verdict: `dispatch --inject` into a
cursor pane on an approval now refuses with agent_prompt_blocked instead of typing
the preamble into the dialog.

Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through
Orca; the same case matrix was replayed live against a built runtime.

terminal list stays untouched: its rows would each need a full tail scan, and
STA-4694 owns the one-call-per-run aggregate.

* fix(orchestration): only call a Cursor approval live while it owns the screen

Independent review found the approval detector trusted one dismissal string, so
any later output that did not contain cursor's follow-up line left the menu
reading as a live wait. Reproduced: a tail of the real menu followed by two lines
of ordinary output returned agent-approval-prompt, which fails tui-idle and
refuses prompt injection on a healthy lane.

Replaced with the structural property the string was standing in for: a live
dialog owns the bottom of the screen, so the last choice may sit at most one line
above the end of the retained tail. That tolerates a status footer or a partial
line mid-redraw without admitting scrollback, and it drops the vendor prose.

Being bottom-of-screen is also the dating this reason needed, so it no longer
requires waitBlockedAt. A tail restored from terminal history carries none, and a
lane parked on a prompt emits no bytes — so before this, an Orca restart made
exactly the lane both issues are about go quiet for good. The startup modals keep
the timestamp rule: their text lingers in scrollback with nothing to say whether
it was answered.

Also from review:
- worker-show and federationShow reuse the verdict showTerminal already computed
  rather than rescanning the tail, so the two can no longer disagree.
- The worker-show test now drives a real runtime, real PTY tail, and the real
  detector; it previously mocked getTerminalInteractiveWait, so it would have
  passed with detection permanently returning null.
- The guard claim is now asserted against the guard: a blocked pane rejects both
  assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane
  still passes.
- Added a non-local (connectionId) pane case, since the verdict is derived from
  retained tail and title state on every host.

* fix(agent-status): stop a hook wait from outliving its agent

A third reviewer caught that the hook branch proved agent ownership from the pane
title alone, while the shared verdict it claimed to reuse also probes the
foreground process. A shell that takes a pane back usually sets something like
`user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh
for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on
a human for half an hour.

Hook evidence now goes through getTerminalAgentStatus, which is the only thing
that can answer whether an agent still owns this PTY. The two prompt branches skip
it: a matched prompt is on the pane's screen now, so it proves itself. That makes
the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait
async, which only showTerminal had to absorb.

Also trims the comments the same reviewer flagged as longer than the repo's rule.

* test(agent-status): pin that a dead pane stops reporting a human wait

A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail
forever, and that no test covered process exit with no trailing output. The
snapshot already refuses an exited pane, and worker-show gates agentWait on proven
identity — this pins both so neither can drift into reporting a worker that needs
intervention as one that needs an answer.

* fix(orchestration): never report an unchecked worker as not waiting

Automated review caught that the three worker paths which return before the wait
is ever evaluated — unattached, missing, and identity_changed — then had their
undefined coerced to null by the emitters. A worker whose process was replaced was
reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting"
when Orca never looked. That is the false negative this field exists to remove.

The field is now emitted only when it was evaluated, so a present null is a claim
about the pane and an absent one means nobody looked — because the host predates
the field, or the worker's identity could not be verified. The CLI and the
worker-show note say that rather than blaming an old host.

Covered on the context-only path, where the regression test fails against the
previous behavior; the supervised and federated emitters take the identical
one-line change.

Also trims the two test-file headers to one statement of purpose.

* fix(agent-status): tighten the Cursor menu match and stop guessing on unknowns

Fourth review round, three findings, each reproduced before acting.

Matching each choice marker with an independent lastIndexOf let text outside the
menu carry the anchor. An agent narrating "next time I'll suggest Run Everything"
after the menu was answered pulled the match down to the bottom of the screen and
revived it. The match is now confined to the last lines of the tail, and a choice
is a line that ends in the key that picks it — prose writes the same words but not
the same shape.

The one line of slack under the dialog went with it. It was a guess; every capture
of a live dialog ends on its last choice, and one line is exactly enough room for
that narration. A redraw caught mid-flight now reads as no wait until the next
poll, which is the safe way to be wrong.

The hook branch awaited a foreground probe that reaches a PTY controller which may
be a remote host, so a wedged probe stalled every caller of showTerminal — a path
that never probed before. It is bounded now, and a timeout leaves the wait
unevaluated rather than claiming there is none.

Which is the same distinction the previous commit only fixed one level up:
getTerminalInteractiveWait itself turned an unreadable pane into `null`, so
showTerminal published "looked, nobody waiting" for a pane it could not read. It
returns undefined there, showTerminal omits the key, and worker-show's text output
prints unknown rather than rendering it the same as none.

* fix(agent-status): bound the wedged probe's cost and stop matching prose keys

Fifth review round. No correctness defects in the shipped behaviour this time; two
robustness holes and the documentation of the contract.

The bounded probe abandoned the wait but not the request, so a coordinator watching
a wedged remote host added one live probe on every poll. It is single-flighted per
PTY now, the way the leaf-absence probe already is.

The trailing-key rule that separates a menu row from the agent narrating a choice
was written as a character class, and any lowercase run up to twelve characters
satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key
names instead, which also lets the glyph forms of those keys through.

The contract wording said an absent agentWait meant an old host or an unverifiable
identity. It also covers an unreadable pane and a probe that did not answer, and a
reader diagnosing an old peer from that would be wrong. Corrected on the type, the
worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no
entry for a field whose absent and null states mean different things.

Also strengthens the worker-show agreement test, which compared the terminal and
observation payloads without asserting either held the expected wait, so it passed
when both were absent.
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.

[Bug]: an agent blocked on an interactive prompt is indistinguishable from one that is working (the coordinator has no signal)

1 participant