@bitkyc08/opencodex 2.74.0 → 2.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/gui/dist/assets/{App-KLz1TAXi.css → App-ia4U0ufY.css} +1 -1
  2. package/gui/dist/assets/{App-B00Yq11u.js → App-xm0zGNOe.js} +3 -3
  3. package/gui/dist/assets/{Tray-D3Ij1Cvf.js → Tray-CGAD2AGk.js} +1 -1
  4. package/gui/dist/assets/{index-BYQQfBGK.js → index-D_RGAT6Q.js} +2 -2
  5. package/gui/dist/assets/{quota-summary-yPtcEhGI.js → quota-summary-CUFq48t-.js} +1 -1
  6. package/gui/dist/index.html +1 -1
  7. package/package.json +1 -1
  8. package/src/adapters/devin/cloud-direct/chat.ts +38 -6
  9. package/src/adapters/devin/live-models.ts +28 -8
  10. package/src/adapters/devin.ts +1 -1
  11. package/src/claude/devin-output-order.ts +98 -0
  12. package/src/codex/auth-api/pool-quota-probe.ts +2 -1
  13. package/src/codex/catalog/retained-sync.ts +9 -0
  14. package/src/codex/inject/config-toml.ts +179 -248
  15. package/src/codex/inject/provider-table.ts +5 -2
  16. package/src/codex/inject/remove.ts +23 -8
  17. package/src/codex/injected-marker.ts +28 -23
  18. package/src/codex/quota-auto-refresh.ts +1 -1
  19. package/src/codex/quota.ts +12 -4
  20. package/src/codex/toml-source-lines.ts +93 -1
  21. package/src/config/live-reconcile.ts +18 -0
  22. package/src/config/rebase-provenance.ts +12 -0
  23. package/src/config.ts +4 -3
  24. package/src/generated/compatibility-version.json +50 -38
  25. package/src/images/loop.ts +2 -0
  26. package/src/integrations/cursor-local-installer.ts +29 -2
  27. package/src/lib/redact.ts +54 -8
  28. package/src/oauth/anthropic-account-refusal.ts +76 -0
  29. package/src/oauth/anthropic-routing.ts +38 -10
  30. package/src/providers/new-model-policy-runtime.ts +83 -0
  31. package/src/providers/new-model-policy.ts +25 -0
  32. package/src/providers/registry/entries-core.ts +3 -0
  33. package/src/providers/registry/model-seeds.ts +31 -2
  34. package/src/providers/xai-transport.ts +2 -2
  35. package/src/server/claude-messages.ts +33 -10
  36. package/src/server/management/agent-settings-routes.ts +3 -1
  37. package/src/server/management/model-rows.ts +34 -4
  38. package/src/server/management/shared.ts +18 -2
  39. package/src/server/responses/adapter-continuation.ts +13 -21
  40. package/src/server/responses/adapter-delivery.ts +2 -2
  41. package/src/server/responses/adapter-dispatch.ts +10 -21
  42. package/src/server/responses/compact.ts +3 -2
  43. package/src/server/responses/core-codex-account.ts +4 -2
  44. package/src/server/responses/passthrough-delivery.ts +2 -1
  45. package/src/server/responses/request-transport.ts +2 -0
  46. package/src/server/responses/run-turn-execution.ts +25 -5
  47. package/src/server/responses/sidecar-execution.ts +9 -14
  48. package/src/web-search/loop.ts +7 -2
@@ -1,3 +1,4 @@
1
+ import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
1
2
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
2
3
  import { isLocalUpstream } from "../../lib/local-upstream";
3
4
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
@@ -57,8 +58,6 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
57
58
  import {
58
59
  AnthropicAccountCooldownError,
59
60
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
60
- rotateAnthropicAccountOn429,
61
- recordAnthropicAccount429,
62
61
  getAnthropicPoolAccessSnapshot,
63
62
  formatAnthropicProviderForLog,
64
63
  } from "../../oauth/anthropic-routing";
@@ -986,27 +985,22 @@ export async function prepareAdapterExchange(
986
985
  if (isNonReplayableResponse(upstreamResponse)) continue recovery;
987
986
  }
988
987
 
