@opengeni/worker-bundle 1.0.0-canary.1 → 1.0.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 (244) hide show
  1. package/dist/activities/agent-run-admission.d.ts +3 -1
  2. package/dist/activities/agent-turn/admission.d.ts +24 -3
  3. package/dist/activities/agent-turn/agent-build.d.ts +4 -0
  4. package/dist/activities/agent-turn/claude-usage-observer.d.ts +58 -0
  5. package/dist/activities/agent-turn/code-search.d.ts +60 -0
  6. package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
  7. package/dist/activities/agent-turn/codex-plan-entitlement.d.ts +105 -0
  8. package/dist/activities/agent-turn/codex.d.ts +3 -2
  9. package/dist/activities/agent-turn/compaction-prep.d.ts +21 -5
  10. package/dist/activities/agent-turn/credential-leases.d.ts +19 -33
  11. package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
  12. package/dist/activities/agent-turn/errors.d.ts +82 -11
  13. package/dist/activities/agent-turn/failure-settlement.d.ts +6 -3
  14. package/dist/activities/agent-turn/final-reply.d.ts +18 -0
  15. package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
  16. package/dist/activities/agent-turn/finalization.d.ts +4 -2
  17. package/dist/activities/agent-turn/github-repository-bindings.d.ts +2 -0
  18. package/dist/activities/agent-turn/governance-model.d.ts +9 -2
  19. package/dist/activities/agent-turn/history-sink.d.ts +2 -0
  20. package/dist/activities/agent-turn/history.d.ts +2 -0
  21. package/dist/activities/agent-turn/input-wait-reply.d.ts +41 -0
  22. package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
  23. package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
  24. package/dist/activities/agent-turn/model-usage.d.ts +6 -0
  25. package/dist/activities/agent-turn/model-wire-dispatch.d.ts +15 -0
  26. package/dist/activities/agent-turn/optional-repositories.d.ts +59 -0
  27. package/dist/activities/agent-turn/provider-dispatch-barrier.d.ts +12 -1
  28. package/dist/activities/agent-turn/quiescence.d.ts +7 -7
  29. package/dist/activities/agent-turn/recovery-warning.d.ts +9 -0
  30. package/dist/activities/agent-turn/refresh-credentials.d.ts +6 -0
  31. package/dist/activities/agent-turn/run-credentials.d.ts +8 -3
  32. package/dist/activities/agent-turn/sandbox-establish.d.ts +2 -1
  33. package/dist/activities/agent-turn/sandbox-provision.d.ts +3 -1
  34. package/dist/activities/agent-turn/sandbox-route.d.ts +3 -20
  35. package/dist/activities/agent-turn/sandbox-runtime.d.ts +2 -2
  36. package/dist/activities/agent-turn/session-title.d.ts +34 -2
  37. package/dist/activities/agent-turn/skill-checkout.d.ts +25 -6
  38. package/dist/activities/agent-turn/skill-read.d.ts +70 -2
  39. package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
  40. package/dist/activities/agent-turn/skill-search.d.ts +3 -1
  41. package/dist/activities/agent-turn/skill-tools.d.ts +15 -2
  42. package/dist/activities/agent-turn/skill-transfer.d.ts +18 -6
  43. package/dist/activities/agent-turn/stream-attempt.d.ts +7 -3
  44. package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
  45. package/dist/activities/agent-turn/tool-environment.d.ts +56 -6
  46. package/dist/activities/agent-turn/tool-policy.d.ts +43 -0
  47. package/dist/activities/agent-turn/turn-context.d.ts +29 -0
  48. package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
  49. package/dist/activities/agent-turn/xai-capacity.d.ts +2 -1
  50. package/dist/activities/automations.d.ts +3 -1
  51. package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
  52. package/dist/activities/codemode-dispatcher.d.ts +9 -2
  53. package/dist/activities/codex-capacity.d.ts +18 -2
  54. package/dist/activities/codex-rotation.d.ts +7 -1
  55. package/dist/activities/context-compaction.d.ts +16 -2
  56. package/dist/activities/gmail-files.d.ts +32 -0
  57. package/dist/activities/goals.d.ts +8 -0
  58. package/dist/activities/knowledge-indexing.d.ts +19 -0
  59. package/dist/activities/mcp-account-routes.d.ts +3 -0
  60. package/dist/activities/mcp-credentials.d.ts +12 -13
  61. package/dist/activities/mcp-operation-authority.d.ts +0 -5
  62. package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
  63. package/dist/activities/open-suffix-resume.d.ts +13 -0
  64. package/dist/activities/parent-wake.d.ts +6 -12
  65. package/dist/activities/retained-screenshots.d.ts +7 -2
  66. package/dist/activities/rig-verification.d.ts +2 -3
  67. package/dist/activities/run-credential-renewal.d.ts +1 -0
  68. package/dist/activities/run-credentials.d.ts +15 -3
  69. package/dist/activities/sandbox-images.d.ts +1 -1
  70. package/dist/activities/sandbox-lease.d.ts +47 -9
  71. package/dist/activities/scheduled-tasks.d.ts +4 -35
  72. package/dist/activities/session-state.d.ts +70 -3
  73. package/dist/activities/types.d.ts +55 -6
  74. package/dist/activities/workspace-capture.d.ts +2 -0
  75. package/dist/activities/workspace-credential-provider.d.ts +46 -0
  76. package/dist/{activities-control-HMFGSKC5.js → activities-control-BNPTRTOE.js} +1896 -418
  77. package/dist/activities-control-BNPTRTOE.js.map +1 -0
  78. package/dist/activities-control.d.ts +182 -4
  79. package/dist/{activities-turn-AP67JADS.js → activities-turn-5LGZOSVU.js} +4476 -1327
  80. package/dist/activities-turn-5LGZOSVU.js.map +1 -0
  81. package/dist/activities.d.ts +182 -4
  82. package/dist/artifact-materializer-entry.js +5 -2
  83. package/dist/artifact-materializer-entry.js.map +1 -1
  84. package/dist/artifact-outbox-entry.js +5 -2
  85. package/dist/artifact-outbox-entry.js.map +1 -1
  86. package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
  87. package/dist/{chunk-A4XQWVZK.js → chunk-3ZTRWDMN.js} +307 -4
  88. package/dist/chunk-3ZTRWDMN.js.map +1 -0
  89. package/dist/{chunk-RAMNYSAW.js → chunk-4ER3YXOT.js} +22 -4
  90. package/dist/chunk-4ER3YXOT.js.map +1 -0
  91. package/dist/{chunk-CYITU5UH.js → chunk-LJX7Z2KP.js} +1445 -343
  92. package/dist/chunk-LJX7Z2KP.js.map +1 -0
  93. package/dist/editable-artifact-materializer-service.d.ts +2 -2
  94. package/dist/editable-artifact-outbox-service.d.ts +2 -2
  95. package/dist/host-export-pump.d.ts +3 -1
  96. package/dist/host-export.d.ts +1 -1
  97. package/dist/host-export.js +1 -1
  98. package/dist/http.d.ts +1 -1
  99. package/dist/index.d.ts +95 -3
  100. package/dist/index.js +137 -43
  101. package/dist/index.js.map +1 -1
  102. package/dist/observability-metrics.d.ts +118 -8
  103. package/dist/retained-process-diagnostics.d.ts +4 -0
  104. package/dist/retained-process-retry.d.ts +7 -0
  105. package/dist/sandbox-resume.d.ts +55 -13
  106. package/dist/sandbox-routing.d.ts +5 -3
  107. package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
  108. package/dist/session-event-fanout.d.ts +6 -0
  109. package/dist/warm-capture-holder.d.ts +3 -0
  110. package/dist/worker-service-lifecycle.d.ts +12 -0
  111. package/dist/workflow-bundle.js +273 -41
  112. package/dist/workflows/activities.d.ts +1 -1
  113. package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
  114. package/dist/workflows/scheduled-tasks.d.ts +4 -0
  115. package/dist/workflows/session.d.ts +36 -0
  116. package/dist/workflows.d.ts +1 -0
  117. package/package.json +21 -19
  118. package/src/activities/agent-run-admission.ts +17 -1
  119. package/src/activities/agent-turn/admission.ts +66 -3
  120. package/src/activities/agent-turn/agent-build.ts +84 -10
  121. package/src/activities/agent-turn/claim.ts +186 -43
  122. package/src/activities/agent-turn/claude-usage-observer.ts +221 -0
  123. package/src/activities/agent-turn/code-search.ts +275 -0
  124. package/src/activities/agent-turn/codex-capacity.ts +172 -8
  125. package/src/activities/agent-turn/codex-plan-entitlement.ts +254 -0
  126. package/src/activities/agent-turn/codex.ts +13 -5
  127. package/src/activities/agent-turn/compaction-prep.ts +170 -102
  128. package/src/activities/agent-turn/credential-leases.ts +122 -216
  129. package/src/activities/agent-turn/credential-restriction.ts +14 -0
  130. package/src/activities/agent-turn/errors.ts +647 -48
  131. package/src/activities/agent-turn/failure-settlement.ts +682 -130
  132. package/src/activities/agent-turn/final-reply.ts +46 -0
  133. package/src/activities/agent-turn/finalization-monitor.ts +120 -0
  134. package/src/activities/agent-turn/finalization.ts +146 -43
  135. package/src/activities/agent-turn/github-repository-bindings.ts +3 -1
  136. package/src/activities/agent-turn/governance-model.ts +69 -28
  137. package/src/activities/agent-turn/history-sink.ts +16 -2
  138. package/src/activities/agent-turn/history.ts +38 -11
  139. package/src/activities/agent-turn/input-wait-reply.ts +103 -0
  140. package/src/activities/agent-turn/knowledge-source-tools.ts +2 -0
  141. package/src/activities/agent-turn/media-artifacts.ts +2 -1
  142. package/src/activities/agent-turn/model-call-admission.ts +94 -0
  143. package/src/activities/agent-turn/model-usage.ts +64 -3
  144. package/src/activities/agent-turn/model-wire-dispatch.ts +27 -0
  145. package/src/activities/agent-turn/optional-repositories.ts +224 -0
  146. package/src/activities/agent-turn/provider-dispatch-barrier.ts +22 -0
  147. package/src/activities/agent-turn/quiescence.ts +10 -7
  148. package/src/activities/agent-turn/recovery-warning.ts +16 -0
  149. package/src/activities/agent-turn/refresh-credentials.ts +44 -0
  150. package/src/activities/agent-turn/run-credentials.ts +116 -55
  151. package/src/activities/agent-turn/run.ts +320 -93
  152. package/src/activities/agent-turn/sandbox-establish.ts +84 -8
  153. package/src/activities/agent-turn/sandbox-provision.ts +3 -1
  154. package/src/activities/agent-turn/sandbox-route.ts +4 -21
  155. package/src/activities/agent-turn/sandbox-runtime.ts +343 -18
  156. package/src/activities/agent-turn/session-title.ts +81 -5
  157. package/src/activities/agent-turn/skill-checkout.ts +111 -24
  158. package/src/activities/agent-turn/skill-install.ts +1 -1
  159. package/src/activities/agent-turn/skill-read.ts +374 -28
  160. package/src/activities/agent-turn/skill-remove.ts +46 -0
  161. package/src/activities/agent-turn/skill-save.ts +1 -1
  162. package/src/activities/agent-turn/skill-search.ts +10 -1
  163. package/src/activities/agent-turn/skill-selection.ts +4 -0
  164. package/src/activities/agent-turn/skill-tools.ts +112 -18
  165. package/src/activities/agent-turn/skill-transfer.ts +68 -23
  166. package/src/activities/agent-turn/stream-attempt.ts +313 -77
  167. package/src/activities/agent-turn/subscription-lease.ts +115 -0
  168. package/src/activities/agent-turn/tool-environment.ts +316 -167
  169. package/src/activities/agent-turn/tool-policy.ts +80 -1
  170. package/src/activities/agent-turn/turn-context.ts +35 -0
  171. package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
  172. package/src/activities/agent-turn/xai-capacity.ts +94 -51
  173. package/src/activities/automations.ts +76 -4
  174. package/src/activities/browser-deadline-checkpoint.ts +279 -0
  175. package/src/activities/capabilities.ts +108 -11
  176. package/src/activities/codemode-dispatcher.ts +36 -8
  177. package/src/activities/codex-capacity.ts +60 -25
  178. package/src/activities/codex-rotation.ts +39 -17
  179. package/src/activities/context-compaction.ts +75 -18
  180. package/src/activities/gmail-files.ts +88 -0
  181. package/src/activities/goals.ts +116 -121
  182. package/src/activities/google-drive-publication.ts +1 -3
  183. package/src/activities/image-generation-operation.ts +1 -1
  184. package/src/activities/knowledge-indexing.ts +269 -15
  185. package/src/activities/knowledge-source-sync.ts +10 -0
  186. package/src/activities/mcp-account-routes.ts +17 -0
  187. package/src/activities/mcp-credentials.ts +83 -194
  188. package/src/activities/mcp-operation-authority.ts +16 -41
  189. package/src/activities/model-call-fact-reconciler.ts +112 -0
  190. package/src/activities/open-suffix-resume.ts +15 -2
  191. package/src/activities/parent-wake.ts +46 -21
  192. package/src/activities/retained-screenshots.ts +82 -11
  193. package/src/activities/rig-verification.ts +38 -37
  194. package/src/activities/run-credential-renewal.ts +13 -1
  195. package/src/activities/run-credentials.ts +59 -17
  196. package/src/activities/run-input.ts +25 -16
  197. package/src/activities/sandbox-images.ts +0 -1
  198. package/src/activities/sandbox-lease.ts +798 -82
  199. package/src/activities/scheduled-tasks.ts +550 -178
  200. package/src/activities/session-state.ts +245 -6
  201. package/src/activities/site-auth-maintenance.ts +3 -0
  202. package/src/activities/types.ts +72 -5
  203. package/src/activities/workflow-wake.ts +2 -17
  204. package/src/activities/workspace-capture.ts +13 -2
  205. package/src/activities/workspace-credential-provider.ts +283 -0
  206. package/src/activities-control.ts +4 -0
  207. package/src/activities-turn.ts +5 -0
  208. package/src/activities.ts +13 -0
  209. package/src/activity-services.ts +16 -4
  210. package/src/browser-deadline-checkpoint-contract.ts +5 -0
  211. package/src/editable-artifact-hint-broker.ts +3 -0
  212. package/src/editable-artifact-materializer-service.ts +7 -3
  213. package/src/editable-artifact-outbox-service.ts +4 -4
  214. package/src/github-rest-mcp.ts +1 -2
  215. package/src/host-export-pump.ts +26 -4
  216. package/src/host-export.ts +3 -0
  217. package/src/http.ts +4 -2
  218. package/src/index.ts +99 -37
  219. package/src/observability-metrics.ts +510 -5
  220. package/src/personal-github-git-credentials.ts +2 -1
  221. package/src/pr-review-credentials.ts +1 -3
  222. package/src/retained-process-diagnostics.ts +50 -0
  223. package/src/retained-process-retry.ts +36 -0
  224. package/src/sandbox-resume.ts +388 -101
  225. package/src/sandbox-routing.ts +150 -25
  226. package/src/sandbox-snapshot-diagnostics.ts +112 -0
  227. package/src/session-event-fanout.ts +23 -0
  228. package/src/warm-capture-holder.ts +15 -0
  229. package/src/worker-service-lifecycle.ts +53 -0
  230. package/src/workflows/activities.ts +16 -1
  231. package/src/workflows/browser-deadline-checkpoint.ts +50 -0
  232. package/src/workflows/file-upload-reaper.ts +2 -0
  233. package/src/workflows/scheduled-tasks.ts +26 -13
  234. package/src/workflows/session.ts +242 -22
  235. package/src/workflows.ts +4 -0
  236. package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
  237. package/dist/activities/packs.d.ts +0 -55
  238. package/dist/activities-control-HMFGSKC5.js.map +0 -1
  239. package/dist/activities-turn-AP67JADS.js.map +0 -1
  240. package/dist/chunk-A4XQWVZK.js.map +0 -1
  241. package/dist/chunk-CYITU5UH.js.map +0 -1
  242. package/dist/chunk-RAMNYSAW.js.map +0 -1
  243. package/src/activities/agent-turn/knowledge-sources.ts +0 -86
  244. package/src/activities/packs.ts +0 -166
