@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12

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 (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -28,7 +28,7 @@ import type { ModelRegistry } from "../config/model-registry";
28
28
  import { formatModelStringWithRouting, resolveModelOverride } from "../config/model-resolver";
29
29
 
30
30
  import type { Settings } from "../config/settings";
31
- import type { RecoveredRetryError } from "../extensibility/shared-events";
31
+ import type { RetryErrorUpdate } from "../extensibility/shared-events";
32
32
  import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
33
33
  import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
34
34
  import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
@@ -161,7 +161,7 @@ export interface TurnRecoveryOptions {
161
161
  initialRetryFallback?: InitialRetryFallbackState;
162
162
  }
163
163
 
164
- type PendingRecoveredRetryError = {
164
+ type PendingRetryError = {
165
165
  entryId: string;
166
166
  persistenceKey: string;
167
167
  recovery: AssistantRetryRecoveryKind;
@@ -184,7 +184,7 @@ export class TurnRecovery {
184
184
  #retryResolve: (() => void) | undefined;
185
185
  #activeRetryFallback: ActiveRetryFallbackState | undefined;
186
186
  #usageReserveApprovedSelector: string | undefined;
187
- #pendingRecoveredRetryErrors: PendingRecoveredRetryError[] = [];
187
+ #pendingRetryErrors: PendingRetryError[] = [];
188
188
  #usageLimitOutcomes = new WeakMap<AssistantMessage, Promise<UsageLimitOutcome>>();
189
189
  #emptyStopRetryCount = 0;
190
190
  #unexpectedStopRetryCount = 0;
@@ -250,14 +250,17 @@ export class TurnRecovery {
250
250
  role: this.#activeRetryFallback.role,
251
251
  });
252
252
  }
253
- const recoveredErrors = await this.#markPendingRecoveredRetryErrors(message);
253
+ const retryErrors = await this.#markPendingRetryErrors({
254
+ status: "recovered",
255
+ supersedingMessage: message,
256
+ });
254
257
  await this.#host.emitSessionEvent({
255
258
  type: "auto_retry_end",
256
259
  success: true,
257
260
  attempt: this.#retryAttempt,
258
- recoveredErrors,
261
+ retryErrors,
259
262
  });
260
- this.#clearPendingRecoveredRetryErrors();
263
+ this.#clearPendingRetryErrors();
261
264
  this.#retryAttempt = 0;
262
265
  }
263
266
 
@@ -272,7 +275,7 @@ export class TurnRecovery {
272
275
  attempt,
273
276
  finalError: message.errorMessage,
274
277
  });
275
- this.#clearPendingRecoveredRetryErrors();
278
+ this.#clearPendingRetryErrors();
276
279
  }
277
280
 
278
281
  /** Persists an otherwise skipped terminal empty error turn. */
@@ -305,8 +308,13 @@ export class TurnRecovery {
305
308
  return this.#runRecoveryCompactionWithRollback(reason, message, allowDefer, options);
306
309
  }
307
310
 
308
- /** Restores the configured primary after fallback cooldown expiry. */
309
- maybeRestoreRetryFallbackPrimary(): Promise<void> {
311
+ /**
312
+ * Restores the configured primary after fallback cooldown expiry.
313
+ * @returns true when the active model was actually switched back to the
314
+ * primary, so callers can re-run the pre-send context-fit check against the
315
+ * reverted (possibly smaller) window before issuing the next request.
316
+ */
317
+ maybeRestoreRetryFallbackPrimary(): Promise<boolean> {
310
318
  return this.#maybeRestoreRetryFallbackPrimary();
311
319
  }
312
320
 
@@ -381,8 +389,8 @@ export class TurnRecovery {
381
389
  }
382
390
  }
383
391
 
384
- #clearPendingRecoveredRetryErrors(): void {
385
- this.#pendingRecoveredRetryErrors = [];
392
+ #clearPendingRetryErrors(): void {
393
+ this.#pendingRetryErrors = [];
386
394
  }
387
395
 