989
- // Opt-in Anthropic OAuth account pool (#294): cool the failed account and retry
990
- // with another eligible OAuth account (bounded per request). Disabled by default.
988
+ // Anthropic OAuth: recover a rate limit or proven account entitlement refusal
989
+ // before output, within the shared request and account rotation limits.
991
990
  while (
992
- upstreamResponse.status === 429
991
+ (upstreamResponse.status === 429 || upstreamResponse.status === 403)
993
992
  && transportState.anthropicPoolAccountId
994
- && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
995
993
  ) {
996
- const nextAccountId = rotateAnthropicAccountOn429(
997
- config,
998
- transportState.anthropicPoolAccountId,
999
- upstreamResponse.headers.get("retry-after"),
1000
- anthropicSessionKey,
1001
- Date.now(),
1002
- upstreamResponse.headers,
1003
- transportState.anthropicRouteDecision,
1004
- );
994
+ const nextAccountId = await rotateAnthropicAccountOnResponse(upstreamResponse, {
995
+ config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
996
+ decision: transportState.anthropicRouteDecision, signal: upstream.signal,
997
+ canRetry: transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
998
+ });
1005
999
  if (!nextAccountId) break;
1006
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1007
1000
  try {
1008
1001
  const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
1009
1002
  if (!admitted) throw new Error("OAuth selection changed during recovery");
1003
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1010
1004
  transportState.anthropicPoolAccountId = admitted.accountId;
1011
1005
  transportState.anthropicPoolFailovers += 1;
1012
1006
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
@@ -1026,11 +1020,6 @@ export async function prepareAdapterExchange(
1026
1020
  break;
1027
1021
  }
1028
1022
  }
1029
- if (upstreamResponse.status === 429 && transportState.anthropicPoolAccountId
1030
- && transportState.anthropicPoolFailovers >= ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST) {
1031
- recordAnthropicAccount429(config, transportState.anthropicPoolAccountId,
1032
- upstreamResponse.headers.get("retry-after"), Date.now(), upstreamResponse.headers);
1033
- }
1034
1023
  // Generic OAuth account failover (#2568) rotates reactively after a refusal when
1035
1024
  // two accounts are stored. Kiro additionally classifies bounded 400/403 refusals;
1036
1025
  // all other providers retain the original 429 loop below.
@@ -1259,7 +1259,8 @@ export async function handleResponsesCompact(
1259
1259
  upstream.headers,
1260
1260
  authCtx.writerGeneration,
1261
1261
  authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
1262
- { modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined },
1262
+ { modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined,
1263
+ poolResponse: authCtx.kind === "pool" },
1263
1264
  );
1264
1265
  }
1265
1266
  recordCompactPoolOutcome(authCtx, upstream.status, {
@@ -1328,7 +1329,7 @@ export async function handleResponsesCompact(
1328
1329
  if (outcomeCtx.kind === "pool") {
1329
1330
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/quota");
1330
1331
  applyAccountQuotaFromUpstreamHeaders(outcomeCtx.accountId, upstream.headers, outcomeCtx.writerGeneration,
1331
- undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter });
1332
+ undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter, poolResponse: true });
1332
1333
  }
1333
1334
  const retryAfter = upstream.headers.get("retry-after");
1334
1335
  const resetAt = [
@@ -127,7 +127,8 @@ export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxPro
127
127
  const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
128
128
  return headers => {
129
129
  if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
130
- applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined });
130
+ applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined,
131
+ poolResponse: authCtx.kind === "pool" });
131
132
  };
132
133
  }
133
134
 
@@ -717,7 +718,8 @@ export async function retryCodexPoolOnAlternateAccount(
717
718
  firstResponse.headers,
718
719
  firstAuthCtx.writerGeneration,
719
720
  firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
720
- { modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined },
721
+ { modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined,
722
+ poolResponse: firstAuthCtx.kind === "pool" },
721
723
  );
722
724
  }
723
725
  const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
@@ -487,7 +487,8 @@ export async function deliverPassthroughResponse(
487
487
  if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
488
488
  applyAccountQuotaFromUpstreamHeaders(admissionState.authCtx.accountId, upstreamResponse.headers,
489
489
  admissionState.authCtx.writerGeneration, admissionState.authCtx.kind === "main-pool" ? admissionState.authCtx.mainQuotaWriter : undefined,
490
- { modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined });
490
+ { modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined,
491
+ poolResponse: admissionState.authCtx.kind === "pool" });
491
492
  }
