@opengeni/worker-bundle 0.28.4-canary.0 → 0.28.4-canary.1
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-selection-diagnostics.d.ts +1 -0
- package/dist/activities/codex-fleet-shadow.d.ts +1 -1
- package/dist/activities/sandbox-lease.d.ts +10 -2
- package/dist/{activities-control-J5QNRWVH.js → activities-control-MKGEOBXT.js} +40 -7
- package/dist/activities-control-MKGEOBXT.js.map +1 -0
- package/dist/{activities-turn-ZJBY3W5D.js → activities-turn-5MZZ6HQA.js} +34 -51
- package/dist/activities-turn-5MZZ6HQA.js.map +1 -0
- package/dist/{chunk-ZNIKZQMN.js → chunk-OA5CSY5N.js} +4 -2
- package/dist/{chunk-ZNIKZQMN.js.map → chunk-OA5CSY5N.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/sandbox-routing.d.ts +6 -1
- package/package.json +18 -18
- package/src/activities/agent-turn/codex-capacity.ts +31 -53
- package/src/activities/agent-turn/codex-selection-diagnostics.ts +1 -0
- package/src/activities/agent-turn/sandbox-establish.ts +2 -0
- package/src/activities/agent-turn/skill-search.ts +2 -16
- package/src/activities/codex-fleet-shadow.ts +3 -0
- package/src/activities/sandbox-lease.ts +47 -2
- package/src/sandbox-routing.ts +5 -1
- package/dist/activities-control-J5QNRWVH.js.map +0 -1
- package/dist/activities-turn-ZJBY3W5D.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -782,7 +782,7 @@ function temporalWorkflowExecutionNotFound(error) {
|
|
|
782
782
|
return isGrpcServiceError(error) && error.code === 5;
|
|
783
783
|
}
|
|
784
784
|
async function createDefaultWorkerActivities(role, dependencies = {}) {
|
|
785
|
-
return role === "control" ? (await import("./activities-control-
|
|
785
|
+
return role === "control" ? (await import("./activities-control-MKGEOBXT.js")).createControlActivities(dependencies) : (await import("./activities-turn-5MZZ6HQA.js")).createTurnActivities(dependencies);
|
|
786
786
|
}
|
|
787
787
|
function resolveOpenGeniWorkflowDefinition(moduleUrl = import.meta.url) {
|
|
788
788
|
const modulePath = fileURLToPath(moduleUrl).replaceAll("\\", "/");
|
|
@@ -2,7 +2,7 @@ import { type Settings } from "@opengeni/config";
|
|
|
2
2
|
import { type Database, type EnrollmentRecord } from "@opengeni/db";
|
|
3
3
|
import type { OpStreamOutputFrame } from "@opengeni/runtime/sandbox";
|
|
4
4
|
import type { EventBus } from "@opengeni/events";
|
|
5
|
-
import { type EstablishedSandboxSession, type RoutingSandboxFirstOperationObserver, type RoutingSandboxOperationObserver, type SelfhostedOpObserver, type SelfhostedRelayConfig, type OpStreamJournal } from "@opengeni/runtime";
|
|
5
|
+
import { type EstablishedSandboxSession, type RoutingSandboxFirstOperationObserver, type RoutingSandboxOperationObserver, type SelfhostedOpObserver, type SelfhostedConnectionBinding, type SelfhostedRelayConfig, type OpStreamJournal } from "@opengeni/runtime";
|
|
6
6
|
export type RoutingWiringServices = {
|
|
7
7
|
db: Database;
|
|
8
8
|
settings: Settings;
|
|
@@ -179,6 +179,8 @@ export type SelfhostedTurnSessionArgs = {
|
|
|
179
179
|
operationResourcePolicy: EnrollmentRecord["operationPolicy"];
|
|
180
180
|
operationResourcePolicySupported: boolean;
|
|
181
181
|
operationCpuQuotaSupported: boolean;
|
|
182
|
+
/** Feature advertisement only; refreshed by exact live admission for each operation. */
|
|
183
|
+
transactionalFsWriteSupported?: boolean;
|
|
182
184
|
/** The active pointer's epoch — the control-op fence echoed to the agent. */
|
|
183
185
|
epoch: number;
|
|
184
186
|
/** The run's declared sandbox environment (the SAME object fed to buildAgent +
|
|
@@ -197,6 +199,9 @@ export type SelfhostedTurnSessionArgs = {
|
|
|
197
199
|
sessionId: string;
|
|
198
200
|
};
|
|
199
201
|
};
|
|
202
|
+
/** Project one enrollment read into one exact runtime route. Capability and
|
|
203
|
+
* process identity must be taken from the same snapshot. */
|
|
204
|
+
export declare function connectionBindingFor(services: RoutingWiringServices, enrollment: EnrollmentRecord | null): SelfhostedConnectionBinding | null;
|
|
200
205
|
export declare function establishSelfhostedTurnSession(services: RoutingWiringServices, args: SelfhostedTurnSessionArgs): Promise<EstablishedSandboxSession>;
|
|
201
206
|
/** Whether the routing proxy should wrap the turn box: the hot-swap feature is
|
|
202
207
|
* gated by the selfhosted flag (the active pointer + swap tools are only
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/worker-bundle",
|
|
3
|
-
"version": "0.28.4-canary.
|
|
3
|
+
"version": "0.28.4-canary.1",
|
|
4
4
|
"description": "OpenGeni worker entry and reusable embedded lifecycle, shipped with a release-coherent pre-bundled Temporal workflow artifact.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -54,23 +54,23 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@llamaindex/liteparse": "2.14.2",
|
|
57
|
-
"@opengeni/agent-proto": "^0.5.1-canary.
|
|
58
|
-
"@opengeni/capabilities": "^0.3.3-canary.
|
|
59
|
-
"@opengeni/codemode": "^0.5.6-canary.
|
|
60
|
-
"@opengeni/codex": "^0.2.22-canary.
|
|
61
|
-
"@opengeni/config": "^1.1.2-canary.
|
|
62
|
-
"@opengeni/contracts": "^3.0.2-canary.
|
|
63
|
-
"@opengeni/core": "^2.9.4-canary.
|
|
64
|
-
"@opengeni/db": "^4.3.3-canary.
|
|
65
|
-
"@opengeni/documents": "^0.8.27-canary.
|
|
66
|
-
"@opengeni/events": "^0.4.25-canary.
|
|
67
|
-
"@opengeni/github": "^0.7.10-canary.
|
|
68
|
-
"@opengeni/network": "^0.3.1-canary.
|
|
69
|
-
"@opengeni/observability": "^0.8.26-canary.
|
|
70
|
-
"@opengeni/runtime": "^2.5.3-canary.
|
|
71
|
-
"@opengeni/storage": "^0.2.127-canary.
|
|
72
|
-
"@opengeni/tool-gateway": "^0.1.7-canary.
|
|
73
|
-
"@opengeni/xai-subscription": "^0.1.4-canary.
|
|
57
|
+
"@opengeni/agent-proto": "^0.5.1-canary.27",
|
|
58
|
+
"@opengeni/capabilities": "^0.3.3-canary.9",
|
|
59
|
+
"@opengeni/codemode": "^0.5.6-canary.1",
|
|
60
|
+
"@opengeni/codex": "^0.2.22-canary.9",
|
|
61
|
+
"@opengeni/config": "^1.1.2-canary.1",
|
|
62
|
+
"@opengeni/contracts": "^3.0.2-canary.1",
|
|
63
|
+
"@opengeni/core": "^2.9.4-canary.1",
|
|
64
|
+
"@opengeni/db": "^4.3.3-canary.1",
|
|
65
|
+
"@opengeni/documents": "^0.8.27-canary.1",
|
|
66
|
+
"@opengeni/events": "^0.4.25-canary.1",
|
|
67
|
+
"@opengeni/github": "^0.7.10-canary.1",
|
|
68
|
+
"@opengeni/network": "^0.3.1-canary.9",
|
|
69
|
+
"@opengeni/observability": "^0.8.26-canary.1",
|
|
70
|
+
"@opengeni/runtime": "^2.5.3-canary.1",
|
|
71
|
+
"@opengeni/storage": "^0.2.127-canary.1",
|
|
72
|
+
"@opengeni/tool-gateway": "^0.1.7-canary.1",
|
|
73
|
+
"@opengeni/xai-subscription": "^0.1.4-canary.9",
|
|
74
74
|
"@temporalio/activity": "^1.17.0",
|
|
75
75
|
"@temporalio/client": "^1.17.0",
|
|
76
76
|
"@temporalio/worker": "^1.17.0",
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
CodexCredentialLeaseAttemptFencedError,
|
|
6
6
|
CodexCredentialFailoverExhaustedError,
|
|
7
7
|
CODEX_CREDENTIAL_LEASE_TTL_MS,
|
|
8
|
-
|
|
8
|
+
recordSessionCodexSelectionForTurnAttempt,
|
|
9
9
|
setSessionCodexPinInTransaction,
|
|
10
10
|
settleCodexCredentialFailover,
|
|
11
11
|
withSessionCodexCapacityMutation,
|
|
@@ -288,19 +288,40 @@ export async function selectCodexTurnCapacity(
|
|
|
288
288
|
leases.codex.confirmedUntilMs !== null;
|
|
289
289
|
if (leases.codex.held) leases.codex.startHeartbeat();
|
|
290
290
|
|
|
291
|
+
const eligibleCount = leased.accounts.filter((account) =>
|
|
292
|
+
isCodexCredentialEligible(account, new Date()),
|
|
293
|
+
).length;
|
|
294
|
+
const selectionReceipt = providerTurn.effectiveCodexCredentialId
|
|
295
|
+
? await recordSessionCodexSelectionForTurnAttempt(db, {
|
|
296
|
+
workspaceId: input.workspaceId,
|
|
297
|
+
sessionId: input.sessionId,
|
|
298
|
+
turnId,
|
|
299
|
+
attemptId: input.attemptId,
|
|
300
|
+
executionGeneration: attempt.executionGeneration,
|
|
301
|
+
credentialId: providerTurn.effectiveCodexCredentialId,
|
|
302
|
+
strategy: leased.rotationStrategy,
|
|
303
|
+
reusedLease: leased.reused,
|
|
304
|
+
pinnedCredentialId: sessionPin,
|
|
305
|
+
pinSource: sessionPinSource,
|
|
306
|
+
eligibleCount,
|
|
307
|
+
connectedCount: leased.accounts.length,
|
|
308
|
+
})
|
|
309
|
+
: null;
|
|
310
|
+
if (selectionReceipt)
|
|
311
|
+
await publishDurableSessionEvents(
|
|
312
|
+
bus,
|
|
313
|
+
input.workspaceId,
|
|
314
|
+
input.sessionId,
|
|
315
|
+
selectionReceipt.events,
|
|
316
|
+
);
|
|
317
|
+
const selectionDiagnostics = selectionReceipt?.diagnostics ?? null;
|
|
291
318
|
const actualOutcome = providerTurn.effectiveCodexCredentialId
|
|
292
319
|
? "selected"
|
|
293
320
|
: rotationDecision.kind === "allCapped" || rotationDecision.kind === "allocatorDisabled"
|
|
294
321
|
? "waiting"
|
|
295
322
|
: "none";
|
|
296
|
-
const actualReason =
|
|
297
|
-
?
|
|
298
|
-
? "lease_reused"
|
|
299
|
-
: sessionPin === providerTurn.effectiveCodexCredentialId
|
|
300
|
-
? "pin"
|
|
301
|
-
: rotationDecision.kind === "active" && rotationDecision.moved
|
|
302
|
-
? "rotation"
|
|
303
|
-
: "active"
|
|
323
|
+
const actualReason = selectionDiagnostics
|
|
324
|
+
? selectionDiagnostics.reason
|
|
304
325
|
: rotationDecision.kind === "allCapped"
|
|
305
326
|
? "all_capped"
|
|
306
327
|
: rotationDecision.kind === "allocatorDisabled"
|
|
@@ -362,9 +383,6 @@ export async function selectCodexTurnCapacity(
|
|
|
362
383
|
});
|
|
363
384
|
}
|
|
364
385
|
|
|
365
|
-
const eligibleCount = leased.accounts.filter((account) =>
|
|
366
|
-
isCodexCredentialEligible(account, new Date()),
|
|
367
|
-
).length;
|
|
368
386
|
const poolDepth = eligibleCount === 0 ? "zero" : eligibleCount === 1 ? "one" : "many";
|
|
369
387
|
observability.incrementCounter({
|
|
370
388
|
name: "opengeni_codex_pool_observations_total",
|
|
@@ -690,34 +708,7 @@ export async function selectCodexTurnCapacity(
|
|
|
690
708
|
return { exit: claimedResult({ status: "cancelled" }) };
|
|
691
709
|
}
|
|
692
710
|
if (providerTurn.effectiveCodexCredentialId) {
|
|
693
|
-
const
|
|
694
|
-
if (priorAccountId !== providerTurn.effectiveCodexCredentialId) {
|
|
695
|
-
await recordSessionActiveCodexCredential(
|
|
696
|
-
db,
|
|
697
|
-
input.workspaceId,
|
|
698
|
-
input.sessionId,
|
|
699
|
-
providerTurn.effectiveCodexCredentialId,
|
|
700
|
-
);
|
|
701
|
-
const rotated = rotationDecision.kind === "active" && rotationDecision.moved;
|
|
702
|
-
await eventing.publish([
|
|
703
|
-
{
|
|
704
|
-
type: "codex.account.switched",
|
|
705
|
-
payload: {
|
|
706
|
-
fromAccountId: priorAccountId,
|
|
707
|
-
toAccountId: providerTurn.effectiveCodexCredentialId,
|
|
708
|
-
reason: rotated ? "rotation" : "manual",
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
]);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const selectionReason = leased.reused
|
|
715
|
-
? "lease_reused"
|
|
716
|
-
: sessionPin === providerTurn.effectiveCodexCredentialId
|
|
717
|
-
? "pin"
|
|
718
|
-
: rotationDecision.kind === "active" && rotationDecision.moved
|
|
719
|
-
? "rotation"
|
|
720
|
-
: "active";
|
|
711
|
+
const selectionReason = selectionDiagnostics!.reason;
|
|
721
712
|
observability.incrementCounter({
|
|
722
713
|
name: "opengeni_codex_credential_selections_total",
|
|
723
714
|
help: "Codex credential selections by strategy and reason.",
|
|
@@ -727,19 +718,6 @@ export async function selectCodexTurnCapacity(
|
|
|
727
718
|
reason: selectionReason,
|
|
728
719
|
},
|
|
729
720
|
});
|
|
730
|
-
await eventing.publish([
|
|
731
|
-
{
|
|
732
|
-
type: "codex.credential.selected",
|
|
733
|
-
payload: {
|
|
734
|
-
credentialId: providerTurn.effectiveCodexCredentialId,
|
|
735
|
-
strategy: leased.rotationStrategy,
|
|
736
|
-
reason: selectionReason,
|
|
737
|
-
eligibleCount,
|
|
738
|
-
connectedCount: leased.accounts.length,
|
|
739
|
-
reused: leased.reused,
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
]);
|
|
743
721
|
}
|
|
744
722
|
} catch (error) {
|
|
745
723
|
credentialSelectionOutcome = "failed";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { codexSelectionDiagnostics } from "@opengeni/db";
|
|
@@ -475,6 +475,8 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
|
|
|
475
475
|
liveMachineEnrollment?.agentCapabilities.operationResourcePolicy === true,
|
|
476
476
|
operationCpuQuotaSupported:
|
|
477
477
|
liveMachineEnrollment?.agentCapabilities.operationCpuQuota === true,
|
|
478
|
+
transactionalFsWriteSupported:
|
|
479
|
+
liveMachineEnrollment?.agentCapabilities.transactionalFsWrite === true,
|
|
478
480
|
...(activeSandboxRecord!.scope === "user" && fileAuthoritySubjectId
|
|
479
481
|
? {
|
|
480
482
|
personalMachineAttempt: {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AttemptToolDefinition } from "@opengeni/codemode";
|
|
2
2
|
import { PublicSkillSearchError, type PublicSkillSearchClient } from "@opengeni/core";
|
|
3
|
-
import { listSkillLibraryEntries } from "@opengeni/runtime/skill-library";
|
|
4
3
|
|
|
5
4
|
export type WorkspaceSkillSearchEntry = Readonly<{
|
|
6
5
|
id: string;
|
|
@@ -23,7 +22,7 @@ export function createSkillSearchAttemptToolDefinition(input: {
|
|
|
23
22
|
codemodePath: ["opengeni", "skill_search"],
|
|
24
23
|
title: "Search Skills",
|
|
25
24
|
description:
|
|
26
|
-
"Find installed workspace Skills or available
|
|
25
|
+
"Find installed workspace Skills or available public Skills. Returns identifiers and install sources, not file contents. Search never installs or starts a sandbox. Scope defaults to all; use installed to avoid external search.",
|
|
27
26
|
inputSchema: {
|
|
28
27
|
type: "object",
|
|
29
28
|
properties: {
|
|
@@ -60,19 +59,6 @@ export function createSkillSearchAttemptToolDefinition(input: {
|
|
|
60
59
|
source: entry.source ?? "workspace",
|
|
61
60
|
installed: !entry.source || entry.source === "workspace",
|
|
62
61
|
}));
|
|
63
|
-
const libraryHits =
|
|
64
|
-
scope === "installed"
|
|
65
|
-
? []
|
|
66
|
-
: listSkillLibraryEntries()
|
|
67
|
-
.filter(matches)
|
|
68
|
-
.slice(0, limit)
|
|
69
|
-
.map((entry) => ({
|
|
70
|
-
id: `library:${entry.id}`,
|
|
71
|
-
name: entry.name,
|
|
72
|
-
description: entry.description,
|
|
73
|
-
source: "library" as const,
|
|
74
|
-
libraryId: entry.id,
|
|
75
|
-
}));
|
|
76
62
|
let publicResult: Awaited<ReturnType<PublicSkillSearchClient["search"]>> | null = null;
|
|
77
63
|
let publicError: {
|
|
78
64
|
source: "skills_sh";
|
|
@@ -93,7 +79,7 @@ export function createSkillSearchAttemptToolDefinition(input: {
|
|
|
93
79
|
}
|
|
94
80
|
const output = {
|
|
95
81
|
workspace: workspaceHits,
|
|
96
|
-
library:
|
|
82
|
+
library: [], // Retained response field for older clients.
|
|
97
83
|
public: publicResult?.items.map((entry) => ({ ...entry })) ?? [],
|
|
98
84
|
// A provider outage is not an empty search. Keep local results useful
|
|
99
85
|
// and expose partial failure explicitly, without leaking network details.
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
appendSessionEventToSandboxGroup,
|
|
27
27
|
bindRetainedProcessProviderIdentity,
|
|
28
28
|
claimWorkspaceArchiveCapture,
|
|
29
|
+
releaseWorkspaceArchiveCapture,
|
|
29
30
|
claimSandboxCheckpointArtifactsForGc,
|
|
30
31
|
claimTerminalRetainedProcesses,
|
|
31
32
|
countActiveRetainedProcessesByOwnerState,
|
|
@@ -249,6 +250,7 @@ export type TerminateBoxFn = (
|
|
|
249
250
|
captureDisposition?: DrainCaptureDisposition,
|
|
250
251
|
capturePolicy?: ProviderWorkspaceCapturePolicy | null,
|
|
251
252
|
diskBackedArchives?: boolean,
|
|
253
|
+
releaseFailedCapture?: () => Promise<void>,
|
|
252
254
|
) => Promise<boolean | ProviderTerminationOutcome>;
|
|
253
255
|
|
|
254
256
|
export type SweepModalOrphansFn = (
|
|
@@ -350,12 +352,13 @@ export class SandboxProviderCaptureTimeoutError extends Error {
|
|
|
350
352
|
}
|
|
351
353
|
}
|
|
352
354
|
|
|
353
|
-
async function awaitProviderCaptureWithLatePublication<T>(input: {
|
|
355
|
+
export async function awaitProviderCaptureWithLatePublication<T>(input: {
|
|
354
356
|
capture: Promise<T>;
|
|
355
357
|
timeoutMs: number;
|
|
356
358
|
timeoutError: SandboxProviderCaptureTimeoutError;
|
|
357
359
|
publishLate: (value: T) => Promise<void>;
|
|
358
360
|
observeLateFailure: (error: unknown) => void;
|
|
361
|
+
observeLatePublicationFailure: (error: unknown) => void;
|
|
359
362
|
}): Promise<T> {
|
|
360
363
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
361
364
|
const timeoutError = input.timeoutError;
|
|
@@ -373,7 +376,9 @@ async function awaitProviderCaptureWithLatePublication<T>(input: {
|
|
|
373
376
|
// durable archive publication: never provider teardown or a cold commit from
|
|
374
377
|
// an activity Temporal has already rejected. A replaced claim fences this
|
|
375
378
|
// callback; Modal candidates then flow to artifact-ledger GC.
|
|
376
|
-
void input.capture
|
|
379
|
+
void input.capture
|
|
380
|
+
.then(input.publishLate, input.observeLateFailure)
|
|
381
|
+
.catch(input.observeLatePublicationFailure);
|
|
377
382
|
throw error;
|
|
378
383
|
} finally {
|
|
379
384
|
if (timeout) clearTimeout(timeout);
|
|
@@ -482,6 +487,7 @@ export function createSandboxLeaseActivities(
|
|
|
482
487
|
captureDisposition,
|
|
483
488
|
capturePolicy,
|
|
484
489
|
diskBackedArchives,
|
|
490
|
+
releaseFailedCapture,
|
|
485
491
|
) =>
|
|
486
492
|
await terminateProviderBox(
|
|
487
493
|
settings,
|
|
@@ -494,6 +500,7 @@ export function createSandboxLeaseActivities(
|
|
|
494
500
|
captureDisposition,
|
|
495
501
|
capturePolicy,
|
|
496
502
|
diskBackedArchives,
|
|
503
|
+
releaseFailedCapture,
|
|
497
504
|
));
|
|
498
505
|
const sweepModalOrphans: SweepModalOrphansFn =
|
|
499
506
|
options.sweepModalOrphans ?? sweepModalOrphansForConfiguredBackend;
|
|
@@ -2803,6 +2810,17 @@ async function terminateDrainableBox(
|
|
|
2803
2810
|
captureDisposition,
|
|
2804
2811
|
capturePolicy,
|
|
2805
2812
|
Boolean(objectStorage),
|
|
2813
|
+
async () => {
|
|
2814
|
+
if (!captureClaim || !lease.instanceId) return;
|
|
2815
|
+
await releaseWorkspaceArchiveCapture(db, {
|
|
2816
|
+
accountId,
|
|
2817
|
+
workspaceId: row.workspaceId,
|
|
2818
|
+
sandboxGroupId: row.sandboxGroupId,
|
|
2819
|
+
captureId: captureClaim.id,
|
|
2820
|
+
expectedEpoch: row.leaseEpoch,
|
|
2821
|
+
expectedInstanceId: lease.instanceId,
|
|
2822
|
+
});
|
|
2823
|
+
},
|
|
2806
2824
|
);
|
|
2807
2825
|
const terminated = typeof termination === "boolean" ? termination : termination.terminated;
|
|
2808
2826
|
if (!terminated) {
|
|
@@ -2936,10 +2954,21 @@ export async function terminateProviderBox(
|
|
|
2936
2954
|
captureDisposition: DrainCaptureDisposition = "capture_required",
|
|
2937
2955
|
claimedCapturePolicy?: ProviderWorkspaceCapturePolicy | null,
|
|
2938
2956
|
diskBackedArchives = false,
|
|
2957
|
+
releaseFailedCapture?: () => Promise<void>,
|
|
2939
2958
|
): Promise<ProviderTerminationOutcome> {
|
|
2940
2959
|
const durableBackendId = (lease.resumeBackendId ?? lease.backend) as string;
|
|
2941
2960
|
const backend = sandboxBackendForSdkBackendId(durableBackendId) ?? durableBackendId;
|
|
2942
2961
|
const logIdentity = providerDrainLogIdentity(lease, backend);
|
|
2962
|
+
const releaseAfterCaptureFailure = async (): Promise<void> => {
|
|
2963
|
+
try {
|
|
2964
|
+
await releaseFailedCapture?.();
|
|
2965
|
+
} catch (error) {
|
|
2966
|
+
observability.warn("sandbox reaper: failed capture claim release failed", {
|
|
2967
|
+
...logIdentity,
|
|
2968
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2943
2972
|
// 'none' / no backend -> nothing to terminate.
|
|
2944
2973
|
if (!backend || backend === "none") {
|
|
2945
2974
|
return { terminated: true, providerMissingBeforeCapture: false };
|
|
@@ -3187,7 +3216,17 @@ export async function terminateProviderBox(
|
|
|
3187
3216
|
await disposeWorkspaceArchive(archive);
|
|
3188
3217
|
}
|
|
3189
3218
|
},
|
|
3219
|
+
observeLatePublicationFailure: (error) => {
|
|
3220
|
+
observability.warn("sandbox reaper: late capture publication cleanup failed", {
|
|
3221
|
+
...logIdentity,
|
|
3222
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3223
|
+
});
|
|
3224
|
+
},
|
|
3190
3225
|
observeLateFailure: (error) => {
|
|
3226
|
+
// Capture has now rejected and this timed-out path can never reach
|
|
3227
|
+
// provider teardown. Release only our unpublished claim so a waiting
|
|
3228
|
+
// turn can re-arm the intact live box; stale callbacks are DB-fenced.
|
|
3229
|
+
void releaseAfterCaptureFailure();
|
|
3191
3230
|
observability.warn("sandbox reaper: timed-out provider capture later failed", {
|
|
3192
3231
|
...logIdentity,
|
|
3193
3232
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -3209,6 +3248,12 @@ export async function terminateProviderBox(
|
|
|
3209
3248
|
error: error instanceof Error ? error.message : String(error),
|
|
3210
3249
|
},
|
|
3211
3250
|
);
|
|
3251
|
+
// An unresolved timeout is not proof of capture failure: retain the fence
|
|
3252
|
+
// until its provider promise settles. A settled failure cannot enter the
|
|
3253
|
+
// teardown path below, so its unpublished claim may safely be released.
|
|
3254
|
+
if (!(error instanceof SandboxProviderCaptureTimeoutError)) {
|
|
3255
|
+
await releaseAfterCaptureFailure();
|
|
3256
|
+
}
|
|
3212
3257
|
// NEVER terminate a box whose snapshot we could not capture.
|
|
3213
3258
|
throw error;
|
|
3214
3259
|
}
|
package/src/sandbox-routing.ts
CHANGED
|
@@ -1383,6 +1383,8 @@ export type SelfhostedTurnSessionArgs = {
|
|
|
1383
1383
|
operationResourcePolicy: EnrollmentRecord["operationPolicy"];
|
|
1384
1384
|
operationResourcePolicySupported: boolean;
|
|
1385
1385
|
operationCpuQuotaSupported: boolean;
|
|
1386
|
+
/** Feature advertisement only; refreshed by exact live admission for each operation. */
|
|
1387
|
+
transactionalFsWriteSupported?: boolean;
|
|
1386
1388
|
/** The active pointer's epoch — the control-op fence echoed to the agent. */
|
|
1387
1389
|
epoch: number;
|
|
1388
1390
|
/** The run's declared sandbox environment (the SAME object fed to buildAgent +
|
|
@@ -1442,7 +1444,7 @@ function opStreamDepsFor(
|
|
|
1442
1444
|
|
|
1443
1445
|
/** Project one enrollment read into one exact runtime route. Capability and
|
|
1444
1446
|
* process identity must be taken from the same snapshot. */
|
|
1445
|
-
function connectionBindingFor(
|
|
1447
|
+
export function connectionBindingFor(
|
|
1446
1448
|
services: RoutingWiringServices,
|
|
1447
1449
|
enrollment: EnrollmentRecord | null,
|
|
1448
1450
|
): SelfhostedConnectionBinding | null {
|
|
@@ -1456,6 +1458,7 @@ function connectionBindingFor(
|
|
|
1456
1458
|
operationResourcePolicy: enrollment.operationPolicy,
|
|
1457
1459
|
operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
1458
1460
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
1461
|
+
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
1459
1462
|
};
|
|
1460
1463
|
}
|
|
1461
1464
|
|
|
@@ -1526,6 +1529,7 @@ export async function establishSelfhostedTurnSession(
|
|
|
1526
1529
|
operationResourcePolicy: args.operationResourcePolicy,
|
|
1527
1530
|
operationResourcePolicySupported: args.operationResourcePolicySupported,
|
|
1528
1531
|
operationCpuQuotaSupported: args.operationCpuQuotaSupported,
|
|
1532
|
+
transactionalFsWriteSupported: args.transactionalFsWriteSupported === true,
|
|
1529
1533
|
resolveOperationAdmission: async () => {
|
|
1530
1534
|
const enrollment = args.personalMachineAttempt
|
|
1531
1535
|
? await resolvePersonalMachineConnectionForAttempt(db, {
|