@opengeni/worker-bundle 1.0.0-canary.0 → 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.
- package/dist/activities/agent-run-admission.d.ts +3 -1
- package/dist/activities/agent-turn/admission.d.ts +24 -3
- package/dist/activities/agent-turn/agent-build.d.ts +4 -0
- package/dist/activities/agent-turn/claude-usage-observer.d.ts +58 -0
- package/dist/activities/agent-turn/code-search.d.ts +60 -0
- package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
- package/dist/activities/agent-turn/codex-plan-entitlement.d.ts +105 -0
- package/dist/activities/agent-turn/codex.d.ts +3 -2
- package/dist/activities/agent-turn/compaction-prep.d.ts +21 -5
- package/dist/activities/agent-turn/credential-leases.d.ts +19 -33
- package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
- package/dist/activities/agent-turn/errors.d.ts +82 -11
- package/dist/activities/agent-turn/failure-settlement.d.ts +6 -3
- package/dist/activities/agent-turn/final-reply.d.ts +18 -0
- package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
- package/dist/activities/agent-turn/finalization.d.ts +4 -2
- package/dist/activities/agent-turn/github-repository-bindings.d.ts +2 -0
- package/dist/activities/agent-turn/governance-model.d.ts +9 -2
- package/dist/activities/agent-turn/history-sink.d.ts +2 -0
- package/dist/activities/agent-turn/history.d.ts +2 -0
- package/dist/activities/agent-turn/input-wait-reply.d.ts +41 -0
- package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
- package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
- package/dist/activities/agent-turn/model-usage.d.ts +6 -0
- package/dist/activities/agent-turn/model-wire-dispatch.d.ts +15 -0
- package/dist/activities/agent-turn/optional-repositories.d.ts +59 -0
- package/dist/activities/agent-turn/provider-dispatch-barrier.d.ts +12 -1
- package/dist/activities/agent-turn/quiescence.d.ts +7 -7
- package/dist/activities/agent-turn/recovery-warning.d.ts +9 -0
- package/dist/activities/agent-turn/refresh-credentials.d.ts +6 -0
- package/dist/activities/agent-turn/run-credentials.d.ts +8 -3
- package/dist/activities/agent-turn/sandbox-establish.d.ts +2 -1
- package/dist/activities/agent-turn/sandbox-provision.d.ts +3 -1
- package/dist/activities/agent-turn/sandbox-route.d.ts +3 -20
- package/dist/activities/agent-turn/sandbox-runtime.d.ts +2 -2
- package/dist/activities/agent-turn/session-title.d.ts +34 -2
- package/dist/activities/agent-turn/skill-checkout.d.ts +25 -6
- package/dist/activities/agent-turn/skill-read.d.ts +70 -2
- package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
- package/dist/activities/agent-turn/skill-search.d.ts +3 -1
- package/dist/activities/agent-turn/skill-tools.d.ts +15 -2
- package/dist/activities/agent-turn/skill-transfer.d.ts +18 -6
- package/dist/activities/agent-turn/stream-attempt.d.ts +7 -3
- package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
- package/dist/activities/agent-turn/tool-environment.d.ts +56 -6
- package/dist/activities/agent-turn/tool-policy.d.ts +43 -0
- package/dist/activities/agent-turn/turn-context.d.ts +29 -0
- package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
- package/dist/activities/agent-turn/xai-capacity.d.ts +2 -1
- package/dist/activities/automations.d.ts +3 -1
- package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
- package/dist/activities/codemode-dispatcher.d.ts +9 -2
- package/dist/activities/codex-capacity.d.ts +18 -2
- package/dist/activities/codex-rotation.d.ts +7 -1
- package/dist/activities/context-compaction.d.ts +16 -2
- package/dist/activities/gmail-files.d.ts +32 -0
- package/dist/activities/goals.d.ts +8 -0
- package/dist/activities/knowledge-indexing.d.ts +19 -0
- package/dist/activities/mcp-account-routes.d.ts +3 -0
- package/dist/activities/mcp-credentials.d.ts +12 -13
- package/dist/activities/mcp-operation-authority.d.ts +0 -5
- package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
- package/dist/activities/open-suffix-resume.d.ts +13 -0
- package/dist/activities/parent-wake.d.ts +6 -12
- package/dist/activities/retained-screenshots.d.ts +7 -2
- package/dist/activities/rig-verification.d.ts +2 -3
- package/dist/activities/run-credential-renewal.d.ts +1 -0
- package/dist/activities/run-credentials.d.ts +15 -3
- package/dist/activities/sandbox-images.d.ts +1 -1
- package/dist/activities/sandbox-lease.d.ts +47 -9
- package/dist/activities/scheduled-tasks.d.ts +4 -35
- package/dist/activities/session-state.d.ts +70 -3
- package/dist/activities/types.d.ts +55 -6
- package/dist/activities/workspace-capture.d.ts +2 -0
- package/dist/activities/workspace-credential-provider.d.ts +46 -0
- package/dist/{activities-control-HMFGSKC5.js → activities-control-BNPTRTOE.js} +1896 -418
- package/dist/activities-control-BNPTRTOE.js.map +1 -0
- package/dist/activities-control.d.ts +182 -4
- package/dist/{activities-turn-AP67JADS.js → activities-turn-5LGZOSVU.js} +4476 -1327
- package/dist/activities-turn-5LGZOSVU.js.map +1 -0
- package/dist/activities.d.ts +182 -4
- package/dist/artifact-materializer-entry.js +5 -2
- package/dist/artifact-materializer-entry.js.map +1 -1
- package/dist/artifact-outbox-entry.js +5 -2
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
- package/dist/{chunk-A4XQWVZK.js → chunk-3ZTRWDMN.js} +307 -4
- package/dist/chunk-3ZTRWDMN.js.map +1 -0
- package/dist/{chunk-RAMNYSAW.js → chunk-4ER3YXOT.js} +22 -4
- package/dist/chunk-4ER3YXOT.js.map +1 -0
- package/dist/{chunk-CYITU5UH.js → chunk-LJX7Z2KP.js} +1445 -343
- package/dist/chunk-LJX7Z2KP.js.map +1 -0
- package/dist/editable-artifact-materializer-service.d.ts +2 -2
- package/dist/editable-artifact-outbox-service.d.ts +2 -2
- package/dist/host-export-pump.d.ts +3 -1
- package/dist/host-export.d.ts +1 -1
- package/dist/host-export.js +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +95 -3
- package/dist/index.js +137 -43
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +118 -8
- package/dist/retained-process-diagnostics.d.ts +4 -0
- package/dist/retained-process-retry.d.ts +7 -0
- package/dist/sandbox-resume.d.ts +55 -13
- package/dist/sandbox-routing.d.ts +5 -3
- package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
- package/dist/session-event-fanout.d.ts +6 -0
- package/dist/warm-capture-holder.d.ts +3 -0
- package/dist/worker-service-lifecycle.d.ts +12 -0
- package/dist/workflow-bundle.js +273 -41
- package/dist/workflows/activities.d.ts +1 -1
- package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
- package/dist/workflows/scheduled-tasks.d.ts +4 -0
- package/dist/workflows/session.d.ts +36 -0
- package/dist/workflows.d.ts +1 -0
- package/package.json +21 -19
- package/src/activities/agent-run-admission.ts +17 -1
- package/src/activities/agent-turn/admission.ts +66 -3
- package/src/activities/agent-turn/agent-build.ts +84 -10
- package/src/activities/agent-turn/claim.ts +186 -43
- package/src/activities/agent-turn/claude-usage-observer.ts +221 -0
- package/src/activities/agent-turn/code-search.ts +275 -0
- package/src/activities/agent-turn/codex-capacity.ts +172 -8
- package/src/activities/agent-turn/codex-plan-entitlement.ts +254 -0
- package/src/activities/agent-turn/codex.ts +13 -5
- package/src/activities/agent-turn/compaction-prep.ts +170 -102
- package/src/activities/agent-turn/credential-leases.ts +122 -216
- package/src/activities/agent-turn/credential-restriction.ts +14 -0
- package/src/activities/agent-turn/errors.ts +647 -48
- package/src/activities/agent-turn/failure-settlement.ts +682 -130
- package/src/activities/agent-turn/final-reply.ts +46 -0
- package/src/activities/agent-turn/finalization-monitor.ts +120 -0
- package/src/activities/agent-turn/finalization.ts +146 -43
- package/src/activities/agent-turn/github-repository-bindings.ts +3 -1
- package/src/activities/agent-turn/governance-model.ts +69 -28
- package/src/activities/agent-turn/history-sink.ts +16 -2
- package/src/activities/agent-turn/history.ts +38 -11
- package/src/activities/agent-turn/input-wait-reply.ts +103 -0
- package/src/activities/agent-turn/knowledge-source-tools.ts +2 -0
- package/src/activities/agent-turn/media-artifacts.ts +2 -1
- package/src/activities/agent-turn/model-call-admission.ts +94 -0
- package/src/activities/agent-turn/model-usage.ts +64 -3
- package/src/activities/agent-turn/model-wire-dispatch.ts +27 -0
- package/src/activities/agent-turn/optional-repositories.ts +224 -0
- package/src/activities/agent-turn/provider-dispatch-barrier.ts +22 -0
- package/src/activities/agent-turn/quiescence.ts +10 -7
- package/src/activities/agent-turn/recovery-warning.ts +16 -0
- package/src/activities/agent-turn/refresh-credentials.ts +44 -0
- package/src/activities/agent-turn/run-credentials.ts +116 -55
- package/src/activities/agent-turn/run.ts +320 -93
- package/src/activities/agent-turn/sandbox-establish.ts +84 -8
- package/src/activities/agent-turn/sandbox-provision.ts +3 -1
- package/src/activities/agent-turn/sandbox-route.ts +4 -21
- package/src/activities/agent-turn/sandbox-runtime.ts +343 -18
- package/src/activities/agent-turn/session-title.ts +81 -5
- package/src/activities/agent-turn/skill-checkout.ts +111 -24
- package/src/activities/agent-turn/skill-install.ts +1 -1
- package/src/activities/agent-turn/skill-read.ts +374 -28
- package/src/activities/agent-turn/skill-remove.ts +46 -0
- package/src/activities/agent-turn/skill-save.ts +1 -1
- package/src/activities/agent-turn/skill-search.ts +10 -1
- package/src/activities/agent-turn/skill-selection.ts +4 -0
- package/src/activities/agent-turn/skill-tools.ts +112 -18
- package/src/activities/agent-turn/skill-transfer.ts +68 -23
- package/src/activities/agent-turn/stream-attempt.ts +313 -77
- package/src/activities/agent-turn/subscription-lease.ts +115 -0
- package/src/activities/agent-turn/tool-environment.ts +316 -167
- package/src/activities/agent-turn/tool-policy.ts +80 -1
- package/src/activities/agent-turn/turn-context.ts +35 -0
- package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
- package/src/activities/agent-turn/xai-capacity.ts +94 -51
- package/src/activities/automations.ts +76 -4
- package/src/activities/browser-deadline-checkpoint.ts +279 -0
- package/src/activities/capabilities.ts +108 -11
- package/src/activities/codemode-dispatcher.ts +36 -8
- package/src/activities/codex-capacity.ts +60 -25
- package/src/activities/codex-rotation.ts +39 -17
- package/src/activities/context-compaction.ts +75 -18
- package/src/activities/gmail-files.ts +88 -0
- package/src/activities/goals.ts +116 -121
- package/src/activities/google-drive-publication.ts +1 -3
- package/src/activities/image-generation-operation.ts +1 -1
- package/src/activities/knowledge-indexing.ts +269 -15
- package/src/activities/knowledge-source-sync.ts +10 -0
- package/src/activities/mcp-account-routes.ts +17 -0
- package/src/activities/mcp-credentials.ts +83 -194
- package/src/activities/mcp-operation-authority.ts +16 -41
- package/src/activities/model-call-fact-reconciler.ts +112 -0
- package/src/activities/open-suffix-resume.ts +15 -2
- package/src/activities/parent-wake.ts +46 -21
- package/src/activities/retained-screenshots.ts +82 -11
- package/src/activities/rig-verification.ts +38 -37
- package/src/activities/run-credential-renewal.ts +13 -1
- package/src/activities/run-credentials.ts +59 -17
- package/src/activities/run-input.ts +25 -16
- package/src/activities/sandbox-images.ts +0 -1
- package/src/activities/sandbox-lease.ts +798 -82
- package/src/activities/scheduled-tasks.ts +550 -178
- package/src/activities/session-state.ts +245 -6
- package/src/activities/site-auth-maintenance.ts +3 -0
- package/src/activities/types.ts +72 -5
- package/src/activities/workflow-wake.ts +2 -17
- package/src/activities/workspace-capture.ts +13 -2
- package/src/activities/workspace-credential-provider.ts +283 -0
- package/src/activities-control.ts +4 -0
- package/src/activities-turn.ts +5 -0
- package/src/activities.ts +13 -0
- package/src/activity-services.ts +16 -4
- package/src/browser-deadline-checkpoint-contract.ts +5 -0
- package/src/editable-artifact-hint-broker.ts +3 -0
- package/src/editable-artifact-materializer-service.ts +7 -3
- package/src/editable-artifact-outbox-service.ts +4 -4
- package/src/github-rest-mcp.ts +1 -2
- package/src/host-export-pump.ts +26 -4
- package/src/host-export.ts +3 -0
- package/src/http.ts +4 -2
- package/src/index.ts +99 -37
- package/src/observability-metrics.ts +510 -5
- package/src/personal-github-git-credentials.ts +2 -1
- package/src/pr-review-credentials.ts +1 -3
- package/src/retained-process-diagnostics.ts +50 -0
- package/src/retained-process-retry.ts +36 -0
- package/src/sandbox-resume.ts +388 -101
- package/src/sandbox-routing.ts +150 -25
- package/src/sandbox-snapshot-diagnostics.ts +112 -0
- package/src/session-event-fanout.ts +23 -0
- package/src/warm-capture-holder.ts +15 -0
- package/src/worker-service-lifecycle.ts +53 -0
- package/src/workflows/activities.ts +16 -1
- package/src/workflows/browser-deadline-checkpoint.ts +50 -0
- package/src/workflows/file-upload-reaper.ts +2 -0
- package/src/workflows/scheduled-tasks.ts +26 -13
- package/src/workflows/session.ts +242 -22
- package/src/workflows.ts +4 -0
- package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
- package/dist/activities/packs.d.ts +0 -55
- package/dist/activities-control-HMFGSKC5.js.map +0 -1
- package/dist/activities-turn-AP67JADS.js.map +0 -1
- package/dist/chunk-A4XQWVZK.js.map +0 -1
- package/dist/chunk-CYITU5UH.js.map +0 -1
- package/dist/chunk-RAMNYSAW.js.map +0 -1
- package/src/activities/agent-turn/knowledge-sources.ts +0 -86
- package/src/activities/packs.ts +0 -166
|
@@ -1,35 +1,58 @@
|
|
|
1
|
+
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
1
2
|
import {
|
|
2
3
|
requestSessionTurnRecovery,
|
|
3
4
|
getSessionGoal,
|
|
4
5
|
armXaiCapacityWait,
|
|
6
|
+
armClaudeCapacityWait,
|
|
7
|
+
reconcileClaudeCapacityWait,
|
|
8
|
+
recordClaudeAccountUsage,
|
|
9
|
+
resolveClaudeAccountCredential,
|
|
10
|
+
loadClaudeAccountCredential,
|
|
11
|
+
ClaudeSubscriptionConnectionChanged,
|
|
5
12
|
reconcileXaiCapacityWait,
|
|
6
13
|
listCodexAccountStatuses,
|
|
7
14
|
quarantineCodexCredentialForLease,
|
|
8
15
|
recordUsageEvent,
|
|
9
16
|
getActiveSessionHistoryItemsPaged,
|
|
17
|
+
recheckCodexCredentialPlan,
|
|
10
18
|
settleCodexCredentialLeaseLoss,
|
|
11
19
|
settleCodexCredentialFailover,
|
|
12
20
|
readLease,
|
|
13
21
|
SandboxLeaseSupersededError,
|
|
14
22
|
isSessionEventPersistenceError,
|
|
23
|
+
SANDBOX_SETUP_RECOVERY_LIMIT,
|
|
15
24
|
type CodexLeaseAccountStatus,
|
|
16
25
|
} from "@opengeni/db";
|
|
17
26
|
import { publishDurableSessionEvents } from "@opengeni/events";
|
|
18
|
-
import {
|
|
19
|
-
|
|
27
|
+
import {
|
|
28
|
+
maxTurnsExceededRunState,
|
|
29
|
+
isModalTaskExecStartPreDispatchUnavailableError,
|
|
30
|
+
isModalCommandStartOutcomeUnknownError,
|
|
31
|
+
isProviderCommandObservationUnavailableError,
|
|
32
|
+
} from "@opengeni/runtime";
|
|
33
|
+
import { ApplicationFailure, CancelledFailure } from "@temporalio/activity";
|
|
20
34
|
import {
|
|
21
35
|
authoritativeCodexCapacityResetAt,
|
|
22
36
|
classifyCodexPin,
|
|
23
37
|
selectCodexCredentialLeaseForTurn,
|
|
24
38
|
type CodexRotationStrategy,
|
|
25
39
|
} from "../codex-rotation";
|
|
26
|
-
import type
|
|
40
|
+
import { TurnExecutionPolicyDefinitionMismatchError, type Settings } from "@opengeni/config";
|
|
27
41
|
import {
|
|
28
42
|
classifyCodexEncryptedArtifactRejection,
|
|
43
|
+
classifyCodexEntitlementRejection,
|
|
29
44
|
classifyCodexUsageLimitError,
|
|
30
45
|
isCodexTransportError,
|
|
31
46
|
type CodexUsageHeaderSnapshot,
|
|
32
47
|
} from "@opengeni/codex";
|
|
48
|
+
import {
|
|
49
|
+
assessCodexPlanEntitlement,
|
|
50
|
+
codexAccountDisplayLabel,
|
|
51
|
+
codexPlanEntitlementFailurePayload,
|
|
52
|
+
codexRequestRejectedFailurePayload,
|
|
53
|
+
type CodexPlanEntitlementFailurePayload,
|
|
54
|
+
type CodexRequestRejectedFailurePayload,
|
|
55
|
+
} from "./codex-plan-entitlement";
|
|
33
56
|
import { TurnAttemptFencedError } from "../turn-attempt-fenced";
|
|
34
57
|
import { deliverFailedChildTurnToParent } from "../parent-wake";
|
|
35
58
|
import type {
|
|
@@ -53,10 +76,13 @@ import {
|
|
|
53
76
|
sandboxRouteTransitionCode,
|
|
54
77
|
safeErrorDiagnostic,
|
|
55
78
|
classifyXaiCredentialFailure,
|
|
79
|
+
classifyClaudeCredentialFailure,
|
|
56
80
|
agentRunFailurePayload,
|
|
81
|
+
agentRunRecoveryFailurePayload,
|
|
57
82
|
codexCredentialCooldownUntil,
|
|
58
83
|
classifyCodexCredentialFailure,
|
|
59
84
|
codexUsageLimitFailurePayload,
|
|
85
|
+
type CodexCredentialFailure,
|
|
60
86
|
} from "./errors";
|
|
61
87
|
import { selectRejectedProviderArtifactHistoryIds } from "./history";
|
|
62
88
|
import { waitForTurnFinalizerStep, turnFinalizerCancellationSignal } from "./quiescence";
|
|
@@ -122,7 +148,7 @@ type CodexCapacityWaitFailurePayload = {
|
|
|
122
148
|
* recover the same durable turn immediately.
|
|
123
149
|
*/
|
|
124
150
|
export function codexDefinitiveFailureDisposition(input: {
|
|
125
|
-
failureKind: "
|
|
151
|
+
failureKind: CodexCredentialFailure["kind"];
|
|
126
152
|
rotationEnabled: boolean;
|
|
127
153
|
pinDisposition: "manual" | "sharded" | "clearStale" | "unpinned";
|
|
128
154
|
decisionKind: "active" | "allCapped" | "none";
|
|
@@ -136,6 +162,17 @@ export function codexDefinitiveFailureDisposition(input: {
|
|
|
136
162
|
input.decisionCredentialId !== null &&
|
|
137
163
|
input.decisionCredentialId !== input.servingCredentialId;
|
|
138
164
|
if (alternateAvailable) return "failover";
|
|
165
|
+
// A plan that no longer includes the model does not recover by itself, and
|
|
166
|
+
// a manual pin or rotation-off pointer names exactly that account. Only a
|
|
167
|
+
// rotation-on pool whose OTHER accounts are temporarily capped is worth a
|
|
168
|
+
// durable wait; everything else fails the turn with typed copy.
|
|
169
|
+
if (input.failureKind === "plan_entitlement") {
|
|
170
|
+
return input.decisionKind === "allCapped" &&
|
|
171
|
+
input.rotationEnabled &&
|
|
172
|
+
input.pinDisposition !== "manual"
|
|
173
|
+
? "wait"
|
|
174
|
+
: "terminal";
|
|
175
|
+
}
|
|
139
176
|
if (
|
|
140
177
|
input.failureKind === "quota" ||
|
|
141
178
|
input.failureKind === "rate_limit" ||
|
|
@@ -167,12 +204,23 @@ export function codexCredentialFailoverLimit(
|
|
|
167
204
|
|
|
168
205
|
/** Build the durable waiter payload without collapsing quota refusals into 403. */
|
|
169
206
|
export function codexCapacityWaitFailurePayload(input: {
|
|
170
|
-
failureKind: "
|
|
207
|
+
failureKind: CodexCredentialFailure["kind"];
|
|
171
208
|
usageLimit: { resetsInSeconds: number | null } | null;
|
|
172
209
|
cooldownSeconds: number | null;
|
|
173
210
|
detail: string;
|
|
174
211
|
allAccounts: boolean;
|
|
212
|
+
planEntitlement?: CodexPlanEntitlementFailurePayload | null;
|
|
175
213
|
}): CodexCapacityWaitFailurePayload {
|
|
214
|
+
if (input.failureKind === "plan_entitlement") {
|
|
215
|
+
return {
|
|
216
|
+
error:
|
|
217
|
+
input.planEntitlement?.error ??
|
|
218
|
+
"The serving ChatGPT account's plan does not include this model. Opengeni is waiting for another connected account to become available.",
|
|
219
|
+
code: "codex_plan_entitlement",
|
|
220
|
+
detail: input.detail,
|
|
221
|
+
retryable: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
176
224
|
if (input.failureKind === "quota") {
|
|
177
225
|
return codexUsageLimitFailurePayload(
|
|
178
226
|
input.usageLimit ?? { resetsInSeconds: input.cooldownSeconds },
|
|
@@ -225,6 +273,42 @@ function acceptedCodexPolicySnapshot(
|
|
|
225
273
|
}
|
|
226
274
|
|
|
227
275
|
export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgentTurnResult> {
|
|
276
|
+
try {
|
|
277
|
+
return await settleTurnFailureInAttempt(deps);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Connectivity can disappear while settling an unrelated run error too.
|
|
280
|
+
// Do not overwrite a possibly committed settlement; the control lane
|
|
281
|
+
// re-reads exact ownership and becomes a stale no-op if it already closed.
|
|
282
|
+
if (deps.attempt.turnId && deps.attempt.triggerEventId) {
|
|
283
|
+
const recovery = postClaimDatabaseRecoveryFailure({
|
|
284
|
+
error,
|
|
285
|
+
turnId: deps.attempt.turnId,
|
|
286
|
+
triggerEventId: deps.attempt.triggerEventId,
|
|
287
|
+
executionGeneration: deps.attempt.executionGeneration,
|
|
288
|
+
requireDatabaseProvenance: true,
|
|
289
|
+
});
|
|
290
|
+
if (recovery) {
|
|
291
|
+
deps.control.activityStatus = "recovering";
|
|
292
|
+
deps.control.turnMetricOutcome = "recovering";
|
|
293
|
+
deps.control.activityError = error;
|
|
294
|
+
throw recovery;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function settleTurnFailureInAttempt(deps: TurnFailureDeps): Promise<RunAgentTurnResult> {
|
|
302
|
+
if (deps.settings.environment === "local" && deps.error instanceof Error) {
|
|
303
|
+
// Keep local startup failures diagnosable without logging error messages,
|
|
304
|
+
// absolute host paths, prompts, credentials, or provider response bodies.
|
|
305
|
+
const locations = (deps.error.stack ?? "")
|
|
306
|
+
.split("\n")
|
|
307
|
+
.slice(1)
|
|
308
|
+
.flatMap((line) => line.match(/(?:apps|packages)\/[A-Za-z0-9_./-]+:\d+:\d+/g) ?? [])
|
|
309
|
+
.slice(0, 8);
|
|
310
|
+
console.error(JSON.stringify({ message: "Local turn failure source locations", locations }));
|
|
311
|
+
}
|
|
228
312
|
const {
|
|
229
313
|
error,
|
|
230
314
|
input,
|
|
@@ -249,6 +333,36 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
249
333
|
acknowledgeLostAttemptOwnership,
|
|
250
334
|
acknowledgeRecoveryQuiescence,
|
|
251
335
|
} = deps;
|
|
336
|
+
// Capture before any recovery/checkpoint DB operation can fail again.
|
|
337
|
+
if (isSessionEventPersistenceError(error)) {
|
|
338
|
+
try {
|
|
339
|
+
const diagnosticId = observability.recordFailureDiagnostic({
|
|
340
|
+
code: error.details.code,
|
|
341
|
+
stage:
|
|
342
|
+
error.details.stage === "session_events.append_generic"
|
|
343
|
+
? "session_events.append_generic"
|
|
344
|
+
: error.details.stage === "session_events.append_for_turn_attempt"
|
|
345
|
+
? "session_events.append_for_turn_attempt"
|
|
346
|
+
: error.details.stage === "session_attempts.claim"
|
|
347
|
+
? "session_attempts.claim"
|
|
348
|
+
: "failure_settlement",
|
|
349
|
+
retryDecision: error.details.retryOutcome,
|
|
350
|
+
error,
|
|
351
|
+
sessionId: input.sessionId,
|
|
352
|
+
...(attempt.turnId ? { turnId: attempt.turnId } : {}),
|
|
353
|
+
attemptId: input.attemptId,
|
|
354
|
+
attempts: error.details.attempts,
|
|
355
|
+
eventTypes: error.details.eventTypes,
|
|
356
|
+
sqlState: error.details.sqlState,
|
|
357
|
+
...(error.details.database.constraint
|
|
358
|
+
? { constraint: error.details.database.constraint }
|
|
359
|
+
: {}),
|
|
360
|
+
});
|
|
361
|
+
observability.error("session event persistence failed", { correlationId: diagnosticId });
|
|
362
|
+
} catch {
|
|
363
|
+
// Failure settlement must not depend on telemetry availability.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
252
366
|
// Graceful worker shutdown (deploy / rollout restart): checkpoint the
|
|
253
367
|
// same current inference for a new fenced attempt instead of failing the
|
|
254
368
|
// session. Conversation truth is already persisted per model response;
|
|
@@ -262,6 +376,69 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
262
376
|
// back to the workflow-claimed turn when the local lookup had not
|
|
263
377
|
// finished yet.
|
|
264
378
|
const recoveryTurnId = attempt.turnId;
|
|
379
|
+
// Unlike proven pre-dispatch failure, a genuine SDK Start/Wait uncertainty
|
|
380
|
+
// cannot reconstruct setup on a replacement attempt. The exact command and
|
|
381
|
+
// writer remain retained by sandbox-runtime; the logical turn is parked as
|
|
382
|
+
// recovering with a durable no-replay marker, not failed or completed.
|
|
383
|
+
const observationUnavailable = isProviderCommandObservationUnavailableError(error);
|
|
384
|
+
if (
|
|
385
|
+
(isModalCommandStartOutcomeUnknownError(error) || observationUnavailable) &&
|
|
386
|
+
recoveryTurnId &&
|
|
387
|
+
attempt.triggerEventId &&
|
|
388
|
+
attempt.executionGeneration > 0
|
|
389
|
+
) {
|
|
390
|
+
let recovery: Awaited<ReturnType<typeof requestSessionTurnRecovery>>;
|
|
391
|
+
try {
|
|
392
|
+
if (eventing.turnStartedPublished) {
|
|
393
|
+
await flushRuntimeBatcher();
|
|
394
|
+
await historySink.reconcileConversationTruth({ requireDurable: true });
|
|
395
|
+
}
|
|
396
|
+
recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
397
|
+
sessionId: input.sessionId,
|
|
398
|
+
turnId: recoveryTurnId,
|
|
399
|
+
triggerEventId: attempt.triggerEventId,
|
|
400
|
+
attemptId: input.attemptId,
|
|
401
|
+
reason: observationUnavailable
|
|
402
|
+
? "sandbox_command_observation_unavailable"
|
|
403
|
+
: "sandbox_command_start_outcome_unknown",
|
|
404
|
+
sandboxSetupOutcomeUnknown: true,
|
|
405
|
+
detail: {
|
|
406
|
+
code: observationUnavailable
|
|
407
|
+
? "sandbox_command_observation_unavailable"
|
|
408
|
+
: "sandbox_command_start_outcome_unknown",
|
|
409
|
+
retryable: false,
|
|
410
|
+
setupOutcome: "unknown",
|
|
411
|
+
replay: "blocked",
|
|
412
|
+
providerRecoveryCount: attempt.providerRecoveryCount,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
} catch (checkpointError) {
|
|
416
|
+
const databaseRecovery = postClaimDatabaseRecoveryFailure({
|
|
417
|
+
error: checkpointError,
|
|
418
|
+
turnId: recoveryTurnId,
|
|
419
|
+
triggerEventId: attempt.triggerEventId,
|
|
420
|
+
executionGeneration: attempt.executionGeneration,
|
|
421
|
+
sandboxSetupOutcomeUnknown: true,
|
|
422
|
+
});
|
|
423
|
+
if (!databaseRecovery) throw checkpointError;
|
|
424
|
+
control.activityStatus = "recovering";
|
|
425
|
+
control.turnMetricOutcome = "recovering";
|
|
426
|
+
control.activityError = error;
|
|
427
|
+
throw databaseRecovery;
|
|
428
|
+
}
|
|
429
|
+
if (recovery.action === "stale") {
|
|
430
|
+
acknowledgeLostAttemptOwnership();
|
|
431
|
+
control.activityStatus = "cancelled";
|
|
432
|
+
control.turnMetricOutcome = "cancelled";
|
|
433
|
+
return claimedResult({ status: "cancelled" });
|
|
434
|
+
}
|
|
435
|
+
acknowledgeRecoveryQuiescence();
|
|
436
|
+
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, recovery.events);
|
|
437
|
+
control.activityStatus = "recovering";
|
|
438
|
+
control.turnMetricOutcome = "recovering";
|
|
439
|
+
control.activityError = error;
|
|
440
|
+
return claimedResult({ status: "recovering", deferredUntilWake: true });
|
|
441
|
+
}
|
|
265
442
|
// A true epoch supersession and a provider lifecycle transition are both
|
|
266
443
|
// recoverable control-plane states, never session failures. A rotation
|
|
267
444
|
// persists an exact group/epoch wait marker so the workflow parks before
|
|
@@ -340,13 +517,11 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
340
517
|
throw recoveryError;
|
|
341
518
|
}
|
|
342
519
|
}
|
|
343
|
-
// A
|
|
344
|
-
//
|
|
345
|
-
// clears the durable pointer to home, the commit is valid but this exact
|
|
346
|
-
// attempt cannot serve a later home operation. Preserve the completed attach
|
|
520
|
+
// A route change can require a different home, filesystem root, or native
|
|
521
|
+
// capability set than this attempt established. Preserve the completed attach
|
|
347
522
|
// and every preceding model/tool receipt, close only the unresolved suffix,
|
|
348
523
|
// and continue the SAME logical turn in a fresh attempt. That next attempt
|
|
349
|
-
// starts from the
|
|
524
|
+
// starts from the committed pointer and establishes its route normally.
|
|
350
525
|
const routeTransitionCode = sandboxRouteTransitionCode(error);
|
|
351
526
|
if (routeTransitionCode && recoveryTurnId && eventing.publish && eventing.turnStartedPublished) {
|
|
352
527
|
try {
|
|
@@ -370,7 +545,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
370
545
|
return claimedResult({ status: "cancelled" });
|
|
371
546
|
}
|
|
372
547
|
if (recovery.action !== "recovering") {
|
|
373
|
-
throw new Error("
|
|
548
|
+
throw new Error("Sandbox route transition could not recover the current turn");
|
|
374
549
|
}
|
|
375
550
|
acknowledgeRecoveryQuiescence();
|
|
376
551
|
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, recovery.events);
|
|
@@ -504,6 +679,27 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
504
679
|
control.turnMetricOutcome = "cancelled";
|
|
505
680
|
throw cancellationFailure;
|
|
506
681
|
}
|
|
682
|
+
if (attempt.turnId && attempt.triggerEventId && attempt.executionGeneration > 0) {
|
|
683
|
+
const recoveryFailure = postClaimDatabaseRecoveryFailure({
|
|
684
|
+
error,
|
|
685
|
+
turnId: attempt.turnId,
|
|
686
|
+
triggerEventId: attempt.triggerEventId,
|
|
687
|
+
executionGeneration: attempt.executionGeneration,
|
|
688
|
+
requireDatabaseProvenance: eventing.turnStartedPublished || attempt.modelRequestStarted,
|
|
689
|
+
});
|
|
690
|
+
if (recoveryFailure) {
|
|
691
|
+
// Stop this non-retryable activity without terminal logical settlement.
|
|
692
|
+
// Do not retry a delta/tool-ledger mutation with an unknown commit, nor
|
|
693
|
+
// claim that DB-down cleanup proved writer exit. Finally still drains
|
|
694
|
+
// the physical writers; the existing DB-only workflow lane closes this
|
|
695
|
+
// exact owner after connectivity returns and admission waits for its
|
|
696
|
+
// ordinary durable quiescence/writer fences.
|
|
697
|
+
control.activityStatus = "recovering";
|
|
698
|
+
control.turnMetricOutcome = "recovering";
|
|
699
|
+
control.activityError = error;
|
|
700
|
+
throw recoveryFailure;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
507
703
|
// The SDK's per-segment turn cap is a pacing valve, not a failure: end
|
|
508
704
|
// the turn gracefully and idle the session so an active goal continues
|
|
509
705
|
// via a synthesized continuation turn (or a user message resumes work).
|
|
@@ -642,13 +838,13 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
642
838
|
leases.codex.generation,
|
|
643
839
|
);
|
|
644
840
|
}
|
|
645
|
-
|
|
646
|
-
leases.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
) {
|
|
841
|
+
const scopedLeaseLost =
|
|
842
|
+
billingState.isClaudeTurn && leases.claude.lost
|
|
843
|
+
? "claude"
|
|
844
|
+
: billingState.isXaiTurn && leases.xai.lost
|
|
845
|
+
? "xai"
|
|
846
|
+
: null;
|
|
847
|
+
if (scopedLeaseLost && eventing.publish && attempt.turnId && eventing.turnStartedPublished) {
|
|
652
848
|
await flushRuntimeBatcher();
|
|
653
849
|
await historySink.reconcileConversationTruth({ requireDurable: true });
|
|
654
850
|
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
@@ -656,8 +852,10 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
656
852
|
turnId: attempt.turnId,
|
|
657
853
|
triggerEventId: attempt.triggerEventId!,
|
|
658
854
|
attemptId: input.attemptId,
|
|
659
|
-
reason: "
|
|
660
|
-
detail: {
|
|
855
|
+
reason: scopedLeaseLost + "_lease_lost",
|
|
856
|
+
detail: {
|
|
857
|
+
provider: scopedLeaseLost === "claude" ? "claude-subscription" : "supergrok-subscription",
|
|
858
|
+
},
|
|
661
859
|
});
|
|
662
860
|
if (recovery.action === "stale") {
|
|
663
861
|
acknowledgeLostAttemptOwnership();
|
|
@@ -678,10 +876,107 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
678
876
|
// 5xx does not classify here and therefore cannot consume another
|
|
679
877
|
// subscription or duplicate a side effect.
|
|
680
878
|
const usageLimit = isCodexTransportError(error) ? classifyCodexUsageLimitError(error) : null;
|
|
681
|
-
|
|
879
|
+
let codexCredentialFailure: CodexCredentialFailure | null =
|
|
682
880
|
billingState.isCodexTurn && providerTurn.effectiveCodexCredentialId
|
|
683
881
|
? classifyCodexCredentialFailure(error)
|
|
684
882
|
: null;
|
|
883
|
+
// Plan entitlement evidence (an explicit plan refusal, or an HTTP 400 with no
|
|
884
|
+
// body) is ambiguous until the serving account's CURRENT plan is re-read. A
|
|
885
|
+
// proven loss becomes a definitive `plan_entitlement` refusal for this model
|
|
886
|
+
// only and walks the pool through the same checkpointed failover below; an
|
|
887
|
+
// unexplained rejection stays terminal with typed copy. Nothing retries the
|
|
888
|
+
// rejected request itself.
|
|
889
|
+
let codexTerminalFailure:
|
|
890
|
+
| CodexPlanEntitlementFailurePayload
|
|
891
|
+
| CodexRequestRejectedFailurePayload
|
|
892
|
+
| null = null;
|
|
893
|
+
let codexPlanEntitlement: {
|
|
894
|
+
modelId: string;
|
|
895
|
+
planType: string | null;
|
|
896
|
+
planObserved: boolean;
|
|
897
|
+
credentialVersion: number | null;
|
|
898
|
+
waitPayload: CodexPlanEntitlementFailurePayload;
|
|
899
|
+
} | null = null;
|
|
900
|
+
const codexEntitlementRejection =
|
|
901
|
+
billingState.isCodexTurn && providerTurn.effectiveCodexCredentialId && !codexCredentialFailure
|
|
902
|
+
? classifyCodexEntitlementRejection(error)
|
|
903
|
+
: null;
|
|
904
|
+
if (
|
|
905
|
+
codexEntitlementRejection &&
|
|
906
|
+
providerTurn.effectiveCodexCredentialId &&
|
|
907
|
+
eventing.publish &&
|
|
908
|
+
attempt.turnId &&
|
|
909
|
+
eventing.turnStartedPublished &&
|
|
910
|
+
leases.codex.holderId &&
|
|
911
|
+
leases.codex.generation !== null
|
|
912
|
+
) {
|
|
913
|
+
const servingCredentialId = providerTurn.effectiveCodexCredentialId;
|
|
914
|
+
const servingAccount = (
|
|
915
|
+
await listCodexAccountStatuses(db, input.workspaceId, attempt.turnId).catch(() => [])
|
|
916
|
+
).find((account) => account.id === servingCredentialId);
|
|
917
|
+
const accountLabel = codexAccountDisplayLabel(servingAccount);
|
|
918
|
+
const recheck = await recheckCodexCredentialPlan(
|
|
919
|
+
db,
|
|
920
|
+
settings,
|
|
921
|
+
input.workspaceId,
|
|
922
|
+
servingCredentialId,
|
|
923
|
+
{
|
|
924
|
+
turnId: attempt.turnId,
|
|
925
|
+
holderId: leases.codex.holderId,
|
|
926
|
+
generation: leases.codex.generation,
|
|
927
|
+
},
|
|
928
|
+
).catch(() => null);
|
|
929
|
+
const modelId = providerTurn.codexProductModelId ?? null;
|
|
930
|
+
const assessment = recheck
|
|
931
|
+
? assessCodexPlanEntitlement(codexEntitlementRejection, recheck, modelId)
|
|
932
|
+
: codexEntitlementRejection.evidence === "plan_entitlement"
|
|
933
|
+
? {
|
|
934
|
+
kind: "entitlement_lost" as const,
|
|
935
|
+
planType: servingAccount?.planType ?? null,
|
|
936
|
+
planObserved: false,
|
|
937
|
+
planChanged: false,
|
|
938
|
+
credentialVersion: null,
|
|
939
|
+
}
|
|
940
|
+
: { kind: "unexplained" as const, planType: null };
|
|
941
|
+
observability.incrementCounter({
|
|
942
|
+
name: "opengeni_codex_plan_rechecks_total",
|
|
943
|
+
help: "Codex plan re-checks after an entitlement-shaped rejection, by outcome.",
|
|
944
|
+
labels: {
|
|
945
|
+
workspace_key: codexWorkspaceKey,
|
|
946
|
+
evidence: codexEntitlementRejection.evidence,
|
|
947
|
+
outcome: assessment.kind,
|
|
948
|
+
source: recheck?.source ?? "none",
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
if (assessment.kind === "entitlement_lost") {
|
|
952
|
+
const payloadInput = {
|
|
953
|
+
accountLabel,
|
|
954
|
+
// Name a plan only when the provider just reported it; a recorded
|
|
955
|
+
// plan may be the very one that changed.
|
|
956
|
+
planType: assessment.planObserved ? assessment.planType : null,
|
|
957
|
+
planChanged: assessment.planChanged,
|
|
958
|
+
modelId,
|
|
959
|
+
rejection: codexEntitlementRejection,
|
|
960
|
+
};
|
|
961
|
+
codexTerminalFailure = codexPlanEntitlementFailurePayload(payloadInput);
|
|
962
|
+
if (modelId) {
|
|
963
|
+
codexCredentialFailure = { kind: "plan_entitlement", cooldownSeconds: null };
|
|
964
|
+
codexPlanEntitlement = {
|
|
965
|
+
modelId,
|
|
966
|
+
planType: assessment.planType,
|
|
967
|
+
planObserved: assessment.planObserved,
|
|
968
|
+
credentialVersion: assessment.credentialVersion,
|
|
969
|
+
waitPayload: codexPlanEntitlementFailurePayload({ ...payloadInput, waiting: true }),
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
} else {
|
|
973
|
+
codexTerminalFailure = codexRequestRejectedFailurePayload({
|
|
974
|
+
accountLabel,
|
|
975
|
+
planType: recheck?.planType ?? null,
|
|
976
|
+
rejection: codexEntitlementRejection,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
685
980
|
if (
|
|
686
981
|
codexCredentialFailure &&
|
|
687
982
|
providerTurn.effectiveCodexCredentialId &&
|
|
@@ -724,7 +1019,9 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
724
1019
|
labels: { workspace_key: codexWorkspaceKey, outcome: "completed" },
|
|
725
1020
|
});
|
|
726
1021
|
const now = new Date();
|
|
727
|
-
const before = await listCodexAccountStatuses(db, input.workspaceId).catch(
|
|
1022
|
+
const before = await listCodexAccountStatuses(db, input.workspaceId, attempt.turnId).catch(
|
|
1023
|
+
() => [],
|
|
1024
|
+
);
|
|
728
1025
|
const servingCached = before.find(
|
|
729
1026
|
(account) => account.id === providerTurn.effectiveCodexCredentialId,
|
|
730
1027
|
);
|
|
@@ -743,10 +1040,14 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
743
1040
|
}
|
|
744
1041
|
: null;
|
|
745
1042
|
const cooldownUntil = codexCredentialCooldownUntil(codexCredentialFailure, serving, now);
|
|
1043
|
+
// A plan re-check may itself have rotated tokens (same family, version
|
|
1044
|
+
// CAS-advanced by this holder); fence the quarantine on that version.
|
|
1045
|
+
const quarantineCredentialVersion =
|
|
1046
|
+
codexPlanEntitlement?.credentialVersion ?? providerTurn.effectiveCodexCredentialVersion;
|
|
746
1047
|
const quarantineResult =
|
|
747
1048
|
leases.codex.holderId &&
|
|
748
1049
|
leases.codex.generation !== null &&
|
|
749
|
-
|
|
1050
|
+
quarantineCredentialVersion !== null
|
|
750
1051
|
? await quarantineCodexCredentialForLease(db, {
|
|
751
1052
|
accountId: input.accountId,
|
|
752
1053
|
workspaceId: input.workspaceId,
|
|
@@ -759,7 +1060,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
759
1060
|
dispatchId: attempt.dispatchId,
|
|
760
1061
|
expectedRedispatches: attempt.redispatchesAtDispatch,
|
|
761
1062
|
credentialId: providerTurn.effectiveCodexCredentialId,
|
|
762
|
-
credentialVersion:
|
|
1063
|
+
credentialVersion: quarantineCredentialVersion,
|
|
763
1064
|
holderId: leases.codex.holderId,
|
|
764
1065
|
generation: leases.codex.generation,
|
|
765
1066
|
maxFailovers: providerTurn.codexCredentialFailoverLimit,
|
|
@@ -776,11 +1077,18 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
776
1077
|
status: "error",
|
|
777
1078
|
lastError: "model request was forbidden for this credential",
|
|
778
1079
|
}
|
|
779
|
-
:
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1080
|
+
: codexCredentialFailure.kind === "plan_entitlement"
|
|
1081
|
+
? {
|
|
1082
|
+
kind: "plan_entitlement",
|
|
1083
|
+
modelId: codexPlanEntitlement!.modelId,
|
|
1084
|
+
planType: codexPlanEntitlement!.planType,
|
|
1085
|
+
planObserved: codexPlanEntitlement!.planObserved,
|
|
1086
|
+
}
|
|
1087
|
+
: {
|
|
1088
|
+
kind: "cooldown",
|
|
1089
|
+
until: cooldownUntil!,
|
|
1090
|
+
cooldownKind: codexCredentialFailure.kind,
|
|
1091
|
+
},
|
|
784
1092
|
})
|
|
785
1093
|
: null;
|
|
786
1094
|
if (
|
|
@@ -892,7 +1200,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
892
1200
|
}
|
|
893
1201
|
let accounts: Awaited<ReturnType<typeof listCodexAccountStatuses>>;
|
|
894
1202
|
try {
|
|
895
|
-
accounts = await listCodexAccountStatuses(db, input.workspaceId);
|
|
1203
|
+
accounts = await listCodexAccountStatuses(db, input.workspaceId, attempt.turnId);
|
|
896
1204
|
} catch (metadataError) {
|
|
897
1205
|
// Current account health/cooldown metadata is still required after
|
|
898
1206
|
// quarantine. Operational database failures re-enter the existing
|
|
@@ -921,6 +1229,9 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
921
1229
|
rotationStrategy: acceptedPolicy.rotationStrategy,
|
|
922
1230
|
existingCredentialId: null,
|
|
923
1231
|
failedCredentialIds: [providerTurn.effectiveCodexCredentialId],
|
|
1232
|
+
...(providerTurn.codexProductModelId
|
|
1233
|
+
? { modelId: providerTurn.codexProductModelId }
|
|
1234
|
+
: {}),
|
|
924
1235
|
policyScope: null,
|
|
925
1236
|
unavailableDiagnostics: [],
|
|
926
1237
|
},
|
|
@@ -1079,9 +1390,10 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1079
1390
|
const capacityAccounts = policyCredentialId
|
|
1080
1391
|
? accounts.filter((account) => account.id === policyCredentialId)
|
|
1081
1392
|
: accounts;
|
|
1082
|
-
const authoritativeResetAt =
|
|
1083
|
-
|
|
1084
|
-
|
|
1393
|
+
const authoritativeResetAt =
|
|
1394
|
+
exactProviderReset || codexCredentialFailure.kind === "plan_entitlement"
|
|
1395
|
+
? (authoritativeCodexCapacityResetAt(capacityAccounts, now) ?? cooldownUntil)
|
|
1396
|
+
: null;
|
|
1085
1397
|
const allAccounts =
|
|
1086
1398
|
acceptedPolicy.rotationEnabled &&
|
|
1087
1399
|
pinDisposition !== "manual" &&
|
|
@@ -1097,6 +1409,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1097
1409
|
: String(error)
|
|
1098
1410
|
: "the same accepted turn is waiting for eligible credential capacity",
|
|
1099
1411
|
allAccounts,
|
|
1412
|
+
planEntitlement: codexPlanEntitlement?.waitPayload ?? null,
|
|
1100
1413
|
});
|
|
1101
1414
|
const evaluated = await armAndReconcileCodexCapacityWait(
|
|
1102
1415
|
{ db, bus },
|
|
@@ -1110,6 +1423,8 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1110
1423
|
goalId: activeGoal?.id ?? null,
|
|
1111
1424
|
goalVersion: activeGoal?.version ?? null,
|
|
1112
1425
|
earliestResetAt: authoritativeResetAt,
|
|
1426
|
+
// plan_entitlement waits only on OTHER capped accounts, so it keeps
|
|
1427
|
+
// their reset/bounded-refresh cadence rather than a mutation-only wait.
|
|
1113
1428
|
resetKind: authoritativeResetAt
|
|
1114
1429
|
? "authoritative"
|
|
1115
1430
|
: codexCredentialFailure.kind === "auth" ||
|
|
@@ -1126,6 +1441,11 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1126
1441
|
{ onArmed: () => (leases.codex.held = false) },
|
|
1127
1442
|
);
|
|
1128
1443
|
control.activityError = error;
|
|
1444
|
+
if (evaluated.action === "stopped") {
|
|
1445
|
+
control.activityStatus = evaluated.sessionStatus === "queued" ? "idle" : "failed";
|
|
1446
|
+
control.turnMetricOutcome = "failed";
|
|
1447
|
+
return claimedResult({ status: control.activityStatus });
|
|
1448
|
+
}
|
|
1129
1449
|
if (evaluated.action === "resumed") {
|
|
1130
1450
|
control.activityStatus = "recovering";
|
|
1131
1451
|
control.turnMetricOutcome = "recovering";
|
|
@@ -1152,83 +1472,265 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1152
1472
|
}
|
|
1153
1473
|
}
|
|
1154
1474
|
}
|
|
1155
|
-
const
|
|
1475
|
+
const xaiFailure =
|
|
1156
1476
|
billingState.isXaiTurn && providerTurn.effectiveXaiCredentialId
|
|
1157
1477
|
? classifyXaiCredentialFailure(error)
|
|
1158
1478
|
: null;
|
|
1479
|
+
const claudeFailure =
|
|
1480
|
+
billingState.isClaudeTurn && providerTurn.effectiveClaudeCredentialId
|
|
1481
|
+
? classifyClaudeCredentialFailure(error)
|
|
1482
|
+
: null;
|
|
1483
|
+
const scopedFailure = claudeFailure ?? xaiFailure;
|
|
1484
|
+
const scopedProvider = claudeFailure ? ("claude" as const) : ("xai" as const);
|
|
1485
|
+
const scopedName = claudeFailure ? "Claude" : "SuperGrok";
|
|
1486
|
+
const scopedCredentialId = claudeFailure
|
|
1487
|
+
? providerTurn.effectiveClaudeCredentialId
|
|
1488
|
+
: providerTurn.effectiveXaiCredentialId;
|
|
1489
|
+
const scopedAuthority = claudeFailure
|
|
1490
|
+
? providerTurn.claudeAuthoritySnapshot
|
|
1491
|
+
: providerTurn.xaiAuthoritySnapshot;
|
|
1492
|
+
const scopedLease = claudeFailure ? leases.claude : leases.xai;
|
|
1493
|
+
const armScopedWait = claudeFailure ? armClaudeCapacityWait : armXaiCapacityWait;
|
|
1494
|
+
const reconcileScopedWait = claudeFailure
|
|
1495
|
+
? reconcileClaudeCapacityWait
|
|
1496
|
+
: reconcileXaiCapacityWait;
|
|
1159
1497
|
if (
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1498
|
+
scopedFailure &&
|
|
1499
|
+
scopedCredentialId &&
|
|
1500
|
+
scopedAuthority &&
|
|
1501
|
+
scopedLease.subjectId &&
|
|
1502
|
+
scopedLease.holderId &&
|
|
1503
|
+
scopedLease.generation !== null &&
|
|
1166
1504
|
eventing.publish &&
|
|
1167
1505
|
attempt.turnId &&
|
|
1168
1506
|
eventing.turnStartedPublished
|
|
1169
1507
|
) {
|
|
1170
1508
|
await flushRuntimeBatcher();
|
|
1171
1509
|
await historySink.reconcileConversationTruth({ requireDurable: true });
|
|
1510
|
+
const recoverChangedAccount = async () => {
|
|
1511
|
+
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
1512
|
+
sessionId: input.sessionId,
|
|
1513
|
+
turnId: attempt.turnId!,
|
|
1514
|
+
triggerEventId: attempt.triggerEventId!,
|
|
1515
|
+
attemptId: input.attemptId,
|
|
1516
|
+
reason: "claude_credential_changed",
|
|
1517
|
+
detail: { code: "claude_credential_changed", retryable: true },
|
|
1518
|
+
});
|
|
1519
|
+
if (recovery.action === "stale") {
|
|
1520
|
+
acknowledgeLostAttemptOwnership();
|
|
1521
|
+
control.activityStatus = "cancelled";
|
|
1522
|
+
control.turnMetricOutcome = "cancelled";
|
|
1523
|
+
return claimedResult({ status: "cancelled" });
|
|
1524
|
+
}
|
|
1525
|
+
acknowledgeRecoveryQuiescence();
|
|
1526
|
+
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, recovery.events);
|
|
1527
|
+
control.activityStatus = "recovering";
|
|
1528
|
+
control.turnMetricOutcome = "recovering";
|
|
1529
|
+
control.activityError = error;
|
|
1530
|
+
return claimedResult({ status: "recovering" });
|
|
1531
|
+
};
|
|
1172
1532
|
const goal = await getSessionGoal(db, input.workspaceId, input.sessionId).catch(() => null);
|
|
1173
1533
|
const activeGoal = goal?.status === "active" ? goal : null;
|
|
1174
1534
|
const now = new Date();
|
|
1535
|
+
let claudeTokenFence: { encryptionKey: Uint8Array; observedAccessToken: string } | undefined;
|
|
1175
1536
|
const cooldownUntil =
|
|
1176
|
-
|
|
1177
|
-
? new Date(now.getTime() + Math.max(1,
|
|
1537
|
+
scopedFailure.kind === "rate_limit"
|
|
1538
|
+
? new Date(now.getTime() + Math.max(1, scopedFailure.cooldownMs ?? 60_000))
|
|
1178
1539
|
: null;
|
|
1540
|
+
if (claudeFailure) {
|
|
1541
|
+
const key = environmentsEncryptionKeyBytes(settings);
|
|
1542
|
+
const matchingReceipts = [...providerTurn.latestClaudeUsage.values()].filter(
|
|
1543
|
+
(value) =>
|
|
1544
|
+
value.expectedConnectionId === scopedCredentialId &&
|
|
1545
|
+
value.expectedCredentialVersion === providerTurn.effectiveClaudeCredentialVersion &&
|
|
1546
|
+
value.upstreamModelId === providerTurn.claudeUpstreamModelId &&
|
|
1547
|
+
(value.responseStatus === (scopedFailure.kind === "rate_limit" ? 429 : 401) ||
|
|
1548
|
+
(value.responseStatus === 200 &&
|
|
1549
|
+
!!claudeFailure.requestId &&
|
|
1550
|
+
value.requestId === claudeFailure.requestId)) &&
|
|
1551
|
+
(!claudeFailure.requestId || value.requestId === claudeFailure.requestId),
|
|
1552
|
+
);
|
|
1553
|
+
const receipt = matchingReceipts.length === 1 ? matchingReceipts[0] : undefined;
|
|
1554
|
+
if (
|
|
1555
|
+
!key ||
|
|
1556
|
+
!providerTurn.claudeUpstreamModelId ||
|
|
1557
|
+
providerTurn.effectiveClaudeCredentialVersion === null
|
|
1558
|
+
)
|
|
1559
|
+
throw new Error("Claude refused request has no exact serving account");
|
|
1560
|
+
if (!receipt) {
|
|
1561
|
+
// Refresh may discover a revoked grant before any physical model call.
|
|
1562
|
+
// Verify durable reconnect evidence instead of inventing a response receipt.
|
|
1563
|
+
const current = await loadClaudeAccountCredential(
|
|
1564
|
+
db,
|
|
1565
|
+
{
|
|
1566
|
+
accountId: input.accountId,
|
|
1567
|
+
workspaceId: input.workspaceId,
|
|
1568
|
+
subjectId: scopedLease.subjectId,
|
|
1569
|
+
credentialId: scopedCredentialId,
|
|
1570
|
+
authoritySnapshot: scopedAuthority,
|
|
1571
|
+
},
|
|
1572
|
+
key,
|
|
1573
|
+
);
|
|
1574
|
+
if (current.version !== providerTurn.effectiveClaudeCredentialVersion)
|
|
1575
|
+
return await recoverChangedAccount();
|
|
1576
|
+
if (scopedFailure.kind !== "auth" || current.usage.refreshStatus !== "reconnect")
|
|
1577
|
+
throw new Error("Claude refused request has no exact account receipt");
|
|
1578
|
+
claudeTokenFence = { encryptionKey: key, observedAccessToken: current.secret.token };
|
|
1579
|
+
}
|
|
1580
|
+
if (receipt) claudeTokenFence = { encryptionKey: key, observedAccessToken: receipt.token };
|
|
1581
|
+
if (
|
|
1582
|
+
receipt &&
|
|
1583
|
+
scopedFailure.kind === "auth" &&
|
|
1584
|
+
!(
|
|
1585
|
+
attempt.claudeAuthRecovery?.credentialId === scopedCredentialId &&
|
|
1586
|
+
attempt.claudeAuthRecovery.credentialVersion === receipt.expectedCredentialVersion
|
|
1587
|
+
)
|
|
1588
|
+
) {
|
|
1589
|
+
const credential = await resolveClaudeAccountCredential(
|
|
1590
|
+
db,
|
|
1591
|
+
settings,
|
|
1592
|
+
{
|
|
1593
|
+
accountId: input.accountId,
|
|
1594
|
+
workspaceId: input.workspaceId,
|
|
1595
|
+
subjectId: scopedLease.subjectId,
|
|
1596
|
+
credentialId: scopedCredentialId,
|
|
1597
|
+
authoritySnapshot: scopedAuthority,
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
expectedCredentialVersion: receipt.expectedCredentialVersion,
|
|
1601
|
+
forceRefresh: true,
|
|
1602
|
+
observedAccessToken: receipt.token,
|
|
1603
|
+
},
|
|
1604
|
+
).catch((refreshError) => {
|
|
1605
|
+
if (refreshError instanceof ClaudeSubscriptionConnectionChanged) return null;
|
|
1606
|
+
throw refreshError;
|
|
1607
|
+
});
|
|
1608
|
+
if (!credential) return await recoverChangedAccount();
|
|
1609
|
+
if (!("reconnectRequired" in credential) && credential.secret.token !== receipt.token) {
|
|
1610
|
+
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
1611
|
+
sessionId: input.sessionId,
|
|
1612
|
+
turnId: attempt.turnId,
|
|
1613
|
+
triggerEventId: attempt.triggerEventId!,
|
|
1614
|
+
attemptId: input.attemptId,
|
|
1615
|
+
reason: "claude_token_renewed",
|
|
1616
|
+
claudeAuthRecovery: {
|
|
1617
|
+
credentialId: scopedCredentialId,
|
|
1618
|
+
credentialVersion: receipt.expectedCredentialVersion,
|
|
1619
|
+
},
|
|
1620
|
+
detail: { code: "claude_token_renewed", retryable: true },
|
|
1621
|
+
});
|
|
1622
|
+
if (recovery.action === "stale") {
|
|
1623
|
+
acknowledgeLostAttemptOwnership();
|
|
1624
|
+
control.activityStatus = "cancelled";
|
|
1625
|
+
control.turnMetricOutcome = "cancelled";
|
|
1626
|
+
return claimedResult({ status: "cancelled" });
|
|
1627
|
+
}
|
|
1628
|
+
acknowledgeRecoveryQuiescence();
|
|
1629
|
+
await publishDurableSessionEvents(
|
|
1630
|
+
bus,
|
|
1631
|
+
input.workspaceId,
|
|
1632
|
+
input.sessionId,
|
|
1633
|
+
recovery.events,
|
|
1634
|
+
);
|
|
1635
|
+
control.activityStatus = "recovering";
|
|
1636
|
+
control.turnMetricOutcome = "recovering";
|
|
1637
|
+
control.activityError = error;
|
|
1638
|
+
return claimedResult({ status: "recovering" });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const recorded = receipt
|
|
1642
|
+
? await recordClaudeAccountUsage(
|
|
1643
|
+
db,
|
|
1644
|
+
{
|
|
1645
|
+
accountId: input.accountId,
|
|
1646
|
+
workspaceId: input.workspaceId,
|
|
1647
|
+
subjectId: scopedLease.subjectId,
|
|
1648
|
+
credentialId: scopedCredentialId,
|
|
1649
|
+
authoritySnapshot: scopedAuthority,
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
encryptionKey: key,
|
|
1653
|
+
token: receipt.token,
|
|
1654
|
+
expectedCredentialVersion: receipt.expectedCredentialVersion,
|
|
1655
|
+
...(receipt.observation ? { observation: receipt.observation } : {}),
|
|
1656
|
+
...(receipt.refresh ? { refresh: receipt.refresh } : {}),
|
|
1657
|
+
...(cooldownUntil
|
|
1658
|
+
? {
|
|
1659
|
+
modelCooldown: {
|
|
1660
|
+
upstreamModelId:
|
|
1661
|
+
receipt.upstreamModelId ?? providerTurn.claudeUpstreamModelId,
|
|
1662
|
+
until: cooldownUntil,
|
|
1663
|
+
},
|
|
1664
|
+
}
|
|
1665
|
+
: {}),
|
|
1666
|
+
},
|
|
1667
|
+
)
|
|
1668
|
+
: true;
|
|
1669
|
+
if (!recorded) return await recoverChangedAccount();
|
|
1670
|
+
}
|
|
1179
1671
|
const failurePayload = {
|
|
1180
1672
|
error:
|
|
1181
|
-
|
|
1182
|
-
? "The serving
|
|
1183
|
-
:
|
|
1184
|
-
? "The serving
|
|
1185
|
-
: "The serving
|
|
1673
|
+
scopedFailure.kind === "auth"
|
|
1674
|
+
? "The serving " + scopedName + " account requires reconnection"
|
|
1675
|
+
: scopedFailure.kind === "forbidden"
|
|
1676
|
+
? "The serving " + scopedName + " account is not authorized for this request"
|
|
1677
|
+
: "The serving " + scopedName + " account is temporarily rate limited",
|
|
1186
1678
|
code:
|
|
1187
|
-
|
|
1188
|
-
? "
|
|
1189
|
-
:
|
|
1190
|
-
? "
|
|
1191
|
-
: "
|
|
1679
|
+
scopedFailure.kind === "auth"
|
|
1680
|
+
? scopedProvider + "_relogin_required"
|
|
1681
|
+
: scopedFailure.kind === "forbidden"
|
|
1682
|
+
? scopedProvider + "_account_forbidden"
|
|
1683
|
+
: scopedProvider + "_account_rate_limited",
|
|
1192
1684
|
detail: "the same accepted turn is waiting for another eligible account",
|
|
1193
1685
|
};
|
|
1194
|
-
const armed = await
|
|
1686
|
+
const armed = await armScopedWait(db, {
|
|
1195
1687
|
accountId: input.accountId,
|
|
1196
1688
|
workspaceId: input.workspaceId,
|
|
1197
|
-
subjectId:
|
|
1689
|
+
subjectId: scopedLease.subjectId,
|
|
1198
1690
|
sessionId: input.sessionId,
|
|
1199
1691
|
turnId: attempt.turnId,
|
|
1200
1692
|
attemptId: input.attemptId,
|
|
1201
1693
|
workflowId: input.workflowId,
|
|
1202
|
-
authoritySnapshot:
|
|
1694
|
+
authoritySnapshot: scopedAuthority,
|
|
1203
1695
|
goalId: activeGoal?.id ?? null,
|
|
1204
1696
|
goalVersion: activeGoal?.version ?? null,
|
|
1205
1697
|
earliestResetAt: cooldownUntil,
|
|
1206
1698
|
failurePayload,
|
|
1207
1699
|
leaseFence: {
|
|
1208
|
-
holderId:
|
|
1209
|
-
generation:
|
|
1700
|
+
holderId: scopedLease.holderId,
|
|
1701
|
+
generation: scopedLease.generation,
|
|
1210
1702
|
},
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1703
|
+
...(claudeFailure
|
|
1704
|
+
? {
|
|
1705
|
+
expectedCredentialVersion: providerTurn.effectiveClaudeCredentialVersion!,
|
|
1706
|
+
...(claudeTokenFence ? { credentialTokenFence: claudeTokenFence } : {}),
|
|
1707
|
+
}
|
|
1708
|
+
: {}),
|
|
1709
|
+
...(!claudeFailure || scopedFailure.kind !== "rate_limit"
|
|
1710
|
+
? {
|
|
1711
|
+
credentialQuarantine:
|
|
1712
|
+
scopedFailure.kind === "auth"
|
|
1713
|
+
? {
|
|
1714
|
+
kind: "status",
|
|
1715
|
+
status: "needs_relogin",
|
|
1716
|
+
lastError: "model request remained unauthorized after refresh",
|
|
1717
|
+
}
|
|
1718
|
+
: scopedFailure.kind === "forbidden"
|
|
1719
|
+
? {
|
|
1720
|
+
kind: "status",
|
|
1721
|
+
status: "error",
|
|
1722
|
+
lastError: "model request was forbidden for this credential",
|
|
1723
|
+
}
|
|
1724
|
+
: { kind: "cooldown", until: cooldownUntil! },
|
|
1725
|
+
}
|
|
1726
|
+
: {}),
|
|
1225
1727
|
now,
|
|
1226
1728
|
});
|
|
1227
1729
|
if (armed.action === "waiting") {
|
|
1228
|
-
|
|
1229
|
-
providerTurn.xaiCredentialQuarantined = true;
|
|
1730
|
+
scopedLease.held = false;
|
|
1731
|
+
if (!claudeFailure) providerTurn.xaiCredentialQuarantined = true;
|
|
1230
1732
|
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, armed.events);
|
|
1231
|
-
const evaluated = await
|
|
1733
|
+
const evaluated = await reconcileScopedWait(db, {
|
|
1232
1734
|
accountId: input.accountId,
|
|
1233
1735
|
workspaceId: input.workspaceId,
|
|
1234
1736
|
sessionId: input.sessionId,
|
|
@@ -1256,7 +1758,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1256
1758
|
return claimedResult({
|
|
1257
1759
|
status: "waiting_capacity",
|
|
1258
1760
|
capacityWait: {
|
|
1259
|
-
provider:
|
|
1761
|
+
provider: scopedProvider,
|
|
1260
1762
|
waiterId: evaluated.waiter.id,
|
|
1261
1763
|
generation: evaluated.waiter.generation,
|
|
1262
1764
|
nextCheckAt: evaluated.waiter.nextCheckAt.toISOString(),
|
|
@@ -1275,7 +1777,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1275
1777
|
turnId: attempt.turnId,
|
|
1276
1778
|
triggerEventId: attempt.triggerEventId!,
|
|
1277
1779
|
attemptId: input.attemptId,
|
|
1278
|
-
reason: "
|
|
1780
|
+
reason: scopedProvider + "_credential_recheck",
|
|
1279
1781
|
detail: failurePayload,
|
|
1280
1782
|
});
|
|
1281
1783
|
if (recovery.action === "stale") {
|
|
@@ -1343,12 +1845,16 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1343
1845
|
if (
|
|
1344
1846
|
!(await eventing.settle!({
|
|
1345
1847
|
events: [
|
|
1848
|
+
...(error.allowance
|
|
1849
|
+
? [{ type: "usage.exhausted" as const, payload: error.allowance }]
|
|
1850
|
+
: []),
|
|
1346
1851
|
{
|
|
1347
1852
|
type: "turn.completed",
|
|
1348
1853
|
payload: {
|
|
1349
1854
|
output: "",
|
|
1350
1855
|
segmentLimit: "budget_exhausted",
|
|
1351
1856
|
detail: error.message,
|
|
1857
|
+
...(error.allowance ?? {}),
|
|
1352
1858
|
},
|
|
1353
1859
|
},
|
|
1354
1860
|
{ type: "session.status.changed", payload: { status: "idle" } },
|
|
@@ -1356,6 +1862,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1356
1862
|
turnStatus: "completed",
|
|
1357
1863
|
sessionStatus: "idle",
|
|
1358
1864
|
activeTurnId: null,
|
|
1865
|
+
...(error.allowance ? { allowanceGoalPause: { rationale: error.allowance.message } } : {}),
|
|
1359
1866
|
}))
|
|
1360
1867
|
) {
|
|
1361
1868
|
return claimedResult({ status: "cancelled" });
|
|
@@ -1452,46 +1959,87 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1452
1959
|
// truth, recover this SAME accepted turn, then let the workflow re-claim
|
|
1453
1960
|
// it after a pacing delay. This is independent of goal state and never
|
|
1454
1961
|
// relies on a synthetic continuation prompt.
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1962
|
+
// A rolling-deployment definition mismatch is a separate configuration
|
|
1963
|
+
// class: only the exact typed setup error can use this checkpoint before
|
|
1964
|
+
// eventing exists. No generic setup/credential failure gains retry authority.
|
|
1965
|
+
const earlyDefinitionMismatch =
|
|
1966
|
+
error instanceof TurnExecutionPolicyDefinitionMismatchError &&
|
|
1967
|
+
!attempt.modelRequestStarted &&
|
|
1968
|
+
!eventing.turnStartedPublished &&
|
|
1969
|
+
!!attempt.turnId &&
|
|
1970
|
+
!!attempt.triggerEventId &&
|
|
1971
|
+
attempt.executionGeneration > 0;
|
|
1972
|
+
const earlyCommandStartUnavailable =
|
|
1973
|
+
isModalTaskExecStartPreDispatchUnavailableError(error) &&
|
|
1974
|
+
!attempt.modelRequestStarted &&
|
|
1975
|
+
!eventing.turnStartedPublished &&
|
|
1976
|
+
!!attempt.turnId &&
|
|
1977
|
+
!!attempt.triggerEventId &&
|
|
1978
|
+
attempt.executionGeneration > 0;
|
|
1979
|
+
const earlyRecoverableSetup = earlyDefinitionMismatch || earlyCommandStartUnavailable;
|
|
1980
|
+
let failure = (
|
|
1981
|
+
earlyDefinitionMismatch
|
|
1982
|
+
? { error: error.message, code: error.code, retryable: true }
|
|
1983
|
+
: (codexTerminalFailure ??
|
|
1984
|
+
agentRunFailurePayload(error, {
|
|
1985
|
+
isCodexTurn: billingState.isCodexTurn,
|
|
1986
|
+
}))
|
|
1987
|
+
) as ReturnType<typeof agentRunFailurePayload>;
|
|
1988
|
+
if (
|
|
1989
|
+
attempt.turnId &&
|
|
1990
|
+
(earlyRecoverableSetup ||
|
|
1991
|
+
(failure.retryable && eventing.publish && eventing.turnStartedPublished))
|
|
1992
|
+
) {
|
|
1485
1993
|
const nextProviderRecoveryCount = attempt.providerRecoveryCount + 1;
|
|
1486
1994
|
const recoveryResult = providerRecoveryResult({
|
|
1487
1995
|
failureCode: failure.code,
|
|
1488
1996
|
attemptNumber: nextProviderRecoveryCount,
|
|
1489
1997
|
retryAfterMs: providerRetryAfterMs(error),
|
|
1490
1998
|
});
|
|
1999
|
+
const setupRecoveryExhausted =
|
|
2000
|
+
earlyCommandStartUnavailable &&
|
|
2001
|
+
recoveryResult.status === "exhausted" &&
|
|
2002
|
+
attempt.providerRecoveryCount === SANDBOX_SETUP_RECOVERY_LIMIT;
|
|
1491
2003
|
try {
|
|
2004
|
+
if (setupRecoveryExhausted) {
|
|
2005
|
+
// This is positive pre-dispatch proof, not an ambiguous command. Park
|
|
2006
|
+
// the SAME accepted turn without resetting or advancing its budget.
|
|
2007
|
+
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
2008
|
+
sessionId: input.sessionId,
|
|
2009
|
+
turnId: attempt.turnId,
|
|
2010
|
+
triggerEventId: attempt.triggerEventId!,
|
|
2011
|
+
attemptId: input.attemptId,
|
|
2012
|
+
reason: "sandbox_command_start_recovery_exhausted",
|
|
2013
|
+
sandboxSetupRecoveryExhausted: true,
|
|
2014
|
+
detail: {
|
|
2015
|
+
code: failure.code,
|
|
2016
|
+
error:
|
|
2017
|
+
"Automatic sandbox setup recovery exhausted after five retries; the accepted turn remains parked without starting a command.",
|
|
2018
|
+
retryable: false,
|
|
2019
|
+
setupOutcome: "not_started",
|
|
2020
|
+
replay: "blocked",
|
|
2021
|
+
recoveryExhausted: true,
|
|
2022
|
+
providerRecoveryCount: SANDBOX_SETUP_RECOVERY_LIMIT,
|
|
2023
|
+
},
|
|
2024
|
+
});
|
|
2025
|
+
if (recovery.action === "stale") {
|
|
2026
|
+
acknowledgeLostAttemptOwnership();
|
|
2027
|
+
control.activityStatus = "cancelled";
|
|
2028
|
+
control.turnMetricOutcome = "cancelled";
|
|
2029
|
+
return claimedResult({ status: "cancelled" });
|
|
2030
|
+
}
|
|
2031
|
+
acknowledgeRecoveryQuiescence();
|
|
2032
|
+
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, recovery.events);
|
|
2033
|
+
control.turnMetricOutcome = "recovering";
|
|
2034
|
+
control.activityStatus = "recovering";
|
|
2035
|
+
control.activityError = error;
|
|
2036
|
+
return claimedResult({ status: "recovering" });
|
|
2037
|
+
}
|
|
1492
2038
|
if (recoveryResult.status === "recovering") {
|
|
1493
|
-
|
|
1494
|
-
|
|
2039
|
+
if (!earlyRecoverableSetup) {
|
|
2040
|
+
await flushRuntimeBatcher();
|
|
2041
|
+
await historySink.reconcileConversationTruth({ requireDurable: true });
|
|
2042
|
+
}
|
|
1495
2043
|
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
1496
2044
|
sessionId: input.sessionId,
|
|
1497
2045
|
turnId: attempt.turnId,
|
|
@@ -1500,7 +2048,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1500
2048
|
reason: failure.code ?? "provider_unavailable",
|
|
1501
2049
|
providerRecoveryCount: nextProviderRecoveryCount,
|
|
1502
2050
|
detail: {
|
|
1503
|
-
...failure,
|
|
2051
|
+
...agentRunRecoveryFailurePayload(error, failure),
|
|
1504
2052
|
continueDelayMs: recoveryResult.continueDelayMs,
|
|
1505
2053
|
providerRecoveryCount: nextProviderRecoveryCount,
|
|
1506
2054
|
},
|
|
@@ -1519,6 +2067,22 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1519
2067
|
return claimedResult(recoveryResult);
|
|
1520
2068
|
}
|
|
1521
2069
|
failure = providerRecoveryExhaustedFailure(failure, recoveryResult);
|
|
2070
|
+
if (earlyRecoverableSetup) {
|
|
2071
|
+
// Setup has no eventing sink yet. Carry only the fixed, safe diagnostic
|
|
2072
|
+
// through Temporal into exact-attempt workflow failure settlement.
|
|
2073
|
+
control.activityStatus = "failed";
|
|
2074
|
+
control.turnMetricOutcome = "failed";
|
|
2075
|
+
control.activityError = error;
|
|
2076
|
+
throw ApplicationFailure.create({
|
|
2077
|
+
message: earlyDefinitionMismatch
|
|
2078
|
+
? `${error.message}. Automatic same-turn configuration recovery exhausted after ${recoveryResult.providerRecoveryCount} retries.`
|
|
2079
|
+
: failure.error,
|
|
2080
|
+
type: earlyDefinitionMismatch
|
|
2081
|
+
? "TurnExecutionPolicyDefinitionMismatchError"
|
|
2082
|
+
: "SandboxCommandStartUnavailableError",
|
|
2083
|
+
nonRetryable: true,
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
1522
2086
|
} catch (recoveryError) {
|
|
1523
2087
|
const escaped =
|
|
1524
2088
|
recoveryResult.status === "recovering"
|
|
@@ -1552,7 +2116,15 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1552
2116
|
providerRecoveryCount: nextProviderRecoveryCount,
|
|
1553
2117
|
},
|
|
1554
2118
|
})
|
|
1555
|
-
:
|
|
2119
|
+
: setupRecoveryExhausted
|
|
2120
|
+
? postClaimDatabaseRecoveryFailure({
|
|
2121
|
+
error: recoveryError,
|
|
2122
|
+
turnId: attempt.turnId,
|
|
2123
|
+
triggerEventId: attempt.triggerEventId!,
|
|
2124
|
+
executionGeneration: attempt.executionGeneration,
|
|
2125
|
+
sandboxSetupRecoveryExhausted: true,
|
|
2126
|
+
})
|
|
2127
|
+
: null;
|
|
1556
2128
|
if (postClaimRecovery) {
|
|
1557
2129
|
control.activityStatus = "recovering";
|
|
1558
2130
|
control.turnMetricOutcome = "recovering";
|
|
@@ -1562,26 +2134,6 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1562
2134
|
throw recoveryError;
|
|
1563
2135
|
}
|
|
1564
2136
|
}
|
|
1565
|
-
if (
|
|
1566
|
-
attempt.turnId &&
|
|
1567
|
-
attempt.triggerEventId &&
|
|
1568
|
-
attempt.executionGeneration > 0 &&
|
|
1569
|
-
!eventing.turnStartedPublished &&
|
|
1570
|
-
!attempt.modelRequestStarted
|
|
1571
|
-
) {
|
|
1572
|
-
const recoveryFailure = postClaimDatabaseRecoveryFailure({
|
|
1573
|
-
error,
|
|
1574
|
-
turnId: attempt.turnId,
|
|
1575
|
-
triggerEventId: attempt.triggerEventId,
|
|
1576
|
-
executionGeneration: attempt.executionGeneration,
|
|
1577
|
-
});
|
|
1578
|
-
if (recoveryFailure) {
|
|
1579
|
-
control.activityStatus = "recovering";
|
|
1580
|
-
control.turnMetricOutcome = "recovering";
|
|
1581
|
-
control.activityError = error;
|
|
1582
|
-
throw recoveryFailure;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
2137
|
control.activityStatus = "failed";
|
|
1586
2138
|
control.activityError = error;
|
|
1587
2139
|
if (!attempt.turnId) {
|