@opengeni/worker-bundle 1.4.0 → 1.4.2
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/sandbox-provision.d.ts +22 -0
- package/dist/activities/agent-turn/turn-context.d.ts +6 -0
- package/dist/activities/agent-turn/web-search.d.ts +5 -1
- package/dist/activities/codemode-dispatcher.d.ts +8 -0
- package/dist/activities/programmatic-approvals.d.ts +0 -17
- package/dist/activities/sandbox-lease.d.ts +4 -0
- package/dist/{activities-control-D5KVEPHI.js → activities-control-AKJZHDTY.js} +53 -13
- package/dist/activities-control-AKJZHDTY.js.map +1 -0
- package/dist/{activities-turn-4Z3M7Z3F.js → activities-turn-AOXNE6BM.js} +6038 -5890
- package/dist/activities-turn-AOXNE6BM.js.map +1 -0
- package/dist/{chunk-FF4HPY7L.js → chunk-DCQ7XTXW.js} +108 -3
- package/dist/chunk-DCQ7XTXW.js.map +1 -0
- package/dist/{chunk-BMTWULF7.js → chunk-WVZQJBGD.js} +55 -17
- package/dist/chunk-WVZQJBGD.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/observability-metrics.d.ts +52 -14
- package/dist/sandbox-resume.d.ts +3 -0
- package/package.json +21 -21
- package/src/activities/agent-turn/errors.ts +7 -7
- package/src/activities/agent-turn/finalization.ts +16 -0
- package/src/activities/agent-turn/run.ts +12 -0
- package/src/activities/agent-turn/sandbox-establish.ts +87 -80
- package/src/activities/agent-turn/sandbox-provision.ts +51 -0
- package/src/activities/agent-turn/stream-attempt.ts +37 -50
- package/src/activities/agent-turn/turn-context.ts +8 -0
- package/src/activities/agent-turn/web-search.ts +135 -6
- package/src/activities/codemode-dispatcher.ts +57 -8
- package/src/activities/programmatic-approvals.ts +0 -26
- package/src/activities/sandbox-lease.ts +63 -10
- package/src/observability-metrics.ts +130 -18
- package/src/sandbox-resume.ts +24 -5
- package/src/sandbox-routing.ts +12 -0
- package/dist/activities-control-D5KVEPHI.js.map +0 -1
- package/dist/activities-turn-4Z3M7Z3F.js.map +0 -1
- package/dist/chunk-BMTWULF7.js.map +0 -1
- package/dist/chunk-FF4HPY7L.js.map +0 -1
|
@@ -78,11 +78,8 @@ import {
|
|
|
78
78
|
generatedImageFromSdkEvent,
|
|
79
79
|
isCompletedGeneratedImageSdkEvent,
|
|
80
80
|
} from "../generated-images";
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
programmaticContinuationNote,
|
|
84
|
-
recoverProgrammaticOperationsBeforeFirstRequest,
|
|
85
|
-
} from "../programmatic-approvals";
|
|
81
|
+
import { programmaticApproval, programmaticContinuationNote } from "../programmatic-approvals";
|
|
82
|
+
import { CodemodeAttemptDispatcher } from "../codemode-dispatcher";
|
|
86
83
|
import { ToolResultSpill } from "./tool-result-spill";
|
|
87
84
|
import { ownedTurnSandboxForAgent } from "./turn-sandbox-access";
|
|
88
85
|
import { createTurnCredentialLeases } from "./credential-leases";
|
|
@@ -665,6 +662,16 @@ export async function runTurnStreamAttempt(
|
|
|
665
662
|
const providerPublishesNativeRequestEvents =
|
|
666
663
|
resolvedModel?.provider.kind === "codex-subscription" ||
|
|
667
664
|
resolvedModel?.provider.kind === "xai-subscription";
|
|
665
|
+
// Bounded provider label for the streaming SLIs — the resolved registry
|
|
666
|
+
// provider id (or the built-in OpenAI/Azure provider), never a raw
|
|
667
|
+
// user-supplied model string. Created before the stream starts so the
|
|
668
|
+
// first request's model entry and provider dispatch are observed.
|
|
669
|
+
const streamTiming = new StreamTimingMetrics(observability, {
|
|
670
|
+
provider: streamProvider,
|
|
671
|
+
});
|
|
672
|
+
// Native Codex/SuperGrok transports report their literal dispatch through
|
|
673
|
+
// the shared turn context; generic transports use the wire-dispatch clock.
|
|
674
|
+
eventing.providerDispatchObserver = () => streamTiming.onProviderDispatch();
|
|
668
675
|
let fallbackProviderRequestStartedAt: number | null = null;
|
|
669
676
|
let fallbackProviderRequestLifecycleStartedAt: number | null = null;
|
|
670
677
|
const recordFallbackProviderDispatchAtWire = async (): Promise<void> => {
|
|
@@ -857,7 +864,10 @@ export async function runTurnStreamAttempt(
|
|
|
857
864
|
}
|
|
858
865
|
attempt.modelRequestStarted = true;
|
|
859
866
|
return await runtime.runStream(agent, runInput!, eventing.modelRunSettings, {
|
|
860
|
-
beforeModelRequest:
|
|
867
|
+
beforeModelRequest: () => {
|
|
868
|
+
streamTiming.onModelRequestEntry();
|
|
869
|
+
return modelCallAdmission.beforeModelRequest();
|
|
870
|
+
},
|
|
861
871
|
onModelResponse: (event) => {
|
|
862
872
|
responseCreditPolicyRevisions.add(creditPolicyRevision);
|
|
863
873
|
return modelCallAdmission.onModelResponse(event);
|
|
@@ -969,6 +979,7 @@ export async function runTurnStreamAttempt(
|
|
|
969
979
|
? {
|
|
970
980
|
onModelTransportStarted: recordFallbackProviderDispatchAtWire,
|
|
971
981
|
onModelTransportDispatched: (clock) => {
|
|
982
|
+
streamTiming.onProviderDispatch();
|
|
972
983
|
eventing.initialModelWireDispatch.record(
|
|
973
984
|
{ provider: streamProvider, dispatchId },
|
|
974
985
|
clock,
|
|
@@ -1010,12 +1021,6 @@ export async function runTurnStreamAttempt(
|
|
|
1010
1021
|
modelCallAdmission.close();
|
|
1011
1022
|
throw error;
|
|
1012
1023
|
}
|
|
1013
|
-
// Bounded provider label for the streaming SLIs — the resolved registry
|
|
1014
|
-
// provider id (or the built-in OpenAI/Azure provider), never a raw
|
|
1015
|
-
// user-supplied model string.
|
|
1016
|
-
const streamTiming = new StreamTimingMetrics(observability, {
|
|
1017
|
-
provider: streamProvider,
|
|
1018
|
-
});
|
|
1019
1024
|
eventing.batcher = createRuntimeBatcher(
|
|
1020
1025
|
async (events) => {
|
|
1021
1026
|
await eventing.publish!(events);
|
|
@@ -1964,44 +1969,26 @@ export async function runTurnStreamAttempt(
|
|
|
1964
1969
|
|
|
1965
1970
|
// Recover committed waits/results before any model dispatch, including crash replacement.
|
|
1966
1971
|
// A new attempt uses its own signed caller; origin provenance never changes.
|
|
1967
|
-
//
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
eventing.codemodeDispatcher?.resumeApproved(
|
|
1988
|
-
`sandbox:${input.attemptId}`,
|
|
1989
|
-
decisionOperationId,
|
|
1990
|
-
),
|
|
1991
|
-
})
|
|
1992
|
-
.then((operations) => {
|
|
1993
|
-
programmaticRecoveryOutcome = "completed";
|
|
1994
|
-
return operations;
|
|
1995
|
-
})
|
|
1996
|
-
.finally(() => {
|
|
1997
|
-
recordTurnStartupPhase(observability, {
|
|
1998
|
-
phase: "programmatic_operation_recovery",
|
|
1999
|
-
provider: turnExecutionPolicy.providerId,
|
|
2000
|
-
backend: activeSandboxBackend ?? groupBoxBackend,
|
|
2001
|
-
outcome: programmaticRecoveryOutcome,
|
|
2002
|
-
durationSeconds: (performance.now() - programmaticRecoveryStartedAt) / 1_000,
|
|
2003
|
-
});
|
|
2004
|
-
});
|
|
1972
|
+
// Join-on-use: lazy MCP preparation is awaited only when stored work must resume.
|
|
1973
|
+
const codemodeOperations = await CodemodeAttemptDispatcher.resumeApproved(
|
|
1974
|
+
db,
|
|
1975
|
+
{
|
|
1976
|
+
accountId: input.accountId,
|
|
1977
|
+
workspaceId: input.workspaceId,
|
|
1978
|
+
sessionId: input.sessionId,
|
|
1979
|
+
turnId: activeTurnId,
|
|
1980
|
+
attemptId: input.attemptId,
|
|
1981
|
+
executionGeneration: attempt.executionGeneration,
|
|
1982
|
+
},
|
|
1983
|
+
`sandbox:${input.attemptId}`,
|
|
1984
|
+
trigger.type === "user.approvalDecision"
|
|
1985
|
+
? (trigger.payload as { approvalId: string }).approvalId
|
|
1986
|
+
: undefined,
|
|
1987
|
+
async () => {
|
|
1988
|
+
await eventing.toolPreparationReady;
|
|
1989
|
+
return eventing.codemodeDispatcher;
|
|
1990
|
+
},
|
|
1991
|
+
);
|
|
2005
1992
|
codemodeContinuationNote = programmaticContinuationNote(codemodeOperations);
|
|
2006
1993
|
programmaticApprovalAcknowledged =
|
|
2007
1994
|
trigger.type === "user.approvalDecision" &&
|
|
@@ -103,6 +103,9 @@ export type SandboxRuntimeState = {
|
|
|
103
103
|
}>;
|
|
104
104
|
turnSandboxProvisioner: TurnSandboxProvisioner<ResumedTurnSandbox> | null;
|
|
105
105
|
resumeManagedGroupBox: (() => Promise<ResumedTurnSandbox>) | null;
|
|
106
|
+
/** Physical resumeBoxForTurn promises still running for this attempt; the
|
|
107
|
+
* finalizer joins them so a cancelled establish completes its own cleanup. */
|
|
108
|
+
inFlightSandboxResumes: Set<Promise<unknown>>;
|
|
106
109
|
prefetchedManagedBox: Promise<ResumedTurnSandbox> | null;
|
|
107
110
|
prefetchedManagedBoxResult: ResumedTurnSandbox | null;
|
|
108
111
|
setupBoxSession: unknown;
|
|
@@ -153,6 +156,9 @@ export type EventingState = {
|
|
|
153
156
|
firstModelRequestPreparationRecorded: boolean;
|
|
154
157
|
firstModelRequestCheckpointAt: number | null;
|
|
155
158
|
initialModelWireDispatch: InitialModelWireDispatchClock;
|
|
159
|
+
/** Diagnostic only: the current stream's timing hook for a native transport's
|
|
160
|
+
* literal provider dispatch. Never joins, fences or fails the request. */
|
|
161
|
+
providerDispatchObserver: (() => void) | null;
|
|
156
162
|
companyBrainContextContributions: readonly ModelContextContributionSummary[] | null;
|
|
157
163
|
/** Skill ids in this turn's frozen, model-visible Skill index; telemetry only. */
|
|
158
164
|
modelVisibleSkillIds: ReadonlySet<string> | null;
|
|
@@ -259,6 +265,7 @@ export function createTurnContext(input: {
|
|
|
259
265
|
firstModelPreparationNestedSandboxPhases: [],
|
|
260
266
|
turnSandboxProvisioner: null,
|
|
261
267
|
resumeManagedGroupBox: null,
|
|
268
|
+
inFlightSandboxResumes: new Set(),
|
|
262
269
|
prefetchedManagedBox: null,
|
|
263
270
|
prefetchedManagedBoxResult: null,
|
|
264
271
|
setupBoxSession: null,
|
|
@@ -301,6 +308,7 @@ export function createTurnContext(input: {
|
|
|
301
308
|
firstModelRequestPreparationRecorded: false,
|
|
302
309
|
firstModelRequestCheckpointAt: null,
|
|
303
310
|
initialModelWireDispatch: new InitialModelWireDispatchClock(),
|
|
311
|
+
providerDispatchObserver: null,
|
|
304
312
|
companyBrainContextContributions: null,
|
|
305
313
|
modelVisibleSkillIds: null,
|
|
306
314
|
},
|
|
@@ -41,7 +41,10 @@ import { hostedWebSearchForTurn } from "./tool-policy";
|
|
|
41
41
|
* call this with the same accepted model and settings, so they agree.
|
|
42
42
|
*/
|
|
43
43
|
export function turnWebSearchPlan(
|
|
44
|
-
resolvedModel: {
|
|
44
|
+
resolvedModel: {
|
|
45
|
+
configured: { hostedWebSearch: boolean };
|
|
46
|
+
provider: { kind?: string };
|
|
47
|
+
} | null,
|
|
45
48
|
settings: WebSearchSettings,
|
|
46
49
|
) {
|
|
47
50
|
return webSearchToolPlan(settings, {
|
|
@@ -92,6 +95,48 @@ function providerFailure(error: WebSearchProviderError): string {
|
|
|
92
95
|
return `${error.message}${error.retryable ? ". You may retry shortly." : "."}`;
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/** Closed outcome set for `opengeni_web_search_calls_total`. */
|
|
99
|
+
export type WebSearchCallOutcome =
|
|
100
|
+
| "ok"
|
|
101
|
+
| "provider_error"
|
|
102
|
+
| "provider_retryable"
|
|
103
|
+
| "billing_refused"
|
|
104
|
+
| "error";
|
|
105
|
+
|
|
106
|
+
type WebSearchObservability = Pick<Observability, "warn" | "incrementCounter" | "observeHistogram">;
|
|
107
|
+
|
|
108
|
+
const WEB_SEARCH_DURATION_BUCKETS = [0.25, 0.5, 1, 2, 4, 8, 15, 30];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* One provider call: a counter by fixed outcome plus its latency. Labels are
|
|
112
|
+
* the operation, the configured provider id, and the outcome only, never the
|
|
113
|
+
* query, URL, or session.
|
|
114
|
+
*/
|
|
115
|
+
function recordWebSearchCall(
|
|
116
|
+
observability: WebSearchObservability,
|
|
117
|
+
operation: "search" | "fetch",
|
|
118
|
+
provider: string,
|
|
119
|
+
outcome: WebSearchCallOutcome,
|
|
120
|
+
startedAt: number,
|
|
121
|
+
): void {
|
|
122
|
+
observability.incrementCounter({
|
|
123
|
+
name: "opengeni_web_search_calls_total",
|
|
124
|
+
help: "Provider web search/fetch calls made by the worker, by operation, provider and outcome.",
|
|
125
|
+
labels: { operation, provider, outcome },
|
|
126
|
+
});
|
|
127
|
+
observability.observeHistogram({
|
|
128
|
+
name: "opengeni_web_search_call_duration_seconds",
|
|
129
|
+
help: "Provider web search/fetch call duration in seconds.",
|
|
130
|
+
buckets: WEB_SEARCH_DURATION_BUCKETS,
|
|
131
|
+
value: (performance.now() - startedAt) / 1000,
|
|
132
|
+
labels: { operation, provider },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function providerFailureOutcome(error: WebSearchProviderError): WebSearchCallOutcome {
|
|
137
|
+
return error.retryable ? "provider_retryable" : "provider_error";
|
|
138
|
+
}
|
|
139
|
+
|
|
95
140
|
/**
|
|
96
141
|
* The `web_search` and `web_fetch` attempt tools for one turn. The caller
|
|
97
142
|
* decides which names the turn gets ({@link webSearchToolPlan}); this only
|
|
@@ -102,7 +147,7 @@ export function webSearchToolDefinitions(input: {
|
|
|
102
147
|
tools: readonly WebSearchProviderToolName[];
|
|
103
148
|
scope: WebSearchCallScope;
|
|
104
149
|
billing: WebSearchBilling;
|
|
105
|
-
observability:
|
|
150
|
+
observability: WebSearchObservability;
|
|
106
151
|
fetch?: typeof fetch;
|
|
107
152
|
}): AttemptToolDefinition[] {
|
|
108
153
|
const config: WebSearchProviderConfig | null = webSearchProviderConfig(input.settings);
|
|
@@ -127,7 +172,10 @@ export function webSearchToolDefinitions(input: {
|
|
|
127
172
|
const definitions: AttemptToolDefinition[] = [];
|
|
128
173
|
|
|
129
174
|
if (input.tools.includes(WEB_SEARCH_TOOL_NAME)) {
|
|
130
|
-
const provider = createWebSearchProvider({
|
|
175
|
+
const provider = createWebSearchProvider({
|
|
176
|
+
...adapterInput,
|
|
177
|
+
endpoint: config.search,
|
|
178
|
+
});
|
|
131
179
|
const pricing = webSearchCallPricing(config, "search");
|
|
132
180
|
definitions.push({
|
|
133
181
|
identity: { serverId: "opengeni", toolName: WEB_SEARCH_TOOL_NAME },
|
|
@@ -151,6 +199,15 @@ export function webSearchToolDefinitions(input: {
|
|
|
151
199
|
request = parseWebSearchArguments(args);
|
|
152
200
|
await input.billing.admit(input.scope, pricing.providerMicros);
|
|
153
201
|
} catch (error) {
|
|
202
|
+
if (error instanceof WebSearchBillingRefusedError) {
|
|
203
|
+
recordWebSearchCall(
|
|
204
|
+
input.observability,
|
|
205
|
+
"search",
|
|
206
|
+
config.search.provider,
|
|
207
|
+
"billing_refused",
|
|
208
|
+
performance.now(),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
154
211
|
if (
|
|
155
212
|
error instanceof WebToolArgumentError ||
|
|
156
213
|
error instanceof WebSearchBillingRefusedError
|
|
@@ -158,8 +215,18 @@ export function webSearchToolDefinitions(input: {
|
|
|
158
215
|
return textResult(error.message, true);
|
|
159
216
|
throw error;
|
|
160
217
|
}
|
|
218
|
+
const startedAt = performance.now();
|
|
161
219
|
try {
|
|
162
|
-
const response = await provider.search(request, {
|
|
220
|
+
const response = await provider.search(request, {
|
|
221
|
+
signal: context.signal,
|
|
222
|
+
});
|
|
223
|
+
recordWebSearchCall(
|
|
224
|
+
input.observability,
|
|
225
|
+
"search",
|
|
226
|
+
config.search.provider,
|
|
227
|
+
"ok",
|
|
228
|
+
startedAt,
|
|
229
|
+
);
|
|
163
230
|
await settle(
|
|
164
231
|
settledCost(
|
|
165
232
|
context.operationId,
|
|
@@ -173,8 +240,29 @@ export function webSearchToolDefinitions(input: {
|
|
|
173
240
|
} catch (error) {
|
|
174
241
|
if (context.signal?.aborted) throw error;
|
|
175
242
|
if (error instanceof WebSearchProviderError) {
|
|
243
|
+
recordWebSearchCall(
|
|
244
|
+
input.observability,
|
|
245
|
+
"search",
|
|
246
|
+
config.search.provider,
|
|
247
|
+
providerFailureOutcome(error),
|
|
248
|
+
startedAt,
|
|
249
|
+
);
|
|
250
|
+
input.observability.warn("web search provider call failed", {
|
|
251
|
+
operation: "search",
|
|
252
|
+
provider: config.search.provider,
|
|
253
|
+
status: error.status ?? undefined,
|
|
254
|
+
retryable: error.retryable,
|
|
255
|
+
error: error.message,
|
|
256
|
+
});
|
|
176
257
|
return textResult(`Web search failed: ${providerFailure(error)}`, true);
|
|
177
258
|
}
|
|
259
|
+
recordWebSearchCall(
|
|
260
|
+
input.observability,
|
|
261
|
+
"search",
|
|
262
|
+
config.search.provider,
|
|
263
|
+
"error",
|
|
264
|
+
startedAt,
|
|
265
|
+
);
|
|
178
266
|
throw error;
|
|
179
267
|
}
|
|
180
268
|
},
|
|
@@ -183,7 +271,10 @@ export function webSearchToolDefinitions(input: {
|
|
|
183
271
|
|
|
184
272
|
if (input.tools.includes(WEB_FETCH_TOOL_NAME) && config.fetch) {
|
|
185
273
|
const fetchEndpoint = config.fetch;
|
|
186
|
-
const provider = createWebFetchProvider({
|
|
274
|
+
const provider = createWebFetchProvider({
|
|
275
|
+
...adapterInput,
|
|
276
|
+
endpoint: fetchEndpoint,
|
|
277
|
+
});
|
|
187
278
|
const pricing = webSearchCallPricing(config, "fetch");
|
|
188
279
|
const pages = new Map<string, WebPage>();
|
|
189
280
|
definitions.push({
|
|
@@ -215,11 +306,28 @@ export function webSearchToolDefinitions(input: {
|
|
|
215
306
|
try {
|
|
216
307
|
await input.billing.admit(input.scope, pricing.providerMicros);
|
|
217
308
|
} catch (error) {
|
|
218
|
-
if (error instanceof WebSearchBillingRefusedError)
|
|
309
|
+
if (error instanceof WebSearchBillingRefusedError) {
|
|
310
|
+
recordWebSearchCall(
|
|
311
|
+
input.observability,
|
|
312
|
+
"fetch",
|
|
313
|
+
fetchEndpoint.provider,
|
|
314
|
+
"billing_refused",
|
|
315
|
+
performance.now(),
|
|
316
|
+
);
|
|
317
|
+
return textResult(error.message, true);
|
|
318
|
+
}
|
|
219
319
|
throw error;
|
|
220
320
|
}
|
|
321
|
+
const startedAt = performance.now();
|
|
221
322
|
try {
|
|
222
323
|
const page = await provider.fetch({ url: request.url }, { signal: context.signal });
|
|
324
|
+
recordWebSearchCall(
|
|
325
|
+
input.observability,
|
|
326
|
+
"fetch",
|
|
327
|
+
fetchEndpoint.provider,
|
|
328
|
+
"ok",
|
|
329
|
+
startedAt,
|
|
330
|
+
);
|
|
223
331
|
await settle(
|
|
224
332
|
settledCost(
|
|
225
333
|
context.operationId,
|
|
@@ -241,8 +349,29 @@ export function webSearchToolDefinitions(input: {
|
|
|
241
349
|
} catch (error) {
|
|
242
350
|
if (context.signal?.aborted) throw error;
|
|
243
351
|
if (error instanceof WebSearchProviderError) {
|
|
352
|
+
recordWebSearchCall(
|
|
353
|
+
input.observability,
|
|
354
|
+
"fetch",
|
|
355
|
+
fetchEndpoint.provider,
|
|
356
|
+
providerFailureOutcome(error),
|
|
357
|
+
startedAt,
|
|
358
|
+
);
|
|
359
|
+
input.observability.warn("web search provider call failed", {
|
|
360
|
+
operation: "fetch",
|
|
361
|
+
provider: fetchEndpoint.provider,
|
|
362
|
+
status: error.status ?? undefined,
|
|
363
|
+
retryable: error.retryable,
|
|
364
|
+
error: error.message,
|
|
365
|
+
});
|
|
244
366
|
return textResult(`Web fetch failed: ${providerFailure(error)}`, true);
|
|
245
367
|
}
|
|
368
|
+
recordWebSearchCall(
|
|
369
|
+
input.observability,
|
|
370
|
+
"fetch",
|
|
371
|
+
fetchEndpoint.provider,
|
|
372
|
+
"error",
|
|
373
|
+
startedAt,
|
|
374
|
+
);
|
|
246
375
|
throw error;
|
|
247
376
|
}
|
|
248
377
|
},
|
|
@@ -61,6 +61,12 @@ export type CodemodeDispatcherTimings = {
|
|
|
61
61
|
claimHeartbeatMs?: number;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const RESUMABLE_STATES: readonly CodemodeOperation["state"][] = [
|
|
65
|
+
"waiting_for_approval",
|
|
66
|
+
"queued",
|
|
67
|
+
"running",
|
|
68
|
+
];
|
|
69
|
+
|
|
64
70
|
/**
|
|
65
71
|
* Owns the only Codemode execution edge for one exact attempt. NATS carries a
|
|
66
72
|
* wake-up only; the durable row is claimed before any side effect and the call
|
|
@@ -190,11 +196,20 @@ export class CodemodeAttemptDispatcher {
|
|
|
190
196
|
);
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Join-on-use recovery before the first model request. The journal read needs
|
|
201
|
+
* no tool environment, so a turn with nothing unfinished returns at once and
|
|
202
|
+
* never waits on lazy MCP preparation. Only a stored waiting/queued/running
|
|
203
|
+
* operation awaits the prepared dispatcher to adopt and execute it.
|
|
204
|
+
*/
|
|
205
|
+
static async resumeApproved(
|
|
206
|
+
db: Database,
|
|
207
|
+
scope: CodemodeDispatcherScope,
|
|
194
208
|
callerSubjectId: string,
|
|
195
|
-
decisionOperationId
|
|
209
|
+
decisionOperationId: string | undefined,
|
|
210
|
+
prepared: () => Promise<CodemodeAttemptDispatcher | null>,
|
|
196
211
|
): Promise<CodemodeOperation[]> {
|
|
197
|
-
const operations = await listTurnCodemodeApprovals(
|
|
212
|
+
const operations = await listTurnCodemodeApprovals(db, scope);
|
|
198
213
|
if (
|
|
199
214
|
decisionOperationId &&
|
|
200
215
|
// Model-tool approval IDs are opaque strings. Only programmatic UUIDs
|
|
@@ -202,15 +217,38 @@ export class CodemodeAttemptDispatcher {
|
|
|
202
217
|
CodemodeOperation.shape.operationId.safeParse(decisionOperationId).success &&
|
|
203
218
|
!operations.some((operation) => operation.operationId === decisionOperationId)
|
|
204
219
|
) {
|
|
205
|
-
const decided = await readTurnCodemodeOperation(
|
|
206
|
-
...
|
|
220
|
+
const decided = await readTurnCodemodeOperation(db, {
|
|
221
|
+
...scope,
|
|
207
222
|
callerSubjectId,
|
|
208
223
|
operationId: decisionOperationId,
|
|
209
224
|
});
|
|
210
225
|
if (decided) operations.push(decided);
|
|
211
226
|
}
|
|
227
|
+
if (!operations.some((operation) => RESUMABLE_STATES.includes(operation.state))) {
|
|
228
|
+
return operations;
|
|
229
|
+
}
|
|
230
|
+
return (await (await prepared())?.resumeOperations(operations, callerSubjectId)) ?? [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async resumeApproved(
|
|
234
|
+
callerSubjectId: string,
|
|
235
|
+
decisionOperationId?: string,
|
|
236
|
+
): Promise<CodemodeOperation[]> {
|
|
237
|
+
return await CodemodeAttemptDispatcher.resumeApproved(
|
|
238
|
+
this.db,
|
|
239
|
+
this.scope,
|
|
240
|
+
callerSubjectId,
|
|
241
|
+
decisionOperationId,
|
|
242
|
+
async () => this,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async resumeOperations(
|
|
247
|
+
operations: CodemodeOperation[],
|
|
248
|
+
callerSubjectId: string,
|
|
249
|
+
): Promise<CodemodeOperation[]> {
|
|
212
250
|
for (const operation of operations) {
|
|
213
|
-
if (!
|
|
251
|
+
if (!RESUMABLE_STATES.includes(operation.state)) continue;
|
|
214
252
|
let effectDigest: string | null = null;
|
|
215
253
|
try {
|
|
216
254
|
effectDigest = this.environment.effectDigest(operation.identity);
|
|
@@ -380,7 +418,17 @@ export class CodemodeAttemptDispatcher {
|
|
|
380
418
|
try {
|
|
381
419
|
if (operation.durableApproval) {
|
|
382
420
|
if (!this.approvalWait) throw new Error("Durable approval yield is unavailable");
|
|
383
|
-
|
|
421
|
+
// The wait gate is sealed while a model request is in flight or the
|
|
422
|
+
// stream is settling. A call arriving then cannot durably wait, but it
|
|
423
|
+
// must not fail outright: prepare it like a non-waiting client, so an
|
|
424
|
+
// allowed call still executes and an Ask settles approval_required.
|
|
425
|
+
try {
|
|
426
|
+
releaseWait = this.approvalWait.beginWait();
|
|
427
|
+
} catch {
|
|
428
|
+
releaseWait = undefined;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (releaseWait) {
|
|
384
432
|
if (
|
|
385
433
|
!(await bindCodemodePreparation(this.db, {
|
|
386
434
|
...this.scope,
|
|
@@ -407,10 +455,11 @@ export class CodemodeAttemptDispatcher {
|
|
|
407
455
|
},
|
|
408
456
|
{
|
|
409
457
|
signal,
|
|
410
|
-
...(
|
|
458
|
+
...(releaseWait ? { transportMeta: { durableApproval: true } } : {}),
|
|
411
459
|
},
|
|
412
460
|
);
|
|
413
461
|
if (preparedCall.waitingForApproval) {
|
|
462
|
+
if (!releaseWait) throw new AttemptToolApprovalRequiredError();
|
|
414
463
|
const waiting = await waitForCodemodeApproval(this.db, {
|
|
415
464
|
...this.scope,
|
|
416
465
|
operationId: operation.operationId,
|
|
@@ -24,29 +24,3 @@ export function programmaticContinuationNote(
|
|
|
24
24
|
"Approval resumes the stored operation, not the earlier program's JavaScript stack. Read results with the current Codemode client's status/resume handle. Do not resubmit arguments for completed, running, waiting, or uncertain operations.",
|
|
25
25
|
].join("\n");
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Recover committed programmatic approval waits/results before the first model
|
|
30
|
-
* request without turning lazy MCP preparation into a first-request barrier.
|
|
31
|
-
*
|
|
32
|
-
* Only eager servers may gate the first request. Resuming a stored operation
|
|
33
|
-
* needs the prepared attempt tool environment, so this waits for full tool
|
|
34
|
-
* preparation only when there is something to resume: an approval decision
|
|
35
|
-
* trigger, or a cheap durable read showing unfinished operations for this
|
|
36
|
-
* logical turn (which covers every recovery that has stored work). Every other
|
|
37
|
-
* turn sends its first request without waiting on any lazy server.
|
|
38
|
-
*/
|
|
39
|
-
export async function recoverProgrammaticOperationsBeforeFirstRequest(input: {
|
|
40
|
-
approvalDecisionId: string | undefined;
|
|
41
|
-
hasUnfinishedOperations: () => Promise<boolean>;
|
|
42
|
-
toolPreparationReady: Promise<void> | null;
|
|
43
|
-
resumeApproved: (
|
|
44
|
-
decisionOperationId: string | undefined,
|
|
45
|
-
) => Promise<CodemodeOperation[]> | undefined;
|
|
46
|
-
}): Promise<CodemodeOperation[]> {
|
|
47
|
-
if (input.approvalDecisionId === undefined && !(await input.hasUnfinishedOperations())) {
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
await input.toolPreparationReady;
|
|
51
|
-
return (await input.resumeApproved(input.approvalDecisionId)) ?? [];
|
|
52
|
-
}
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
recordRecoveredModalProviderCreate,
|
|
54
54
|
markWarmBillingStopCutoff,
|
|
55
55
|
listMeterableWarmLeases,
|
|
56
|
+
countInteractionOnlyWarmLeasesByIdle,
|
|
56
57
|
listSandboxViewerForceDrainWorkspaceIds,
|
|
57
58
|
markSandboxCheckpointArtifactDeletePending,
|
|
58
59
|
persistDrainSnapshot,
|
|
@@ -174,6 +175,8 @@ import {
|
|
|
174
175
|
type RetainedProcessReconciliationOutcome,
|
|
175
176
|
type SandboxInventoryProjectionDomain,
|
|
176
177
|
recordSandboxLeaseGauges,
|
|
178
|
+
recordModalSandboxInventoryGauges,
|
|
179
|
+
recordInteractionOnlyLeaseGauges,
|
|
177
180
|
recordSandboxOrphansTerminated,
|
|
178
181
|
recordSandboxCommandContainment,
|
|
179
182
|
recordSandboxProviderMissingBeforeCapture,
|
|
@@ -2688,6 +2691,17 @@ async function refreshQueueLeaseAndCreditGauges(
|
|
|
2688
2691
|
);
|
|
2689
2692
|
},
|
|
2690
2693
|
),
|
|
2694
|
+
refreshSandboxInventoryGauge(
|
|
2695
|
+
observability,
|
|
2696
|
+
"interaction_idle",
|
|
2697
|
+
"interaction-only-lease",
|
|
2698
|
+
async () => {
|
|
2699
|
+
recordInteractionOnlyLeaseGauges(
|
|
2700
|
+
observability,
|
|
2701
|
+
await countInteractionOnlyWarmLeasesByIdle(db, await listMeterableWarmLeases(db)),
|
|
2702
|
+
);
|
|
2703
|
+
},
|
|
2704
|
+
),
|
|
2691
2705
|
refreshSandboxInventoryGauge(observability, "expired_drains", "expired-draining", async () => {
|
|
2692
2706
|
recordExpiredDrainingSandboxLeaseGauges(
|
|
2693
2707
|
observability,
|
|
@@ -2876,6 +2890,16 @@ export function modalOrphanTerminationStillEligible(
|
|
|
2876
2890
|
if (latest.some((lease) => lease.liveness === "warming" && lease.instanceId === null)) {
|
|
2877
2891
|
return false;
|
|
2878
2892
|
}
|
|
2893
|
+
return modalOrphanCandidateStillUnowned(latest, candidate);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/** Whether no live lease owns the candidate box by exact instance or by its
|
|
2897
|
+
* attribution tags. Unlike termination eligibility this ignores the pending
|
|
2898
|
+
* create postponement: a postponed orphan is still running without a lease. */
|
|
2899
|
+
export function modalOrphanCandidateStillUnowned(
|
|
2900
|
+
latest: Awaited<ReturnType<typeof listLiveModalSandboxLeaseAttributions>>,
|
|
2901
|
+
candidate: ModalOrphanSweepTermination,
|
|
2902
|
+
): boolean {
|
|
2879
2903
|
if (latest.some((lease) => lease.instanceId === candidate.sandboxId)) {
|
|
2880
2904
|
return false;
|
|
2881
2905
|
}
|
|
@@ -2925,6 +2949,30 @@ async function sweepModalOrphansForConfiguredBackend(
|
|
|
2925
2949
|
return modalOrphanTerminationStillEligible(latest, candidate);
|
|
2926
2950
|
},
|
|
2927
2951
|
});
|
|
2952
|
+
// Only a complete listing is an inventory; a pass cut short by the
|
|
2953
|
+
// termination budget leaves the previous projection to age out as stale.
|
|
2954
|
+
// Re-read durable ownership after the listing so a lease that started or
|
|
2955
|
+
// finished while the app was being listed is not reported as a leak.
|
|
2956
|
+
if (result.inventory.complete) {
|
|
2957
|
+
try {
|
|
2958
|
+
const latest = await listLiveModalSandboxLeaseAttributions(db);
|
|
2959
|
+
recordModalSandboxInventoryGauges(observability, {
|
|
2960
|
+
running: result.inventory.running,
|
|
2961
|
+
unleased: result.inventory.unterminated.filter((candidate) =>
|
|
2962
|
+
modalOrphanCandidateStillUnowned(latest, candidate),
|
|
2963
|
+
).length,
|
|
2964
|
+
liveLeaseInstancesMissing: result.inventory.missingLiveLeaseInstanceIds.filter(
|
|
2965
|
+
(instanceId) => latest.some((lease) => lease.instanceId === instanceId),
|
|
2966
|
+
).length,
|
|
2967
|
+
});
|
|
2968
|
+
recordSandboxInventoryProjectionSuccess(observability, "modal_provider");
|
|
2969
|
+
} catch (error) {
|
|
2970
|
+
recordSandboxInventoryProjectionFailure(observability, "modal_provider");
|
|
2971
|
+
observability.warn("sandbox reaper: Modal inventory ownership re-read failed", {
|
|
2972
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2928
2976
|
for (const terminated of result.terminated) {
|
|
2929
2977
|
observability.warn("sandbox reaper: terminated Modal orphan sandbox", {
|
|
2930
2978
|
sandboxId: terminated.sandboxId,
|
|
@@ -3524,15 +3572,18 @@ async function terminateDrainableBox(
|
|
|
3524
3572
|
// with draining->cold. Until this succeeds, arrivals remain fenced by that
|
|
3525
3573
|
// exact claim; a timestamp or a failed provider call can never reopen a box
|
|
3526
3574
|
// while termination may still be in flight.
|
|
3527
|
-
const { wentCold, backgroundCommandEvents } = await confirmDrainCold(
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3575
|
+
const { wentCold, unpublishedProviderLost, backgroundCommandEvents } = await confirmDrainCold(
|
|
3576
|
+
db,
|
|
3577
|
+
{
|
|
3578
|
+
accountId,
|
|
3579
|
+
workspaceId: row.workspaceId,
|
|
3580
|
+
sandboxGroupId: row.sandboxGroupId,
|
|
3581
|
+
expectedEpoch: row.leaseEpoch,
|
|
3582
|
+
...(captureClaim ? { expectedCaptureId: captureClaim.id } : {}),
|
|
3583
|
+
providerMissingBeforeCapture: providerMissing,
|
|
3584
|
+
idleCommandContainmentMs: settings.sandboxIdleCommandContainmentMs,
|
|
3585
|
+
},
|
|
3586
|
+
);
|
|
3536
3587
|
// The command terminal events and agent inputs are already durable in the
|
|
3537
3588
|
// cold commit; this is only best-effort live fanout.
|
|
3538
3589
|
await publishDurableSessionEvents(bus, row.workspaceId, backgroundCommandEvents, (error) => {
|
|
@@ -3545,7 +3596,9 @@ async function terminateDrainableBox(
|
|
|
3545
3596
|
// Only the exact successful cold commit counts provider loss. A missing
|
|
3546
3597
|
// probe, a stale capture, a failed commit, or a retried child is not another
|
|
3547
3598
|
// observed loss. Keep this outside the best-effort session event writer.
|
|
3548
|
-
|
|
3599
|
+
// An unpublished warming replacement (its creator died before warm
|
|
3600
|
+
// publication) held no workspace; its absence is not a capture loss.
|
|
3601
|
+
if (providerMissing && !unpublishedProviderLost) {
|
|
3549
3602
|
recordSandboxProviderMissingBeforeCapture(observability, backend);
|
|
3550
3603
|
}
|
|
3551
3604
|
if (lease.unobservableCommandDrainIds?.length) {
|