388
396
  /**
@@ -432,7 +440,7 @@ export class TurnRecovery {
432
440
  return parts.join("; ");
433
441
  }
434
442
 
435
- async #recordPendingRecoveredRetryError(
443
+ async #recordPendingRetryError(
436
444
  message: AssistantMessage,
437
445
  id: number,
438
446
  options: { switchedCredential: boolean; switchedModel: boolean; delayMs: number },
@@ -451,11 +459,11 @@ export class TurnRecovery {
451
459
  break;
452
460
  }
453
461
  if (!branchEntry) return;
454
- if (this.#pendingRecoveredRetryErrors.some(error => error.entryId === branchEntry.id)) return;
462
+ if (this.#pendingRetryErrors.some(error => error.entryId === branchEntry.id)) return;
455
463
  const rateLimited = AIError.is(id, AIError.Flag.UsageLimit);
456
464
  const recovery = this.#retryRecoveryKind(id, options.switchedCredential, options.switchedModel, options.delayMs);
457
465
  const note = this.#retryRecoveryNote(recovery, rateLimited);
458
- this.#pendingRecoveredRetryErrors.push({
466
+ this.#pendingRetryErrors.push({
459
467
  entryId: branchEntry.id,
460
468
  persistenceKey,
461
469
  recovery,
@@ -464,24 +472,17 @@ export class TurnRecovery {
464
472
  });
465
473
  }
466
474
 
467
- async #markPendingRecoveredRetryErrors(supersedingMessage: AssistantMessage): Promise<RecoveredRetryError[]> {
468
- if (this.#pendingRecoveredRetryErrors.length === 0) return [];
475
+ async #markPendingRetryErrors(
476
+ completion: { status: "recovered"; supersedingMessage: AssistantMessage } | { status: "superseded" },
477
+ ): Promise<RetryErrorUpdate[]> {
478
+ if (this.#pendingRetryErrors.length === 0) return [];
469
479
  const branch = this.#host.sessionManager.getBranch();
470
480
  const branchById = new Map<string, SessionEntry>();
471
481
  for (const entry of branch) {
472
482
  branchById.set(entry.id, entry);
473
483
  }
474
- const recoveredAt = new Date().toISOString();
475
- const supersededBy: AssistantRetryRecovery["supersededBy"] = {
476
- timestamp: supersedingMessage.timestamp,
477
- provider: supersedingMessage.provider,
478
- model: supersedingMessage.model,
479
- };
480
- if (supersedingMessage.responseId) {
481
- supersededBy.responseId = supersedingMessage.responseId;
482
- }
483
- const recoveredErrors: RecoveredRetryError[] = [];
484
- for (const pending of this.#pendingRecoveredRetryErrors) {
484
+ const retryErrors: RetryErrorUpdate[] = [];
485
+ for (const pending of this.#pendingRetryErrors) {
485
486
  let entry = branchById.get(pending.entryId);
486
487
  if (entry?.type !== "message" || entry.message.role !== "assistant") {
487
488
  entry = branch
@@ -495,27 +496,45 @@ export class TurnRecovery {
495
496
  );
496
497
  }
497
498
  if (entry?.type !== "message" || entry.message.role !== "assistant") continue;
498
- const retryRecovery: AssistantRetryRecovery = {
499
- kind: "auto-retry",
500
- status: "recovered",
501
- attempt: pending.attempt,
502
- recoveredAt,
503
- recovery: pending.recovery,
504
- note: pending.note,
505
- supersededBy,
506
- };
499
+ let retryRecovery: AssistantRetryRecovery;
500
+ if (completion.status === "recovered") {
501
+ retryRecovery = {
502
+ kind: "auto-retry",
503
+ status: "recovered",
504
+ attempt: pending.attempt,
505
+ recoveredAt: new Date().toISOString(),
506
+ recovery: pending.recovery,
507
+ note: pending.note,
508
+ supersededBy: {
509
+ timestamp: completion.supersedingMessage.timestamp,
510
+ ...(completion.supersedingMessage.responseId === undefined
511
+ ? {}
512
+ : { responseId: completion.supersedingMessage.responseId }),
513
+ provider: completion.supersedingMessage.provider,
514
+ model: completion.supersedingMessage.model,
515
+ },
516
+ };
517
+ } else {
518
+ retryRecovery = {
519
+ kind: "auto-retry",
520
+ status: "superseded",
521
+ attempt: pending.attempt,
522
+ recovery: pending.recovery,
523
+ note: pending.note,
524
+ };
525
+ }
507
526
  entry.message.retryRecovery = retryRecovery;
508
- recoveredErrors.push({
527
+ retryErrors.push({
509
528
  entryId: entry.id,
510
529
  persistenceKey: pending.persistenceKey,
511
530
  note: retryRecovery.note,
512
531
  retryRecovery,
513
532
  });
514
533
  }
515
- if (recoveredErrors.length > 0) {
534
+ if (retryErrors.length > 0) {
516
535
  await this.#host.sessionManager.rewriteEntries();
517
536
  }
518
- return recoveredErrors;
537
+ return retryErrors;
519
538
  }
520
539
 
521
540
  async #handleEmptyAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
@@ -547,7 +566,7 @@ export class TurnRecovery {
547
566
  attempt: this.#retryAttempt > 0 ? this.#retryAttempt : attempts,
548
567
  finalError,
549
568
  });
550
- this.#clearPendingRecoveredRetryErrors();
569
+ this.#clearPendingRetryErrors();
551
570
  this.#retryAttempt = 0;
552
571
  this.resolveRetry();
553
572
  // A zero-content turn carries no transcript value, while its provider usage
@@ -905,9 +924,9 @@ export class TurnRecovery {
905
924
  }
906
925
 
907
926
  /**
908
- * Check if an error is retryable (transient errors or usage limits).
927
+ * Check if an error is retryable (transient errors, usage limits, or
928
+ * account-scoped policy denials that can rotate credentials).
909
929
  * Context overflow is NOT retryable (handled by compaction instead).
910
- * Usage-limit errors are retryable because the retry handler performs credential switching.
911
930
  */
