@opengeni/worker-bundle 2.3.1 → 2.3.2-canary.37090756480001
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/claude-usage-observer.d.ts +30 -6
- package/dist/activities/agent-turn/credential-leases.d.ts +5 -32
- package/dist/activities/agent-turn/errors.d.ts +3 -0
- package/dist/activities/agent-turn/finalization-monitor.d.ts +1 -1
- package/dist/activities/agent-turn/finalization.d.ts +2 -0
- package/dist/activities/agent-turn/quiescence.d.ts +3 -4
- package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
- package/dist/activities/agent-turn/turn-context.d.ts +1 -1
- package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
- package/dist/activities/session-state.d.ts +2 -2
- package/dist/activities/types.d.ts +7 -0
- package/dist/{activities-control-3HAH4FMG.js → activities-control-3BY4IJKL.js} +166 -35
- package/dist/activities-control-3BY4IJKL.js.map +1 -0
- package/dist/activities-control.d.ts +6 -4
- package/dist/{activities-turn-KU25J3VQ.js → activities-turn-TTNAVGKN.js} +263 -308
- package/dist/activities-turn-TTNAVGKN.js.map +1 -0
- package/dist/activities.d.ts +6 -4
- package/dist/{chunk-3K3ULWEE.js → chunk-PA7Z55P4.js} +242 -17
- package/dist/chunk-PA7Z55P4.js.map +1 -0
- package/dist/{chunk-YG27NEMX.js → chunk-YOI3Z3GF.js} +9 -1
- package/dist/chunk-YOI3Z3GF.js.map +1 -0
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +82 -16
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +6 -1
- package/dist/sandbox-resume.d.ts +3 -0
- package/dist/session-event-fanout.d.ts +6 -0
- package/dist/worker-service-lifecycle.d.ts +12 -0
- package/dist/workflow-bundle.js +3 -2
- package/package.json +21 -21
- package/src/activities/agent-turn/claim.ts +13 -0
- package/src/activities/agent-turn/claude-usage-observer.ts +99 -8
- package/src/activities/agent-turn/credential-leases.ts +86 -213
- package/src/activities/agent-turn/errors.ts +61 -1
- package/src/activities/agent-turn/failure-settlement.ts +59 -5
- package/src/activities/agent-turn/finalization-monitor.ts +3 -4
- package/src/activities/agent-turn/finalization.ts +8 -3
- package/src/activities/agent-turn/quiescence.ts +3 -4
- package/src/activities/agent-turn/run.ts +24 -20
- package/src/activities/agent-turn/sandbox-establish.ts +2 -0
- package/src/activities/agent-turn/sandbox-runtime.ts +5 -1
- package/src/activities/agent-turn/skill-selection.ts +2 -0
- package/src/activities/agent-turn/subscription-lease.ts +115 -0
- package/src/activities/agent-turn/turn-context.ts +1 -4
- package/src/activities/codex-rotation.ts +2 -12
- package/src/activities/model-call-fact-reconciler.ts +112 -0
- package/src/activities/sandbox-lease.ts +59 -8
- package/src/activities/session-state.ts +29 -4
- package/src/activities/types.ts +7 -0
- package/src/activities-control.ts +2 -0
- package/src/activities-turn.ts +5 -0
- package/src/activities.ts +7 -0
- package/src/http.ts +4 -2
- package/src/index.ts +39 -9
- package/src/observability-metrics.ts +27 -0
- package/src/sandbox-resume.ts +9 -0
- package/src/sandbox-routing.ts +77 -0
- package/src/session-event-fanout.ts +23 -0
- package/src/worker-service-lifecycle.ts +53 -0
- package/src/workflows/file-upload-reaper.ts +2 -0
- package/src/workflows/session.ts +6 -1
- package/dist/activities-control-3HAH4FMG.js.map +0 -1
- package/dist/activities-turn-KU25J3VQ.js.map +0 -1
- package/dist/chunk-3K3ULWEE.js.map +0 -1
- package/dist/chunk-YG27NEMX.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SandboxCapabilitiesChangedError } from "./provider-dispatch-barrier";
|
|
2
|
+
import { ClaudeSubscriptionConnectionUnavailable } from "./claude-usage-observer";
|
|
2
3
|
import {
|
|
3
4
|
ActiveSessionHistoryLimitExceededError,
|
|
4
5
|
ApprovalRunStateLimitExceededError,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from "@opengeni/db";
|
|
13
14
|
import {
|
|
14
15
|
ActiveBackendUnresolvableError,
|
|
16
|
+
AnthropicProviderRejection,
|
|
15
17
|
CompactionProviderResponseError,
|
|
16
18
|
EmptyCompactionSummaryError,
|
|
17
19
|
compactionProviderRejection,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
isMcpTransportConnectivityError,
|
|
22
24
|
isModalTaskExecStartPreDispatchUnavailableError,
|
|
23
25
|
isModalCommandStartOutcomeUnknownError,
|
|
26
|
+
isProviderCommandObservationUnavailableError,
|
|
24
27
|
isRoutingMutationOutcomeUnknownError,
|
|
25
28
|
RoutingWorkspaceRootChangedError,
|
|
26
29
|
ResponsesStreamingTerminalError,
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
providerQuotaExhaustedMessage,
|
|
35
38
|
SelfhostedWorkspaceRootChangedError,
|
|
36
39
|
UNKNOWN_MODEL_FINISH_REASON_CODE,
|
|
40
|
+
AnthropicRequestError,
|
|
37
41
|
} from "@opengeni/runtime";
|
|
38
42
|
import {
|
|
39
43
|
mcpTransportRequestFailureDiagnostic,
|
|
@@ -396,6 +400,7 @@ export function postClaimDatabaseRecoveryFailure(input: {
|
|
|
396
400
|
triggerEventId: string;
|
|
397
401
|
executionGeneration: number;
|
|
398
402
|
sandboxSetupOutcomeUnknown?: true;
|
|
403
|
+
sandboxSetupRecoveryExhausted?: true;
|
|
399
404
|
providerRecovery?: {
|
|
400
405
|
failureCode: string;
|
|
401
406
|
providerRecoveryCount: number;
|
|
@@ -403,7 +408,13 @@ export function postClaimDatabaseRecoveryFailure(input: {
|
|
|
403
408
|
}): ApplicationFailure | null {
|
|
404
409
|
const code = retryableDatabaseFailureCode(input.error);
|
|
405
410
|
if (!code || input.executionGeneration < 1) return null;
|
|
406
|
-
if (
|
|
411
|
+
if (
|
|
412
|
+
(input.sandboxSetupOutcomeUnknown && input.sandboxSetupRecoveryExhausted) ||
|
|
413
|
+
((input.sandboxSetupOutcomeUnknown || input.sandboxSetupRecoveryExhausted) &&
|
|
414
|
+
input.providerRecovery)
|
|
415
|
+
) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
407
418
|
if (
|
|
408
419
|
input.providerRecovery &&
|
|
409
420
|
(!Number.isSafeInteger(input.providerRecovery.providerRecoveryCount) ||
|
|
@@ -419,6 +430,7 @@ export function postClaimDatabaseRecoveryFailure(input: {
|
|
|
419
430
|
executionGeneration: input.executionGeneration,
|
|
420
431
|
code,
|
|
421
432
|
...(input.sandboxSetupOutcomeUnknown ? { sandboxSetupOutcomeUnknown: true } : {}),
|
|
433
|
+
...(input.sandboxSetupRecoveryExhausted ? { sandboxSetupRecoveryExhausted: true } : {}),
|
|
422
434
|
...(input.providerRecovery
|
|
423
435
|
? {
|
|
424
436
|
providerFailureCode: input.providerRecovery.failureCode,
|
|
@@ -1032,15 +1044,44 @@ function isRawDatabaseQueryError(error: unknown): boolean {
|
|
|
1032
1044
|
);
|
|
1033
1045
|
}
|
|
1034
1046
|
|
|
1047
|
+
function anthropicRequestDiagnostic(error: unknown): AnthropicRequestError | undefined {
|
|
1048
|
+
if (error instanceof AnthropicRequestError) return error;
|
|
1049
|
+
return error instanceof Error && error.cause instanceof AnthropicRequestError
|
|
1050
|
+
? error.cause
|
|
1051
|
+
: undefined;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1035
1054
|
export function agentRunFailurePayload(
|
|
1036
1055
|
error: unknown,
|
|
1037
1056
|
options: { isCodexTurn?: boolean } = {},
|
|
1038
1057
|
): ReturnType<typeof baseAgentRunFailurePayload> {
|
|
1039
1058
|
const failure = baseAgentRunFailurePayload(error, options);
|
|
1040
1059
|
const diagnostic = materializationVerificationDiagnostic(error);
|
|
1060
|
+
const anthropic = anthropicRequestDiagnostic(error);
|
|
1061
|
+
if (anthropic) {
|
|
1062
|
+
return {
|
|
1063
|
+
...failure,
|
|
1064
|
+
code: failure.code ?? anthropic.code,
|
|
1065
|
+
retryable: failure.retryable ?? false,
|
|
1066
|
+
...(anthropic.detail ? { detail: anthropic.detail } : {}),
|
|
1067
|
+
...(anthropic.request_id ? { requestId: anthropic.request_id } : {}),
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1041
1070
|
return diagnostic ? { ...failure, materializationDiagnostic: diagnostic } : failure;
|
|
1042
1071
|
}
|
|
1043
1072
|
|
|
1073
|
+
/** Keep Anthropic provider text on terminal failures, never recovery events. */
|
|
1074
|
+
export function agentRunRecoveryFailurePayload(
|
|
1075
|
+
error: unknown,
|
|
1076
|
+
failure: ReturnType<typeof agentRunFailurePayload>,
|
|
1077
|
+
): ReturnType<typeof agentRunFailurePayload> {
|
|
1078
|
+
if (!anthropicRequestDiagnostic(error)) return failure;
|
|
1079
|
+
// Project a copy: retry exhaustion still needs the terminal diagnostic.
|
|
1080
|
+
const recovery = { ...failure };
|
|
1081
|
+
delete recovery.detail;
|
|
1082
|
+
return recovery;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1044
1085
|
function baseAgentRunFailurePayload(
|
|
1045
1086
|
error: unknown,
|
|
1046
1087
|
options: { isCodexTurn?: boolean } = {},
|
|
@@ -1111,6 +1152,17 @@ function baseAgentRunFailurePayload(
|
|
|
1111
1152
|
...(Object.keys(database).length > 0 ? { database } : {}),
|
|
1112
1153
|
};
|
|
1113
1154
|
}
|
|
1155
|
+
if (error instanceof ClaudeSubscriptionConnectionUnavailable) {
|
|
1156
|
+
return { error: error.message, code: error.code, retryable: false };
|
|
1157
|
+
}
|
|
1158
|
+
if (error instanceof AnthropicProviderRejection) {
|
|
1159
|
+
return {
|
|
1160
|
+
error: error.message,
|
|
1161
|
+
code: error.code === "content_policy_violation" ? "provider_safety_refusal" : error.code,
|
|
1162
|
+
retryable: false,
|
|
1163
|
+
...(error.request_id ? { requestId: error.request_id } : {}),
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1114
1166
|
const safetyRefusalDiagnostic = providerSafetyRefusalDiagnostic(error);
|
|
1115
1167
|
if (safetyRefusalDiagnostic !== undefined) {
|
|
1116
1168
|
return {
|
|
@@ -1194,6 +1246,14 @@ function baseAgentRunFailurePayload(
|
|
|
1194
1246
|
retryable: true,
|
|
1195
1247
|
};
|
|
1196
1248
|
}
|
|
1249
|
+
if (isProviderCommandObservationUnavailableError(error)) {
|
|
1250
|
+
return {
|
|
1251
|
+
error:
|
|
1252
|
+
"A managed sandbox command cannot be observed. Its exact invocation and writer remain retained; setup is blocked without replay until the incomplete operation can be reconciled.",
|
|
1253
|
+
code: "sandbox_command_observation_unavailable",
|
|
1254
|
+
retryable: false,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1197
1257
|
if (isModalCommandStartOutcomeUnknownError(error)) {
|
|
1198
1258
|
return {
|
|
1199
1259
|
error:
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
readLease,
|
|
14
14
|
SandboxLeaseSupersededError,
|
|
15
15
|
isSessionEventPersistenceError,
|
|
16
|
+
SANDBOX_SETUP_RECOVERY_LIMIT,
|
|
16
17
|
type CodexLeaseAccountStatus,
|
|
17
18
|
} from "@opengeni/db";
|
|
18
19
|
import { publishDurableSessionEvents } from "@opengeni/events";
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
maxTurnsExceededRunState,
|
|
21
22
|
isModalTaskExecStartPreDispatchUnavailableError,
|
|
22
23
|
isModalCommandStartOutcomeUnknownError,
|
|
24
|
+
isProviderCommandObservationUnavailableError,
|
|
23
25
|
} from "@opengeni/runtime";
|
|
24
26
|
import { ApplicationFailure, CancelledFailure } from "@temporalio/activity";
|
|
25
27
|
import {
|
|
@@ -68,6 +70,7 @@ import {
|
|
|
68
70
|
safeErrorDiagnostic,
|
|
69
71
|
classifyXaiCredentialFailure,
|
|
70
72
|
agentRunFailurePayload,
|
|
73
|
+
agentRunRecoveryFailurePayload,
|
|
71
74
|
codexCredentialCooldownUntil,
|
|
72
75
|
classifyCodexCredentialFailure,
|
|
73
76
|
codexUsageLimitFailurePayload,
|
|
@@ -343,8 +346,9 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
343
346
|
// cannot reconstruct setup on a replacement attempt. The exact command and
|
|
344
347
|
// writer remain retained by sandbox-runtime; the logical turn is parked as
|
|
345
348
|
// recovering with a durable no-replay marker, not failed or completed.
|
|
349
|
+
const observationUnavailable = isProviderCommandObservationUnavailableError(error);
|
|
346
350
|
if (
|
|
347
|
-
isModalCommandStartOutcomeUnknownError(error) &&
|
|
351
|
+
(isModalCommandStartOutcomeUnknownError(error) || observationUnavailable) &&
|
|
348
352
|
recoveryTurnId &&
|
|
349
353
|
attempt.triggerEventId &&
|
|
350
354
|
attempt.executionGeneration > 0
|
|
@@ -360,10 +364,14 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
360
364
|
turnId: recoveryTurnId,
|
|
361
365
|
triggerEventId: attempt.triggerEventId,
|
|
362
366
|
attemptId: input.attemptId,
|
|
363
|
-
reason:
|
|
367
|
+
reason: observationUnavailable
|
|
368
|
+
? "sandbox_command_observation_unavailable"
|
|
369
|
+
: "sandbox_command_start_outcome_unknown",
|
|
364
370
|
sandboxSetupOutcomeUnknown: true,
|
|
365
371
|
detail: {
|
|
366
|
-
code:
|
|
372
|
+
code: observationUnavailable
|
|
373
|
+
? "sandbox_command_observation_unavailable"
|
|
374
|
+
: "sandbox_command_start_outcome_unknown",
|
|
367
375
|
retryable: false,
|
|
368
376
|
setupOutcome: "unknown",
|
|
369
377
|
replay: "blocked",
|
|
@@ -1749,7 +1757,45 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1749
1757
|
attemptNumber: nextProviderRecoveryCount,
|
|
1750
1758
|
retryAfterMs: providerRetryAfterMs(error),
|
|
1751
1759
|
});
|
|
1760
|
+
const setupRecoveryExhausted =
|
|
1761
|
+
earlyCommandStartUnavailable &&
|
|
1762
|
+
recoveryResult.status === "exhausted" &&
|
|
1763
|
+
attempt.providerRecoveryCount === SANDBOX_SETUP_RECOVERY_LIMIT;
|
|
1752
1764
|
try {
|
|
1765
|
+
if (setupRecoveryExhausted) {
|
|
1766
|
+
// This is positive pre-dispatch proof, not an ambiguous command. Park
|
|
1767
|
+
// the SAME accepted turn without resetting or advancing its budget.
|
|
1768
|
+
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
1769
|
+
sessionId: input.sessionId,
|
|
1770
|
+
turnId: attempt.turnId,
|
|
1771
|
+
triggerEventId: attempt.triggerEventId!,
|
|
1772
|
+
attemptId: input.attemptId,
|
|
1773
|
+
reason: "sandbox_command_start_recovery_exhausted",
|
|
1774
|
+
sandboxSetupRecoveryExhausted: true,
|
|
1775
|
+
detail: {
|
|
1776
|
+
code: failure.code,
|
|
1777
|
+
error:
|
|
1778
|
+
"Automatic sandbox setup recovery exhausted after five retries; the accepted turn remains parked without starting a command.",
|
|
1779
|
+
retryable: false,
|
|
1780
|
+
setupOutcome: "not_started",
|
|
1781
|
+
replay: "blocked",
|
|
1782
|
+
recoveryExhausted: true,
|
|
1783
|
+
providerRecoveryCount: SANDBOX_SETUP_RECOVERY_LIMIT,
|
|
1784
|
+
},
|
|
1785
|
+
});
|
|
1786
|
+
if (recovery.action === "stale") {
|
|
1787
|
+
acknowledgeLostAttemptOwnership();
|
|
1788
|
+
control.activityStatus = "cancelled";
|
|
1789
|
+
control.turnMetricOutcome = "cancelled";
|
|
1790
|
+
return claimedResult({ status: "cancelled" });
|
|
1791
|
+
}
|
|
1792
|
+
acknowledgeRecoveryQuiescence();
|
|
1793
|
+
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, recovery.events);
|
|
1794
|
+
control.turnMetricOutcome = "recovering";
|
|
1795
|
+
control.activityStatus = "recovering";
|
|
1796
|
+
control.activityError = error;
|
|
1797
|
+
return claimedResult({ status: "recovering" });
|
|
1798
|
+
}
|
|
1753
1799
|
if (recoveryResult.status === "recovering") {
|
|
1754
1800
|
if (!earlyRecoverableSetup) {
|
|
1755
1801
|
await flushRuntimeBatcher();
|
|
@@ -1763,7 +1809,7 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1763
1809
|
reason: failure.code ?? "provider_unavailable",
|
|
1764
1810
|
providerRecoveryCount: nextProviderRecoveryCount,
|
|
1765
1811
|
detail: {
|
|
1766
|
-
...failure,
|
|
1812
|
+
...agentRunRecoveryFailurePayload(error, failure),
|
|
1767
1813
|
continueDelayMs: recoveryResult.continueDelayMs,
|
|
1768
1814
|
providerRecoveryCount: nextProviderRecoveryCount,
|
|
1769
1815
|
},
|
|
@@ -1831,7 +1877,15 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1831
1877
|
providerRecoveryCount: nextProviderRecoveryCount,
|
|
1832
1878
|
},
|
|
1833
1879
|
})
|
|
1834
|
-
:
|
|
1880
|
+
: setupRecoveryExhausted
|
|
1881
|
+
? postClaimDatabaseRecoveryFailure({
|
|
1882
|
+
error: recoveryError,
|
|
1883
|
+
turnId: attempt.turnId,
|
|
1884
|
+
triggerEventId: attempt.triggerEventId!,
|
|
1885
|
+
executionGeneration: attempt.executionGeneration,
|
|
1886
|
+
sandboxSetupRecoveryExhausted: true,
|
|
1887
|
+
})
|
|
1888
|
+
: null;
|
|
1835
1889
|
if (postClaimRecovery) {
|
|
1836
1890
|
control.activityStatus = "recovering";
|
|
1837
1891
|
control.turnMetricOutcome = "recovering";
|
|
@@ -41,7 +41,7 @@ export function startTurnFinalizationMonitor(input: {
|
|
|
41
41
|
observability: Observability;
|
|
42
42
|
details: TurnHeartbeatDetails;
|
|
43
43
|
heartbeat: (details: TurnHeartbeatDetails) => void;
|
|
44
|
-
|
|
44
|
+
requestWorkerDrain: () => void;
|
|
45
45
|
timeoutMs?: number;
|
|
46
46
|
slowAfterMs?: number;
|
|
47
47
|
}) {
|
|
@@ -93,10 +93,9 @@ export function startTurnFinalizationMonitor(input: {
|
|
|
93
93
|
// Diagnostic failure must never disable physical containment.
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
|
-
terminateWorker: input.
|
|
96
|
+
terminateWorker: input.requestWorkerDrain,
|
|
97
97
|
});
|
|
98
|
-
// Record the precursor while the process remains scrapeable.
|
|
99
|
-
// increment immediately before process.exit can never reach Prometheus.
|
|
98
|
+
// Record the precursor while the process remains scrapeable.
|
|
100
99
|
slowTimer = setTimeout(
|
|
101
100
|
() =>
|
|
102
101
|
diagnose(() => {
|
|
@@ -74,6 +74,8 @@ export type TurnFinalizationDeps = {
|
|
|
74
74
|
wakeSessionWorkflow: ActivityServices["wakeSessionWorkflow"];
|
|
75
75
|
signalSessionAttemptQuiesced: ActivityServices["signalSessionAttemptQuiesced"];
|
|
76
76
|
signalCodexCapacityWorkflow: ActivityServices["signalCodexCapacityWorkflow"];
|
|
77
|
+
requestWorkerDrain: ActivityServices["requestWorkerDrain"];
|
|
78
|
+
turnFinalizationTimeoutMs: ActivityServices["turnFinalizationTimeoutMs"];
|
|
77
79
|
cancellationSignal: AbortSignal | undefined;
|
|
78
80
|
sandboxResumeController: AbortController;
|
|
79
81
|
activityContext: ReturnType<typeof currentActivityContext>;
|
|
@@ -113,6 +115,9 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
113
115
|
deps.eventing.heartbeatTimer = startActivityHeartbeat(deps.activityContext, details);
|
|
114
116
|
const monitor = startTurnFinalizationMonitor({
|
|
115
117
|
observability: deps.observability,
|
|
118
|
+
...(deps.turnFinalizationTimeoutMs === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: { timeoutMs: deps.turnFinalizationTimeoutMs }),
|
|
116
121
|
details,
|
|
117
122
|
heartbeat: (value) => {
|
|
118
123
|
try {
|
|
@@ -121,7 +126,7 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
121
126
|
// A closed Temporal transport is not proof of physical quiescence.
|
|
122
127
|
}
|
|
123
128
|
},
|
|
124
|
-
|
|
129
|
+
requestWorkerDrain: deps.requestWorkerDrain,
|
|
125
130
|
});
|
|
126
131
|
try {
|
|
127
132
|
monitor.enter("tool_writers");
|
|
@@ -411,12 +416,12 @@ async function finalizeTurnAttemptSteps(
|
|
|
411
416
|
// best-effort (same discipline as today's usage write). Both writers skip
|
|
412
417
|
// version/updatedAt, so neither can race the token-refresh CAS.
|
|
413
418
|
monitor.enter("provider_leases");
|
|
414
|
-
for (const
|
|
419
|
+
for (const snapshot of providerTurn.latestClaudeUsage.values()) {
|
|
415
420
|
await waitForTurnFinalizerStep(
|
|
416
421
|
recordClaudeSubscriptionUsage(
|
|
417
422
|
db,
|
|
418
423
|
settings,
|
|
419
|
-
{ accountId: input.accountId, workspaceId: input.workspaceId, scope },
|
|
424
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId, scope: snapshot.scope },
|
|
420
425
|
snapshot,
|
|
421
426
|
).catch(() => null),
|
|
422
427
|
finalizerSignal,
|
|
@@ -91,10 +91,9 @@ export const QUIESCENCE_PROOF_SIGNAL_MAX_RETRY_MS = 5_000;
|
|
|
91
91
|
export const TURN_QUIESCENCE_WATCHDOG_MS = 5 * 60_000;
|
|
92
92
|
export const TURN_FINALIZER_STEP_TIMEOUT_MS = 30_000;
|
|
93
93
|
|
|
94
|
-
/** A fenced activity must never heartbeat forever while
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* then commits the exact quiescence receipt. */
|
|
94
|
+
/** A fenced activity must never heartbeat forever while physical cleanup is
|
|
95
|
+
* stuck. The callback requests host-owned containment, which drains peer turns
|
|
96
|
+
* before applying its final exit policy. A timeout is never quiescence proof. */
|
|
98
97
|
export function armTurnQuiescenceWatchdog(input: {
|
|
99
98
|
enabled: boolean;
|
|
100
99
|
timeoutMs?: number;
|
|
@@ -183,6 +183,8 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
|
|
|
183
183
|
bus,
|
|
184
184
|
runtime,
|
|
185
185
|
summarizeContextForCompaction,
|
|
186
|
+
requestWorkerDrain,
|
|
187
|
+
turnFinalizationTimeoutMs,
|
|
186
188
|
objectStorage,
|
|
187
189
|
observability,
|
|
188
190
|
wakeSessionWorkflow,
|
|
@@ -944,28 +946,28 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
|
|
|
944
946
|
);
|
|
945
947
|
const withClaudeUsage = <T>(fn: () => Promise<T>): Promise<T> =>
|
|
946
948
|
withClaudeUsageObserver(claudeUsageObserver, fn, async (providerId, headers) => {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
949
|
+
return claudeUsageObserver.prepareRequestWithObserver(
|
|
950
|
+
providerId,
|
|
951
|
+
headers,
|
|
952
|
+
async (binding) => {
|
|
953
|
+
const credential = await resolveClaudeSubscriptionCredential(
|
|
954
|
+
db,
|
|
955
|
+
settings,
|
|
956
|
+
{
|
|
957
|
+
accountId: input.accountId,
|
|
958
|
+
workspaceId: input.workspaceId,
|
|
959
|
+
scope: binding.scope,
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
expectedConnectionId: binding.expectedConnectionId,
|
|
963
|
+
expectedCredentialVersion: binding.expectedCredentialVersion,
|
|
964
|
+
},
|
|
965
|
+
);
|
|
966
|
+
if (credential && "reconnectRequired" in credential)
|
|
967
|
+
throw new ClaudeSubscriptionReconnectRequired();
|
|
968
|
+
return credential;
|
|
960
969
|
},
|
|
961
970
|
);
|
|
962
|
-
if (!credential) return headers; // A replaced connection never lends its new token to this turn.
|
|
963
|
-
if ("reconnectRequired" in credential)
|
|
964
|
-
throw new ClaudeSubscriptionReconnectRequired();
|
|
965
|
-
claudeUsageObserver.renew(providerId, credential);
|
|
966
|
-
headers.set("authorization", `Bearer ${credential.token}`);
|
|
967
|
-
headers.delete("x-api-key");
|
|
968
|
-
return headers;
|
|
969
971
|
});
|
|
970
972
|
const withCodex = <T>(fn: () => Promise<T>): Promise<T> =>
|
|
971
973
|
codexContext ? codexRequestStorage.run(codexContext, fn) : fn();
|
|
@@ -1844,6 +1846,8 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
|
|
|
1844
1846
|
});
|
|
1845
1847
|
} finally {
|
|
1846
1848
|
await finalizeTurnAttempt({
|
|
1849
|
+
turnFinalizationTimeoutMs,
|
|
1850
|
+
requestWorkerDrain,
|
|
1847
1851
|
input,
|
|
1848
1852
|
settings,
|
|
1849
1853
|
db,
|
|
@@ -656,6 +656,7 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
|
|
|
656
656
|
freshSandboxReadinessReplacementBudget,
|
|
657
657
|
onSandboxLost: publishSandboxLost,
|
|
658
658
|
objectStorage,
|
|
659
|
+
bus,
|
|
659
660
|
},
|
|
660
661
|
{
|
|
661
662
|
accountId: input.accountId,
|
|
@@ -735,6 +736,7 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
|
|
|
735
736
|
freshSandboxReadinessReplacementBudget,
|
|
736
737
|
onSandboxLost: publishSandboxLost,
|
|
737
738
|
objectStorage,
|
|
739
|
+
bus,
|
|
738
740
|
},
|
|
739
741
|
{
|
|
740
742
|
accountId: input.accountId,
|
|
@@ -13,6 +13,7 @@ import { createProviderCommandRetainer } from "@opengeni/db/retained-provider-co
|
|
|
13
13
|
import {
|
|
14
14
|
RoutingMutationOutcomeUnknownError,
|
|
15
15
|
ProviderCommandStartOutcomeUnknownError,
|
|
16
|
+
ProviderCommandObservationUnavailableError,
|
|
16
17
|
isModalCommandStartOutcomeUnknownError,
|
|
17
18
|
withModalCommandStartSignal,
|
|
18
19
|
resolveModalCheckpointProviderBindingForSession,
|
|
@@ -84,7 +85,10 @@ function internalMutationUnknownCommand(
|
|
|
84
85
|
if (!value || typeof value !== "object" || seen.has(value)) continue;
|
|
85
86
|
seen.add(value);
|
|
86
87
|
try {
|
|
87
|
-
if (
|
|
88
|
+
if (
|
|
89
|
+
value instanceof ProviderCommandStartOutcomeUnknownError ||
|
|
90
|
+
value instanceof ProviderCommandObservationUnavailableError
|
|
91
|
+
) {
|
|
88
92
|
unknown = true;
|
|
89
93
|
// Zod reads ordinary properties. First make a bounded own-data-only
|
|
90
94
|
// copy so neither validation nor equality executes descriptor getters.
|
|
@@ -20,6 +20,7 @@ export function configuredBundledSkillNames(context: BundledSkillConfiguration):
|
|
|
20
20
|
{ name: "document-parsing", include: () => true },
|
|
21
21
|
{ name: "opengeni-skills", include: () => true },
|
|
22
22
|
{ name: "opengeni-projects", include: () => true },
|
|
23
|
+
{ name: "opengeni-schedules", include: () => tools.has("scheduled_tasks_create") },
|
|
23
24
|
{ name: "opengeni-documents", include: artifacts },
|
|
24
25
|
{ name: "opengeni-spreadsheets", include: artifacts },
|
|
25
26
|
{ name: "opengeni-presentations", include: artifacts },
|
|
@@ -43,6 +44,7 @@ export function loadConfiguredBundledSkills(context: BundledSkillConfiguration)
|
|
|
43
44
|
const names = new Set(configuredBundledSkillNames(context));
|
|
44
45
|
const artifacts = loadNativeToolSkillArtifacts({
|
|
45
46
|
projects: names.has("opengeni-projects"),
|
|
47
|
+
schedules: names.has("opengeni-schedules"),
|
|
46
48
|
editableArtifacts: [
|
|
47
49
|
"opengeni-documents",
|
|
48
50
|
"opengeni-spreadsheets",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type LeaseRenewReason = "timer" | "runtime_event" | "model_usage";
|
|
2
|
+
export type LeaseLossReason = "deadline" | "not_found";
|
|
3
|
+
|
|
4
|
+
type LeaseIdentity = { turnId: string; holderId: string; generation: number };
|
|
5
|
+
export type SubscriptionLeaseDeps = {
|
|
6
|
+
ttlMs: number;
|
|
7
|
+
getTurnId: () => string | undefined;
|
|
8
|
+
heartbeat: (identity: LeaseIdentity) => Promise<Date | null>;
|
|
9
|
+
onLost: (reason: LeaseLossReason) => void;
|
|
10
|
+
onRenewed?: (reason: LeaseRenewReason) => void;
|
|
11
|
+
onError: (error: unknown, reason: LeaseRenewReason) => void;
|
|
12
|
+
lostError: (reason: LeaseLossReason) => Error;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Cross-provider lease lifecycle. Only on-time DB confirmation extends ownership. */
|
|
17
|
+
export class SubscriptionTurnLease {
|
|
18
|
+
held = false;
|
|
19
|
+
lost = false;
|
|
20
|
+
lossReason: LeaseLossReason | null = null;
|
|
21
|
+
holderId: string | null = null;
|
|
22
|
+
generation: number | null = null;
|
|
23
|
+
confirmedUntilMs: number | null = null;
|
|
24
|
+
private heartbeatInFlight = false;
|
|
25
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | undefined;
|
|
26
|
+
private readonly now: () => number;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly leaseDeps: SubscriptionLeaseDeps) {
|
|
29
|
+
if (!Number.isFinite(leaseDeps.ttlMs) || leaseDeps.ttlMs <= 0)
|
|
30
|
+
throw new Error("Subscription lease TTL must be positive");
|
|
31
|
+
this.now = leaseDeps.now ?? (() => performance.now());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private expired = (deadline: number | null): boolean =>
|
|
35
|
+
deadline === null || !Number.isFinite(deadline) || this.now() >= deadline;
|
|
36
|
+
|
|
37
|
+
markLost = (reason: LeaseLossReason): void => {
|
|
38
|
+
if (this.lost) return;
|
|
39
|
+
this.lost = true;
|
|
40
|
+
this.lossReason = reason;
|
|
41
|
+
this.leaseDeps.onLost(reason);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
assertUsable = (): void => {
|
|
45
|
+
if (this.lost) throw this.leaseDeps.lostError(this.lossReason ?? "not_found");
|
|
46
|
+
if (
|
|
47
|
+
!this.held ||
|
|
48
|
+
this.holderId === null ||
|
|
49
|
+
this.generation === null ||
|
|
50
|
+
this.expired(this.confirmedUntilMs)
|
|
51
|
+
) {
|
|
52
|
+
this.markLost("deadline");
|
|
53
|
+
throw this.leaseDeps.lostError("deadline");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
renew = async (reason: LeaseRenewReason = "timer"): Promise<void> => {
|
|
58
|
+
const turnId = this.leaseDeps.getTurnId();
|
|
59
|
+
if (!turnId || !this.held || !this.holderId || this.generation === null || this.lost) return;
|
|
60
|
+
if (this.expired(this.confirmedUntilMs)) {
|
|
61
|
+
this.markLost("deadline");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.heartbeatInFlight) return;
|
|
65
|
+
this.heartbeatInFlight = true;
|
|
66
|
+
const priorDeadline = this.confirmedUntilMs;
|
|
67
|
+
const startedAt = this.now();
|
|
68
|
+
const identity = { turnId, holderId: this.holderId, generation: this.generation };
|
|
69
|
+
try {
|
|
70
|
+
const renewedUntil = await this.leaseDeps.heartbeat(identity);
|
|
71
|
+
// A late confirmation is not proof of uninterrupted ownership, even if
|
|
72
|
+
// Postgres extended the row. A changed holder must also discard its reply.
|
|
73
|
+
if (
|
|
74
|
+
!this.held ||
|
|
75
|
+
this.lost ||
|
|
76
|
+
this.leaseDeps.getTurnId() !== identity.turnId ||
|
|
77
|
+
this.holderId !== identity.holderId ||
|
|
78
|
+
this.generation !== identity.generation
|
|
79
|
+
)
|
|
80
|
+
return;
|
|
81
|
+
if (!renewedUntil) this.markLost("not_found");
|
|
82
|
+
else if (this.expired(priorDeadline) || this.expired(startedAt + this.leaseDeps.ttlMs))
|
|
83
|
+
this.markLost("deadline");
|
|
84
|
+
else {
|
|
85
|
+
this.confirmedUntilMs = startedAt + this.leaseDeps.ttlMs;
|
|
86
|
+
this.leaseDeps.onRenewed?.(reason);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (
|
|
90
|
+
!this.held ||
|
|
91
|
+
this.lost ||
|
|
92
|
+
this.leaseDeps.getTurnId() !== identity.turnId ||
|
|
93
|
+
this.holderId !== identity.holderId ||
|
|
94
|
+
this.generation !== identity.generation
|
|
95
|
+
)
|
|
96
|
+
return;
|
|
97
|
+
if (this.expired(this.confirmedUntilMs)) this.markLost("deadline");
|
|
98
|
+
else this.leaseDeps.onError(error, reason);
|
|
99
|
+
} finally {
|
|
100
|
+
this.heartbeatInFlight = false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
startHeartbeat = (): void => {
|
|
105
|
+
if (!this.leaseDeps.getTurnId() || this.heartbeatTimer) return;
|
|
106
|
+
this.heartbeatTimer = setInterval(() => void this.renew("timer"), 60_000);
|
|
107
|
+
this.heartbeatTimer.unref?.();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
stopHeartbeat = (): void => {
|
|
111
|
+
if (!this.heartbeatTimer) return;
|
|
112
|
+
clearInterval(this.heartbeatTimer);
|
|
113
|
+
this.heartbeatTimer = undefined;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -191,10 +191,7 @@ export type ProviderTurnState = {
|
|
|
191
191
|
// scraped. Lives on the turn context so the finalizer sees it; the sink is
|
|
192
192
|
// wired into codexContext.onUsageHeaders by the orchestrator.
|
|
193
193
|
latestCodexUsage: CodexUsageHeaderSnapshot | null;
|
|
194
|
-
latestClaudeUsage: Map<
|
|
195
|
-
"workspace" | "organization",
|
|
196
|
-
import("./claude-usage-observer").CapturedClaudeUsage
|
|
197
|
-
>;
|
|
194
|
+
latestClaudeUsage: Map<string, import("./claude-usage-observer").CapturedClaudeUsage>;
|
|
198
195
|
lastCodexRequestOpaqueArtifacts: readonly string[];
|
|
199
196
|
};
|
|
200
197
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { subscriptionAccountShardIndex } from "@opengeni/config";
|
|
1
2
|
// Multi-account P3 — the PURE rotation ranker. Zero I/O: no provider calls, no
|
|
2
3
|
// decrypts, no db. It consumes the already-loaded, metadata-only account list
|
|
3
4
|
// (cached usage columns + the exhausted_until cooldown column) and returns the
|
|
@@ -223,17 +224,6 @@ export function isCodexAccountEligible(acct: CodexRotationAccount, now: Date): b
|
|
|
223
224
|
return isCodexCredentialEligible(acct, now);
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
/** Deterministic 32-bit FNV-1a over a UTF-16 code-unit stream. Pure, allocation-free. */
|
|
227
|
-
function fnv1a32(input: string): number {
|
|
228
|
-
let hash = 0x811c9dc5;
|
|
229
|
-
for (let i = 0; i < input.length; i++) {
|
|
230
|
-
hash ^= input.charCodeAt(i);
|
|
231
|
-
// 32-bit FNV prime multiply via Math.imul; `>>> 0` keeps it unsigned.
|
|
232
|
-
hash = Math.imul(hash, 0x01000193);
|
|
233
|
-
}
|
|
234
|
-
return hash >>> 0;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
227
|
/**
|
|
238
228
|
* Session-sharded HOME account (AM-6): the deterministic account a session runs on
|
|
239
229
|
* under the "sharded" strategy — `stableAccountList[ hash(sessionId) % N ]` over the
|
|
@@ -263,7 +253,7 @@ export function shardCredentialForSession(args: {
|
|
|
263
253
|
if (eligibles.length === 0) {
|
|
264
254
|
return null;
|
|
265
255
|
}
|
|
266
|
-
const index =
|
|
256
|
+
const index = subscriptionAccountShardIndex(sessionId, eligibles.length);
|
|
267
257
|
return eligibles[index]!.id;
|
|
268
258
|
}
|
|
269
259
|
|