Bridges emit typed provider/recovery hints; the runtime acts on the kind (WS4 L1) - #2235
Closed
SawyerHood wants to merge 2 commits into
Closed
Bridges emit typed provider/recovery hints; the runtime acts on the kind (WS4 L1)#2235SawyerHood wants to merge 2 commits into
SawyerHood wants to merge 2 commits into
Conversation
…ind (WS4 L1) One payload, two carriers: a rejected runtime request carries its hint as `error.data.recovery` (JSON-RPC id correlates); an unsolicited condition rides the `provider/recovery` notification. The runtime keys on `kind`: sessionArchived unarchives and retries once, authRequired becomes a typed auth_required error, restartRecommended restarts the bridge process and resumes, staleTurn drops the steer, rateLimited retries a retryable rejection on a bounded ladder. The codex bridge emits sessionArchived on rejections, authRequired and rateLimited on terminal account errors, rebuilds its app-server child before the next turn, and owns the empty-rollout rename retry. The claude bridge emits authRequired/rateLimited from the SDK's typed assistant error. The codex-shaped runtime text gates stay for the deletion layer (L2). Co-Authored-By: Claude <noreply@anthropic.com>
…for the recovery hint Co-Authored-By: Claude <noreply@anthropic.com>
Collaborator
Author
Coordinator review — WS4 layer 1 (#2235)PASS against Verified on the branch
Oracle (reported, consistent with the diff) 1,921 tests green; parity codex 16/0, claude 13/0 with zero event and row diffs; A4 307 threads, zero row diffs, perf gate clean at load 2.8; ratchet 148/40 unchanged (L2 lowers it). Process note The
|
SawyerHood
deleted the
bb/ws4-runtime-recovery-process-topology-skills-pi-thr_qdx8ashimg
branch
August 21, 2026 21:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WS4 layer 1 of 7 (runtime recovery, process topology, skills, pi, AI services). Design:
ideal-state-provider-api.md§"Runtime" — the runtime matches no error text. Additive: every codex-shaped text gate in the runtime stays until L2 deletes it.What was wrong
The runtime recovered from provider failures by matching provider prose:
CODEX_ARCHIVED_SESSION_ERROR_PATTERNgated unarchive-and-retry,CODEX_EMPTY_ROLLOUT_RENAME_ERROR_PATTERNdrove a rename retry ladder inside the runtime, andCODEX_ACCOUNT_RESTART_PROVIDER_ERROR_{CATEGORIES,TEXT_PATTERN}decided when to restart a codex process before the next turn. Aprovider/recoverynotification existed in the protocol (#2124) but no bridge emitted it and the runtime only logged it. A third-party bridge had no way to say "this session is archived" or "log in again" that the runtime would act on.What changed
One payload, two carriers (coordinator decision, 3-lens judge panel):
error.data.recovery { kind, message, retryable }. The JSON-RPC id is the correlation; no runtime slot, no ordering rule, nothreadIdin the payload.provider/recoverynotification is for unsolicited hints only (a terminal 401/429 mid-turn). Never both for one event.Protocol / kit / SDK (
packages/provider-bridge-protocol,packages/plugin-sdk)errors.ts:providerRecoveryHintSchema,bridgeErrorDataSchema(optional, passthrough).providerRecoveryNotificationSchemais now the hint plusthreadId?; same keys, G3 grammar snapshot unchanged.createBridgeIo().sendError(id, code, message, data?);BridgeRecoveryError { code, message, recovery }mapped byrunBridgeRequest's catch (theProviderRequestDecodeError→INVALID_PARAMSpattern).JsonRpcResponseError.recoveryparsed insettleJsonRpcResponse; a malformeddatais a plain failure.experimental_BridgeRecoveryError,BridgeSendError,providerRecoveryHintSchema,bridgeErrorDataSchema,ProviderRecoveryHint,BridgeErrorData; entry indocs/api_to_audit.md.PROVIDER_BRIDGE_PROTOCOL_VERSIONbump (datais optional and additive). NoHOST_DAEMON_PROTOCOL_VERSIONbump:AgentRuntimeProviderRecoveryHintis unchanged and the daemon's RPC failureerrorCodeis already a free string.Runtime (
packages/agent-runtime/src/runtime.ts), keyed onkindonly:sessionArchived(retryable: true): unarchive, retry the request once. The legacy text gate stays beside it (hint wins) until L2.authRequired: the rejection becomesAgentRuntimeRecoveryError { code: "auth_required" }; the daemon'sgetErrorCodereads the string code, so the server seesauth_requiredwith no regex on the way.rateLimited:retryable: true→ re-send on a bounded ladder (rateLimitRetry.delaysMs, default[2 s, 8 s], injectable so tests run in ms), then a typedrate_limitederror;retryable: false→ typed error, never re-run.staleTurn:steerTurndrops the steer and returns{ status: "stale" }(the ACP-gatedNO_ACTIVE_TURNbranch stays until L2).restartRecommended: restart the bridge process and resume every thread it hosted — right away when the hinted thread is idle, otherwise before its next turn. A bridge process hosts every thread of its provider in the environment, so the restart defers while another hosted thread is mid-turn (logged, retried at the next turn): a recommendation never kills another thread's work.thread/name/setnow pass a recovery context, so an archived source on a rewind recovers through the fork's own rejection.Codex bridge (
plugins/provider-codex)sessionArchivedasdata.recoveryon resume (SESSION_NOT_RESTORABLE), fork of an archived source, maintenance, turn/start and steer rejections.Unsolicited
authRequired/rateLimitedon a terminal account error, and the session is marked so the next turn rebuilds the app-server child from its rollout (session/replaced) — the Run Codex app-server per thread #130 account restart moved from the runtime into the bridge that owns the child. Deliberate behavior change in the text fallback (used only when codex reportserrorInfo.categoryunknown):/\b(?:40[19]|429|auth(?:entication|orization)?|credits?|quota|rate[-\s]?limit(?:ed)?|unauthori[sz]ed|usage limit)\b/iauthRequired:/\b(?:40[13]|auth(?:entication|orization)?|unauthori[sz]ed)\b/i—rateLimited:/\b(?:429|credits?|quota|rate[-\s]?limit(?:ed)?|usage limit)\b/iauthRequired+ rebuildauthRequired+ rebuildrateLimited+ rebuildThe empty-rollout rename retry (50/200 ms ladder) lives in the bridge; exhaustion is a plain error with no hint (a not-ready rollout is no recovery kind). The runtime's copy stays dormant until L2.
fake-codex-app-server.mjsgainsarchiveStatePath(archive state across children),turnCursorPath(scripted turns across a rebuilt child),renameEmptyRolloutFailures, and refuses forks of archived sources.Claude bridge (
plugins/provider-claude-code): unsolicitedauthRequired(authentication_failed,oauth_org_not_allowed) andrateLimitedfrom the SDK's typed assistant error, one hint per turn. The bridge already replaces its CLI child before the next turn; it never asks the runtime for a restart.restartRecommendedtherefore has no first-party emitter in this layer — kind, runtime action, scripted-echo knob and conformance rule only.Scripted echo (
tests/scripted-echo-provider):failMethods[].recovery,recover:<kind>/recover_now:<kind>prompt tokens, runtime archive state,staleTurndata on itsNO_ACTIVE_TURN.Conformance kit: new rule
recovery/session-archived(archive → resume must be rejected with the hint; a bridge with no archive produces no result, liketurn/settles-without-activity); the client's error type gainsdata. Codex and echo reports pin it green.Docs:
docs/provider-bridge-protocol.md"Recovery hints" (action table + carrier rule),docs/provider-plugin-api.md(carrier rule in prose; the G10-mapped block is untouched),docs/api_to_audit.md.How you verified
New tests (fail before, pass after):
packages/agent-runtime/src/runtime.recovery.test.ts— one case per kind under provider idfake(the legacy text gates cannot fire), plus the panel's four: two in-flight requests on one thread with different hints, a rewind staging fork of an archived source,thread/forkof an archived source, and a bridge exit as a plain rejection with no action.plugins/provider-codex/src/bridge/bridge.recovery.test.ts— terminal 401 → oneauthRequirednotification, child rebuilt (session/replaced) on the next turn; rename ladder pass and exhaustion.bridge.archived-resume.test.tspinsdata.recoveryon resume and fork.plugins/provider-claude-codebridge.test.tspins exactly oneauthRequirednotification on the SDK auth failure.recovery/session-archivedexpectedpassin the codex and echo suites.Oracle (all at
--concurrency 4):turbo typecheck11/11;turbo test: provider-bridge-protocol 218, agent-runtime 453, plugin-sdk 127, codex 201, claude-code 323, scripted-echo 1, host-daemon 555, provider-parity 43 — all green.pnpm parity --old ~/.bb/parity-old-main --new .: codex 16 passed / 1 skipped, claude-code 13 passed / 1 skipped, zero event and row diffs — the hint is a runtime signal, never a timeline event, so no allowlist entry. Rerecorded lanes: codexarchived-resume(the-32002line gainsdata.recovery, no inserted line), codex and claudeauth-failure(oneprovider/recoverynotification line each).rows-last-run.json:diffThreads: []).--base origin/main: 148 references across 40 core files, unchanged.Part of the provider-plugin migration (WS4); L2 deletes the runtime text gates this layer makes redundant.