492
493
  if (terminalBodyWillRecord) {
493
494
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
@@ -1,3 +1,4 @@
1
+ import { bindAnthropicRefusalCredential } from "../../oauth/anthropic-account-refusal";
1
2
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
3
  import type { PreparedResponsesRequest } from "./request-prepare";
3
4
  import type { OAuthAccessSnapshot } from "../../oauth";
@@ -521,6 +522,7 @@ export async function prepareResponsesTransport(
521
522
  try {
522
523
  const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
523
524
  if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation) {
525
+ bindAnthropicRefusalCredential(response, snapshot);
524
526
  recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration);
525
527
  }
526
528
  } catch { /* best-effort observation cannot fail the response */ }
@@ -50,6 +50,7 @@ import { undeclaredToolCallMessage } from "../responses-undeclared-tool-guard";
50
50
  import { planWebSearch } from "../../web-search";
51
51
  import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
52
52
  import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
53
+ import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
53
54
 
54
55
  // LOCAL PATCH (runturn-websearch): top-level fields route binding or the
55
56
  // adapter itself may write during a turn. Iteration-local `turnParsed` objects
@@ -181,6 +182,13 @@ export async function executeResponsesRunTurn(
181
182
 
182
183
  const runTurnAbort = new AbortController();
183
184
  const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
185
+ const devinProducers = new Set<AbortController>();
186
+ const orderMessagesOutput = (source: AsyncIterable<AdapterEvent>): AsyncIterable<AdapterEvent> =>
187
+ inboundWire === "anthropic" && transportState.runTurnAdapter.name === "devin" && !routedCompaction
188
+ ? orderDevinMessagesOutput(source, translatorBudget, runTurnAbort.signal, () => {
189
+ for (const producer of devinProducers) producer.abort();
190
+ })
191
+ : source;
184
192
  const queue = createAdapterEventQueue({
185
193
  onBacklogExceeded: () => runTurnAbort.abort(),
186
194
  });
@@ -229,6 +237,8 @@ export async function executeResponsesRunTurn(
229
237
  options.onCompactionRecoveryAdapterEvent?.(event);
230
238
  targetQueue.push(event);
231
239
  };
240
+ let producerAbort = runTurnAbort;
241
+ let cleanupProducerAbort: (() => void) | undefined;
232
242
  try {
233
243
  if (!pacingSlotAcquired) {
234
244
  pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
@@ -261,11 +271,16 @@ export async function executeResponsesRunTurn(
261
271
  turnScopedPacing: true,
262
272
  },
263
273
  );
274
+ if (inboundWire === "anthropic" && transportState.runTurnAdapter.name === "devin" && !routedCompaction) {
275
+ producerAbort = new AbortController();
276
+ cleanupProducerAbort = linkAbortSignal(producerAbort, runTurnAbort.signal);
277
+ devinProducers.add(producerAbort);
278
+ }
264
279
  await transportState.runTurnAdapter.runTurn?.(
265
280
  turnParsed,
266
281
  {
267
282
  headers: requestState.selectedForwardHeaders,
268
- abortSignal: runTurnAbort.signal,
283
+ abortSignal: producerAbort.signal,
269
284
  comboAttempt: options.comboAttempt === true,
270
285
  translatorBudget,
271
286
  providerFetch: runTurnProviderFetch,
@@ -321,6 +336,8 @@ export async function executeResponsesRunTurn(
321
336
  message: err instanceof Error ? err.message : String(err),
322
337
  });
323
338
  } finally {
339
+ devinProducers.delete(producerAbort);
340
+ cleanupProducerAbort?.();
324
341
  releaseProviderRequestSlot(pacingSlot);
325
342
  // Cursor assigns a stable conversation id inside runTurn on the first headerless
326
343
  // turn; backfill so Logs can filter/total that opening request (#330 / #522).
@@ -345,14 +362,14 @@ export async function executeResponsesRunTurn(
345
362
  });
346
363
  void runTurnAttempt(iterQueue, undefined, false, iterParsed);
347
364
  const stream = iterQueue.stream();
348
- if (!runTurnFailoverArmed()) return stream;
365
+ if (!runTurnFailoverArmed()) return orderMessagesOutput(stream);
349
366
  // LOCAL PATCH (runturn-websearch): a post-search iteration can open on a
350
367
  // 429 too — the search cells already reached the client, so only this
351
368
  // answer call rotates. Preflight replays it on the next account with the
352
369
  // grown history intact; a mid-stream error still ends the turn as before.
353
- return (async function* () {
370
+ return orderMessagesOutput((async function* () {
354
371
  yield* await preflightRunTurnFailover(stream, iterParsed);
355
- })();
372
+ })());
356
373
  };
357
374
  // Rebind the turn to an admitted account. The failed attempt emitted no client-visible bytes,
358
375
  // so replay is safe, but a Cursor conversation/checkpoint is credential-scoped: carrying its
@@ -614,7 +631,7 @@ export async function executeResponsesRunTurn(
614
631
  onBacklogExceeded: () => runTurnAbort.abort(),
615
632
  });
