Skip to content

fix(orchestration): a child whose provider turn failed reports failed (#1018) - #1044

Open
Juliusolsson05 wants to merge 6 commits into
mainfrom
fix/orchestration-api-error
Open

Juliusolsson05 wants to merge 6 commits into
mainfrom
fix/orchestration-api-error

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Closes #1018.

Why

An orchestration child whose provider failed before any output reported prompt_sent / waiting forever.

  • In the renderer, the lifecycle read "idle with no output" as waiting, and main's prompt overlay turned that into prompt_sent.
  • On 2026-09-19, six OpenCode children in one run hit a zai usage limit within about 90 s. orchestration_wait_agents still showed every one as prompt_sent with no errorSummary.
  • The structured OpenCode runtime also lost the text: every failure read "OpenCode session error".

Staged, evidence first

  1. Recorder / catalog (testing/fixtures/orchestration-api-error/). This covers 27 real api_errors: OpenCode structured, OpenCode Terminal and Codex, from feed-debug, the OpenCode databases and Codex rollouts. It also covers Claude's committed error entries. Each is catalogued with its sequence around the error, and there are 6 sanitized fixtures with provenance. The key facts:
    • OpenCode emits no api_error while it retries (processActive stays true, 10/10).
    • The error lands before idle in 27/27, so a pure function of state is enough.
    • Claude's semantic api_error is not final (Claude retries); its committed isApiErrorMessage entry is.
  2. Rule (terminalProviderFailure in orchestrationMcp.ts): there is a failure signal, the provider is idle, and the failure is newer, by producer time, than the latest real output. The lifecycle checks it after running and before completed, so a Claude limit no longer reads as the child's answer.
  3. Record: errorSummary plus a new failedAt. Main's overlay reports prompt_sent when the parent prompts the failed child again. orchestration_wait_agents already treats failed as done.
  4. Text: opencode-headless #15 is merged (the pointer is bumped here). It reads error.data.message, so the parent sees "Usage limit reached for 5 hour. Your limit will reset at …".

Tests

  • orchestrationFailure.test.ts replays the recorded sequences through the real semantic fold:
    • OpenCode structured and Terminal usage limits end failed with the provider's text;
    • a retry in flight is never failed;
    • Codex's error arrives while the process is still active and is failed only once idle;
    • Claude's limit entry is a failure, and a later answer beats it.
  • OrchestrationBridge.test.ts: a failed child that is prompted again reads prompt_sent.
  • Every case fails with its branch removed. Orchestration suites: 49/49. tsc is clean.

Known gaps (from the catalog)

  • Neither OpenCode runtime forwards error.name or statusCode, so a user abort also reads failed with the message "Aborted". That's correct for coordination, because the child will not continue on its own, but it has no label.
  • There is no recording of a Claude mid-stream error followed by Claude's own retry.

🤖 Generated with Claude Code

Juliusolsson05 and others added 6 commits September 19, 2026 01:59
…1018)

The instrumentation stage. A catalog from real feed-debug recordings,
OpenCode databases, Codex rollouts and Claude transcripts on this machine:
27 recorded api_errors across OpenCode structured, OpenCode Terminal and
Codex, and Claude's committed error entries. For each, the sequence
around the error: retries, process state, idle, and turn end. It gives a
precise terminal-failure rule in runtime fields, with its evidence and
unknowns, plus six sanitized fixture sequences with provenance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nces (#1018)

The recorded feed-debug streams replayed through the renderer's real
semantic fold:
- OpenCode structured and Terminal usage limits end failed with the
  provider's text;
- a retry still in flight is never failed;
- Codex's error, which arrives while the process is still active, is
  failed only once idle;
- Claude's committed limit entry is a failure, not an answer, and a later
  real answer beats it.

In main, a failed child the parent prompts again reads prompt_sent. Each
case fails with its branch removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#1018)

A child whose provider failed before any output (a usage limit, an auth
rejection) read waiting, and main turned that into prompt_sent forever.
Parents burned wait timeouts, and six children in one run stalled at
once. terminalProviderFailure implements the catalog's rule:
- the failure is a semantic api_error (OpenCode, Codex, Grok), or for
  Claude the committed isApiErrorMessage entry, because Claude retries on
  its own;
- the provider is idle. Errors land before idle in 27 of 27 recordings,
  so no timer is needed;
- the failure is newer, by producer time, than the latest real output.

The lifecycle checks it after running and before completed, so a Claude
limit no longer reads as the child's answer. The record carries
errorSummary and a new failedAt, and main reports prompt_sent when the
parent prompts the failed child again. orchestration_wait_agents already
treats failed as done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only a dispatcher lookup change and its test, so package.json and the
lockfile are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd instance errors never do

Review of #1044 (CHANGES REQUESTED):

1. Grok regression, reproduced by the reviewer. Grok transcript rows
   carry no timestamp, so an old api_error was always newer than the
   latest output. A child whose turn 1 failed and whose turn 2 answered
   read failed forever, and main turned that into prompt_sent, which is
   #1018 again. The signal is back to the catalog's §3.1 rule: OpenCode
   from opencode-sse, Codex from proxy, Claude's committed entry, and
   nothing for Grok, which has no recording to build a rule from. A real
   assistant row with an unparseable timestamp now answers "not
   failed".
2. Non-terminal OpenCode errors:
   - session-less instance errors from a broken SKILL.md, agent,
     command or plugin file;
   - the delta-buffer overflow;
   - a user's Esc (MessageAbortedError).
   Each is now named in errorType by the packages (opencode-headless#16,
   opencode-terminal-headless#4, both bumped here), and none makes a
   child failed. They still show in the feed.
3. Tests for the cases that must NOT fail:
   - Grok, through the real Grok mapper;
   - an instance error after the final answer;
   - the recorded Esc (case b);
   - the recorded 403 then user re-prompt (case c): failed while idle,
     running once the new turn starts;
   - Codex recovery.
   The first three fail with the rule change reverted. The retry-in-
   flight test is now labelled a guard; it passed before #1018 too.
4. idleOrchestrationAgents' WHY claimed Dispatch paints failed workers
   with an error. It paints runtime state, so a usage-limited worker
   can look idle there; the comment now says why excluding it matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ule; pin the fail-open clause

Delta re-review of #1044 (APPROVE) left two items, both fixed here:
- nit 2: terminalProviderFailure read meta?.kind with no default. A
  Claude child whose meta has no kind therefore had no failure signal,
  while lifecycleStateForRuntime already defaults to DEFAULT_PROVIDER.
  The test fails without the default.
- finding 1: the fail-open clause had no test. An assistant row whose
  time cannot be read now pins "not failed", and the same runtime
  without that row still fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 19, 2026
…1047

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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(orchestration): a child whose provider turn fails with an API error reports prompt_sent/waiting forever

1 participant