Skip to content

fix(automations): a headless run is watched past tui-idle timeouts by the one run observer - #25733

Merged
OrcaWin merged 3 commits into
OrcaWin/node-rt-phase3from
OrcaWin/node-rt-p3-headless-wait-timeout
Oct 6, 2026
Merged

OrcaWin merged 3 commits into
OrcaWin/node-rt-phase3from
OrcaWin/node-rt-p3-headless-wait-timeout

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​232 $\color{#cf222e}{\Huge{\mathbf{−}}}$​121 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​111
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​189 $\color{#cf222e}{\Huge{\mathbf{−}}}$​137 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​52

ELI5

When an automation ran on a remote Orca server and its agent kept working for more than 5 minutes, Orca marked the run "failed" and stopped watching it, even though the agent was fine. Now the run is watched for as long as the agent works, and it ends as completed or failed based on what the agent actually did.

What Changed

Before: a server dispatch (orcad and Electron orca serve) followed each run with its own short completion loop. That loop waited for the terminal to look idle, and that wait gives up with a timeout error after 5 minutes. The error escaped the loop, and the dispatch runner published the run as dispatch_failed. Nothing watched the run after that, so a long agent turn always read as failed. Two independent reviewers found this (Astra pass 5 §2 and round 8).

After: the server dispatcher no longer runs its own loop. It hands the launched run to the run-completion watcher, which already exists. That watcher uses the runtime terminal observer, and the observer already:

  • re-arms a wait that timed out, so a long turn is still being watched when it ends;
  • stops cleanly when cancelled (the service stopping);
  • gives up after 6 hours without a completion signal.

The checks from #25700 move into that observer. They run only when the observer is given a run and agent evidence:

  • Completed: the agent's own status for the run's pane arrived after the run started.
  • Failed with the shell's message: the shell refused the agent's own command.
  • Otherwise: idle means done, after the 2-minute agent-start window.

The separate completion loop is deleted. Two smaller changes support this:

  • The dispatcher remembers the terminal handle each launch returned (at most 256), so the watcher can find the run even before a pane-key lookup resolves.
  • The precheck result is now saved when the run is marked dispatched, because a watched run's completion is written without it.

Why

  • One loop instead of two. The observer already handles timeouts, cancellation and the time limit, so reusing it means less code than fixing the second loop.
  • The same rules everywhere. Runs the watcher picks up again after a restart now get the same agent-status and missing-command rules as freshly dispatched runs.
  • No double reporting. A run whose dispatch reports no completion was already passed to the watcher by the dispatch runner. That path existed before, so nothing else reports these runs.

Linked Issue

Fixes # (Astra-5 §2 / round-8 finding)

Visual Proof

N/A. No UI change.

Testing

  • New runtime-terminal-run-observer-evidence.test.ts:

    • Wait timeout: the agent works through two 5-minute waits that time out and is still unsettled; once it reports and goes idle, the run completes.
    • Cancellation: the run stops with request_aborted and records nothing.
    • fix(automations): a headless run never reads completed when its agent never ran #25700's rules, now in the observer: a refused command fails the run for bash, zsh, dash, fish and PowerShell; status from the agent completes it; an agent that never reports completes after the window; an agent's tool output never fails the run.
  • runtime-automation-service.test.ts: the server dispatch returns no completion and never waits on the terminal itself. The watcher resolves the launch's own handle even when the pane key doesn't resolve yet. Run against the old dispatcher, this test fails.

  • Locally the automations suites pass (33 files, 239 tests). Typecheck and the changed-code quality gate are clean.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

This applies to server hosts on any platform (Linux, macOS, Windows, SSH). The desktop runner is unchanged. No wire changes.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@pullfrog pullfrog 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.

Important

This branch ships a red test. src/main/orcad/orcad-automations.test.ts fails with TypeError: runtime.getTerminalHandleForPaneKey is not a function, because the headless dispatch path now always calls watchRun (the dispatcher no longer returns a completion) and its fake runtime was never updated. CI's unit job runs this file. Fix before merge.

Reviewed changes

  • Headless watchdog unified into the run observer: headless-run-completion.ts is deleted; its missing-command and idle-grace rules move to new automation-run-agent-evidence.ts (judgeIdleRun, AGENT_START_GRACE_MS), and the existing observer now consumes them.
  • Observer judges satisfied tui-idle waits by agent evidence: when an AutomationRun and evidence are supplied, runtime-terminal-run-observer.ts skips the reused-pane busy edge and resolves the run via judgeIdleRun, re-polling every 1s while the agent may still be starting. Wait timeouts keep re-arming under the existing 6h deadline.
  • Dispatcher stops owning a completion loop: runtime-automation-service.ts returns no completion, remembers each launch's handle by pane key (capped at 256) for resolveRunTerminal, and runHeadlessAutomationDispatch now always hands the run to watchRun; the watcher forwards run into observeCompletion.
  • Precheck survives the watched completion: headless-dispatch-runner.ts persists precheckResult on the dispatched write, since the watcher's markDispatchResult carries no precheck.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

terminalObserver: {
...observer,
resolveRunTerminal: (run) =>
observer.resolveRunTerminal(run) ??

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.

This wrapper now runs on the headless dispatch path, and a runtime stub that only faked the old loop breaks. The dispatcher no longer returns a completion, so runHeadlessAutomationDispatch always calls watchRun → watcher.watch → resolveRunTerminal, which calls the observer's default runtime.getTerminalHandleForPaneKey(...) and, when the watcher starts observing, the evidence's runtime.getAgentStatusRowsForPane(...) (automation-run-agent-evidence.ts:26). src/main/orcad/orcad-automations.test.ts's headlessRuntime() fakes waitForTerminal/readTerminal but neither of those, so the suite fails with TypeError: runtime.getTerminalHandleForPaneKey is not a function (and would next hit getAgentStatusRowsForPane).

Technical details
# Headless dispatch now needs the terminal host's pane methods on every runtime stub

## Affected sites
- `src/main/automations/runtime-automation-service.ts:44` — `resolveRunTerminal` wrapper calls `observer.resolveRunTerminal` unconditionally.
- `src/main/orcad/orcad-automations.test.ts:16` — `headlessRuntime()` stubs `waitForTerminal`/`readTerminal` but not `getTerminalHandleForPaneKey` or `getAgentStatusRowsForPane`.
- `src/main/automations/headless-dispatch-runner.ts:75` — `if (!launch.completion)` is now always true, so `watchRun` always fires.

## Required outcome
- `pnpm test src/main/orcad/orcad-automations.test.ts` passes; every runtime stub driving the headless dispatch path satisfies its terminal-host dependencies.

## Suggested approach
- Add `getTerminalHandleForPaneKey: vi.fn(() => 'term-1')` and `getAgentStatusRowsForPane: vi.fn(() => [])` to that test's fake runtime.

@OrcaWin
OrcaWin marked this pull request as draft October 6, 2026 01:54
@OrcaWin
OrcaWin marked this pull request as ready for review October 6, 2026 02:01
@pullfrog

pullfrog Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

This run was cancelled 🛑

The workflow was cancelled before completion. Please check the link below for details.

Pullfrog  | View workflow run | via Pullfrog | 𝕏

m4air added 3 commits October 5, 2026 20:27
… the one run observer

The headless dispatcher awaited a single tui-idle wait, which rejects after
its 5-minute default, so a healthy agent working longer was published as
dispatch_failed and never observed again. The dispatcher now hands the run
to its completion watcher, whose runtime observer already re-arms wait
timeouts, honours cancellation and bounds total observation; the agent
status and missing-command checks fold into that observer, and the separate
completion loop is gone.
…w passes

Real-host: a stub that exited before the window left an idle shell with no
agent status, and the observer re-armed a tui-idle wait that never resolves
for an already-idle shell, so it timed out instead of completing. The
observer now keeps judging the pane while its output is unchanged, and only
waits again once the pane changes.
@OrcaWin
OrcaWin force-pushed the OrcaWin/node-rt-p3-headless-wait-timeout branch from 52131e1 to f6e0b67 Compare October 6, 2026 03:39
@OrcaWin
OrcaWin merged commit 03934f3 into OrcaWin/node-rt-phase3 Oct 6, 2026
38 checks passed
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