Skip to content

Surface provider/error on thread.failed instead of a generic workflow failure - #2079

Open
MPIsaac-Per wants to merge 1 commit into
get-bb:mainfrom
MPIsaac-Per:michaelisaac/mpi-1469-bb-surface-provider-429s-on-failed-workflow-calls
Open

Surface provider/error on thread.failed instead of a generic workflow failure#2079
MPIsaac-Per wants to merge 1 commit into
get-bb:mainfrom
MPIsaac-Per:michaelisaac/mpi-1469-bb-surface-provider-429s-on-failed-workflow-calls

Conversation

@MPIsaac-Per

Copy link
Copy Markdown
Contributor

What was wrong

getLastThreadErrorMessage() only read system/error rows. Providers persist failures as provider/error (with optional detail and errorInfo for category, HTTP status, and provider code). On a workflow child that died on a Claude 429, emitThreadFailed() therefore published error: null, and the workflows plugin stored error ?? "Workflow worker failed". bb workflows status and history showed that fallback. The 429 was only in the hidden child thread.

Root cause and independent repro: #1914, report.

What changed

TIER-2. No HOST_DAEMON_PROTOCOL_VERSION bump. thread.failed stays { thread, error: string | null }. No plugin SDK change.

  • packages/db/src/data/events.tsgetLatestThreadErrorEventRow() returns the newest system/error or provider/error for one thread, ordered by sequence descending. A later terminal failure outranks an earlier willRetry: true row. Filtering willRetry out would hide a 429 that is itself the newest event.
  • apps/server/src/services/threads/thread-data.tsgetLastThreadErrorMessage() uses that query. system/error still returns message. provider/error formats detail (else message), then category, http, and providerCode when present.
  • Workflows already persist a non-null thread.failed.error string; no plugin change.

Deviation from the issue comment's optional structured errorInfo / resetsAt field: this slice keeps the existing string payload.

How you verified

Red before green in apps/server/test/services/plugins/plugin-thread-events.test.ts:

  • delivers thread.failed with a provider/error detail and metadata — failed before the query/format change; now asserts the exact string 429 rate limit: too many requests | category: rate-limit | http: 429 | providerCode: rate_limit_event.
  • surfaces a terminal provider/error over an earlier willRetry provider/error — sequence 1 willRetry: true does not beat sequence 2 terminal.
  • Existing delivers thread.failed with the latest system/error message still passes.

Query contract: packages/db/test/data/events.test.ts returns the newest system/error or provider/error by sequence.

pnpm exec turbo run test --filter=@bb/db -- --run test/data/events.test.ts
pnpm exec turbo run test --filter=@bb/server -- --run test/services/plugins/plugin-thread-events.test.ts
pnpm exec turbo run typecheck --filter=@bb/db --filter=@bb/server

db: 84 passed. server plugin-thread-events: 13 passed. typecheck: 0 errors. eslint on the five changed files: 0 errors.

Fixes #1914

AGENT GENERATED: by Grok 4.6

… failure (get-bb#1914)

Provider failures persist as provider/error. thread.failed only read
system/error, so workflows fell back to "Workflow worker failed" and hid
429s. Look up the newest of both types and format provider detail with
category, HTTP status, and provider code.
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.

A rate-limited workflow call reports only 'Workflow worker failed' — the 429 is hidden in a child thread

1 participant