@@ -1,7 +1,13 @@
1
+ import { ClaudeSubscriptionReconnectRequired } from "@opengeni/db";
2
+ import { DrizzleQueryError } from "drizzle-orm";
3
+ import { SandboxCapabilitiesChangedError } from "./provider-dispatch-barrier";
4
+ import { ClaudeSubscriptionConnectionUnavailable } from "./claude-usage-observer";
1
5
  import {
2
6
  ActiveSessionHistoryLimitExceededError,
3
7
  ApprovalRunStateLimitExceededError,
4
8
  nestedPostgresSqlState,
9
+ databaseFailureCode,
10
+ isRetryablePersistenceSqlState,
5
11
  safeDatabaseErrorFacts,
6
12
  isRetryableDatabaseTransportFailure,
7
13
  isSessionEventPersistenceError,
@@ -9,14 +15,31 @@ import {
9
15
  } from "@opengeni/db";
10
16
  import {
11
17
  ActiveBackendUnresolvableError,
18
+ AnthropicProviderRejection,
12
19
  CompactionProviderResponseError,
13
20
  EmptyCompactionSummaryError,
21
+ compactionProviderRejection,
22
+ describeCompactionProviderRejection,
23
+ type CompactionProviderRejection,
14
24
  isMcpRequestTimeoutError,
15
25
  isMcpTransportConnectivityError,
16
- isModalTaskExecStartDnsResolutionError,
26
+ isModalTaskExecStartPreDispatchUnavailableError,
27
+ isModalCommandStartOutcomeUnknownError,
28
+ isProviderCommandObservationUnavailableError,
29
+ isRoutingMutationOutcomeUnknownError,
17
30
  RoutingWorkspaceRootChangedError,
31
+ ResponsesStreamingTerminalError,
32
+ SandboxMaterializationVerificationError,
33
+ materializationVerificationDiagnostic,
34
+ type MaterializationVerificationDiagnostic,
35
+ PROVIDER_QUOTA_EXHAUSTED_CODE,
36
+ type ProviderQuotaExhaustion,
37
+ type ProviderQuotaScope,
38
+ classifyProviderQuotaError,
39
+ providerQuotaExhaustedMessage,
18
40
  SelfhostedWorkspaceRootChangedError,
19
41
  UNKNOWN_MODEL_FINISH_REASON_CODE,
42
+ AnthropicRequestError,
20
43
  } from "@opengeni/runtime";
21
44
  import {
22
45
  mcpTransportRequestFailureDiagnostic,
@@ -27,11 +50,19 @@ import { CODEX_USAGE_EXHAUSTED_PCT } from "../codex-rotation";
27
50
  import { RetainedAttachmentTransportLimitError } from "../run-input";
28
51
  import type { CodexAccountStatus } from "@opengeni/db";
29
52
  import {
53
+ CODEX_USAGE_LIMIT_ERROR_TYPE,
30
54
  CodexReloginRequired,
55
+ classifyCodexEncryptedArtifactRejection,
56
+ classifyCodexEntitlementRejection,
31
57
  classifyCodexResponseTimeoutError,
32
58
  classifyCodexUsageLimitError,
33
59
  isCodexTransportError,
34
60
  } from "@opengeni/codex";
61
+ import {
62
+ CodexPlanEntitlementError,
63
+ codexPlanEntitlementFailurePayload,
64
+ codexRequestRejectedFailurePayload,
65
+ } from "./codex-plan-entitlement";
35
66
  import {
36
67
  classifyXaiSubscriptionStreamingTerminalError,
37
68
  classifyXaiSubscriptionStreamIdleTimeoutError,
@@ -63,6 +94,12 @@ import {
63
94
  export const PROVIDER_BACKPRESSURE_DELAY_MS = 60_000;
64
95
  export const PROVIDER_CONNECTIVITY_BACKOFF_MS = [2_000, 5_000, 15_000, 30_000, 60_000] as const;
65
96
  export const MAX_AUTOMATIC_PROVIDER_RECOVERIES = PROVIDER_CONNECTIVITY_BACKOFF_MS.length;
97
+ /**
98
+ * Minimum wait per rate-limited recovery. Providers such as Azure OpenAI often
99
+ * answer a per-minute token limit with a `retry-after` of about a second, which
100
+ * alone would spend every automatic recovery before the window resets.
101
+ */
102
+ export const PROVIDER_RATE_LIMIT_BACKOFF_MS = [10_000, 20_000, 40_000, 60_000, 120_000] as const;
66
103
  export const POST_COMPACTION_CONTINUATION_EMPTY_CODE = "post_compaction_continuation_empty";
67
104
 
68
105
  export class PostCompactionContinuationEmptyError extends Error {
@@ -106,12 +143,21 @@ export function providerRecoveryResult(input: {
106
143
  : null;
107
144
  const continueDelayMs =
108
145
  input.failureCode === "provider_rate_limited"
109
- ? (providerDelay ?? PROVIDER_BACKPRESSURE_DELAY_MS)
146
+ ? Math.max(
147
+ providerDelay ?? PROVIDER_BACKPRESSURE_DELAY_MS,
148
+ PROVIDER_RATE_LIMIT_BACKOFF_MS[
149
+ Math.min(
150
+ Math.max(Math.trunc(input.attemptNumber) - 1, 0),
151
+ PROVIDER_RATE_LIMIT_BACKOFF_MS.length - 1,
152
+ )
153
+ ]!,
154
+ )
110
155
  : input.failureCode === "provider_unavailable" ||
111
156
  input.failureCode === "upstream_connectivity_unavailable" ||
112
157
  input.failureCode === "sandbox_command_start_unavailable" ||
113
158
  input.failureCode === "mcp_transport_timeout" ||
114
159
  input.failureCode === "mcp_transport_unavailable" ||
160
+ input.failureCode === "turn_execution_policy_definition_mismatch" ||
115
161
  input.failureCode === POST_COMPACTION_CONTINUATION_EMPTY_CODE
116
162
  ? Math.max(
117
163
  providerDelay ?? 0,
@@ -187,6 +233,13 @@ export function providerRetryAfterMs(error: unknown, nowMs = Date.now()): number
187
233
  value.error && typeof value.error === "object"
188
234
  ? (value.error as Record<string, unknown>)
189
235
  : null;
236
+ const milliseconds = Number(
237
+ headerValue(value.headers, "retry-after-ms") ??
238
+ headerValue(value.responseHeaders, "retry-after-ms") ??
239
+ headerValue(body?.headers, "retry-after-ms") ??
240
+ undefined,
241
+ );
242
+ if (Number.isFinite(milliseconds) && milliseconds > 0) return Math.ceil(milliseconds);
190
243
  const directSeconds = Number(
191
244
  value.retry_after_seconds ?? body?.retry_after_seconds ?? value.retryAfterSeconds,
192
245
  );
@@ -269,22 +322,38 @@ export function escapedMcpTimeoutRecoveryFailure(input: {
269
322
  * Convert the atomic claim transaction's failure into a small, stable
270
323
  * Temporal wire contract. The original error remains in activity diagnostics,
271
324
  * but SQL text, parameters, and arbitrary invariant messages never enter
272
- * workflow history. Every database failure is safe to re-read after backoff
273
- * because the claim transaction contains no model/tool effects. Database
274
- * constraints and authorization guards can also be repaired by a deployment or
275
- * authority refresh, so they must preserve pending work. Only a non-database
276
- * claim invariant is terminal.
325
+ * workflow history. Operational failures retry; other persistence rejections
326
+ * retain accepted work behind a durable explicit-recheck fence. Repeating the
327
+ * same rejected transaction without changed conditions is not recovery.
277
328
  */
278
329
  export function preClaimAdmissionFailure(error: unknown): ApplicationFailure {
279
330
  const persistenceFailure = isSessionEventPersistenceError(error) ? error : null;
280
331
  const retryableCode = retryableDatabaseFailureCode(error);
281
- // The workflow retries after a durable re-read and bounded delay. A
282
- // SessionEventPersistenceError proves the failure happened inside the atomic
283
- // claim boundary, before provider or tool work; retaining it is safer than
284
- // destroying accepted machine input on an application SQLSTATE.
332
+ const rejectedClaim =
333
+ persistenceFailure?.details.stage === "session_attempts.claim" &&
334
+ persistenceFailure.details.retryOutcome === "not_retryable" &&
335
+ !retryableCode;
285
336
  const detail: PreClaimFailureDetail = {
286
- disposition: persistenceFailure || retryableCode ? "retryable" : "permanent",
337
+ disposition: rejectedClaim
338
+ ? "blocked"
339
+ : persistenceFailure || retryableCode
340
+ ? "retryable"
341
+ : "permanent",
287
342
  code: retryableCode ?? persistenceFailure?.details.code ?? "claim_invariant",
343
+ ...(persistenceFailure && rejectedClaim
344
+ ? {
345
+ sqlState: persistenceFailure.details.sqlState,
346
+ reason:
347
+ persistenceFailure.details.stage === "session_attempts.claim" &&
348
+ persistenceFailure.details.sqlState === "OG001"
349
+ ? ("initiator_membership_required" as const)
350
+ : persistenceFailure.details.stage === "session_attempts.claim" &&
351
+ persistenceFailure.details.sqlState === "OG002"
352
+ ? ("personal_resource_grant_required" as const)
353
+ : ("database_claim_rejected" as const),
354
+ retryPolicy: "explicit_recheck" as const,
355
+ }
356
+ : {}),
288
357
  };
289
358
  return ApplicationFailure.create({
290
359
  message: PRE_CLAIM_FAILURE_MESSAGE,
@@ -294,20 +363,177 @@ export function preClaimAdmissionFailure(error: unknown): ApplicationFailure {
294
363
  });
295
364
  }
296
365
 
366
+ /** Complete structured-cause graph or no recovery authority. Budget distinct
367
+ * objects (including array containers), not queue positions/duplicate refs.
368
+ * The separate link ceiling bounds huge duplicate arrays without truncating
369
+ * them into permission. Cycles are harmless; overflow/unreadable edges hold. */
370
+ function databaseRecoveryCauseGraph(error: unknown): Map<object, Set<object>> | null {
371
+ const graph = new Map<object, Set<object>>();
372
+ const queue: object[] = [];
373
+ let links = 0;
374
+ const add = (value: unknown): boolean => {
375
+ if (!value || typeof value !== "object" || graph.has(value)) return true;
376
+ if (graph.size >= 64) return false;
377
+ graph.set(value, new Set());
378
+ queue.push(value);
379
+ return true;
380
+ };
381
+ if (!add(error)) return null;
382
+ try {
383
+ for (const current of queue) {
384
+ const record = current as Record<string, unknown>;
385
+ const children = Array.isArray(current)
386
+ ? current
387
+ : ["cause", "original", "driverError", "error", "errors"].map((key) => record[key]);
388
+ for (const child of children) {
389
+ if (++links > 4096 || !add(child)) return null;
390
+ if (child && typeof child === "object") graph.get(current)!.add(child);
391
+ }
392
+ }
393
+ } catch {
394
+ return null;
395
+ }
396
+ return graph;
397
+ }
398
+
297
399
  function retryableDatabaseFailureCode(
298
400
  error: unknown,
401
+ requireDatabaseProvenance = false,
299
402
  ): PostClaimDatabaseRecoveryDetail["code"] | null {
300
- const persistenceFailure = isSessionEventPersistenceError(error) ? error : null;
301
- const sqlState = persistenceFailure?.details.sqlState ?? null;
302
- if (sqlState === null && isRetryableDatabaseTransportFailure(error)) {
303
- return "db_failure";
403
+ try {
404
+ const graph = databaseRecoveryCauseGraph(error);
405
+ if (!graph) return null;
406
+ const transports = new Set<object>();
407
+ const boundaries = new Set<object>();
408
+ const ownDatabaseNodes = new Set<object>();
409
+ const codes = new Set<PostClaimDatabaseRecoveryDetail["code"]>();
410
+ // Ask the canonical transport predicate about ONLY this node's facts. Its
411
+ // recursive search must not pair a DB sibling with an unrelated provider.
412
+ for (const node of graph.keys()) {
413
+ if (isRoutingMutationOutcomeUnknownError(node)) return null;
414
+ const record = node as Record<string, unknown>;
415
+ if (
416
+ requireDatabaseProvenance
417
+ ? isRunningTurnDatabaseTransportFailure(record)
418
+ : isRetryableDatabaseTransportFailure({ code: record.code, errno: record.errno })
419
+ )
420
+ transports.add(node);
421
+ const sqlState = isSessionEventPersistenceError(node)
422
+ ? node.details.sqlState
423
+ : record.name === "PostgresError" && typeof record.code === "string"
424
+ ? record.code
425
+ : null;
426
+ if (
427
+ requireDatabaseProvenance
428
+ ? isRunningTurnDatabaseConnectionSqlState(sqlState)
429
+ : isDatabaseConnectionSqlState(sqlState)
430
+ )
431
+ transports.add(node);
432
+ if (node instanceof DrizzleQueryError || isSessionEventPersistenceError(node)) {
433
+ // Only actual errors raised at our ORM/typed persistence boundary own
434
+ // their driver subtree. A PostgresError name, SDK wrapper or provider
435
+ // socket by itself is never own-client provenance for a running turn.
436
+ const queue: object[] = [node];
437
+ for (const source of queue) {
438
+ if (ownDatabaseNodes.has(source)) continue;
439
+ ownDatabaseNodes.add(source);
440
+ queue.push(...graph.get(source)!);
441
+ }
442
+ }
443
+ }
444
+ const hasOwnTransport = (boundary: object): boolean => {
445
+ const seen = new Set<object>();
446
+ const queue = [boundary];
447
+ for (const node of queue) {
448
+ if (seen.has(node)) continue;
449
+ seen.add(node);
450
+ if (transports.has(node)) return true;
451
+ queue.push(...graph.get(node)!);
452
+ }
453
+ return false;
454
+ };
455
+ // Inspect ALL trusted DB evidence before returning a permit. A permanent,
456
+ // auth or uncertain SQLSTATE vetoes every sibling regardless of traversal
457
+ // order; a deeper transport/reset cannot override it either.
458
+ for (const node of graph.keys()) {
459
+ const record = node as Record<string, unknown>;
460
+ if (node instanceof DrizzleQueryError) {
461
+ boundaries.add(node);
462
+ if (hasOwnTransport(node)) codes.add("db_failure");
463
+ }
464
+ const typed = isSessionEventPersistenceError(node);
465
+ if (!typed && record.name !== "PostgresError") continue;
466
+ boundaries.add(node);
467
+ const sqlState = typed
468
+ ? node.details.sqlState
469
+ : typeof record.code === "string"
470
+ ? record.code
471
+ : null;
472
+ const connectionOutage = requireDatabaseProvenance
473
+ ? isRunningTurnDatabaseConnectionSqlState(sqlState)
474
+ : isDatabaseConnectionSqlState(sqlState);
475
+ const code = typed
476
+ ? retryablePersistenceFailureCode(sqlState)
477
+ : connectionOutage || isRetryablePersistenceSqlState(sqlState)
478
+ ? databaseFailureCode(sqlState)
479
+ : null;
480
+ if (
481
+ !code ||
482
+ (requireDatabaseProvenance &&
483
+ !connectionOutage &&
484
+ !(sqlState === null && hasOwnTransport(node)))
485
+ )
486
+ return null;
487
+ if (!requireDatabaseProvenance || ownDatabaseNodes.has(node)) codes.add(code);
488
+ }
489
+ // Preserve the legacy pre-execution transport-only allowance, but never
490
+ // borrow it across an explicit DB boundary with no eligible own evidence.
491
+ if (!requireDatabaseProvenance && boundaries.size === 0 && transports.size > 0)
492
+ codes.add("db_failure");
493
+ // Stable classification for multiple positive DB siblings too.
494
+ for (const code of ["db_failure", "db_deadlock", "db_serialization_failure"] as const)
495
+ if (codes.has(code)) return code;
496
+ return null;
497
+ } catch {
498
+ // Unreadable structured facts are no more authority than unreadable edges.
499
+ return null;
304
500
  }
501
+ }
502
+
503
+ const RUNNING_TURN_DATABASE_TRANSPORT_CODES = new Set([
504
+ "ECONNREFUSED",
505
+ "ECONNRESET",
506
+ "CONNECT_TIMEOUT",
507
+ ]);
508
+
509
+ function isRunningTurnDatabaseTransportFailure(record: Record<string, unknown>): boolean {
510
+ return [record.code, record.errno].some(
511
+ (code) => typeof code === "string" && RUNNING_TURN_DATABASE_TRANSPORT_CODES.has(code),
512
+ );
513
+ }
514
+
515
+ function isRunningTurnDatabaseConnectionSqlState(sqlState: string | null): boolean {
516
+ return (
517
+ sqlState !== null &&
518
+ (/^08[0-9A-Z]{3}$/.test(sqlState) || ["57P01", "57P02", "57P03"].includes(sqlState))
519
+ );
520
+ }
521
+
522
+ function isDatabaseConnectionSqlState(sqlState: string | null): boolean {
523
+ return (
524
+ sqlState !== null &&
525
+ (sqlState.startsWith("08") || ["57P01", "57P02", "57P03"].includes(sqlState))
526
+ );
527
+ }
528
+
529
+ function retryablePersistenceFailureCode(
530
+ sqlState: string | null,
531
+ ): PostClaimDatabaseRecoveryDetail["code"] | null {
305
532
  if (
306
- !persistenceFailure ||
307
533
  !(
308
534
  sqlState === null ||
309
535
  sqlState.startsWith("08") ||
310
- sqlState.startsWith("40") ||
536
+ isRetryablePersistenceSqlState(sqlState) ||
311
537
  sqlState.startsWith("53") ||
312
538
  sqlState === "55P03" ||
313
539
  sqlState === "57014" ||
@@ -319,26 +545,43 @@ function retryableDatabaseFailureCode(
319
545
  ) {
320
546
  return null;
321
547
  }
322
- return persistenceFailure.details.code;
548
+ return databaseFailureCode(sqlState);
323
549
  }
324
550
 
325
551
  /**
326
- * Carry one exact claimed-but-not-started attempt into the workflow's DB-only
327
- * recovery lane. Permanent database/state failures remain terminal; only the
328
- * same operational outage classes that are safe before claim are admitted.
552
+ * Carry one exact claimed attempt into the workflow's DB-only
553
+ * recovery lane. Permanent database/state failures remain terminal. The
554
+ * running-turn lane additionally requires a closed own-client outage class;
555
+ * existing pre-execution recovery classifications are unchanged.
329
556
  */
330
557
  export function postClaimDatabaseRecoveryFailure(input: {
331
558
  error: unknown;
332
559
  turnId: string;
333
560
  triggerEventId: string;
334
561
  executionGeneration: number;
562
+ /** Require an actual own-client boundary and the closed running-turn allowlist. */
563
+ requireDatabaseProvenance?: boolean;
564
+ sandboxSetupOutcomeUnknown?: true;
565
+ sandboxSetupRecoveryExhausted?: true;
335
566
  providerRecovery?: {
336
567
  failureCode: string;
337
568
  providerRecoveryCount: number;
338
569
  };
339
570
  }): ApplicationFailure | null {
340
- const code = retryableDatabaseFailureCode(input.error);
341
- if (!code || input.executionGeneration < 1) return null;
571
+ const code = retryableDatabaseFailureCode(input.error, input.requireDatabaseProvenance);
572
+ if (
573
+ !code ||
574
+ input.executionGeneration < 1 ||
575
+ (input.requireDatabaseProvenance && code !== "db_failure")
576
+ )
577
+ return null;
578
+ if (
579
+ (input.sandboxSetupOutcomeUnknown && input.sandboxSetupRecoveryExhausted) ||
580
+ ((input.sandboxSetupOutcomeUnknown || input.sandboxSetupRecoveryExhausted) &&
581
+ input.providerRecovery)
582
+ ) {
583
+ return null;
584
+ }
342
585
  if (
343
586
  input.providerRecovery &&
344
587
  (!Number.isSafeInteger(input.providerRecovery.providerRecoveryCount) ||
@@ -353,6 +596,8 @@ export function postClaimDatabaseRecoveryFailure(input: {
353
596
  triggerEventId: input.triggerEventId,
354
597
  executionGeneration: input.executionGeneration,
355
598
  code,
599
+ ...(input.sandboxSetupOutcomeUnknown ? { sandboxSetupOutcomeUnknown: true } : {}),
600
+ ...(input.sandboxSetupRecoveryExhausted ? { sandboxSetupRecoveryExhausted: true } : {}),
356
601
  ...(input.providerRecovery
357
602
  ? {
358
603
  providerFailureCode: input.providerRecovery.failureCode,
@@ -439,8 +684,25 @@ export function sandboxLifecycleTransitionDiagnostic(
439
684
  return null;
440
685
  }
441
686
 
687
+ export function modelPreparationFailureEventPayload(error: unknown, durationMs: number) {
688
+ const transition = sandboxLifecycleTransitionDiagnostic(error);
689
+ return {
690
+ phase: "model_preparation",
691
+ durationMs: Math.max(0, Math.round(durationMs)),
692
+ expectedTransition: transition !== null,
693
+ ...(transition
694
+ ? {
695
+ failureCategory: "drain_capture_wait",
696
+ failureStage: "lifecycle_wait",
697
+ failureCode: transition.reason,
698
+ retryable: true,
699
+ }
700
+ : {}),
701
+ };
702
+ }
703
+
442
704
  /**
443
- * Recognize the one active-route transition that cannot finish inside its
705
+ * Recognize active-route transitions that cannot finish inside their
444
706
  * originating attempt. A Modal-home session may start on a Connected Machine
445
707
  * without creating or leasing its managed home box. When an explicit attach
446
708
  * clears the active pointer back to home, the pointer commit is authoritative,
@@ -453,7 +715,11 @@ export function sandboxLifecycleTransitionDiagnostic(
453
715
  */
454
716
  export function sandboxRouteTransitionCode(
455
717
  error: unknown,
456
- ): "home_unavailable_this_turn" | "workspace_root_changed_this_turn" | null {
718
+ ):
719
+ | "home_unavailable_this_turn"
720
+ | "workspace_root_changed_this_turn"
721
+ | "native_capabilities_changed_this_attempt"
722
+ | null {
457
723
  const pending: unknown[] = [error];
458
724
  const seen = new WeakSet<object>();
459
725
  let inspected = 0;
@@ -466,6 +732,13 @@ export function sandboxRouteTransitionCode(
466
732
 
467
733
  try {
468
734
  const record = current as Record<string, unknown>;
735
+ if (
736
+ (current instanceof SandboxCapabilitiesChangedError ||
737
+ record.name === "SandboxCapabilitiesChangedError") &&
738
+ record.code === "native_capabilities_changed_this_attempt"
739
+ ) {
740
+ return "native_capabilities_changed_this_attempt";
741
+ }
469
742
  if (
470
743
  (current instanceof ActiveBackendUnresolvableError ||
471
744
  record.name === "ActiveBackendUnresolvableError") &&
@@ -574,7 +847,29 @@ export function safeErrorForTelemetry(error: unknown): Error {
574
847
  return safe;
575
848
  }
576
849
 
850
+ /**
851
+ * Guidance appended to a definitive provider rejection. Repeating the exact
852
+ * request cannot succeed, so the generic "send another message to retry"
853
+ * advice is wrong here: a new prompt re-sends the same rejected history.
854
+ */
855
+ export const COMPACTION_PROVIDER_REJECTION_GUIDANCE =
856
+ "The provider refused this exact request, so repeating it fails the same way until the conversation changes. If a new message fails again, start a new session.";
857
+
577
858
  export function compactionFailureReasonFromError(error: unknown): string {
859
+ const rejection = compactionProviderRejection(error);
860
+ if (rejection) {
861
+ return compactionFailureReason(
862
+ `the model provider rejected the compaction request (${describeCompactionProviderRejection(rejection)}). Active history was preserved. ${COMPACTION_PROVIDER_REJECTION_GUIDANCE}`,
863
+ );
864
+ }
865
+ // An exhausted provider quota is not retried (see agentRunFailurePayload),
866
+ // so name the refusal plainly instead of the raw diagnostic envelope.
867
+ const quota = classifyProviderQuotaExhaustionError(error);
868
+ if (quota) {
869
+ return compactionFailureReason(
870
+ `${providerQuotaExhaustedMessage(quota.scope)} Active history was preserved.`,
871
+ );
872
+ }
578
873
  if (
579
874
  error instanceof CompactionProviderResponseError ||
580
875
  error instanceof EmptyCompactionSummaryError
@@ -585,6 +880,39 @@ export function compactionFailureReasonFromError(error: unknown): string {
585
880
  return compactionFailureReason(`unexpected ${errorName}`);
586
881
  }
587
882
 
883
+ /**
884
+ * Exact `turn.failed` payload for a terminal compaction failure. A definitive
885
+ * provider rejection additionally carries its closed identifier record so the
886
+ * timeline, API consumers, and operators can name the rejected field without
887
+ * parsing the message.
888
+ */
889
+ export function compactionFailureTurnEventPayload(
890
+ error: unknown,
891
+ overrides: { error?: string } = {},
892
+ ): {
893
+ error: string;
894
+ code: "context_compaction_failed";
895
+ retryable: false;
896
+ recovery: "user_message";
897
+ compacted: false;
898
+ providerRejection?: CompactionProviderRejection;
899
+ quotaScope?: ProviderQuotaScope;
900
+ } {
901
+ const rejection = compactionProviderRejection(error);
902
+ // The same closed marker as a `provider_quota_exhausted` turn failure, so
903
+ // clients can name the exhausted limit and offer another model here too.
904
+ const quota = rejection ? null : classifyProviderQuotaExhaustionError(error);
905
+ return {
906
+ error: overrides.error ?? compactionFailureReasonFromError(error),
907
+ code: "context_compaction_failed",
908
+ retryable: false,
909
+ recovery: "user_message",
910
+ compacted: false,
911
+ ...(rejection ? { providerRejection: rejection } : {}),
912
+ ...(quota ? { quotaScope: quota.scope } : {}),
913
+ };
914
+ }
915
+
588
916
  export function isCompactionSummaryFailure(error: unknown): boolean {
589
917
  return (
590
918
  error instanceof CompactionProviderResponseError || error instanceof EmptyCompactionSummaryError
@@ -594,12 +922,24 @@ export function isCompactionSummaryFailure(error: unknown): boolean {
594
922
  export function shouldRecoverCompactionProviderFailure(error: unknown): boolean {
595
923
  if (!(error instanceof CompactionProviderResponseError)) return false;
596
924
  if (isCodexTransportError(error) && classifyCodexUsageLimitError(error)) return true;
925
+ // Codex may reject an opaque artifact it minted itself on the compaction
926
+ // request exactly as it can on an ordinary request. Failure settlement
927
+ // invalidates only the exact participating artifacts and recovers the same
928
+ // logical turn; when nothing can be invalidated it fails closed there.
929
+ if (classifyCodexEncryptedArtifactRejection(error)) return true;
930
+ // A ChatGPT plan that no longer includes the model refuses the compaction
931
+ // request exactly as it refuses an ordinary one (often with an empty 400).
932
+ // Failure settlement re-checks the plan and, when it proves the loss,
933
+ // excludes that account for the model and fails the same turn over; an
934
+ // unexplained rejection still fails there with typed copy.
935
+ if (classifyCodexEntitlementRejection(error)) return true;
597
936
  return agentRunFailurePayload(error).retryable === true;
598
937
  }
599
938
 
600
939
  export function classifyContextWindowOverflowError(
601
940
  error: unknown,
602
941
  ): { message: string; code?: string; detail?: string } | null {
942
+ if (isProviderSafetyRefusal(error)) return null;
603
943
  const fields = collectErrorStrings(error);
604
944
  const matched = fields.find(
605
945
  (value) =>
@@ -666,6 +1006,9 @@ export function collectErrorStrings(value: unknown, seen = new WeakSet<object>()
666
1006
  }
667
1007
  seen.add(value);
668
1008
  const out: string[] = [];
1009
+ // This diagnostic is already provider-owned and byte-bounded. Do not widen
1010
+ // generic detail traversal to arbitrary application payloads.
1011
+ if (value instanceof ResponsesStreamingTerminalError) out.push(value.detail);
669
1012
  const record = value as Record<string, unknown>;
670
1013
  for (const key of ["message", "code", "type", "name", "param"]) {
671
1014
  const field = record[key];
@@ -728,9 +1071,12 @@ export function isExactStatuslessUpstreamConnectivityMessage(message: string): b
728
1071
  }
729
1072
 
730
1073
  function providerSafetyRefusalDiagnostic(error: unknown): string | undefined {
1074
+ if (error instanceof ResponsesStreamingTerminalError) {
1075
+ return error.category === "safety" ? error.detail : undefined;
1076
+ }
731
1077
  return collectErrorStrings(error).find(
732
1078
  (value) =>
733
- /^(?:content_policy_violation|content_filter|safety_violation|bio_policy|cyber_policy)$/.test(
1079
+ /^(?:content_policy_violation|content_filter|safety_violation|bio_policy|cyber_policy|misalignment_policy_violation)$/.test(
734
1080
  value,
735
1081
  ) || /\bthis request was blocked by our safety systems\b/i.test(value),
736
1082
  );
@@ -741,6 +1087,9 @@ function isProviderSafetyRefusal(error: unknown): boolean {
741
1087
  }
742
1088
 
743
1089
  export function isTransientProviderError(error: unknown): boolean {
1090
+ if (error instanceof ResponsesStreamingTerminalError) {
1091
+ return error.category === "unavailable";
1092
+ }
744
1093
  // A semantic refusal can arrive inside a 5xx transport envelope.
745
1094
  if (isProviderSafetyRefusal(error)) return false;
746
1095
  const status =
@@ -774,6 +1123,27 @@ export function isTransientProviderError(error: unknown): boolean {
774
1123
  );
775
1124
  }
776
1125
 
1126
+ /** An explicit `usage_limit_reached` type or code string anywhere on the error chain. */
1127
+ function hasCodexUsageLimitType(error: unknown): boolean {
1128
+ return collectErrorStrings(error).some((value) => value.includes(CODEX_USAGE_LIMIT_ERROR_TYPE));
1129
+ }
1130
+
1131
+ /**
1132
+ * Recognize an exhausted API-key provider quota (a daily or monthly allowance,
1133
+ * a free-tier day cap, or an account out of credits) as distinct from an
1134
+ * ordinary per-minute rate limit. Retrying within the bounded same-turn budget
1135
+ * cannot succeed, so the turn fails promptly instead. Subscription transports
1136
+ * own their quota semantics through credential rotation and durable capacity
1137
+ * waits, so a Codex or SuperGrok transport error never classifies here.
1138
+ */
1139
+ export function classifyProviderQuotaExhaustionError(
1140
+ error: unknown,
1141
+ ): ProviderQuotaExhaustion | null {
1142
+ if (isCodexTransportError(error) || isXaiSubscriptionTransportError(error)) return null;
1143
+ // The same reader the OpenAI SDK retry veto uses, so the two never disagree.
1144
+ return classifyProviderQuotaError(error);
1145
+ }
1146
+
777
1147
  export type XaiCredentialFailure = {
778
1148
  kind: "auth" | "forbidden" | "rate_limit";
779
1149
  cooldownMs: number | null;
@@ -848,9 +1218,63 @@ function findPostgresDriverError(error: unknown): Record<string, unknown> | null
848
1218
  return null;
849
1219
  }
850
1220
 
1221
+ function isRawDatabaseQueryError(error: unknown): boolean {
1222
+ return (
1223
+ error instanceof Error &&
1224
+ "query" in error &&
1225
+ typeof error.query === "string" &&
1226
+ "params" in error &&
1227
+ Array.isArray(error.params)
1228
+ );
1229
+ }
1230
+
1231
+ function anthropicRequestDiagnostic(error: unknown): AnthropicRequestError | undefined {
1232
+ if (error instanceof AnthropicRequestError) return error;
1233
+ return error instanceof Error && error.cause instanceof AnthropicRequestError
1234
+ ? error.cause
1235
+ : undefined;
1236
+ }
1237
+
851
1238
  export function agentRunFailurePayload(
852
1239
  error: unknown,
853
1240
  options: { isCodexTurn?: boolean } = {},
1241
+ ): ReturnType<typeof baseAgentRunFailurePayload> {
1242
+ const failure = baseAgentRunFailurePayload(error, options);
1243
+ const diagnostic = materializationVerificationDiagnostic(error);
1244
+ const anthropic = anthropicRequestDiagnostic(error);
1245
+ if (anthropic) {
1246
+ const authenticationRejected =
1247
+ anthropic.status === 401 &&
1248
+ (error === anthropic ||
1249
+ (error instanceof Error && (error as Error & { status?: unknown }).status === 401));
1250
+ return {
1251
+ ...failure,
1252
+ code:
1253
+ failure.code ??
1254
+ (authenticationRejected ? "anthropic_authentication_error" : anthropic.code),
1255
+ retryable: failure.retryable ?? false,
1256
+ ...(anthropic.detail ? { detail: anthropic.detail } : {}),
1257
+ ...(anthropic.request_id ? { requestId: anthropic.request_id } : {}),
1258
+ };
1259
+ }
1260
+ return diagnostic ? { ...failure, materializationDiagnostic: diagnostic } : failure;
1261
+ }
1262
+
1263
+ /** Keep Anthropic provider text on terminal failures, never recovery events. */
1264
+ export function agentRunRecoveryFailurePayload(
1265
+ error: unknown,
1266
+ failure: ReturnType<typeof agentRunFailurePayload>,
1267
+ ): ReturnType<typeof agentRunFailurePayload> {
1268
+ if (!anthropicRequestDiagnostic(error)) return failure;
1269
+ // Project a copy: retry exhaustion still needs the terminal diagnostic.
1270
+ const recovery = { ...failure };
1271
+ delete recovery.detail;
1272
+ return recovery;
1273
+ }
1274
+
1275
+ function baseAgentRunFailurePayload(
1276
+ error: unknown,
1277
+ options: { isCodexTurn?: boolean } = {},
854
1278
  ): {
855
1279
  error: string;
856
1280
  code?: string;
@@ -870,12 +1294,30 @@ export function agentRunFailurePayload(
870
1294
  database?: Record<string, string>;
871
1295
  historyPersistenceStage?: MandatoryHistoryPersistenceStage;
872
1296
  mcpTransportDiagnostic?: McpTransportRequestFailureDiagnostic;
1297
+ materializationDiagnostic?: MaterializationVerificationDiagnostic;
1298
+ quotaScope?: ProviderQuotaScope;
1299
+ /** Closed Codex plan key on `codex_plan_entitlement` / `codex_request_rejected`. */
1300
+ planType?: string | null;
1301
+ /** Product model id a `codex_plan_entitlement` failure refers to. */
1302
+ model?: string | null;
873
1303
  } {
1304
+ if (error instanceof SandboxMaterializationVerificationError) {
1305
+ return {
1306
+ error: error.message,
1307
+ code: error.code,
1308
+ retryable: false,
1309
+ materializationDiagnostic: error.diagnostic,
1310
+ };
1311
+ }
874
1312
  if (error instanceof RetainedAttachmentTransportLimitError) {
875
1313
  return { error: error.message, code: "retained_attachment_transport_limit", retryable: false };
876
1314
  }
877
1315
  if (error instanceof MandatoryHistoryPersistenceError) {
878
- const underlying = isSessionEventPersistenceError(error.cause)
1316
+ const databaseFailure =
1317
+ isSessionEventPersistenceError(error.cause) ||
1318
+ findPostgresDriverError(error.cause) !== null ||
1319
+ isRawDatabaseQueryError(error.cause);
1320
+ const underlying = databaseFailure
879
1321
  ? agentRunFailurePayload(error.cause, options)
880
1322
  : {
881
1323
  error: error.cause instanceof Error ? error.cause.message : String(error.cause),
@@ -885,6 +1327,32 @@ export function agentRunFailurePayload(
885
1327
  historyPersistenceStage: error.stage,
886
1328
  };
887
1329
  }
1330
+ // Raw ORM wrappers can contain the full SQL and its parameters. Classify a
1331
+ // real driver before provider message heuristics; the original cause stays
1332
+ // available to internal diagnostics. This payload grants no replay authority.
1333
+ const postgresDriverError = findPostgresDriverError(error);
1334
+ const rawOrmFailure = isRawDatabaseQueryError(error);
1335
+ if ((postgresDriverError || rawOrmFailure) && !isSessionEventPersistenceError(error)) {
1336
+ const database = safeDatabaseErrorFacts(postgresDriverError ?? error);
1337
+ const sqlState = postgresDriverError ? nestedPostgresSqlState(postgresDriverError) : null;
1338
+ return {
1339
+ error: "Opengeni encountered a database error.",
1340
+ code: databaseFailureCode(sqlState),
1341
+ sqlState,
1342
+ ...(Object.keys(database).length > 0 ? { database } : {}),
1343
+ };
1344
+ }
1345
+ if (error instanceof ClaudeSubscriptionConnectionUnavailable) {
1346
+ return { error: error.message, code: error.code, retryable: false };
1347
+ }
1348
+ if (error instanceof AnthropicProviderRejection) {
1349
+ return {
1350
+ error: error.message,
1351
+ code: error.code === "content_policy_violation" ? "provider_safety_refusal" : error.code,
1352
+ retryable: false,
1353
+ ...(error.request_id ? { requestId: error.request_id } : {}),
1354
+ };
1355
+ }
888
1356
  const safetyRefusalDiagnostic = providerSafetyRefusalDiagnostic(error);
889
1357
  if (safetyRefusalDiagnostic !== undefined) {
890
1358
  return {
@@ -895,6 +1363,41 @@ export function agentRunFailurePayload(
895
1363
  detail: safetyRefusalDiagnostic,
896
1364
  };
897
1365
  }
1366
+ if (error instanceof ResponsesStreamingTerminalError) {
1367
+ const quota =
1368
+ error.category === "unknown" || error.category === "rate_limit"
1369
+ ? classifyProviderQuotaExhaustionError({
1370
+ code: error.code,
1371
+ error: { code: error.code, type: error.type, message: error.detail },
1372
+ retryAfterSeconds: error.retryAfterSeconds,
1373
+ })
1374
+ : null;
1375
+ if (quota) {
1376
+ return {
1377
+ error: providerQuotaExhaustedMessage(quota.scope),
1378
+ code: PROVIDER_QUOTA_EXHAUSTED_CODE,
1379
+ retryable: false,
1380
+ quotaScope: quota.scope,
1381
+ detail: error.detail,
1382
+ };
1383
+ }
1384
+ return {
1385
+ error:
1386
+ error.category === "rate_limit"
1387
+ ? "Model provider rate limit hit. Try again in a minute or lower the reasoning effort."
1388
+ : error.category === "unavailable"
1389
+ ? "The model provider is temporarily unavailable. The same turn will retry after a short delay."
1390
+ : "The model provider rejected the response. Automatic retries stopped.",
1391
+ code:
1392
+ error.category === "rate_limit"
1393
+ ? "provider_rate_limited"
1394
+ : error.category === "unavailable"
1395
+ ? "provider_unavailable"
1396
+ : "provider_request_rejected",
1397
+ retryable: error.category === "rate_limit" || error.category === "unavailable",
1398
+ detail: error.detail,
1399
+ };
1400
+ }
898
1401
  const message = error instanceof Error ? error.message : String(error);
899
1402
  const status =
900
1403
  typeof error === "object" && error !== null
@@ -933,10 +1436,29 @@ export function agentRunFailurePayload(
933
1436
  retryable: true,
934
1437
  };
935
1438
  }
936
- if (isModalTaskExecStartDnsResolutionError(error)) {
1439
+ if (isProviderCommandObservationUnavailableError(error)) {
937
1440
  return {
938
1441
  error:
939
- "The managed sandbox command transport was temporarily unreachable before the command started. The same turn will retry after a short delay.",
1442
+ "A managed sandbox command cannot be observed. Its exact invocation and writer remain retained; setup is blocked without replay until the incomplete operation can be reconciled.",
1443
+ code: "sandbox_command_observation_unavailable",
1444
+ retryable: false,
1445
+ };
1446
+ }
1447
+ if (isModalCommandStartOutcomeUnknownError(error)) {
1448
+ return {
1449
+ error:
1450
+ "A managed sandbox command has an unknown outcome. Its original invocation remains fenced; setup is blocked without replay until the incomplete operation can be reconciled.",
1451
+ code: "sandbox_command_start_outcome_unknown",
1452
+ retryable: false,
1453
+ };
1454
+ }
1455
+ if (
1456
+ !isRoutingMutationOutcomeUnknownError(error) &&
1457
+ isModalTaskExecStartPreDispatchUnavailableError(error)
1458
+ ) {
1459
+ return {
1460
+ error:
1461
+ "The managed sandbox command router was not ready before the command was sent. The same turn will retry after a short delay.",
940
1462
  code: "sandbox_command_start_unavailable",
941
1463
  retryable: true,
942
1464
  };
@@ -1001,6 +1523,29 @@ export function agentRunFailurePayload(
1001
1523
  ...(Object.keys(details.database).length > 0 ? { database: details.database } : {}),
1002
1524
  };
1003
1525
  }
1526
+ // Codex plan entitlement: the settlement path normally re-checks the plan
1527
+ // and records a precise payload. These branches keep any other path from
1528
+ // surfacing the SDK's raw "400 status code (no body)" text.
1529
+ if (error instanceof CodexPlanEntitlementError) {
1530
+ return error.payload;
1531
+ }
1532
+ const entitlementRejection = classifyCodexEntitlementRejection(error);
1533
+ if (entitlementRejection) {
1534
+ return entitlementRejection.evidence === "plan_entitlement"
1535
+ ? codexPlanEntitlementFailurePayload({
1536
+ accountLabel: null,
1537
+ planType: null,
1538
+ planChanged: false,
1539
+ modelId: null,
1540
+ rejection: entitlementRejection,
1541
+ })
1542
+ : codexRequestRejectedFailurePayload({
1543
+ accountLabel: null,
1544
+ planType: null,
1545
+ rejection: entitlementRejection,
1546
+ planChecked: false,
1547
+ });
1548
+ }
1004
1549
  // A ChatGPT/Codex usage cap is a HARD limit, not transient backpressure: it
1005
1550
  // must NOT be reported as a generic, retryable rate-limit (which would loop a
1006
1551
  // goal against a capped backend). Surface a precise, actionable message with
@@ -1011,8 +1556,11 @@ export function agentRunFailurePayload(
1011
1556
  // `usage_limit_reached` shape must still outrank generic 429 retryability.
1012
1557
  // Credential quarantine/failover remains separately provenance-gated by
1013
1558
  // `isCodexTransportError`; this branch only chooses the truthful user payload.
1559
+ // The looser "429 ... usage limit" wording counts only on a Codex transport
1560
+ // error: an API-key provider's 429 that says "usage limit" is provider quota
1561
+ // evidence, not a ChatGPT/Codex subscription cap.
1014
1562
  const usageLimit = classifyCodexUsageLimitError(error);
1015
- if (usageLimit) {
1563
+ if (usageLimit && (isCodexTransportError(error) || hasCodexUsageLimitType(error))) {
1016
1564
  return codexUsageLimitFailurePayload(usageLimit, message);
1017
1565
  }
1018
1566
  const codexTimeout = classifyCodexResponseTimeoutError(error, {
@@ -1064,10 +1612,33 @@ export function agentRunFailurePayload(
1064
1612
  retryable: true,
1065
1613
  };
1066
1614
  }
1615
+ // An exhausted quota also arrives as HTTP 429 (or 402), but no retry within
1616
+ // the finite same-turn budget can succeed. Fail the turn promptly with a
1617
+ // distinct code so the client can offer another model; ordinary short rate
1618
+ // limits fall through to the retryable branch below.
1619
+ if (status === 402 && code === "anthropic_billing_error") {
1620
+ return {
1621
+ error: "Claude could not bill this request. Check the account's billing and payment details.",
1622
+ code: "provider_billing_error",
1623
+ retryable: false,
1624
+ ...(message ? { detail: message } : {}),
1625
+ };
1626
+ }
1627
+ const quota = classifyProviderQuotaExhaustionError(error);
1628
+ if (quota) {
1629
+ return {
1630
+ error: providerQuotaExhaustedMessage(quota.scope),
1631
+ code: PROVIDER_QUOTA_EXHAUSTED_CODE,
1632
+ retryable: false,
1633
+ quotaScope: quota.scope,
1634
+ ...(message ? { detail: message } : {}),
1635
+ };
1636
+ }
1067
1637
  if (
1068
1638
  status === 429 ||
1069
- code === "rate_limit_exceeded" ||
1070
- /(?:too many requests|rate.?limit|\b429\b)/i.test(message)
1639
+ ((status === undefined || !Number.isFinite(status)) &&
1640
+ (code === "rate_limit_exceeded" ||
1641
+ /(?:too many requests|rate.?limit|\b429\b)/i.test(message)))
1071
1642
  ) {
1072
1643
  return {
1073
1644
  error: "Model provider rate limit hit. Try again in a minute or lower the reasoning effort.",
@@ -1084,30 +1655,22 @@ export function agentRunFailurePayload(
1084
1655
  if (isExactStatuslessUpstreamConnectivityMessage(message)) {
1085
1656
  return {
1086
1657
  error:
1087
- "OpenGeni could not reach an upstream service. The same turn will retry after a short delay.",
1658
+ "Opengeni could not reach an upstream service. The same turn will retry after a short delay.",
1088
1659
  code: "upstream_connectivity_unavailable",
1089
1660
  retryable: true,
1090
1661
  };
1091
1662
  }
1092
1663
  return { error: message, code: "provider_unavailable", retryable: true };
1093
1664
  }
1094
- const postgresDriverError = findPostgresDriverError(error);
1095
- if (postgresDriverError) {
1096
- const database = safeDatabaseErrorFacts(postgresDriverError);
1097
- const sqlState = nestedPostgresSqlState(postgresDriverError);
1098
- if (sqlState !== null || Object.keys(database).length > 0) {
1099
- return {
1100
- error: message,
1101
- sqlState,
1102
- ...(Object.keys(database).length > 0 ? { database } : {}),
1103
- };
1104
- }
1105
- }
1106
1665
  return { error: message };
1107
1666
  }
1108
1667
 
1109
1668
  export type CodexCredentialFailure = {
1110
- kind: "auth" | "forbidden" | "rate_limit" | "quota";
1669
+ /**
1670
+ * `plan_entitlement` is produced only after a plan re-check proves the
1671
+ * serving account's current plan does not include the requested model.
1672
+ */
1673
+ kind: "auth" | "forbidden" | "rate_limit" | "quota" | "plan_entitlement";
1111
1674
  cooldownSeconds: number | null;
1112
1675
  };
1113
1676
 
@@ -1128,7 +1691,11 @@ export function codexCredentialCooldownUntil(
1128
1691
  > | null,
1129
1692
  now: Date,
1130
1693
  ): Date | null {
1131
- if (failure.kind === "auth" || failure.kind === "forbidden") {
1694
+ if (
1695
+ failure.kind === "auth" ||
1696
+ failure.kind === "forbidden" ||
1697
+ failure.kind === "plan_entitlement"
1698
+ ) {
1132
1699
  return null;
1133
1700
  }
1134
1701
  const providerReset =
@@ -1188,6 +1755,12 @@ export function classifyCodexCredentialFailure(error: unknown): CodexCredentialF
1188
1755
  if (!isCodexTransportError(error)) {
1189
1756
  return null;
1190
1757
  }
1758
+ // Plan/model entitlement evidence (an explicit plan refusal, or an empty
1759
+ // HTTP 400) is not account health or quota. The worker re-checks the plan
1760
+ // first; only a proven entitlement loss walks the pool, as `plan_entitlement`.
1761
+ if (classifyCodexEntitlementRejection(error)) {
1762
+ return null;
1763
+ }
1191
1764
  const usageLimit = classifyCodexUsageLimitError(error);
1192
1765
  if (usageLimit) {
1193
1766
  return { kind: "quota", cooldownSeconds: usageLimit.resetsInSeconds };
@@ -1292,3 +1865,29 @@ export function codexUsageLimitFailurePayload(
1292
1865
  // open indefinitely for a goal-bearing session; cap the continuation hold so the
1293
1866
  // goal re-evaluates at most this far out (it will re-pause if still capped).
1294
1867
  export const CODEX_USAGE_LIMIT_MAX_RESUME_MS = 60 * 60_000; // 1h
1868
+
1869
+ /** Only typed provider backpressure or a verified reconnect requirement can rotate Claude. */
1870
+ export function classifyClaudeCredentialFailure(
1871
+ error: unknown,
1872
+ ): (XaiCredentialFailure & { requestId?: string }) | null {
1873
+ if (isProviderSafetyRefusal(error)) return null;
1874
+ let current: unknown = error;
1875
+ for (let depth = 0; depth < 6 && current instanceof Error; depth++) {
1876
+ if (current instanceof ClaudeSubscriptionReconnectRequired)
1877
+ return { kind: "auth", cooldownMs: null };
1878
+ if (current instanceof AnthropicRequestError && current.status === 401)
1879
+ return {
1880
+ kind: "auth",
1881
+ cooldownMs: null,
1882
+ ...(current.request_id ? { requestId: current.request_id } : {}),
1883
+ };
1884
+ if (current instanceof AnthropicRequestError && current.status === 429)
1885
+ return {
1886
+ kind: "rate_limit",
1887
+ cooldownMs: providerRetryAfterMs(current) ?? PROVIDER_BACKPRESSURE_DELAY_MS,
1888
+ ...(current.request_id ? { requestId: current.request_id } : {}),
1889
+ };
1890
+ current = current.cause;
1891
+ }
1892
+ return null;
1893
+ }