616
633
  void runTurnAttempt(retryQueue, "empty-completion");
617
- return retryQueue.stream();
634
+ return orderMessagesOutput(retryQueue.stream());
618
635
  };
619
636
 
620
637
  const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
@@ -651,6 +668,7 @@ export async function executeResponsesRunTurn(
651
668
  retryAfter: resolveClientRetryAfter({ status: httpStatus, message: error.message }),
652
669
  });
653
670
  };
671
+ // Messages ingress forces internal streaming, including buffered client requests.
654
672
  if (parsed.stream) {
655
673
  try {
656
674
  void runTurn();
@@ -699,6 +717,8 @@ export async function executeResponsesRunTurn(
699
717
  }
700
718
  eventSource = preflight.stream;
701
719
  }
720
+ // Preflight sees raw output before Messages delays text for the late signature.
721
+ eventSource = orderMessagesOutput(eventSource);
702
722
  // LOCAL PATCH (runturn-websearch): intercept web_search calls across
703
723
  // iterations; terminal output keeps flowing through the same queue/bridge.
704
724
  if (wsPlan) {
@@ -1,3 +1,4 @@
1
+ import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
1
2
  import type { ResponsesRequestContext } from "./core-options";
2
3
  import type { PreparedResponsesRequest } from "./request-prepare";
3
4
  import type { ResponsesTransport } from "./request-transport";
@@ -32,7 +33,6 @@ import { persistKiroAccountState } from "../../providers/kiro-account-state-disk
32
33
  import { readDisplaySafeErrorText } from "./core-errors";
33
34
  import {
34
35
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
35
- rotateAnthropicAccountOn429,
36
36
  getAnthropicPoolAccessSnapshot,
37
37
  formatAnthropicProviderForLog,
38
38
  } from "../../oauth/anthropic-routing";
@@ -183,7 +183,7 @@ export async function executeResponsesSidecars(
183
183
  retryParsed?: OcxParsedRequest,
184
184
  originalResponse?: Response,
185
185
  ): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
186
- if (route.providerName !== "kiro" && originalResponse && originalResponse.status !== 429) return null;
186
+ if (route.providerName !== "kiro" && !(route.providerName === "anthropic" && originalResponse?.status === 403) && originalResponse && originalResponse.status !== 429) return null;
187
187
  const refusal = route.providerName === "kiro" && originalResponse
188
188
  ? classifyKiroRefusal(originalResponse.status,
189
189
  await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
@@ -200,7 +200,7 @@ export async function executeResponsesSidecars(
200
200
  // sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
201
201
  // rotation in the attempt row and in the Logs UI.
202
202
  let recoveryKind: AttemptRecoveryKind = "key-429";
203
- const rotated = route.providerName !== "kiro" || originalResponse?.status === 429
203
+ const rotated = !originalResponse || originalResponse.status === 429
204
204
  ? rotateProviderTransportOn429(config, route.providerName, route.provider, {
205
205
  retryAfter,
206
206
  now: Date.now(),
@@ -260,7 +260,6 @@ export async function executeResponsesSidecars(
260
260
  // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
261
261
  // the very same 429 on the main response path rotated.
262
262
  transportState.anthropicPoolAccountId
263
- && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
264
263
  ) {
265
264
  // Same intersection for the Anthropic roster: its own per-request bound still applies,
266
265
  // and the shared budget decides whether this request may spend another send at all.
@@ -268,16 +267,12 @@ export async function executeResponsesSidecars(
268
267
  "auth-recovery",
269
268
  `${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
270
269
  );
271
- if (!hop.allowed) return null;
272
- const nextAccountId = rotateAnthropicAccountOn429(
273
- config,
274
- transportState.anthropicPoolAccountId,
275
- retryAfter,
276
- anthropicSessionKey,
277
- Date.now(),
278
- responseHeaders,
279
- transportState.anthropicRouteDecision,
280
- );
270
+ const nextAccountId = await rotateAnthropicAccountOnResponse(
271
+ originalResponse ?? new Response(null, { status: 429, headers: responseHeaders ?? (retryAfter ? { "retry-after": retryAfter } : undefined) }), {
272
+ config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
273
+ decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
274
+ canRetry: hop.allowed && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
275
+ });
281
276
  if (!nextAccountId) {
282
277
  hop.permit?.release();
283
278
  return null;
@@ -325,7 +325,7 @@ export interface WebSearchLoopDeps {
325
325
  /** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
326
326
  onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
327
327
  /**
328
- * 429 failover hook: rotate the provider's active credential and return a rebuilt adapter,
328
+ * Account/key failover hook (429, plus provider-classified pre-output refusals): rotate the provider's active credential and return a rebuilt adapter,
329
329
  * or null when the pool is exhausted. Async hooks support OAuth refresh; existing synchronous
330
330
  * key-pool hooks remain valid.
331
331
  *
@@ -346,6 +346,7 @@ export interface WebSearchLoopDeps {
346
346
  retryAfterHeader: string | null,
347
347
  responseHeaders?: Headers,
348
348
  retryParsed?: OcxParsedRequest,
349
+ // A 403 requires this complete response; status-only callbacks retain 429 semantics.
349
350
  originalResponse?: Response,
350
351
  ) =>
351
352
  | { adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind }
@@ -405,6 +406,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
405
406
  // One iteration beyond the forced answer is reserved for its empty-answer recovery below.
406
407
  const HARD_CAP = maxSearches + 3;
407
408
  let emptyAnswerRetries = 0;
409
+ let accountRefusalOutputStarted = false;
408
410
  const connectTimeoutMs = deps.connectTimeoutMs ?? 200_000;
409
411
  const routedModelStallTimeoutMs = deps.routedModelStallTimeoutMs ?? 200_000;
410
412
 
@@ -571,6 +573,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
571
573
  // 429 key-failover parity with the normal routed path: rotate pool keys until one responds
572
574
  // or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
573
575
  while ((prepared.response.status === 429
576
+ || (prepared.response.status === 403 && deps.incomingMeta?.providerName === "anthropic" && !accountRefusalOutputStarted)
574
577
  || (iterParsed._kiroAuthContext && (prepared.response.status === 400 || prepared.response.status === 403))) && deps.on429) {
575
578
  const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
576
579
  iterParsed, prepared.response);
@@ -663,10 +666,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
663
666
  // Codex show only `Working` until both Kiro attempts had finished (often 30-40 seconds).
664
667
  // Tool events remain buffered below, so the decision to invoke the hosted sidecar is still
665
668
  // atomic and no search call can escape before its stream has validated successfully.
666
- else if (event.type === "text_delta" && event.phase === "commentary") yield event;
669
+ else if (event.type === "text_delta" && event.phase === "commentary") { accountRefusalOutputStarted = true; yield event; }
667
670
  else if (liveWindowOpen && LIVE_STREAMABLE.has(event.type)) {
668
671
  // Live events are ALSO buffered: the scanner still needs them for thinking extraction
669
672
  // and the forced-answer output check; only the terminal replay skips them (by count).
673
+ accountRefusalOutputStarted = true;
670
674
  yield event;
671
675
  streamedPassthroughCount++;
672
676
  events.push(event);
@@ -937,6 +941,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
937
941
  // The thinking that led to the search belongs to the FIRST call's assistant replay turn.
938
942
  const iterationThinking = extractIterationThinking(split.passthrough);
939
943
  for (const [callIndex, call] of split.calls.entries()) {
944
+ accountRefusalOutputStarted = true;
940
945
  yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
941
946
  }
942
947
  } catch (e) {