@opengeni/worker-bundle 0.28.3 → 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/agent-turn/mcp-operation-read-tool.d.ts +16 -0
- package/dist/activities/codex-fleet-shadow.d.ts +1 -1
- package/dist/activities/mcp-operation-authority.d.ts +16 -0
- package/dist/activities/mcp-operation-observer.d.ts +14 -0
- package/dist/activities/mcp-operation-reader.d.ts +45 -0
- package/dist/activities/mcp-operation-store.d.ts +6 -0
- package/dist/activities/sandbox-lease.d.ts +12 -4
- package/dist/{activities-control-UXGPMDL3.js → activities-control-MKGEOBXT.js} +90 -71
- package/dist/activities-control-MKGEOBXT.js.map +1 -0
- package/dist/{activities-turn-NOYDE7PX.js → activities-turn-5MZZ6HQA.js} +15691 -815
- package/dist/activities-turn-5MZZ6HQA.js.map +1 -0
- package/dist/artifact-materializer-entry.js +2 -0
- package/dist/artifact-materializer-entry.js.map +1 -1
- package/dist/artifact-outbox-entry.js +2 -0
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/{chunk-BARTLVBK.js → chunk-A4XQWVZK.js} +25 -25
- package/dist/chunk-A4XQWVZK.js.map +1 -0
- package/dist/{chunk-NNXLW6V7.js → chunk-OA5CSY5N.js} +147 -112
- package/dist/chunk-OA5CSY5N.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/host-export.js +1 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +12 -4
- package/dist/sandbox-archive-storage.d.ts +15 -3
- package/dist/sandbox-resume.d.ts +1 -1
- package/dist/sandbox-routing.d.ts +6 -1
- package/package.json +18 -17
- package/src/activities/agent-turn/claim.ts +3 -3
- 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/finalization.ts +10 -5
- package/src/activities/agent-turn/mcp-operation-read-tool.ts +61 -0
- package/src/activities/agent-turn/sandbox-establish.ts +2 -0
- package/src/activities/agent-turn/skill-search.ts +2 -16
- package/src/activities/agent-turn/stream-attempt.ts +1 -1
- package/src/activities/agent-turn/tool-environment.ts +65 -0
- package/src/activities/codex-fleet-shadow.ts +3 -0
- package/src/activities/mcp-credentials.ts +94 -40
- package/src/activities/mcp-operation-authority.ts +76 -0
- package/src/activities/mcp-operation-observer.ts +86 -0
- package/src/activities/mcp-operation-reader.ts +108 -0
- package/src/activities/mcp-operation-store.ts +98 -0
- package/src/activities/sandbox-lease.ts +111 -73
- package/src/observability-metrics.ts +28 -24
- package/src/sandbox-archive-storage.ts +82 -18
- package/src/sandbox-resume.ts +79 -63
- package/src/sandbox-routing.ts +5 -1
- package/dist/activities-control-UXGPMDL3.js.map +0 -1
- package/dist/activities-turn-NOYDE7PX.js.map +0 -1
- package/dist/chunk-BARTLVBK.js.map +0 -1
- package/dist/chunk-NNXLW6V7.js.map +0 -1
|
@@ -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,
|
|
@@ -82,7 +83,10 @@ import {
|
|
|
82
83
|
// box is typed as provider loss. Stale capture reconciliation separately uses
|
|
83
84
|
// establishSandboxSessionFromEnvelope in strict resume-only mode so it can
|
|
84
85
|
// reuse the runtime's provider-ready proof without ever cold-restoring.
|
|
85
|
-
|
|
86
|
+
captureWorkspaceArchiveForStorage,
|
|
87
|
+
disposeWorkspaceArchive,
|
|
88
|
+
type VerifiedWorkspaceArchivePayload,
|
|
89
|
+
type VerifiedHostWorkspaceArchive,
|
|
86
90
|
cancelSelfhostedOp,
|
|
87
91
|
assertConsistentSandboxProviderIdentity,
|
|
88
92
|
createSandboxClientForBackend,
|
|
@@ -160,9 +164,7 @@ import {
|
|
|
160
164
|
} from "../opensandbox-kubernetes-inventory";
|
|
161
165
|
import type { ObjectStorage } from "@opengeni/storage";
|
|
162
166
|
import {
|
|
163
|
-
|
|
164
|
-
deleteUnpublishedWorkspaceArchiveObject,
|
|
165
|
-
deleteWorkspaceArchiveObjectKeys,
|
|
167
|
+
persistWorkspaceArchiveCandidate,
|
|
166
168
|
putVersion1TarArchiveOrInline,
|
|
167
169
|
} from "../sandbox-archive-storage";
|
|
168
170
|
|
|
@@ -210,7 +212,7 @@ type DrainSandboxClient = {
|
|
|
210
212
|
* aborts the terminate before client.delete()).
|
|
211
213
|
*/
|
|
212
214
|
export type PersistArchiveFn = (
|
|
213
|
-
archiveBase64: string | null,
|
|
215
|
+
archiveBase64: string | VerifiedHostWorkspaceArchive | null,
|
|
214
216
|
archiveMetadata?: WorkspaceArchiveDescriptor,
|
|
215
217
|
providerSession?: unknown,
|
|
216
218
|
providerBinding?: Awaited<
|
|
@@ -247,6 +249,8 @@ export type TerminateBoxFn = (
|
|
|
247
249
|
providerCaptureRequestId?: string,
|
|
248
250
|
captureDisposition?: DrainCaptureDisposition,
|
|
249
251
|
capturePolicy?: ProviderWorkspaceCapturePolicy | null,
|
|
252
|
+
diskBackedArchives?: boolean,
|
|
253
|
+
releaseFailedCapture?: () => Promise<void>,
|
|
250
254
|
) => Promise<boolean | ProviderTerminationOutcome>;
|
|
251
255
|
|
|
252
256
|
export type SweepModalOrphansFn = (
|
|
@@ -348,12 +352,13 @@ export class SandboxProviderCaptureTimeoutError extends Error {
|
|
|
348
352
|
}
|
|
349
353
|
}
|
|
350
354
|
|
|
351
|
-
async function awaitProviderCaptureWithLatePublication<T>(input: {
|
|
355
|
+
export async function awaitProviderCaptureWithLatePublication<T>(input: {
|
|
352
356
|
capture: Promise<T>;
|
|
353
357
|
timeoutMs: number;
|
|
354
358
|
timeoutError: SandboxProviderCaptureTimeoutError;
|
|
355
359
|
publishLate: (value: T) => Promise<void>;
|
|
356
360
|
observeLateFailure: (error: unknown) => void;
|
|
361
|
+
observeLatePublicationFailure: (error: unknown) => void;
|
|
357
362
|
}): Promise<T> {
|
|
358
363
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
359
364
|
const timeoutError = input.timeoutError;
|
|
@@ -371,7 +376,9 @@ async function awaitProviderCaptureWithLatePublication<T>(input: {
|
|
|
371
376
|
// durable archive publication: never provider teardown or a cold commit from
|
|
372
377
|
// an activity Temporal has already rejected. A replaced claim fences this
|
|
373
378
|
// callback; Modal candidates then flow to artifact-ledger GC.
|
|
374
|
-
void input.capture
|
|
379
|
+
void input.capture
|
|
380
|
+
.then(input.publishLate, input.observeLateFailure)
|
|
381
|
+
.catch(input.observeLatePublicationFailure);
|
|
375
382
|
throw error;
|
|
376
383
|
} finally {
|
|
377
384
|
if (timeout) clearTimeout(timeout);
|
|
@@ -479,6 +486,8 @@ export function createSandboxLeaseActivities(
|
|
|
479
486
|
providerCaptureRequestId,
|
|
480
487
|
captureDisposition,
|
|
481
488
|
capturePolicy,
|
|
489
|
+
diskBackedArchives,
|
|
490
|
+
releaseFailedCapture,
|
|
482
491
|
) =>
|
|
483
492
|
await terminateProviderBox(
|
|
484
493
|
settings,
|
|
@@ -490,6 +499,8 @@ export function createSandboxLeaseActivities(
|
|
|
490
499
|
providerCaptureRequestId,
|
|
491
500
|
captureDisposition,
|
|
492
501
|
capturePolicy,
|
|
502
|
+
diskBackedArchives,
|
|
503
|
+
releaseFailedCapture,
|
|
493
504
|
));
|
|
494
505
|
const sweepModalOrphans: SweepModalOrphansFn =
|
|
495
506
|
options.sweepModalOrphans ?? sweepModalOrphansForConfiguredBackend;
|
|
@@ -2675,7 +2686,7 @@ async function terminateDrainableBox(
|
|
|
2675
2686
|
? (lease.recovery.archive.current?.revision ?? null)
|
|
2676
2687
|
: null;
|
|
2677
2688
|
const persistArchive: PersistArchiveFn = async (
|
|
2678
|
-
archiveBase64: string | null,
|
|
2689
|
+
archiveBase64: string | VerifiedHostWorkspaceArchive | null,
|
|
2679
2690
|
archiveMetadata?: WorkspaceArchiveDescriptor,
|
|
2680
2691
|
_providerSession?: unknown,
|
|
2681
2692
|
providerBinding?: Awaited<
|
|
@@ -2691,6 +2702,7 @@ async function terminateDrainableBox(
|
|
|
2691
2702
|
const archiveMetrics = runtimeMetricsHooksForObservability(observability);
|
|
2692
2703
|
let checkpointArtifactId: string | null = null;
|
|
2693
2704
|
if (
|
|
2705
|
+
typeof archiveBase64 === "string" &&
|
|
2694
2706
|
archiveBase64 &&
|
|
2695
2707
|
archiveMetadata?.version === 2 &&
|
|
2696
2708
|
(archiveMetadata.provider === "modal_snapshot_filesystem" ||
|
|
@@ -2735,59 +2747,33 @@ async function terminateDrainableBox(
|
|
|
2735
2747
|
if (!archiveMetadata) {
|
|
2736
2748
|
throw new Error("Sandbox snapshot publication requires a verified archive descriptor");
|
|
2737
2749
|
}
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
if (published.workspaceArchiveRef && objectStorage) {
|
|
2766
|
-
if (!result.wrote) {
|
|
2767
|
-
await deleteUnpublishedWorkspaceArchiveObject(
|
|
2768
|
-
objectStorage,
|
|
2769
|
-
published.workspaceArchiveRef,
|
|
2770
|
-
archiveMetrics,
|
|
2771
|
-
);
|
|
2772
|
-
} else {
|
|
2773
|
-
const afterLease = await readLease(db, row.workspaceId, row.sandboxGroupId);
|
|
2774
|
-
const afterKeys = collectWorkspaceArchiveObjectKeys(
|
|
2775
|
-
(afterLease?.resumeState as Record<string, unknown> | null | undefined) ?? null,
|
|
2776
|
-
);
|
|
2777
|
-
await deleteWorkspaceArchiveObjectKeys(
|
|
2778
|
-
objectStorage,
|
|
2779
|
-
[...priorKeys].filter((key) => !afterKeys.has(key)),
|
|
2780
|
-
).catch(() => undefined);
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
} catch (error) {
|
|
2784
|
-
await deleteUnpublishedWorkspaceArchiveObject(
|
|
2785
|
-
objectStorage,
|
|
2786
|
-
workspaceArchiveRef,
|
|
2787
|
-
archiveMetrics,
|
|
2788
|
-
);
|
|
2789
|
-
throw error;
|
|
2790
|
-
}
|
|
2750
|
+
const published = await putVersion1TarArchiveOrInline({
|
|
2751
|
+
backend: lease.backend,
|
|
2752
|
+
objectStorage,
|
|
2753
|
+
accountId,
|
|
2754
|
+
workspaceId: row.workspaceId,
|
|
2755
|
+
sandboxGroupId: row.sandboxGroupId,
|
|
2756
|
+
archive:
|
|
2757
|
+
typeof archiveBase64 === "object"
|
|
2758
|
+
? archiveBase64
|
|
2759
|
+
: {
|
|
2760
|
+
bytes: Buffer.from(archiveBase64, "base64"),
|
|
2761
|
+
descriptor: archiveMetadata,
|
|
2762
|
+
base64: archiveBase64,
|
|
2763
|
+
},
|
|
2764
|
+
metrics: archiveMetrics,
|
|
2765
|
+
});
|
|
2766
|
+
result = await persistWorkspaceArchiveCandidate({
|
|
2767
|
+
objectStorage,
|
|
2768
|
+
...(published.workspaceArchiveRef ? { ref: published.workspaceArchiveRef } : {}),
|
|
2769
|
+
persist: () =>
|
|
2770
|
+
persistDrainSnapshot(db, {
|
|
2771
|
+
...baseInput,
|
|
2772
|
+
workspaceArchiveMeta: archiveMetadata,
|
|
2773
|
+
...published,
|
|
2774
|
+
...(checkpointArtifactId ? { checkpointArtifactId } : {}),
|
|
2775
|
+
}),
|
|
2776
|
+
});
|
|
2791
2777
|
}
|
|
2792
2778
|
if (!result.wrote && checkpointArtifactId) {
|
|
2793
2779
|
await markSandboxCheckpointArtifactDeletePending(db, {
|
|
@@ -2823,6 +2809,18 @@ async function terminateDrainableBox(
|
|
|
2823
2809
|
captureClaim?.providerRequestId ?? attempt.operationId,
|
|
2824
2810
|
captureDisposition,
|
|
2825
2811
|
capturePolicy,
|
|
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
|
+
},
|
|
2826
2824
|
);
|
|
2827
2825
|
const terminated = typeof termination === "boolean" ? termination : termination.terminated;
|
|
2828
2826
|
if (!terminated) {
|
|
@@ -2955,10 +2953,22 @@ export async function terminateProviderBox(
|
|
|
2955
2953
|
providerCaptureRequestId?: string,
|
|
2956
2954
|
captureDisposition: DrainCaptureDisposition = "capture_required",
|
|
2957
2955
|
claimedCapturePolicy?: ProviderWorkspaceCapturePolicy | null,
|
|
2956
|
+
diskBackedArchives = false,
|
|
2957
|
+
releaseFailedCapture?: () => Promise<void>,
|
|
2958
2958
|
): Promise<ProviderTerminationOutcome> {
|
|
2959
2959
|
const durableBackendId = (lease.resumeBackendId ?? lease.backend) as string;
|
|
2960
2960
|
const backend = sandboxBackendForSdkBackendId(durableBackendId) ?? durableBackendId;
|
|
2961
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
|
+
};
|
|
2962
2972
|
// 'none' / no backend -> nothing to terminate.
|
|
2963
2973
|
if (!backend || backend === "none") {
|
|
2964
2974
|
return { terminated: true, providerMissingBeforeCapture: false };
|
|
@@ -3147,7 +3157,7 @@ export async function terminateProviderBox(
|
|
|
3147
3157
|
// snapshot must re-throw BEFORE any terminate so files are never lost — the next
|
|
3148
3158
|
// sweep retries, the provider idle-timeout is the backstop. A NotFound here (the
|
|
3149
3159
|
// box raced gone between resume and persist) is success: nothing to persist.
|
|
3150
|
-
let verifiedArchive:
|
|
3160
|
+
let verifiedArchive: VerifiedWorkspaceArchivePayload | undefined;
|
|
3151
3161
|
const sessionWorkspacePersistence = (
|
|
3152
3162
|
session as { state?: { workspacePersistence?: unknown } } | undefined
|
|
3153
3163
|
)?.state?.workspacePersistence;
|
|
@@ -3162,10 +3172,15 @@ export async function terminateProviderBox(
|
|
|
3162
3172
|
: null;
|
|
3163
3173
|
try {
|
|
3164
3174
|
if (captureDisposition !== "archive_published" && session?.persistWorkspace) {
|
|
3165
|
-
const capture =
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3175
|
+
const capture = captureWorkspaceArchiveForStorage(
|
|
3176
|
+
session,
|
|
3177
|
+
Date.now(),
|
|
3178
|
+
{
|
|
3179
|
+
requestId: providerCaptureRequestId ?? randomUUID(),
|
|
3180
|
+
strategy: liveCapturePolicy.strategy,
|
|
3181
|
+
},
|
|
3182
|
+
diskBackedArchives,
|
|
3183
|
+
);
|
|
3169
3184
|
verifiedArchive = await awaitProviderCaptureWithLatePublication({
|
|
3170
3185
|
capture,
|
|
3171
3186
|
timeoutMs: settings.sandboxSnapshotTimeoutMs,
|
|
@@ -3179,7 +3194,7 @@ export async function terminateProviderBox(
|
|
|
3179
3194
|
publishLate: async (archive) => {
|
|
3180
3195
|
try {
|
|
3181
3196
|
const result = await persistArchive(
|
|
3182
|
-
archive.base64,
|
|
3197
|
+
archive.kind === "host_spool" ? archive : archive.base64,
|
|
3183
3198
|
archive.descriptor,
|
|
3184
3199
|
session,
|
|
3185
3200
|
checkpointBinding,
|
|
@@ -3197,9 +3212,21 @@ export async function terminateProviderBox(
|
|
|
3197
3212
|
...logIdentity,
|
|
3198
3213
|
error: error instanceof Error ? error.message : String(error),
|
|
3199
3214
|
});
|
|
3215
|
+
} finally {
|
|
3216
|
+
await disposeWorkspaceArchive(archive);
|
|
3200
3217
|
}
|
|
3201
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
|
+
},
|
|
3202
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();
|
|
3203
3230
|
observability.warn("sandbox reaper: timed-out provider capture later failed", {
|
|
3204
3231
|
...logIdentity,
|
|
3205
3232
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -3221,6 +3248,12 @@ export async function terminateProviderBox(
|
|
|
3221
3248
|
error: error instanceof Error ? error.message : String(error),
|
|
3222
3249
|
},
|
|
3223
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
|
+
}
|
|
3224
3257
|
// NEVER terminate a box whose snapshot we could not capture.
|
|
3225
3258
|
throw error;
|
|
3226
3259
|
}
|
|
@@ -3237,12 +3270,17 @@ export async function terminateProviderBox(
|
|
|
3237
3270
|
...logIdentity,
|
|
3238
3271
|
});
|
|
3239
3272
|
} else if (verifiedArchive) {
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3273
|
+
let wrote: boolean;
|
|
3274
|
+
try {
|
|
3275
|
+
({ wrote } = await persistArchive(
|
|
3276
|
+
verifiedArchive.kind === "host_spool" ? verifiedArchive : verifiedArchive.base64,
|
|
3277
|
+
verifiedArchive.descriptor,
|
|
3278
|
+
session,
|
|
3279
|
+
checkpointBinding,
|
|
3280
|
+
));
|
|
3281
|
+
} finally {
|
|
3282
|
+
await disposeWorkspaceArchive(verifiedArchive);
|
|
3283
|
+
}
|
|
3246
3284
|
if (!wrote) {
|
|
3247
3285
|
observability.info(
|
|
3248
3286
|
"sandbox reaper: lease re-armed during persist — leaving box RUNNING (no terminate)",
|
|
@@ -413,7 +413,7 @@ export function recordWorkerDeathRecoveryMetrics(
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
export class TurnLifecycleMetrics {
|
|
416
|
-
private readonly
|
|
416
|
+
private readonly attempts = new Map<string, { startedAt: number; lastProgressAt: number }>();
|
|
417
417
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
418
418
|
|
|
419
419
|
constructor(
|
|
@@ -424,42 +424,46 @@ export class TurnLifecycleMetrics {
|
|
|
424
424
|
} = {},
|
|
425
425
|
) {}
|
|
426
426
|
|
|
427
|
-
start(
|
|
427
|
+
start(input: { attemptId: string }): void {
|
|
428
428
|
const now = this.now();
|
|
429
|
-
this.
|
|
429
|
+
this.attempts.set(input.attemptId, { startedAt: now, lastProgressAt: now });
|
|
430
430
|
this.ensureTimer();
|
|
431
431
|
this.refreshGauges();
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
progress(
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
437
|
-
|
|
434
|
+
progress(input: { attemptId: string }): void {
|
|
435
|
+
const attempt = this.attempts.get(input.attemptId);
|
|
436
|
+
if (!attempt) return;
|
|
437
|
+
attempt.lastProgressAt = this.now();
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
finish(
|
|
441
|
-
|
|
440
|
+
finish(input: {
|
|
441
|
+
attemptId: string;
|
|
442
|
+
outcome: TurnOutcome | null;
|
|
443
|
+
durationSeconds?: number;
|
|
444
|
+
}): void {
|
|
445
|
+
const startedAt = this.attempts.get(input.attemptId)?.startedAt;
|
|
442
446
|
if (startedAt !== undefined) {
|
|
443
|
-
this.
|
|
447
|
+
this.attempts.delete(input.attemptId);
|
|
444
448
|
}
|
|
445
|
-
if (outcome) {
|
|
449
|
+
if (input.outcome) {
|
|
446
450
|
const observedDuration =
|
|
447
|
-
durationSeconds ??
|
|
451
|
+
input.durationSeconds ??
|
|
448
452
|
(startedAt === undefined ? 0 : Math.max(0, (this.now() - startedAt) / 1000));
|
|
449
453
|
this.observability.incrementCounter({
|
|
450
454
|
name: "opengeni_turns_total",
|
|
451
455
|
help: "Total agent turns by terminal outcome.",
|
|
452
|
-
labels: { outcome },
|
|
456
|
+
labels: { outcome: input.outcome },
|
|
453
457
|
});
|
|
454
458
|
this.observability.observeHistogram({
|
|
455
459
|
name: "opengeni_turn_duration_seconds",
|
|
456
460
|
help: "Agent turn duration in seconds by terminal outcome.",
|
|
457
|
-
labels: { outcome },
|
|
461
|
+
labels: { outcome: input.outcome },
|
|
458
462
|
value: observedDuration,
|
|
459
463
|
});
|
|
460
464
|
}
|
|
461
465
|
this.refreshGauges();
|
|
462
|
-
if (this.
|
|
466
|
+
if (this.attempts.size === 0) {
|
|
463
467
|
this.stopTimer();
|
|
464
468
|
}
|
|
465
469
|
}
|
|
@@ -467,42 +471,42 @@ export class TurnLifecycleMetrics {
|
|
|
467
471
|
refreshGauges(): void {
|
|
468
472
|
this.observability.setGauge({
|
|
469
473
|
name: "opengeni_turns_inflight",
|
|
470
|
-
help: "Current number of in-flight agent
|
|
471
|
-
value: this.
|
|
474
|
+
help: "Current number of in-flight physical agent-turn attempts in this worker process.",
|
|
475
|
+
value: this.attempts.size,
|
|
472
476
|
});
|
|
473
477
|
this.observability.setGauge({
|
|
474
478
|
name: "opengeni_turn_oldest_inflight_age_seconds",
|
|
475
|
-
help: "Age in seconds of the oldest in-flight agent
|
|
479
|
+
help: "Age in seconds of the oldest in-flight physical agent-turn attempt in this worker process.",
|
|
476
480
|
value: this.oldestInflightAgeSeconds(),
|
|
477
481
|
});
|
|
478
482
|
this.observability.setGauge({
|
|
479
483
|
name: "opengeni_turn_oldest_no_progress_age_seconds",
|
|
480
|
-
help: "Seconds since durable progress for the least recently progressing in-flight turn.",
|
|
484
|
+
help: "Seconds since durable progress for the least recently progressing in-flight physical agent-turn attempt.",
|
|
481
485
|
value: this.oldestNoProgressAgeSeconds(),
|
|
482
486
|
});
|
|
483
487
|
}
|
|
484
488
|
|
|
485
489
|
stop(): void {
|
|
486
|
-
this.
|
|
490
|
+
this.attempts.clear();
|
|
487
491
|
this.refreshGauges();
|
|
488
492
|
this.stopTimer();
|
|
489
493
|
}
|
|
490
494
|
|
|
491
495
|
private oldestInflightAgeSeconds(): number {
|
|
492
|
-
if (this.
|
|
496
|
+
if (this.attempts.size === 0) {
|
|
493
497
|
return 0;
|
|
494
498
|
}
|
|
495
499
|
let oldest = Number.POSITIVE_INFINITY;
|
|
496
|
-
for (const { startedAt } of this.
|
|
500
|
+
for (const { startedAt } of this.attempts.values()) {
|
|
497
501
|
oldest = Math.min(oldest, startedAt);
|
|
498
502
|
}
|
|
499
503
|
return Math.max(0, (this.now() - oldest) / 1000);
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
private oldestNoProgressAgeSeconds(): number {
|
|
503
|
-
if (this.
|
|
507
|
+
if (this.attempts.size === 0) return 0;
|
|
504
508
|
let leastRecentProgress = Number.POSITIVE_INFINITY;
|
|
505
|
-
for (const { lastProgressAt } of this.
|
|
509
|
+
for (const { lastProgressAt } of this.attempts.values()) {
|
|
506
510
|
leastRecentProgress = Math.min(leastRecentProgress, lastProgressAt);
|
|
507
511
|
}
|
|
508
512
|
return Math.max(0, (this.now() - leastRecentProgress) / 1000);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { uploadWorkspaceArchiveSpool, type ObjectStorage } from "@opengeni/storage";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { VerifiedHostWorkspaceArchive } from "@opengeni/runtime/sandbox";
|
|
2
4
|
import {
|
|
3
5
|
parseWorkspaceArchiveObjectRef,
|
|
4
6
|
workspaceArchiveObjectKey,
|
|
@@ -35,43 +37,102 @@ export async function putTarWorkspaceArchiveObject(input: {
|
|
|
35
37
|
accountId: string;
|
|
36
38
|
workspaceId: string;
|
|
37
39
|
sandboxGroupId: string;
|
|
38
|
-
archive:
|
|
40
|
+
archive:
|
|
41
|
+
| { bytes: Uint8Array; descriptor: WorkspaceArchiveDescriptor }
|
|
42
|
+
| VerifiedHostWorkspaceArchive;
|
|
39
43
|
}): Promise<WorkspaceArchiveObjectRef> {
|
|
40
|
-
|
|
44
|
+
// Snapshot caller-owned metadata before any provider callback can mutate it.
|
|
45
|
+
const descriptor = structuredClone(input.archive.descriptor);
|
|
46
|
+
if (descriptor.version !== 1) {
|
|
41
47
|
throw new Error("Object-storage workspace archives are portable tar only");
|
|
42
48
|
}
|
|
43
49
|
const key = workspaceArchiveObjectKey({
|
|
44
50
|
accountId: input.accountId,
|
|
45
51
|
workspaceId: input.workspaceId,
|
|
46
52
|
sandboxGroupId: input.sandboxGroupId,
|
|
47
|
-
revision:
|
|
48
|
-
|
|
49
|
-
await input.objectStorage.putObject({
|
|
50
|
-
key,
|
|
51
|
-
contentType: "application/x-tar",
|
|
52
|
-
body: input.archive.bytes,
|
|
53
|
-
sha256: input.archive.descriptor.archiveSha256,
|
|
53
|
+
revision: descriptor.revision,
|
|
54
|
+
uploadId: randomUUID(),
|
|
54
55
|
});
|
|
56
|
+
const archive = input.archive;
|
|
57
|
+
if ("spool" in archive) {
|
|
58
|
+
if (
|
|
59
|
+
archive.spool.byteSize !== descriptor.archiveBytes ||
|
|
60
|
+
archive.spool.sha256 !== descriptor.archiveSha256
|
|
61
|
+
) {
|
|
62
|
+
throw new Error("Workspace archive spool does not match its verified descriptor");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const source =
|
|
66
|
+
"spool" in archive
|
|
67
|
+
? archive.spool
|
|
68
|
+
: {
|
|
69
|
+
path: "",
|
|
70
|
+
byteSize: archive.bytes.byteLength,
|
|
71
|
+
sha256: descriptor.archiveSha256,
|
|
72
|
+
async *open() {
|
|
73
|
+
for (let offset = 0; offset < archive.bytes.length; offset += 1024 * 1024)
|
|
74
|
+
yield archive.bytes.subarray(offset, offset + 1024 * 1024);
|
|
75
|
+
},
|
|
76
|
+
async dispose() {},
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
await uploadWorkspaceArchiveSpool(input.objectStorage, key, {
|
|
80
|
+
...source,
|
|
81
|
+
byteSize: descriptor.archiveBytes,
|
|
82
|
+
sha256: descriptor.archiveSha256,
|
|
83
|
+
open: source.open.bind(source),
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// This fresh locator has never been offered to database publication. An
|
|
87
|
+
// ambiguous provider upload can leave an orphan, never overwrite a retained
|
|
88
|
+
// archive or justify deleting another attempt's object.
|
|
89
|
+
await input.objectStorage.deleteObject(key).catch(() => undefined);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
55
92
|
return {
|
|
56
93
|
schema: "sandbox_archive_object_v1",
|
|
57
94
|
key,
|
|
58
|
-
sha256:
|
|
59
|
-
bytes:
|
|
95
|
+
sha256: descriptor.archiveSha256,
|
|
96
|
+
bytes: descriptor.archiveBytes,
|
|
60
97
|
backend: input.objectStorage.backend,
|
|
61
98
|
};
|
|
62
99
|
}
|
|
63
100
|
|
|
101
|
+
/** The caller owns a fresh candidate returned by this module. A thrown or lost
|
|
102
|
+
* database response is UNKNOWN: it may have committed, so never delete on catch.
|
|
103
|
+
* Only the transaction's exact candidate disposition can authorize cleanup. */
|
|
104
|
+
export async function persistWorkspaceArchiveCandidate<
|
|
105
|
+
T extends {
|
|
106
|
+
wrote: boolean;
|
|
107
|
+
candidateDisposition?: "adopted" | "already_referenced" | "unused";
|
|
108
|
+
},
|
|
109
|
+
>(input: {
|
|
110
|
+
objectStorage?: ObjectStorage | null;
|
|
111
|
+
ref?: WorkspaceArchiveObjectRef;
|
|
112
|
+
persist: () => Promise<T>;
|
|
113
|
+
}): Promise<T> {
|
|
114
|
+
const objectStorage = input.objectStorage;
|
|
115
|
+
const ref = input.ref ? { ...input.ref } : undefined;
|
|
116
|
+
const result = await input.persist();
|
|
117
|
+
if (objectStorage && ref && result.candidateDisposition === "unused") {
|
|
118
|
+
await deleteUnpublishedWorkspaceArchiveObject(objectStorage, ref);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
64
123
|
export async function putVersion1TarArchiveOrInline(input: {
|
|
65
124
|
backend: string;
|
|
66
125
|
objectStorage?: ObjectStorage | null | undefined;
|
|
67
126
|
accountId: string;
|
|
68
127
|
workspaceId: string;
|
|
69
128
|
sandboxGroupId: string;
|
|
70
|
-
archive:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
129
|
+
archive:
|
|
130
|
+
| VerifiedHostWorkspaceArchive
|
|
131
|
+
| {
|
|
132
|
+
bytes: Uint8Array;
|
|
133
|
+
descriptor: WorkspaceArchiveDescriptor;
|
|
134
|
+
base64: string;
|
|
135
|
+
};
|
|
75
136
|
metrics?: {
|
|
76
137
|
onWorkspaceArchiveObject?: (input: {
|
|
77
138
|
outcome: "put" | "put_failed" | "deleted_unpublished";
|
|
@@ -83,12 +144,15 @@ export async function putVersion1TarArchiveOrInline(input: {
|
|
|
83
144
|
workspaceArchiveRef?: WorkspaceArchiveObjectRef;
|
|
84
145
|
}> {
|
|
85
146
|
if (input.archive.descriptor.version !== 1) {
|
|
147
|
+
if ("spool" in input.archive) throw new Error("Native snapshots cannot use a portable spool");
|
|
86
148
|
return { workspaceArchive: input.archive.base64 };
|
|
87
149
|
}
|
|
88
150
|
if (input.backend === "opensandbox" && !input.objectStorage) {
|
|
89
151
|
throw new WorkspaceArchiveObjectStorageRequiredError("opensandbox");
|
|
90
152
|
}
|
|
91
153
|
if (!input.objectStorage) {
|
|
154
|
+
if ("spool" in input.archive)
|
|
155
|
+
throw new WorkspaceArchiveObjectStorageRequiredError(input.backend);
|
|
92
156
|
return { workspaceArchive: input.archive.base64 };
|
|
93
157
|
}
|
|
94
158
|
try {
|
|
@@ -97,7 +161,7 @@ export async function putVersion1TarArchiveOrInline(input: {
|
|
|
97
161
|
accountId: input.accountId,
|
|
98
162
|
workspaceId: input.workspaceId,
|
|
99
163
|
sandboxGroupId: input.sandboxGroupId,
|
|
100
|
-
archive:
|
|
164
|
+
archive: input.archive,
|
|
101
165
|
});
|
|
102
166
|
try {
|
|
103
167
|
input.metrics?.onWorkspaceArchiveObject?.({
|