@opengeni/worker-bundle 1.0.1 → 2.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-turn/codex.d.ts +1 -1
- package/dist/activities/agent-turn/compaction-prep.d.ts +1 -5
- package/dist/activities/agent-turn/errors.d.ts +31 -5
- package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
- package/dist/activities/agent-turn/finalization.d.ts +1 -1
- package/dist/activities/agent-turn/governance-model.d.ts +0 -2
- package/dist/activities/agent-turn/quiescence.d.ts +1 -0
- package/dist/activities/agent-turn/recovery-warning.d.ts +8 -0
- package/dist/activities/agent-turn/run-credentials.d.ts +0 -1
- package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -1
- package/dist/activities/agent-turn/sandbox-route.d.ts +0 -20
- package/dist/activities/agent-turn/sandbox-runtime.d.ts +1 -1
- package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
- package/dist/activities/agent-turn/skill-search.d.ts +1 -1
- package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
- package/dist/activities/agent-turn/tool-environment.d.ts +0 -4
- package/dist/activities/agent-turn/turn-context.d.ts +2 -0
- package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
- package/dist/activities/codex-capacity.d.ts +6 -1
- package/dist/activities/codex-rotation.d.ts +1 -1
- package/dist/activities/context-compaction.d.ts +7 -1
- package/dist/activities/mcp-account-routes.d.ts +22 -0
- package/dist/activities/mcp-credentials.d.ts +12 -13
- package/dist/activities/mcp-operation-authority.d.ts +0 -5
- package/dist/activities/rig-verification.d.ts +2 -3
- package/dist/activities/sandbox-images.d.ts +1 -1
- package/dist/activities/sandbox-lease.d.ts +4 -3
- package/dist/activities/session-state.d.ts +57 -2
- package/dist/activities/types.d.ts +12 -3
- package/dist/{activities-control-HMFGSKC5.js → activities-control-BOLIY3H4.js} +366 -187
- package/dist/activities-control-BOLIY3H4.js.map +1 -0
- package/dist/activities-control.d.ts +110 -2
- package/dist/{activities-turn-AP67JADS.js → activities-turn-2B3WMHBB.js} +767 -471
- package/dist/activities-turn-2B3WMHBB.js.map +1 -0
- package/dist/activities.d.ts +110 -2
- package/dist/artifact-materializer-entry.js +5 -2
- package/dist/artifact-materializer-entry.js.map +1 -1
- package/dist/artifact-outbox-entry.js +2 -2
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/{chunk-A4XQWVZK.js → chunk-I7HKKMUJ.js} +77 -1
- package/dist/chunk-I7HKKMUJ.js.map +1 -0
- package/dist/{chunk-CYITU5UH.js → chunk-TVEE2UUC.js} +275 -146
- package/dist/chunk-TVEE2UUC.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/index.d.ts +55 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +11 -3
- package/dist/retained-process-diagnostics.d.ts +4 -0
- package/dist/sandbox-resume.d.ts +2 -12
- package/dist/sandbox-routing.d.ts +5 -3
- package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
- package/dist/warm-capture-holder.d.ts +3 -0
- package/dist/workflow-bundle.js +51 -11
- package/package.json +15 -15
- package/src/activities/agent-turn/agent-build.ts +42 -5
- package/src/activities/agent-turn/claim.ts +12 -0
- package/src/activities/agent-turn/codex-capacity.ts +26 -4
- package/src/activities/agent-turn/codex.ts +6 -2
- package/src/activities/agent-turn/compaction-prep.ts +53 -72
- package/src/activities/agent-turn/errors.ts +98 -10
- package/src/activities/agent-turn/failure-settlement.ts +49 -28
- package/src/activities/agent-turn/finalization-monitor.ts +121 -0
- package/src/activities/agent-turn/finalization.ts +79 -21
- package/src/activities/agent-turn/governance-model.ts +15 -19
- package/src/activities/agent-turn/history.ts +24 -10
- package/src/activities/agent-turn/quiescence.ts +3 -0
- package/src/activities/agent-turn/recovery-warning.ts +18 -0
- package/src/activities/agent-turn/run-credentials.ts +3 -7
- package/src/activities/agent-turn/run.ts +68 -52
- package/src/activities/agent-turn/sandbox-establish.ts +10 -2
- package/src/activities/agent-turn/sandbox-route.ts +0 -20
- package/src/activities/agent-turn/sandbox-runtime.ts +40 -7
- 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 +1 -1
- package/src/activities/agent-turn/skill-selection.ts +2 -0
- package/src/activities/agent-turn/skill-tools.ts +10 -5
- package/src/activities/agent-turn/stream-attempt.ts +38 -15
- package/src/activities/agent-turn/tool-environment.ts +44 -29
- package/src/activities/agent-turn/turn-context.ts +3 -0
- package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
- package/src/activities/capabilities.ts +17 -2
- package/src/activities/codex-capacity.ts +38 -9
- package/src/activities/codex-rotation.ts +18 -5
- package/src/activities/context-compaction.ts +26 -4
- package/src/activities/goals.ts +1 -0
- package/src/activities/google-drive-publication.ts +1 -3
- package/src/activities/mcp-account-routes.ts +139 -0
- package/src/activities/mcp-credentials.ts +83 -194
- package/src/activities/mcp-operation-authority.ts +16 -41
- package/src/activities/parent-wake.ts +8 -0
- package/src/activities/rig-verification.ts +38 -37
- package/src/activities/run-input.ts +25 -16
- package/src/activities/sandbox-images.ts +0 -1
- package/src/activities/sandbox-lease.ts +177 -44
- package/src/activities/scheduled-tasks.ts +118 -93
- package/src/activities/session-state.ts +83 -3
- package/src/activities/types.ts +18 -2
- package/src/activities/workflow-wake.ts +2 -17
- package/src/editable-artifact-materializer-service.ts +7 -3
- package/src/editable-artifact-outbox-service.ts +4 -4
- package/src/github-rest-mcp.ts +0 -1
- package/src/index.ts +2 -1
- package/src/observability-metrics.ts +105 -2
- package/src/personal-github-git-credentials.ts +0 -1
- package/src/pr-review-credentials.ts +0 -2
- package/src/retained-process-diagnostics.ts +50 -0
- package/src/sandbox-resume.ts +58 -73
- package/src/sandbox-routing.ts +44 -19
- package/src/sandbox-snapshot-diagnostics.ts +112 -0
- package/src/warm-capture-holder.ts +15 -0
- package/src/workflows/session.ts +64 -2
- 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/src/activities/agent-turn/knowledge-sources.ts +0 -86
- package/src/activities/packs.ts +0 -166
|
@@ -11,10 +11,16 @@ import {
|
|
|
11
11
|
ActiveBackendUnresolvableError,
|
|
12
12
|
CompactionProviderResponseError,
|
|
13
13
|
EmptyCompactionSummaryError,
|
|
14
|
+
compactionProviderRejection,
|
|
15
|
+
describeCompactionProviderRejection,
|
|
16
|
+
type CompactionProviderRejection,
|
|
14
17
|
isMcpRequestTimeoutError,
|
|
15
18
|
isMcpTransportConnectivityError,
|
|
16
19
|
isModalTaskExecStartDnsResolutionError,
|
|
17
20
|
RoutingWorkspaceRootChangedError,
|
|
21
|
+
SandboxMaterializationVerificationError,
|
|
22
|
+
materializationVerificationDiagnostic,
|
|
23
|
+
type MaterializationVerificationDiagnostic,
|
|
18
24
|
SelfhostedWorkspaceRootChangedError,
|
|
19
25
|
UNKNOWN_MODEL_FINISH_REASON_CODE,
|
|
20
26
|
} from "@opengeni/runtime";
|
|
@@ -28,6 +34,7 @@ import { RetainedAttachmentTransportLimitError } from "../run-input";
|
|
|
28
34
|
import type { CodexAccountStatus } from "@opengeni/db";
|
|
29
35
|
import {
|
|
30
36
|
CodexReloginRequired,
|
|
37
|
+
classifyCodexEncryptedArtifactRejection,
|
|
31
38
|
classifyCodexResponseTimeoutError,
|
|
32
39
|
classifyCodexUsageLimitError,
|
|
33
40
|
isCodexTransportError,
|
|
@@ -269,22 +276,38 @@ export function escapedMcpTimeoutRecoveryFailure(input: {
|
|
|
269
276
|
* Convert the atomic claim transaction's failure into a small, stable
|
|
270
277
|
* Temporal wire contract. The original error remains in activity diagnostics,
|
|
271
278
|
* but SQL text, parameters, and arbitrary invariant messages never enter
|
|
272
|
-
* workflow history.
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* authority refresh, so they must preserve pending work. Only a non-database
|
|
276
|
-
* claim invariant is terminal.
|
|
279
|
+
* workflow history. Operational failures retry; other persistence rejections
|
|
280
|
+
* retain accepted work behind a durable explicit-recheck fence. Repeating the
|
|
281
|
+
* same rejected transaction without changed conditions is not recovery.
|
|
277
282
|
*/
|
|
278
283
|
export function preClaimAdmissionFailure(error: unknown): ApplicationFailure {
|
|
279
284
|
const persistenceFailure = isSessionEventPersistenceError(error) ? error : null;
|
|
280
285
|
const retryableCode = retryableDatabaseFailureCode(error);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
const rejectedClaim =
|
|
287
|
+
persistenceFailure?.details.stage === "session_attempts.claim" &&
|
|
288
|
+
persistenceFailure.details.retryOutcome === "not_retryable" &&
|
|
289
|
+
!retryableCode;
|
|
285
290
|
const detail: PreClaimFailureDetail = {
|
|
286
|
-
disposition:
|
|
291
|
+
disposition: rejectedClaim
|
|
292
|
+
? "blocked"
|
|
293
|
+
: persistenceFailure || retryableCode
|
|
294
|
+
? "retryable"
|
|
295
|
+
: "permanent",
|
|
287
296
|
code: retryableCode ?? persistenceFailure?.details.code ?? "claim_invariant",
|
|
297
|
+
...(persistenceFailure && rejectedClaim
|
|
298
|
+
? {
|
|
299
|
+
sqlState: persistenceFailure.details.sqlState,
|
|
300
|
+
reason:
|
|
301
|
+
persistenceFailure.details.stage === "session_attempts.claim" &&
|
|
302
|
+
persistenceFailure.details.sqlState === "OG001"
|
|
303
|
+
? ("initiator_membership_required" as const)
|
|
304
|
+
: persistenceFailure.details.stage === "session_attempts.claim" &&
|
|
305
|
+
persistenceFailure.details.sqlState === "OG002"
|
|
306
|
+
? ("personal_resource_grant_required" as const)
|
|
307
|
+
: ("database_claim_rejected" as const),
|
|
308
|
+
retryPolicy: "explicit_recheck" as const,
|
|
309
|
+
}
|
|
310
|
+
: {}),
|
|
288
311
|
};
|
|
289
312
|
return ApplicationFailure.create({
|
|
290
313
|
message: PRE_CLAIM_FAILURE_MESSAGE,
|
|
@@ -574,7 +597,21 @@ export function safeErrorForTelemetry(error: unknown): Error {
|
|
|
574
597
|
return safe;
|
|
575
598
|
}
|
|
576
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Guidance appended to a definitive provider rejection. Repeating the exact
|
|
602
|
+
* request cannot succeed, so the generic "send another message to retry"
|
|
603
|
+
* advice is wrong here: a new prompt re-sends the same rejected history.
|
|
604
|
+
*/
|
|
605
|
+
export const COMPACTION_PROVIDER_REJECTION_GUIDANCE =
|
|
606
|
+
"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.";
|
|
607
|
+
|
|
577
608
|
export function compactionFailureReasonFromError(error: unknown): string {
|
|
609
|
+
const rejection = compactionProviderRejection(error);
|
|
610
|
+
if (rejection) {
|
|
611
|
+
return compactionFailureReason(
|
|
612
|
+
`the model provider rejected the compaction request (${describeCompactionProviderRejection(rejection)}). Active history was preserved. ${COMPACTION_PROVIDER_REJECTION_GUIDANCE}`,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
578
615
|
if (
|
|
579
616
|
error instanceof CompactionProviderResponseError ||
|
|
580
617
|
error instanceof EmptyCompactionSummaryError
|
|
@@ -585,6 +622,34 @@ export function compactionFailureReasonFromError(error: unknown): string {
|
|
|
585
622
|
return compactionFailureReason(`unexpected ${errorName}`);
|
|
586
623
|
}
|
|
587
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Exact `turn.failed` payload for a terminal compaction failure. A definitive
|
|
627
|
+
* provider rejection additionally carries its closed identifier record so the
|
|
628
|
+
* timeline, API consumers, and operators can name the rejected field without
|
|
629
|
+
* parsing the message.
|
|
630
|
+
*/
|
|
631
|
+
export function compactionFailureTurnEventPayload(
|
|
632
|
+
error: unknown,
|
|
633
|
+
overrides: { error?: string } = {},
|
|
634
|
+
): {
|
|
635
|
+
error: string;
|
|
636
|
+
code: "context_compaction_failed";
|
|
637
|
+
retryable: false;
|
|
638
|
+
recovery: "user_message";
|
|
639
|
+
compacted: false;
|
|
640
|
+
providerRejection?: CompactionProviderRejection;
|
|
641
|
+
} {
|
|
642
|
+
const rejection = compactionProviderRejection(error);
|
|
643
|
+
return {
|
|
644
|
+
error: overrides.error ?? compactionFailureReasonFromError(error),
|
|
645
|
+
code: "context_compaction_failed",
|
|
646
|
+
retryable: false,
|
|
647
|
+
recovery: "user_message",
|
|
648
|
+
compacted: false,
|
|
649
|
+
...(rejection ? { providerRejection: rejection } : {}),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
588
653
|
export function isCompactionSummaryFailure(error: unknown): boolean {
|
|
589
654
|
return (
|
|
590
655
|
error instanceof CompactionProviderResponseError || error instanceof EmptyCompactionSummaryError
|
|
@@ -594,6 +659,11 @@ export function isCompactionSummaryFailure(error: unknown): boolean {
|
|
|
594
659
|
export function shouldRecoverCompactionProviderFailure(error: unknown): boolean {
|
|
595
660
|
if (!(error instanceof CompactionProviderResponseError)) return false;
|
|
596
661
|
if (isCodexTransportError(error) && classifyCodexUsageLimitError(error)) return true;
|
|
662
|
+
// Codex may reject an opaque artifact it minted itself on the compaction
|
|
663
|
+
// request exactly as it can on an ordinary request. Failure settlement
|
|
664
|
+
// invalidates only the exact participating artifacts and recovers the same
|
|
665
|
+
// logical turn; when nothing can be invalidated it fails closed there.
|
|
666
|
+
if (classifyCodexEncryptedArtifactRejection(error)) return true;
|
|
597
667
|
return agentRunFailurePayload(error).retryable === true;
|
|
598
668
|
}
|
|
599
669
|
|
|
@@ -851,6 +921,15 @@ function findPostgresDriverError(error: unknown): Record<string, unknown> | null
|
|
|
851
921
|
export function agentRunFailurePayload(
|
|
852
922
|
error: unknown,
|
|
853
923
|
options: { isCodexTurn?: boolean } = {},
|
|
924
|
+
): ReturnType<typeof baseAgentRunFailurePayload> {
|
|
925
|
+
const failure = baseAgentRunFailurePayload(error, options);
|
|
926
|
+
const diagnostic = materializationVerificationDiagnostic(error);
|
|
927
|
+
return diagnostic ? { ...failure, materializationDiagnostic: diagnostic } : failure;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function baseAgentRunFailurePayload(
|
|
931
|
+
error: unknown,
|
|
932
|
+
options: { isCodexTurn?: boolean } = {},
|
|
854
933
|
): {
|
|
855
934
|
error: string;
|
|
856
935
|
code?: string;
|
|
@@ -870,7 +949,16 @@ export function agentRunFailurePayload(
|
|
|
870
949
|
database?: Record<string, string>;
|
|
871
950
|
historyPersistenceStage?: MandatoryHistoryPersistenceStage;
|
|
872
951
|
mcpTransportDiagnostic?: McpTransportRequestFailureDiagnostic;
|
|
952
|
+
materializationDiagnostic?: MaterializationVerificationDiagnostic;
|
|
873
953
|
} {
|
|
954
|
+
if (error instanceof SandboxMaterializationVerificationError) {
|
|
955
|
+
return {
|
|
956
|
+
error: error.message,
|
|
957
|
+
code: error.code,
|
|
958
|
+
retryable: false,
|
|
959
|
+
materializationDiagnostic: error.diagnostic,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
874
962
|
if (error instanceof RetainedAttachmentTransportLimitError) {
|
|
875
963
|
return { error: error.message, code: "retained_attachment_transport_limit", retryable: false };
|
|
876
964
|
}
|
|
@@ -225,6 +225,16 @@ function acceptedCodexPolicySnapshot(
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgentTurnResult> {
|
|
228
|
+
if (deps.settings.environment === "local" && deps.error instanceof Error) {
|
|
229
|
+
// Keep local startup failures diagnosable without logging error messages,
|
|
230
|
+
// absolute host paths, prompts, credentials, or provider response bodies.
|
|
231
|
+
const locations = (deps.error.stack ?? "")
|
|
232
|
+
.split("\n")
|
|
233
|
+
.slice(1)
|
|
234
|
+
.flatMap((line) => line.match(/(?:apps|packages)\/[A-Za-z0-9_./-]+:\d+:\d+/g) ?? [])
|
|
235
|
+
.slice(0, 8);
|
|
236
|
+
console.error(JSON.stringify({ message: "Local turn failure source locations", locations }));
|
|
237
|
+
}
|
|
228
238
|
const {
|
|
229
239
|
error,
|
|
230
240
|
input,
|
|
@@ -249,6 +259,36 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
249
259
|
acknowledgeLostAttemptOwnership,
|
|
250
260
|
acknowledgeRecoveryQuiescence,
|
|
251
261
|
} = deps;
|
|
262
|
+
// Capture before any recovery/checkpoint DB operation can fail again.
|
|
263
|
+
if (isSessionEventPersistenceError(error)) {
|
|
264
|
+
try {
|
|
265
|
+
const diagnosticId = observability.recordFailureDiagnostic({
|
|
266
|
+
code: error.details.code,
|
|
267
|
+
stage:
|
|
268
|
+
error.details.stage === "session_events.append_generic"
|
|
269
|
+
? "session_events.append_generic"
|
|
270
|
+
: error.details.stage === "session_events.append_for_turn_attempt"
|
|
271
|
+
? "session_events.append_for_turn_attempt"
|
|
272
|
+
: error.details.stage === "session_attempts.claim"
|
|
273
|
+
? "session_attempts.claim"
|
|
274
|
+
: "failure_settlement",
|
|
275
|
+
retryDecision: error.details.retryOutcome,
|
|
276
|
+
error,
|
|
277
|
+
sessionId: input.sessionId,
|
|
278
|
+
...(attempt.turnId ? { turnId: attempt.turnId } : {}),
|
|
279
|
+
attemptId: input.attemptId,
|
|
280
|
+
attempts: error.details.attempts,
|
|
281
|
+
eventTypes: error.details.eventTypes,
|
|
282
|
+
sqlState: error.details.sqlState,
|
|
283
|
+
...(error.details.database.constraint
|
|
284
|
+
? { constraint: error.details.database.constraint }
|
|
285
|
+
: {}),
|
|
286
|
+
});
|
|
287
|
+
observability.error("session event persistence failed", { correlationId: diagnosticId });
|
|
288
|
+
} catch {
|
|
289
|
+
// Failure settlement must not depend on telemetry availability.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
252
292
|
// Graceful worker shutdown (deploy / rollout restart): checkpoint the
|
|
253
293
|
// same current inference for a new fenced attempt instead of failing the
|
|
254
294
|
// session. Conversation truth is already persisted per model response;
|
|
@@ -724,7 +764,9 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
724
764
|
labels: { workspace_key: codexWorkspaceKey, outcome: "completed" },
|
|
725
765
|
});
|
|
726
766
|
const now = new Date();
|
|
727
|
-
const before = await listCodexAccountStatuses(db, input.workspaceId).catch(
|
|
767
|
+
const before = await listCodexAccountStatuses(db, input.workspaceId, attempt.turnId).catch(
|
|
768
|
+
() => [],
|
|
769
|
+
);
|
|
728
770
|
const servingCached = before.find(
|
|
729
771
|
(account) => account.id === providerTurn.effectiveCodexCredentialId,
|
|
730
772
|
);
|
|
@@ -892,7 +934,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
892
934
|
}
|
|
893
935
|
let accounts: Awaited<ReturnType<typeof listCodexAccountStatuses>>;
|
|
894
936
|
try {
|
|
895
|
-
accounts = await listCodexAccountStatuses(db, input.workspaceId);
|
|
937
|
+
accounts = await listCodexAccountStatuses(db, input.workspaceId, attempt.turnId);
|
|
896
938
|
} catch (metadataError) {
|
|
897
939
|
// Current account health/cooldown metadata is still required after
|
|
898
940
|
// quarantine. Operational database failures re-enter the existing
|
|
@@ -1126,6 +1168,11 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1126
1168
|
{ onArmed: () => (leases.codex.held = false) },
|
|
1127
1169
|
);
|
|
1128
1170
|
control.activityError = error;
|
|
1171
|
+
if (evaluated.action === "stopped") {
|
|
1172
|
+
control.activityStatus = evaluated.sessionStatus === "queued" ? "idle" : "failed";
|
|
1173
|
+
control.turnMetricOutcome = "failed";
|
|
1174
|
+
return claimedResult({ status: control.activityStatus });
|
|
1175
|
+
}
|
|
1129
1176
|
if (evaluated.action === "resumed") {
|
|
1130
1177
|
control.activityStatus = "recovering";
|
|
1131
1178
|
control.turnMetricOutcome = "recovering";
|
|
@@ -1455,32 +1502,6 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1455
1502
|
let failure = agentRunFailurePayload(error, {
|
|
1456
1503
|
isCodexTurn: billingState.isCodexTurn,
|
|
1457
1504
|
}) as ReturnType<typeof agentRunFailurePayload>;
|
|
1458
|
-
if (isSessionEventPersistenceError(error)) {
|
|
1459
|
-
// Preserve the exact source message in the internal runtime diagnostic;
|
|
1460
|
-
// SQLSTATE/catalog facts remain separate classification attributes.
|
|
1461
|
-
observability.error("session event persistence failed", {
|
|
1462
|
-
accountId: input.accountId,
|
|
1463
|
-
workspaceId: input.workspaceId,
|
|
1464
|
-
sessionId: input.sessionId,
|
|
1465
|
-
turnId: attempt.turnId,
|
|
1466
|
-
attemptId: input.attemptId,
|
|
1467
|
-
code: error.details.code,
|
|
1468
|
-
sqlState: error.details.sqlState ?? "unknown",
|
|
1469
|
-
stage: error.details.stage,
|
|
1470
|
-
eventTypes: error.details.eventTypes.join(","),
|
|
1471
|
-
correlationId: error.details.correlationId,
|
|
1472
|
-
attempts: error.details.attempts,
|
|
1473
|
-
retryOutcome: error.details.retryOutcome,
|
|
1474
|
-
dbSeverity: error.details.database.severity,
|
|
1475
|
-
dbSchema: error.details.database.schema,
|
|
1476
|
-
dbTable: error.details.database.table,
|
|
1477
|
-
dbColumn: error.details.database.column,
|
|
1478
|
-
dbDataType: error.details.database.dataType,
|
|
1479
|
-
dbConstraint: error.details.database.constraint,
|
|
1480
|
-
dbRoutine: error.details.database.routine,
|
|
1481
|
-
error: error.message,
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
1505
|
if (failure.retryable && eventing.publish && attempt.turnId && eventing.turnStartedPublished) {
|
|
1485
1506
|
const nextProviderRecoveryCount = attempt.providerRecoveryCount + 1;
|
|
1486
1507
|
const recoveryResult = providerRecoveryResult({
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Observability } from "@opengeni/observability";
|
|
2
|
+
import type { TurnHeartbeatDetails } from "../../op-journal";
|
|
3
|
+
import { armTurnQuiescenceWatchdog } from "./quiescence";
|
|
4
|
+
|
|
5
|
+
export const TURN_FINALIZATION_STAGES = [
|
|
6
|
+
"tool_writers",
|
|
7
|
+
"credential_renewals",
|
|
8
|
+
"credential_cleanup",
|
|
9
|
+
"quiescence_receipt",
|
|
10
|
+
"event_flush",
|
|
11
|
+
"provider_leases",
|
|
12
|
+
"workspace_capture",
|
|
13
|
+
"tool_close",
|
|
14
|
+
"sandbox_provisioning",
|
|
15
|
+
"sandbox_rotation",
|
|
16
|
+
"workspace_snapshot",
|
|
17
|
+
"sandbox_release",
|
|
18
|
+
] as const;
|
|
19
|
+
export type TurnFinalizationStage = (typeof TURN_FINALIZATION_STAGES)[number];
|
|
20
|
+
|
|
21
|
+
const INFLIGHT = {
|
|
22
|
+
name: "opengeni_turn_finalization_inflight",
|
|
23
|
+
help: "Physical turn finalizers currently waiting in each cleanup stage.",
|
|
24
|
+
};
|
|
25
|
+
const SLOW_STAGES = {
|
|
26
|
+
name: "opengeni_turn_finalization_slow_total",
|
|
27
|
+
help: "Turn cleanup stages observed to exceed thirty seconds.",
|
|
28
|
+
};
|
|
29
|
+
const initialized = new WeakSet<Observability>();
|
|
30
|
+
function diagnose(operation: () => void): void {
|
|
31
|
+
try {
|
|
32
|
+
operation();
|
|
33
|
+
} catch {
|
|
34
|
+
// Diagnostics cannot own physical cleanup or containment.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Starts only after agent execution ends. A cleanup-stage deadline never caps
|
|
39
|
+
* agent work and never detaches a physical writer to manufacture quiescence. */
|
|
40
|
+
export function startTurnFinalizationMonitor(input: {
|
|
41
|
+
observability: Observability;
|
|
42
|
+
details: TurnHeartbeatDetails;
|
|
43
|
+
heartbeat: (details: TurnHeartbeatDetails) => void;
|
|
44
|
+
terminateWorker: () => void;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
slowAfterMs?: number;
|
|
47
|
+
}) {
|
|
48
|
+
const { observability, details } = input;
|
|
49
|
+
if (!initialized.has(observability)) {
|
|
50
|
+
for (const stage of TURN_FINALIZATION_STAGES) {
|
|
51
|
+
diagnose(() => observability.incrementGauge({ ...INFLIGHT, labels: { stage }, amount: 0 }));
|
|
52
|
+
diagnose(() =>
|
|
53
|
+
observability.incrementCounter({ ...SLOW_STAGES, labels: { stage }, amount: 0 }),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
initialized.add(observability);
|
|
57
|
+
}
|
|
58
|
+
let stage: TurnFinalizationStage | null = null;
|
|
59
|
+
let disarm = () => {};
|
|
60
|
+
let stopped = false;
|
|
61
|
+
let slowTimer: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
const leave = () => {
|
|
63
|
+
disarm();
|
|
64
|
+
clearTimeout(slowTimer);
|
|
65
|
+
if (stage) {
|
|
66
|
+
const previous = stage;
|
|
67
|
+
diagnose(() =>
|
|
68
|
+
observability.incrementGauge({ ...INFLIGHT, labels: { stage: previous }, amount: -1 }),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
stage = null;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
enter(next: TurnFinalizationStage) {
|
|
75
|
+
if (stopped || stage === next) return;
|
|
76
|
+
leave();
|
|
77
|
+
stage = next;
|
|
78
|
+
details.phase = "finalizing";
|
|
79
|
+
details.finalizationStage = next;
|
|
80
|
+
details.finalizationStageStartedAt = new Date().toISOString();
|
|
81
|
+
diagnose(() => observability.incrementGauge({ ...INFLIGHT, labels: { stage: next } }));
|
|
82
|
+
disarm = armTurnQuiescenceWatchdog({
|
|
83
|
+
enabled: true,
|
|
84
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
85
|
+
onTimeout: () => {
|
|
86
|
+
try {
|
|
87
|
+
observability.error("turn finalization stalled; containing worker", {
|
|
88
|
+
surface: "turn_finalization",
|
|
89
|
+
outcome: "containment",
|
|
90
|
+
reason: next,
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
// Diagnostic failure must never disable physical containment.
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
terminateWorker: input.terminateWorker,
|
|
97
|
+
});
|
|
98
|
+
// Record the precursor while the process remains scrapeable. A counter
|
|
99
|
+
// increment immediately before process.exit can never reach Prometheus.
|
|
100
|
+
slowTimer = setTimeout(
|
|
101
|
+
() =>
|
|
102
|
+
diagnose(() => {
|
|
103
|
+
observability.incrementCounter({ ...SLOW_STAGES, labels: { stage: next } });
|
|
104
|
+
observability.warn("turn finalization is taking longer than expected", {
|
|
105
|
+
surface: "turn_finalization",
|
|
106
|
+
outcome: "slow",
|
|
107
|
+
reason: next,
|
|
108
|
+
});
|
|
109
|
+
}),
|
|
110
|
+
input.slowAfterMs ?? 30_000,
|
|
111
|
+
);
|
|
112
|
+
slowTimer.unref?.();
|
|
113
|
+
diagnose(() => input.heartbeat(details));
|
|
114
|
+
},
|
|
115
|
+
stop() {
|
|
116
|
+
if (stopped) return;
|
|
117
|
+
stopped = true;
|
|
118
|
+
leave();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
commitSessionAttemptQuiescence,
|
|
3
3
|
listPendingSessionTurns,
|
|
4
|
-
|
|
4
|
+
recordCodexAccountUsageForFinalization,
|
|
5
5
|
releaseCodexCredentialLease,
|
|
6
6
|
releaseXaiCredentialLease,
|
|
7
7
|
updateXaiQuotaMetadata,
|
|
@@ -13,7 +13,8 @@ import { clearRunCredentialsForAttempt } from "@opengeni/runtime";
|
|
|
13
13
|
import { fetchXaiSubscriptionQuota } from "@opengeni/xai-subscription";
|
|
14
14
|
import type { Settings } from "@opengeni/config";
|
|
15
15
|
import { signalCodexCapacityWakeTargets } from "../codex-capacity";
|
|
16
|
-
import type
|
|
16
|
+
import { startActivityHeartbeat, type currentActivityContext } from "../streaming";
|
|
17
|
+
import { startTurnFinalizationMonitor } from "./finalization-monitor";
|
|
17
18
|
import type { CodemodeTokenRenewalController } from "../codemode-token-renewal";
|
|
18
19
|
import type { RunCredentialRenewalController } from "../run-credential-renewal";
|
|
19
20
|
import type {
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
makeMachineOpObserver,
|
|
33
34
|
recordSessionEventAppendPhase,
|
|
34
35
|
turnLifecycleMetricsFor,
|
|
36
|
+
runtimeMetricsHooksForObservability,
|
|
35
37
|
} from "../../observability-metrics";
|
|
36
38
|
import {
|
|
37
39
|
maybePersistWarmWorkspaceSnapshot,
|
|
@@ -43,7 +45,6 @@ import { safeErrorDiagnostic, safeErrorForTelemetry } from "./errors";
|
|
|
43
45
|
import {
|
|
44
46
|
assertPhysicalToolQuiescenceForCancellation,
|
|
45
47
|
assertSessionAttemptQuiescenceRecoveryDurable,
|
|
46
|
-
armTurnQuiescenceWatchdog,
|
|
47
48
|
clearAttemptCredentialsWithSettledFence,
|
|
48
49
|
drainAttemptOwnedSandboxWriters,
|
|
49
50
|
persistOrSignalSessionAttemptQuiescence,
|
|
@@ -99,6 +100,42 @@ export type TurnFinalizationDeps = {
|
|
|
99
100
|
};
|
|
100
101
|
|
|
101
102
|
export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<void> {
|
|
103
|
+
const details = deps.eventing.heartbeatDetails ?? {
|
|
104
|
+
sessionId: deps.input.sessionId,
|
|
105
|
+
turnId: deps.attempt.turnId,
|
|
106
|
+
opAcks: {},
|
|
107
|
+
};
|
|
108
|
+
deps.eventing.heartbeatDetails = details;
|
|
109
|
+
// Keep one heartbeat owner through all physical cleanup, with the same op
|
|
110
|
+
// acknowledgements used during execution. It must no longer say "running".
|
|
111
|
+
if (deps.eventing.heartbeatTimer) clearInterval(deps.eventing.heartbeatTimer);
|
|
112
|
+
deps.eventing.heartbeatTimer = startActivityHeartbeat(deps.activityContext, details);
|
|
113
|
+
const monitor = startTurnFinalizationMonitor({
|
|
114
|
+
observability: deps.observability,
|
|
115
|
+
details,
|
|
116
|
+
heartbeat: (value) => {
|
|
117
|
+
try {
|
|
118
|
+
deps.activityContext?.heartbeat({ ...value, at: new Date().toISOString() });
|
|
119
|
+
} catch {
|
|
120
|
+
// A closed Temporal transport is not proof of physical quiescence.
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
terminateWorker: () => process.exit(1),
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
monitor.enter("tool_writers");
|
|
127
|
+
await finalizeTurnAttemptSteps(deps, monitor);
|
|
128
|
+
} finally {
|
|
129
|
+
monitor.stop();
|
|
130
|
+
if (deps.eventing.heartbeatTimer) clearInterval(deps.eventing.heartbeatTimer);
|
|
131
|
+
deps.eventing.heartbeatTimer = undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function finalizeTurnAttemptSteps(
|
|
136
|
+
deps: TurnFinalizationDeps,
|
|
137
|
+
monitor: ReturnType<typeof startTurnFinalizationMonitor>,
|
|
138
|
+
): Promise<void> {
|
|
102
139
|
const {
|
|
103
140
|
input,
|
|
104
141
|
settings,
|
|
@@ -142,17 +179,6 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
142
179
|
let finalizationError: unknown;
|
|
143
180
|
let physicalToolQuiescenceConfirmed = !control.acknowledgeQuiescence;
|
|
144
181
|
let quiescenceReceiptOrProofDurable = !control.acknowledgeQuiescence;
|
|
145
|
-
const disarmQuiescenceWatchdog = armTurnQuiescenceWatchdog({
|
|
146
|
-
enabled: control.acknowledgeQuiescence,
|
|
147
|
-
onTimeout: () => {
|
|
148
|
-
observability.error("turn quiescence drain exceeded hard containment deadline", {
|
|
149
|
-
"opengeni.session_id": input.sessionId,
|
|
150
|
-
"opengeni.turn_id": attempt.turnId ?? "",
|
|
151
|
-
"opengeni.attempt_id": input.attemptId,
|
|
152
|
-
});
|
|
153
|
-
},
|
|
154
|
-
terminateWorker: () => process.exit(1),
|
|
155
|
-
});
|
|
156
182
|
const finalizerSignal = turnFinalizerCancellationSignal(
|
|
157
183
|
cancellationSignal,
|
|
158
184
|
control.activityStatus,
|
|
@@ -200,6 +226,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
200
226
|
// and durably settled before workspace capture. Only receipt
|
|
201
227
|
// publication remains conditional on acknowledgeQuiescence.
|
|
202
228
|
toolCancellationFence,
|
|
229
|
+
onStage: monitor.enter,
|
|
203
230
|
cancellationReason: cancellationSignal?.reason ?? new Error("TURN_ATTEMPT_FINALIZED"),
|
|
204
231
|
gitCredentialRenewals: gitRenewalsToStop,
|
|
205
232
|
codemodeTokenRenewal: codemodeRenewalToStop,
|
|
@@ -211,6 +238,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
211
238
|
// attempt's generation before publishing physical quiescence. A failure
|
|
212
239
|
// deliberately keeps the receipt closed.
|
|
213
240
|
if (credentialSessionToClear) {
|
|
241
|
+
monitor.enter("credential_cleanup");
|
|
214
242
|
const clearAttemptCredentials = async (): Promise<void> =>
|
|
215
243
|
await clearRunCredentialsForAttempt(credentialSessionToClear, {
|
|
216
244
|
sessionId: input.sessionId,
|
|
@@ -258,6 +286,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
258
286
|
// and before lease, cache, recording, or provider cleanup. Its
|
|
259
287
|
// transaction also enqueues the exact workflow wake that will admit
|
|
260
288
|
// the replacement; Temporal activity terminalization does neither.
|
|
289
|
+
monitor.enter("quiescence_receipt");
|
|
261
290
|
const proof: SessionAttemptQuiescenceProof = {
|
|
262
291
|
accountId: input.accountId,
|
|
263
292
|
workspaceId: input.workspaceId,
|
|
@@ -304,6 +333,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
304
333
|
signalProof: signalSessionAttemptQuiesced,
|
|
305
334
|
heartbeat: (deliveryAttempt, retryMs) => {
|
|
306
335
|
activityContext?.heartbeat({
|
|
336
|
+
...eventing.heartbeatDetails,
|
|
307
337
|
phase: "quiescence-proof-delivery",
|
|
308
338
|
sessionId: input.sessionId,
|
|
309
339
|
attemptId: input.attemptId,
|
|
@@ -336,7 +366,6 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
336
366
|
},
|
|
337
367
|
});
|
|
338
368
|
quiescenceReceiptOrProofDurable = true;
|
|
339
|
-
disarmQuiescenceWatchdog();
|
|
340
369
|
if (recoveryMode === "signal") {
|
|
341
370
|
observability.info("agent turn quiescence proof handed to workflow recovery", {
|
|
342
371
|
"opengeni.session_id": input.sessionId,
|
|
@@ -351,6 +380,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
351
380
|
// the turn. Sync observer → buffer → single awaited append here (no unawaited
|
|
352
381
|
// DB write inside the activity). Scoped to this turn; skipped if no turnId
|
|
353
382
|
// (the op ran under a turn, so on the normal path turnId is set).
|
|
383
|
+
monitor.enter("event_flush");
|
|
354
384
|
const machineOpEvents = machineOpObserver.drainEvents();
|
|
355
385
|
if (machineOpEvents.length > 0 && attempt.turnId && attempt.executionGeneration > 0) {
|
|
356
386
|
await waitForTurnFinalizerStep(
|
|
@@ -377,17 +407,34 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
377
407
|
// Multi-account P4: flush the serving account's free per-turn caches ONCE,
|
|
378
408
|
// best-effort (same discipline as today's usage write). Both writers skip
|
|
379
409
|
// version/updatedAt, so neither can race the token-refresh CAS.
|
|
410
|
+
monitor.enter("provider_leases");
|
|
380
411
|
if (providerTurn.effectiveCodexCredentialId) {
|
|
381
412
|
// Part A: the latest scraped usage-header snapshot → the P2 usage cache. A
|
|
382
413
|
// full both-windows snapshot (parseCodexUsageHeaders gates on both), so this
|
|
383
414
|
// is byte-identical to the /wham/usage write — no partial-window clobber.
|
|
384
|
-
if (
|
|
415
|
+
if (
|
|
416
|
+
providerTurn.latestCodexUsage &&
|
|
417
|
+
attempt.turnId &&
|
|
418
|
+
leases.codex.held &&
|
|
419
|
+
leases.codex.holderId &&
|
|
420
|
+
leases.codex.generation !== null &&
|
|
421
|
+
providerTurn.effectiveCodexCredentialVersion !== null
|
|
422
|
+
) {
|
|
385
423
|
const usageMutation = await waitForTurnFinalizerStep(
|
|
386
|
-
|
|
424
|
+
recordCodexAccountUsageForFinalization(
|
|
387
425
|
db,
|
|
388
426
|
input.workspaceId,
|
|
389
427
|
providerTurn.effectiveCodexCredentialId,
|
|
390
428
|
providerTurn.latestCodexUsage,
|
|
429
|
+
{
|
|
430
|
+
turnId: attempt.turnId,
|
|
431
|
+
sessionId: input.sessionId,
|
|
432
|
+
attemptId: input.attemptId,
|
|
433
|
+
executionGeneration: attempt.executionGeneration,
|
|
434
|
+
holderId: leases.codex.holderId,
|
|
435
|
+
generation: leases.codex.generation,
|
|
436
|
+
credentialVersion: providerTurn.effectiveCodexCredentialVersion,
|
|
437
|
+
},
|
|
391
438
|
).catch(() => null),
|
|
392
439
|
finalizerSignal,
|
|
393
440
|
);
|
|
@@ -502,6 +549,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
502
549
|
) {
|
|
503
550
|
// Review reads yield to queued work. Recovery snapshots have their own
|
|
504
551
|
// physical-settlement requirement and are drained later in finalization.
|
|
552
|
+
monitor.enter("workspace_capture");
|
|
505
553
|
sandboxState.turnEndCaptureInProgress = true;
|
|
506
554
|
const captureSandbox = sandboxState.resolvedSandbox;
|
|
507
555
|
const captureSession = sandboxState.setupBoxSession as ChannelASession;
|
|
@@ -549,6 +597,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
549
597
|
},
|
|
550
598
|
});
|
|
551
599
|
}
|
|
600
|
+
monitor.enter("tool_close");
|
|
552
601
|
eventing.toolPreparationClosing = true;
|
|
553
602
|
if (eventing.toolPreparationReady) {
|
|
554
603
|
await waitForTurnFinalizerStep(
|
|
@@ -569,9 +618,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
569
618
|
finalizerSignal,
|
|
570
619
|
);
|
|
571
620
|
}
|
|
572
|
-
|
|
573
|
-
clearInterval(eventing.heartbeatTimer);
|
|
574
|
-
}
|
|
621
|
+
monitor.enter("sandbox_provisioning");
|
|
575
622
|
if (sandboxState.turnSandboxProvisioner?.hasStarted()) {
|
|
576
623
|
await waitForTurnFinalizerStep(
|
|
577
624
|
sandboxState.turnSandboxProvisioner.waitForSettled(30_000),
|
|
@@ -599,12 +646,14 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
599
646
|
if (sandboxState.leaseHeartbeatTimer) {
|
|
600
647
|
stopLeaseHeartbeat();
|
|
601
648
|
}
|
|
649
|
+
monitor.enter("sandbox_rotation");
|
|
602
650
|
if (sandboxState.rotationPreemptionInFlight) {
|
|
603
651
|
await sandboxState.rotationPreemptionInFlight.catch(() => undefined);
|
|
604
652
|
}
|
|
605
653
|
// No heartbeat can register another capture after this point. Join the
|
|
606
654
|
// actual provider continuation before either starting a turn-end capture
|
|
607
655
|
// or entering the proof-bearing holder release.
|
|
656
|
+
monitor.enter("workspace_snapshot");
|
|
608
657
|
await drainInFlightWarmSnapshot();
|
|
609
658
|
if (sandboxState.resolvedSandbox) {
|
|
610
659
|
// TURN-END mid-session snapshot (sandbox-file-persistence): fold the
|
|
@@ -622,7 +671,13 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
622
671
|
settledTurnId
|
|
623
672
|
) {
|
|
624
673
|
const snapshot = maybePersistWarmWorkspaceSnapshot(
|
|
625
|
-
{
|
|
674
|
+
{
|
|
675
|
+
db,
|
|
676
|
+
settings,
|
|
677
|
+
objectStorage,
|
|
678
|
+
sandboxMetrics: runtimeMetricsHooksForObservability(observability),
|
|
679
|
+
observability,
|
|
680
|
+
},
|
|
626
681
|
{
|
|
627
682
|
accountId: input.accountId,
|
|
628
683
|
workspaceId: input.workspaceId,
|
|
@@ -667,10 +722,13 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
667
722
|
// Temporal cancellation: the eager listener may already have dropped
|
|
668
723
|
// the holder, and this idempotent proof-bearing pass still must run.
|
|
669
724
|
stopLeaseHeartbeat();
|
|
725
|
+
monitor.enter("sandbox_rotation");
|
|
670
726
|
if (sandboxState.rotationPreemptionInFlight) {
|
|
671
727
|
await sandboxState.rotationPreemptionInFlight.catch(() => undefined);
|
|
672
728
|
}
|
|
729
|
+
monitor.enter("workspace_snapshot");
|
|
673
730
|
await drainInFlightWarmSnapshot();
|
|
731
|
+
monitor.enter("sandbox_release");
|
|
674
732
|
const sandboxReleaseTargets = new Set(sandboxState.lateSandboxesAwaitingWriterDrain);
|
|
675
733
|
sandboxState.lateSandboxesAwaitingWriterDrain.clear();
|
|
676
734
|
if (sandboxState.resolvedSandbox) {
|