912
931
  isRetryableError(message: AssistantMessage): boolean {
913
932
  if (message.stopReason !== "error") return false;
@@ -918,12 +937,11 @@ export class TurnRecovery {
918
937
  const contextWindow = this.#host.model()?.contextWindow ?? 0;
919
938
  if (AIError.isContextOverflow(message, contextWindow)) return false;
920
939
 
921
- // A classifier refusal/sensitivity stop is the model's decision, not a route
922
- // failure, but only after we confirm no replay-unsafe output has already
923
- // streamed. Committed text, images, tool calls, or server tools must not be
924
- // discarded and replayed.
940
+ // Credential rotation and classifier fallbacks are safe only before
941
+ // committed text, images, tool calls, or server tools. Thinking-only
942
+ // output remains replay-safe.
925
943
  if (this.#hasReplayUnsafeOutput(message)) return false;
926
- if (this.isClassifierRefusal(message)) return true;
944
+ if (AIError.is(id, AIError.Flag.AccountPolicy) || this.isClassifierRefusal(message)) return true;
927
945
  return AIError.retriable(id);
928
946
  }
929
947
 
@@ -1433,10 +1451,10 @@ export class TurnRecovery {
1433
1451
  return true;
1434
1452
  }
1435
1453
 
1436
- async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
1437
- if (!this.#activeRetryFallback) return;
1438
- if (this.#activeRetryFallback.pinned) return;
1439
- if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
1454
+ async #maybeRestoreRetryFallbackPrimary(): Promise<boolean> {
1455
+ if (!this.#activeRetryFallback) return false;
1456
+ if (this.#activeRetryFallback.pinned) return false;
1457
+ if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return false;
1440
1458
 
1441
1459
  const {
1442
1460
  originalSelector: originalSelectorRaw,
@@ -1446,19 +1464,19 @@ export class TurnRecovery {
1446
1464
  const originalSelector = parseRetryFallbackSelector(originalSelectorRaw, this.#host.modelRegistry);
1447
1465
  if (!originalSelector) {
1448
1466
  this.clearActiveRetryFallback();
1449
- return;
1467
+ return false;
1450
1468
  }
1451
1469
 
1452
1470
  const currentModel = this.#host.model();
1453
- if (!currentModel) return;
1471
+ if (!currentModel) return false;
1454
1472
  const currentSelector = formatRetryFallbackSelector(currentModel, this.#host.thinkingLevel());
1455
1473
  if (currentSelector === originalSelector.raw) {
1456
1474
  if (!this.isRetryFallbackSelectorSuppressed(originalSelector)) {
1457
1475
  this.clearActiveRetryFallback();
1458
1476
  }
1459
- return;
1477
+ return false;
1460
1478
  }
1461
- if (this.isRetryFallbackSelectorSuppressed(originalSelector)) return;
1479
+ if (this.isRetryFallbackSelectorSuppressed(originalSelector)) return false;
1462
1480
 
1463
1481
  const resolvedPrimary = resolveModelOverride(
1464
1482
  [originalSelector.raw],
@@ -1467,9 +1485,9 @@ export class TurnRecovery {
1467
1485
  );
1468
1486
  const primaryModel =
1469
1487
  resolvedPrimary.model ?? this.#host.modelRegistry.find(originalSelector.provider, originalSelector.id);
1470
- if (!primaryModel) return;
1488
+ if (!primaryModel) return false;
1471
1489
  const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, this.#host.sessionId());
1472
- if (!apiKey) return;
1490
+ if (!apiKey) return false;
1473
1491
 
1474
1492
  const currentThinkingLevel = this.#host.configuredThinkingLevel();
1475
1493
  const thinkingToApply =
@@ -1480,6 +1498,7 @@ export class TurnRecovery {
1480
1498
  this.#host.settings.getStorage()?.recordModelUsage(primarySelector);
1481
1499
  this.#host.setThinkingLevel(thinkingToApply);
1482
1500
  this.clearActiveRetryFallback();
1501
+ return true;
1483
1502
  }
1484
1503
 
1485
1504
  #parseRetryAfterMsFromError(errorMessage: string): number | undefined {
@@ -1581,27 +1600,28 @@ export class TurnRecovery {
1581
1600
 
1582
1601
  const errorMessage = message.errorMessage || "Unknown error";
1583
1602
  const id = this.#classifyRetryMessage(message);
1603
+ const rateLimitReason = parseRateLimitReason(errorMessage);
1584
1604
  const staleOpenAIResponsesReplayError = AIError.is(id, AIError.Flag.StaleResponsesItem);
1605
+ const accountPolicyDenial = AIError.is(id, AIError.Flag.AccountPolicy);
1585
1606
  const recordedUsageLimitOutcome = await this.#usageLimitOutcomes.get(message);
1586
1607
  const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
1587
1608
  let delayMs = staleOpenAIResponsesReplayError
1588
1609
  ? 0
1589
1610
  : calculateRetryBackoffDelayMs(retrySettings.baseDelayMs, this.#retryAttempt);
1590
- // Concurrency caps shed-and-backoff (5s) rather than burning a sibling
1591
- // credential, so the usage-limit rotation branch below is deliberately
1592
- // skipped for them. Apply the reason-based backoff to the transient
1593
- // same-model retry path too otherwise the default exponential base
1594
- // (≈500ms) re-hits the cap immediately and burns the retry budget while
1595
- // the concurrency slot stays occupied. A categorical 402 billing cap whose
1596
- // body merely mentions concurrency is still a usage limit (handled below),
1597
- // so gate on the flag matching the rotation decision.
1611
+ // Transient rate/concurrency caps stay on the same credential, but must
1612
+ // honor their reason-specific windows. The default exponential base
1613
+ // (≈500ms, capped at 8s) otherwise re-hits the cap and burns the retry
1614
+ // budget before either window can clear. An explicit provider
1615
+ // retry-after is authoritative in both directions, so the heuristic
1616
+ // window only applies when the error carries no parsed timing.
1598
1617
  if (
1599
1618
  !staleOpenAIResponsesReplayError &&
1600
1619
  !AIError.is(id, AIError.Flag.UsageLimit) &&
1601
- parseRateLimitReason(errorMessage) === "CONCURRENT_LIMIT"
1620
+ parsedRetryAfterMs === undefined &&
1621
+ (rateLimitReason === "CONCURRENT_LIMIT" || rateLimitReason === "RATE_LIMIT_EXCEEDED")
1602
1622
  ) {
1603
- const concurrentBackoffMs = calculateRateLimitBackoffMs("CONCURRENT_LIMIT");
1604
- if (concurrentBackoffMs > delayMs) delayMs = concurrentBackoffMs;
1623
+ const reasonBackoffMs = calculateRateLimitBackoffMs(rateLimitReason);
1624
+ if (reasonBackoffMs > delayMs) delayMs = reasonBackoffMs;
1605
1625
  }
1606
1626
  let switchedCredential = false;
1607
1627
  let switchedModel = false;
@@ -1652,6 +1672,14 @@ export class TurnRecovery {
1652
1672
  const currentSelector = currentModel
1653
1673
  ? formatRetryFallbackSelector(currentModel, this.#host.thinkingLevel())
1654
1674
  : undefined;
1675
+ if (accountPolicyDenial && currentModel) {
1676
+ switchedCredential = await this.#host.modelRegistry.authStorage.rotateSessionCredential(
1677
+ currentModel.provider,
1678
+ this.#host.sessionId(),
1679
+ { error: errorMessage, modelId: currentModel.id },
1680
+ );
1681
+ if (switchedCredential) delayMs = 0;
1682
+ }
1655
1683
  if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
1656
1684
  // A refusal chain stops at the retry budget: the exhausted-attempt
1657
1685
  // last resort is for provider failures, not classifier decisions.
@@ -1676,26 +1704,29 @@ export class TurnRecovery {
1676
1704
  }
1677
1705
 
1678
1706
  if (retryBudgetExhausted) {
1679
- if (!switchedModel) {
1707
+ if (!switchedModel && !switchedCredential) {
1708
+ const attempt = this.#retryAttempt - 1;
1709
+ message.errorMessage = `Retry budget exhausted after ${attempt} ${attempt === 1 ? "retry" : "retries"}: ${errorMessage}`;
1680
1710
  await this.persistTerminalEmptyErrorTurn(message);
1681
- // Max retries exceeded and no fallback model to switch to: emit
1682
- // final failure and reset.
1711
+ const retryErrors = await this.#markPendingRetryErrors({ status: "superseded" });
1683
1712
  await this.#host.emitSessionEvent({
1684
1713
  type: "auto_retry_end",
1685
1714
  success: false,
1686
- attempt: this.#retryAttempt - 1,
1687
- finalError: message.errorMessage,
1715
+ attempt,
1716
+ finalError: errorMessage,
1717
+ retryErrors,
1688
1718
  });
1689
- this.#clearPendingRecoveredRetryErrors();
1719
+ this.#clearPendingRetryErrors();
1690
1720
  this.#retryAttempt = 0;
1691
1721
  this.resolveRetry(); // Resolve so waitForRetry() completes
1692
1722
  return false;
1693
1723
  }
1694
- // The fallback model gets a fresh retry budget leaving the spent
1695
- // counter in place would exhaust it again on its first error.
1696
- this.#retryAttempt = 1;
1724
+ // A fallback model gets a fresh retry budget. Credential rotation
1725
+ // instead keeps the cumulative attempt count while bypassing the
1726
+ // same-route budget: every distinct account must be tried first.
1727
+ if (switchedModel) this.#retryAttempt = 1;
1697
1728
  }
1698
- if (classifierRefusal && !switchedModel) {
1729
+ if ((classifierRefusal || accountPolicyDenial) && !switchedCredential && !switchedModel) {
1699
1730
  // A prior attempt in this saga already announced `auto_retry_start`
1700
1731
  // (retryAttempt was incremented for each call to this method, so > 1
1701
1732
  // means at least one earlier attempt started the loop) but this
@@ -1711,7 +1742,7 @@ export class TurnRecovery {
1711
1742
  attempt: this.#retryAttempt - 1,
1712
1743
  finalError: errorMessage,
1713
1744
  });
1714
- this.#clearPendingRecoveredRetryErrors();
1745
+ this.#clearPendingRetryErrors();
1715
1746
  }
1716
1747
  this.#retryAttempt = 0;
1717
1748
  this.resolveRetry();
@@ -1736,7 +1767,7 @@ export class TurnRecovery {
1736
1767
  attempt: this.#retryAttempt - 1,
1737
1768
  finalError: errorMessage,
1738
1769
  });
1739
- this.#clearPendingRecoveredRetryErrors();
1770
+ this.#clearPendingRetryErrors();
1740
1771
  }
1741
1772
  this.#retryAttempt = 0;
1742
1773
  this.resolveRetry();
@@ -1761,12 +1792,12 @@ export class TurnRecovery {
1761
1792
  attempt,
1762
1793
  finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${maxDelayMs}ms). Original error: ${errorMessage}`,
1763
1794
  });
1764
- this.#clearPendingRecoveredRetryErrors();
1795
+ this.#clearPendingRetryErrors();
1765
1796
  this.resolveRetry();
1766
1797
  return false;
1767
1798
  }
1768
1799
 
1769
- await this.#recordPendingRecoveredRetryError(message, id, { switchedCredential, switchedModel, delayMs });
1800
+ await this.#recordPendingRetryError(message, id, { switchedCredential, switchedModel, delayMs });
1770
1801
 
1771
1802
  await this.#host.emitSessionEvent({
1772
1803
  type: "auto_retry_start",
@@ -1808,7 +1839,7 @@ export class TurnRecovery {
1808
1839
  attempt,
1809
1840
  finalError: "Retry cancelled",
1810
1841
  });
1811
- this.#clearPendingRecoveredRetryErrors();
1842
+ this.#clearPendingRetryErrors();
1812
1843
  this.resolveRetry();
1813
1844
  return false;
1814
1845
  }
@@ -1881,7 +1912,7 @@ export class TurnRecovery {
1881
1912
  attempt,
1882
1913
  finalError: `Retry continuation failed locally: ${localError}. Original error: ${message.errorMessage ?? "Unknown error"}`,
1883
1914
  });
1884
- this.#clearPendingRecoveredRetryErrors();
1915
+ this.#clearPendingRetryErrors();
1885
1916
  this.resolveRetry();
1886
1917
  }
1887
1918