@opengeni/db 4.3.2 → 4.3.3-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/canonical-human-identities.js +3 -3
- package/dist/{chunk-UVBPOGA7.js → chunk-232VC2LW.js} +2 -2
- package/dist/{chunk-DVSLOUEI.js → chunk-2ZMTQVTR.js} +2 -2
- package/dist/{chunk-AI4NC6XW.js → chunk-6TKL3GQO.js} +47 -1
- package/dist/chunk-6TKL3GQO.js.map +1 -0
- package/dist/{chunk-F4BIHJ7J.js → chunk-EYF6MH6C.js} +4 -4
- package/dist/{chunk-I4ITNMX2.js → chunk-GVZJ7XSR.js} +16 -4
- package/dist/{chunk-I4ITNMX2.js.map → chunk-GVZJ7XSR.js.map} +1 -1
- package/dist/{chunk-XBXANJQS.js → chunk-M7PSMVFF.js} +2 -2
- package/dist/{chunk-PFBUHOI5.js → chunk-QLA3UQNX.js} +3 -3
- package/dist/{chunk-YVYBAWBU.js → chunk-THJGMEK5.js} +53 -1
- package/dist/chunk-THJGMEK5.js.map +1 -0
- package/dist/{chunk-UUPY6IGP.js → chunk-UK57QA2H.js} +2 -2
- package/dist/codex-selection-diagnostics.d.ts +12 -0
- package/dist/connection-token-resolver.d.ts +2 -0
- package/dist/database.d.ts +7 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/host-mcp-bindings.d.ts +3 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +345 -75
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-operations.d.ts +95 -0
- package/dist/mcp-operations.js +95 -0
- package/dist/mcp-operations.js.map +1 -0
- package/dist/plugin-packages.d.ts +1 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +2 -2
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/sandbox-transition-wait.d.ts +10 -0
- package/dist/schema.d.ts +517 -0
- package/dist/schema.js +3 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +4 -4
- package/dist/session-event-slices.js +2 -2
- package/dist/session-mcp-progress.d.ts +8 -1
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +4 -4
- package/drizzle/0459_mcp_operations.sql +396 -0
- package/drizzle/0460_host_export_message_attribution.sql +177 -0
- package/package.json +10 -6
- package/src/codex-selection-diagnostics.ts +25 -0
- package/src/computer-sessions.ts +5 -1
- package/src/connection-token-resolver.ts +2 -0
- package/src/database.ts +23 -2
- package/src/host-mcp-bindings.ts +14 -1
- package/src/index.ts +370 -60
- package/src/mcp-operations.ts +189 -0
- package/src/plugin-packages.ts +3 -0
- package/src/provision-roles.ts +6 -0
- package/src/runtime-posture.ts +50 -0
- package/src/sandbox-transition-wait.ts +45 -0
- package/src/schema.ts +46 -0
- package/src/session-mcp-progress.ts +26 -5
- package/dist/chunk-AI4NC6XW.js.map +0 -1
- package/dist/chunk-YVYBAWBU.js.map +0 -1
- /package/dist/{chunk-UVBPOGA7.js.map → chunk-232VC2LW.js.map} +0 -0
- /package/dist/{chunk-DVSLOUEI.js.map → chunk-2ZMTQVTR.js.map} +0 -0
- /package/dist/{chunk-F4BIHJ7J.js.map → chunk-EYF6MH6C.js.map} +0 -0
- /package/dist/{chunk-XBXANJQS.js.map → chunk-M7PSMVFF.js.map} +0 -0
- /package/dist/{chunk-PFBUHOI5.js.map → chunk-QLA3UQNX.js.map} +0 -0
- /package/dist/{chunk-UUPY6IGP.js.map → chunk-UK57QA2H.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { codexSelectionDiagnostics } from "./codex-selection-diagnostics";
|
|
2
|
+
export { codexSelectionDiagnostics } from "./codex-selection-diagnostics";
|
|
1
3
|
import {
|
|
2
4
|
latestStartedSessionTurnQuery,
|
|
3
5
|
withLatestStartedSessionPolicy,
|
|
@@ -272,6 +274,7 @@ import {
|
|
|
272
274
|
SANDBOX_PROVIDER_INSTANCE_ID_FIELDS_BY_BACKEND,
|
|
273
275
|
parseWorkspaceArchiveDescriptor,
|
|
274
276
|
parseWorkspaceArchiveObjectRef,
|
|
277
|
+
validateWorkspaceArchiveObjectRef,
|
|
275
278
|
workspaceArchivePayloadPresent,
|
|
276
279
|
omitInlineWorkspaceArchiveWhenObjectRefPresent,
|
|
277
280
|
type WorkspaceArchiveObjectRef,
|
|
@@ -334,7 +337,6 @@ import {
|
|
|
334
337
|
} from "@opengeni/contracts";
|
|
335
338
|
import {
|
|
336
339
|
environmentsEncryptionKeyBytes,
|
|
337
|
-
SANDBOX_LIFECYCLE_RETRY_HANDOFF_GRACE_MS,
|
|
338
340
|
SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS,
|
|
339
341
|
WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
340
342
|
WORKSPACE_OPENROUTER_CONNECTION_ROLE,
|
|
@@ -342,6 +344,7 @@ import {
|
|
|
342
344
|
VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
343
345
|
type Settings,
|
|
344
346
|
} from "@opengeni/config";
|
|
347
|
+
import { SandboxTransitionWaitBudget } from "./sandbox-transition-wait";
|
|
345
348
|
import { normalizeWorkspaceMembershipPermissions } from "./workspace-membership-permissions";
|
|
346
349
|
import {
|
|
347
350
|
canonicalizePersistedHistoryItem,
|
|
@@ -398,6 +401,8 @@ import {
|
|
|
398
401
|
export { LOSSLESS_TEXT_PREFIX } from "./lossless-json";
|
|
399
402
|
import {
|
|
400
403
|
projectSessionMcpProgressText,
|
|
404
|
+
projectSessionTextPrefix,
|
|
405
|
+
sessionTextStoragePrefixChars,
|
|
401
406
|
SESSION_MCP_PROGRESS_STORAGE_CHARS,
|
|
402
407
|
sessionMcpProgressScalarIsEncodedSql,
|
|
403
408
|
} from "./session-mcp-progress";
|
|
@@ -29659,6 +29664,114 @@ export async function setSessionCodexPin(
|
|
|
29659
29664
|
}
|
|
29660
29665
|
|
|
29661
29666
|
/** Written by the worker at the turn boundary; drives the in-session indicator. */
|
|
29667
|
+
/** Commit observed assignment and its events together under the exact live attempt.
|
|
29668
|
+
* Allocation still uses its frozen policy snapshot; this is observation only.
|
|
29669
|
+
* Replays return the same receipt and never manufacture a second switch.
|
|
29670
|
+
*/
|
|
29671
|
+
export async function recordSessionCodexSelectionForTurnAttempt(
|
|
29672
|
+
db: Database,
|
|
29673
|
+
input: {
|
|
29674
|
+
workspaceId: string;
|
|
29675
|
+
sessionId: string;
|
|
29676
|
+
turnId: string;
|
|
29677
|
+
attemptId: string;
|
|
29678
|
+
executionGeneration: number;
|
|
29679
|
+
credentialId: string;
|
|
29680
|
+
strategy: string;
|
|
29681
|
+
reusedLease: boolean;
|
|
29682
|
+
pinnedCredentialId: string | null;
|
|
29683
|
+
pinSource: "manual" | "policy" | null;
|
|
29684
|
+
eligibleCount: number;
|
|
29685
|
+
connectedCount: number;
|
|
29686
|
+
},
|
|
29687
|
+
): Promise<{ events: SessionEvent[]; diagnostics: ReturnType<typeof codexSelectionDiagnostics> }> {
|
|
29688
|
+
return await withWorkspaceSessionEventActivityRls(db, input.workspaceId, true, async (tx) => {
|
|
29689
|
+
const fence = await lockTurnAttemptWriteFenceTx(tx, {
|
|
29690
|
+
workspaceId: input.workspaceId,
|
|
29691
|
+
sessionId: input.sessionId,
|
|
29692
|
+
turnId: input.turnId,
|
|
29693
|
+
executionGeneration: input.executionGeneration,
|
|
29694
|
+
attemptId: input.attemptId,
|
|
29695
|
+
sessionLock: "no_key_update",
|
|
29696
|
+
});
|
|
29697
|
+
if (!fence.allowed || !fence.session) throw new CodexCredentialLeaseAttemptFencedError();
|
|
29698
|
+
const key = `opengeni:codex-selection:${input.attemptId}`;
|
|
29699
|
+
const prior = await tx
|
|
29700
|
+
.select()
|
|
29701
|
+
.from(schema.sessionEvents)
|
|
29702
|
+
.where(
|
|
29703
|
+
and(
|
|
29704
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
29705
|
+
eq(schema.sessionEvents.sessionId, input.sessionId),
|
|
29706
|
+
inArray(schema.sessionEvents.clientEventId, [key, `${key}:switch`]),
|
|
29707
|
+
),
|
|
29708
|
+
)
|
|
29709
|
+
.orderBy(asc(schema.sessionEvents.sequence));
|
|
29710
|
+
if (prior.length) {
|
|
29711
|
+
const receipt = prior.find((event) => event.type === "codex.credential.selected");
|
|
29712
|
+
if (!receipt) throw new Error("Codex selection receipt missing");
|
|
29713
|
+
const payload = sessionEventPayloadRecord(receipt.payload, receipt.payloadCodecVersion);
|
|
29714
|
+
if (payload.credentialId !== input.credentialId)
|
|
29715
|
+
throw new Error("An attempt cannot record two different Codex selections");
|
|
29716
|
+
return {
|
|
29717
|
+
events: prior.map(mapEvent),
|
|
29718
|
+
diagnostics: {
|
|
29719
|
+
transition: payload.transition,
|
|
29720
|
+
source: payload.source,
|
|
29721
|
+
reason: payload.reason,
|
|
29722
|
+
} as ReturnType<typeof codexSelectionDiagnostics>,
|
|
29723
|
+
};
|
|
29724
|
+
}
|
|
29725
|
+
const previousCredentialId = fence.session.codexLastCredentialId;
|
|
29726
|
+
const diagnostics = codexSelectionDiagnostics({ ...input, previousCredentialId });
|
|
29727
|
+
const events: AppendEventInput[] = [];
|
|
29728
|
+
if (previousCredentialId !== null && previousCredentialId !== input.credentialId) {
|
|
29729
|
+
events.push({
|
|
29730
|
+
type: "codex.account.switched",
|
|
29731
|
+
clientEventId: `${key}:switch`,
|
|
29732
|
+
payload: {
|
|
29733
|
+
fromAccountId: previousCredentialId,
|
|
29734
|
+
toAccountId: input.credentialId,
|
|
29735
|
+
reason: diagnostics.source === "manual_pin" ? "manual" : "rotation",
|
|
29736
|
+
},
|
|
29737
|
+
});
|
|
29738
|
+
}
|
|
29739
|
+
events.push({
|
|
29740
|
+
type: "codex.credential.selected",
|
|
29741
|
+
clientEventId: key,
|
|
29742
|
+
payload: {
|
|
29743
|
+
credentialId: input.credentialId,
|
|
29744
|
+
strategy: input.strategy,
|
|
29745
|
+
...diagnostics,
|
|
29746
|
+
previousCredentialId,
|
|
29747
|
+
eligibleCount: input.eligibleCount,
|
|
29748
|
+
connectedCount: input.connectedCount,
|
|
29749
|
+
reused: input.reusedLease,
|
|
29750
|
+
},
|
|
29751
|
+
});
|
|
29752
|
+
await tx
|
|
29753
|
+
.update(schema.sessions)
|
|
29754
|
+
.set({ codexLastCredentialId: input.credentialId })
|
|
29755
|
+
.where(
|
|
29756
|
+
and(
|
|
29757
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
29758
|
+
eq(schema.sessions.id, input.sessionId),
|
|
29759
|
+
),
|
|
29760
|
+
);
|
|
29761
|
+
const appended = await appendSessionEventsForTurnAttempt(
|
|
29762
|
+
tx,
|
|
29763
|
+
input.workspaceId,
|
|
29764
|
+
input.sessionId,
|
|
29765
|
+
input.turnId,
|
|
29766
|
+
input.executionGeneration,
|
|
29767
|
+
input.attemptId,
|
|
29768
|
+
events,
|
|
29769
|
+
);
|
|
29770
|
+
if (!appended.accepted) throw new CodexCredentialLeaseAttemptFencedError();
|
|
29771
|
+
return { events: appended.events, diagnostics };
|
|
29772
|
+
});
|
|
29773
|
+
}
|
|
29774
|
+
|
|
29662
29775
|
export async function recordSessionActiveCodexCredential(
|
|
29663
29776
|
db: Database,
|
|
29664
29777
|
workspaceId: string,
|
|
@@ -35194,6 +35307,120 @@ export async function listSessionsForSubject(
|
|
|
35194
35307
|
}
|
|
35195
35308
|
|
|
35196
35309
|
/** Read a session with the caller subject's personal pin projection. */
|
|
35310
|
+
/** Read failure evidence at the already-authorized detail cursor, never a browser page.
|
|
35311
|
+
* Event rows are immutable; the sequence fence keeps these reads coherent even
|
|
35312
|
+
* if another turn is accepted while the detail projection is being assembled.
|
|
35313
|
+
* Keep this off fleet/list/worker reads. Existing type/turn indexes cover it.
|
|
35314
|
+
*/
|
|
35315
|
+
async function sessionFailureDiagnostics(
|
|
35316
|
+
db: Database,
|
|
35317
|
+
workspaceId: string,
|
|
35318
|
+
session: { id: string; lastSequence: number; status: string },
|
|
35319
|
+
): Promise<Session["failureDiagnostics"]> {
|
|
35320
|
+
if (session.status !== "failed") return null;
|
|
35321
|
+
const scope = and(
|
|
35322
|
+
eq(schema.sessionEvents.workspaceId, workspaceId),
|
|
35323
|
+
eq(schema.sessionEvents.sessionId, session.id),
|
|
35324
|
+
lte(schema.sessionEvents.sequence, session.lastSequence),
|
|
35325
|
+
isNull(schema.sessionEvents.duplicateOfEventId),
|
|
35326
|
+
or(
|
|
35327
|
+
isNull(schema.sessionEvents.turnAssociation),
|
|
35328
|
+
eq(schema.sessionEvents.turnAssociation, "current"),
|
|
35329
|
+
),
|
|
35330
|
+
);
|
|
35331
|
+
// Transfer only codec-aware prefixes. Cutting an encoded storage scalar
|
|
35332
|
+
// directly would show base64 rather than the accepted logical error text.
|
|
35333
|
+
const maxChars = 1024;
|
|
35334
|
+
const storageChars = sessionTextStoragePrefixChars(maxChars);
|
|
35335
|
+
const diagnosticFields = ["error", "message", "detail", "lastRetryableError", "code", "status"];
|
|
35336
|
+
const boundedFields = diagnosticFields.flatMap((field) => {
|
|
35337
|
+
const scalar = sql`case when jsonb_typeof(${schema.sessionEvents.payload}->${field}::text) = 'string'
|
|
35338
|
+
then ${schema.sessionEvents.payload}->>${field}::text else null end`;
|
|
35339
|
+
return [
|
|
35340
|
+
sql`${field}::text`,
|
|
35341
|
+
sql`jsonb_build_object(
|
|
35342
|
+
'prefix', left(${scalar}, ${storageChars}), 'chars', char_length(${scalar}),
|
|
35343
|
+
'encoded', ${sessionMcpProgressScalarIsEncodedSql(scalar, schema.sessionEvents.payloadCodecVersion)})`,
|
|
35344
|
+
];
|
|
35345
|
+
});
|
|
35346
|
+
const projectedPayload = sql<Record<string, unknown>>`jsonb_build_object(
|
|
35347
|
+
${sql.join(boundedFields, sql`, `)},
|
|
35348
|
+
'providerRecoveryCount', case when jsonb_typeof(${schema.sessionEvents.payload}->'providerRecoveryCount') = 'number'
|
|
35349
|
+
and length((${schema.sessionEvents.payload}->'providerRecoveryCount')::text) <= 16
|
|
35350
|
+
then ${schema.sessionEvents.payload}->'providerRecoveryCount' else 'null'::jsonb end)`;
|
|
35351
|
+
const latest = async (type: string) => {
|
|
35352
|
+
const [row] = await db
|
|
35353
|
+
.select({
|
|
35354
|
+
id: schema.sessionEvents.id,
|
|
35355
|
+
sequence: schema.sessionEvents.sequence,
|
|
35356
|
+
turnId: schema.sessionEvents.turnId,
|
|
35357
|
+
occurredAt: schema.sessionEvents.occurredAt,
|
|
35358
|
+
codecVersion: schema.sessionEvents.payloadCodecVersion,
|
|
35359
|
+
payload: projectedPayload,
|
|
35360
|
+
})
|
|
35361
|
+
.from(schema.sessionEvents)
|
|
35362
|
+
.where(and(scope, eq(schema.sessionEvents.type, type)))
|
|
35363
|
+
.orderBy(desc(schema.sessionEvents.sequence))
|
|
35364
|
+
.limit(1);
|
|
35365
|
+
if (!row) return null;
|
|
35366
|
+
const payload: Record<string, unknown> = {
|
|
35367
|
+
providerRecoveryCount: row.payload.providerRecoveryCount,
|
|
35368
|
+
};
|
|
35369
|
+
const truncatedFields: string[] = [];
|
|
35370
|
+
for (const field of diagnosticFields) {
|
|
35371
|
+
const stored = row.payload[field] as {
|
|
35372
|
+
prefix: string | null;
|
|
35373
|
+
chars: number | null;
|
|
35374
|
+
encoded: boolean;
|
|
35375
|
+
};
|
|
35376
|
+
const projected = projectSessionTextPrefix(
|
|
35377
|
+
stored.prefix,
|
|
35378
|
+
stored.chars,
|
|
35379
|
+
row.codecVersion,
|
|
35380
|
+
stored.encoded,
|
|
35381
|
+
maxChars,
|
|
35382
|
+
);
|
|
35383
|
+
payload[field] = projected.text;
|
|
35384
|
+
if (projected.textTruncated || (projected.originalChars ?? 0) > maxChars)
|
|
35385
|
+
truncatedFields.push(field);
|
|
35386
|
+
}
|
|
35387
|
+
// Six 1024-character logical strings remain below 40KiB even when JSON
|
|
35388
|
+
// escapes every character as six bytes. Stored values remain untouched.
|
|
35389
|
+
payload.projection = { fieldsOnly: true, fieldLimitChars: maxChars, truncatedFields };
|
|
35390
|
+
return { ...row, payload, occurredAt: row.occurredAt.toISOString() };
|
|
35391
|
+
};
|
|
35392
|
+
const turnFailure = await latest("turn.failed");
|
|
35393
|
+
const latestStatus = await latest("session.status.changed");
|
|
35394
|
+
const statusPayload = latestStatus?.payload as Record<string, unknown> | undefined;
|
|
35395
|
+
// Do not associate an earlier failure with a later recorded revival.
|
|
35396
|
+
if (
|
|
35397
|
+
latestStatus &&
|
|
35398
|
+
(!turnFailure || latestStatus.sequence > turnFailure.sequence) &&
|
|
35399
|
+
typeof statusPayload?.status === "string" &&
|
|
35400
|
+
statusPayload.status !== "failed"
|
|
35401
|
+
)
|
|
35402
|
+
return null;
|
|
35403
|
+
const preClaimFailure =
|
|
35404
|
+
statusPayload?.status === "failed" && statusPayload.code === "pre_claim_failure"
|
|
35405
|
+
? latestStatus
|
|
35406
|
+
: null;
|
|
35407
|
+
const failure =
|
|
35408
|
+
preClaimFailure &&
|
|
35409
|
+
(!turnFailure ||
|
|
35410
|
+
(preClaimFailure.sequence > turnFailure.sequence &&
|
|
35411
|
+
(!preClaimFailure.turnId || preClaimFailure.turnId !== turnFailure.turnId)))
|
|
35412
|
+
? preClaimFailure
|
|
35413
|
+
: turnFailure;
|
|
35414
|
+
if (!failure) return null;
|
|
35415
|
+
return {
|
|
35416
|
+
eventId: failure.id,
|
|
35417
|
+
sequence: failure.sequence,
|
|
35418
|
+
turnId: failure.turnId ?? null,
|
|
35419
|
+
occurredAt: failure.occurredAt,
|
|
35420
|
+
payload: failure.payload,
|
|
35421
|
+
};
|
|
35422
|
+
}
|
|
35423
|
+
|
|
35197
35424
|
export async function getSessionForSubject(
|
|
35198
35425
|
db: Database,
|
|
35199
35426
|
workspaceId: string,
|
|
@@ -35203,8 +35430,22 @@ export async function getSessionForSubject(
|
|
|
35203
35430
|
): Promise<Session | null> {
|
|
35204
35431
|
return await withWorkspaceSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
35205
35432
|
const [row] = await scopedDb
|
|
35206
|
-
.select({
|
|
35433
|
+
.select({
|
|
35434
|
+
session: schema.sessions,
|
|
35435
|
+
pin: schema.sessionPins,
|
|
35436
|
+
cursor: schema.sessionEventCursors,
|
|
35437
|
+
})
|
|
35207
35438
|
.from(schema.sessions)
|
|
35439
|
+
// Status and replay cursor must share one statement snapshot. Otherwise a
|
|
35440
|
+
// concurrent revival can pair an old failed row with a new running cursor,
|
|
35441
|
+
// causing the browser to skip the very event that would refresh its status.
|
|
35442
|
+
.leftJoin(
|
|
35443
|
+
schema.sessionEventCursors,
|
|
35444
|
+
and(
|
|
35445
|
+
eq(schema.sessionEventCursors.workspaceId, schema.sessions.workspaceId),
|
|
35446
|
+
eq(schema.sessionEventCursors.sessionId, schema.sessions.id),
|
|
35447
|
+
),
|
|
35448
|
+
)
|
|
35208
35449
|
.leftJoin(
|
|
35209
35450
|
schema.sessionPins,
|
|
35210
35451
|
and(
|
|
@@ -35229,25 +35470,40 @@ export async function getSessionForSubject(
|
|
|
35229
35470
|
)
|
|
35230
35471
|
.limit(1);
|
|
35231
35472
|
if (!row) return null;
|
|
35232
|
-
|
|
35233
|
-
row.
|
|
35234
|
-
|
|
35473
|
+
if (
|
|
35474
|
+
!row.cursor ||
|
|
35475
|
+
row.cursor.accountId !== row.session.accountId ||
|
|
35476
|
+
row.cursor.lastSequence < row.session.lastSequence
|
|
35477
|
+
) {
|
|
35478
|
+
throw new Error(`Session event cursor invariant failed for session ${sessionId}`);
|
|
35479
|
+
}
|
|
35480
|
+
const [session] = await withLatestStartedSessionPolicy(
|
|
35481
|
+
scopedDb,
|
|
35482
|
+
workspaceId,
|
|
35483
|
+
await withCurrentSessionInputWait(scopedDb, workspaceId, [
|
|
35484
|
+
{ ...row.session, lastSequence: row.cursor.lastSequence },
|
|
35485
|
+
]),
|
|
35486
|
+
);
|
|
35235
35487
|
if (!session) throw new Error(`Session event cursor missing for session ${sessionId}`);
|
|
35236
35488
|
const mcpServers = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [
|
|
35237
35489
|
sessionId,
|
|
35238
35490
|
]);
|
|
35239
35491
|
const tenancyActivated = await sessionTenancyProductActivated(scopedDb, workspaceId);
|
|
35492
|
+
const failureDiagnostics = await sessionFailureDiagnostics(scopedDb, workspaceId, session);
|
|
35240
35493
|
return projectSessionForRelatedAccess(
|
|
35241
|
-
|
|
35242
|
-
|
|
35243
|
-
|
|
35244
|
-
|
|
35245
|
-
|
|
35246
|
-
|
|
35247
|
-
|
|
35248
|
-
|
|
35249
|
-
|
|
35250
|
-
|
|
35494
|
+
{
|
|
35495
|
+
...(await mapSessionWithControl(
|
|
35496
|
+
scopedDb,
|
|
35497
|
+
session,
|
|
35498
|
+
mcpServers.get(sessionId) ?? [],
|
|
35499
|
+
mapSessionPin(row.pin),
|
|
35500
|
+
mapSessionAttention(session, row.pin),
|
|
35501
|
+
mapSessionArchive(row.pin),
|
|
35502
|
+
undefined,
|
|
35503
|
+
{ subjectId, activated: tenancyActivated },
|
|
35504
|
+
)),
|
|
35505
|
+
failureDiagnostics,
|
|
35506
|
+
},
|
|
35251
35507
|
relatedSessionAccess,
|
|
35252
35508
|
);
|
|
35253
35509
|
});
|
|
@@ -43118,19 +43374,6 @@ function archiveCaptureRemainingMs(
|
|
|
43118
43374
|
: null;
|
|
43119
43375
|
}
|
|
43120
43376
|
|
|
43121
|
-
function extendSandboxTransitionDeadline(
|
|
43122
|
-
currentDeadline: number,
|
|
43123
|
-
hardDeadline: number,
|
|
43124
|
-
remainingCaptureMs: number | null | undefined,
|
|
43125
|
-
now: number,
|
|
43126
|
-
): number {
|
|
43127
|
-
if (remainingCaptureMs === null || remainingCaptureMs === undefined) return currentDeadline;
|
|
43128
|
-
return Math.min(
|
|
43129
|
-
hardDeadline,
|
|
43130
|
-
Math.max(currentDeadline, now + remainingCaptureMs + SANDBOX_LIFECYCLE_RETRY_HANDOFF_GRACE_MS),
|
|
43131
|
-
);
|
|
43132
|
-
}
|
|
43133
|
-
|
|
43134
43377
|
function providerlessLeaseBackendPredicateSql() {
|
|
43135
43378
|
const providerlessBackends = Object.entries(SANDBOX_PROVIDER_INSTANCE_ID_FIELDS_BY_BACKEND)
|
|
43136
43379
|
.filter(([, fields]) => fields.length === 0)
|
|
@@ -43975,33 +44218,27 @@ export async function acquireLease(
|
|
|
43975
44218
|
// NTP/VM resume may change timestamp interpretation, but must never stretch
|
|
43976
44219
|
// or prematurely consume the caller's resource budget. PostgreSQL projects
|
|
43977
44220
|
// an active claim's remaining time on its own authoritative clock; that may
|
|
43978
|
-
// extend a positive caller budget after a rolling config change
|
|
43979
|
-
//
|
|
44221
|
+
// extend a positive caller budget once after a rolling config change. Later
|
|
44222
|
+
// capture retries must not keep renewing a waiter's observational budget.
|
|
43980
44223
|
const startedAt = performance.now();
|
|
43981
|
-
const
|
|
43982
|
-
let deadline = Math.min(hardDeadline, startedAt + captureWaitMs);
|
|
44224
|
+
const waitBudget = new SandboxTransitionWaitBudget(captureWaitMs, startedAt);
|
|
43983
44225
|
let delayMs = 25;
|
|
43984
44226
|
for (;;) {
|
|
43985
44227
|
const result = await acquireLeaseOnce(db, input);
|
|
43986
44228
|
const now = performance.now();
|
|
43987
44229
|
if (captureWaitMs > 0 && result.role === "fenced" && result.reason === "capture_in_progress") {
|
|
43988
|
-
|
|
43989
|
-
deadline,
|
|
43990
|
-
hardDeadline,
|
|
43991
|
-
result.captureRemainingMs,
|
|
43992
|
-
now,
|
|
43993
|
-
);
|
|
44230
|
+
waitBudget.observeCapture(result.captureRemainingMs, now);
|
|
43994
44231
|
}
|
|
43995
44232
|
if (
|
|
43996
44233
|
result.role !== "fenced" ||
|
|
43997
44234
|
result.reason === "superseded" ||
|
|
43998
44235
|
result.reason === "rotation_in_progress" ||
|
|
43999
|
-
now >= deadline
|
|
44236
|
+
now >= waitBudget.deadline
|
|
44000
44237
|
) {
|
|
44001
44238
|
return result;
|
|
44002
44239
|
}
|
|
44003
44240
|
await waitForSandboxTransition(
|
|
44004
|
-
Math.min(delayMs, Math.max(1, deadline - now)),
|
|
44241
|
+
Math.min(delayMs, Math.max(1, waitBudget.deadline - now)),
|
|
44005
44242
|
input.waitSignal,
|
|
44006
44243
|
"Sandbox lease transition wait cancelled",
|
|
44007
44244
|
);
|
|
@@ -50127,8 +50364,7 @@ async function advanceWorkspaceGenerationForAuthority(
|
|
|
50127
50364
|
throw new Error("Workspace archive capture wait is invalid");
|
|
50128
50365
|
}
|
|
50129
50366
|
const startedAt = performance.now();
|
|
50130
|
-
const
|
|
50131
|
-
let deadline = Math.min(hardDeadline, startedAt + captureWaitMs);
|
|
50367
|
+
const waitBudget = new SandboxTransitionWaitBudget(captureWaitMs, startedAt);
|
|
50132
50368
|
let delayMs = 25;
|
|
50133
50369
|
let captureWaitStartedAt: number | undefined;
|
|
50134
50370
|
let captureWaitOutcome: "completed" | "failed" = "failed";
|
|
@@ -50149,23 +50385,18 @@ async function advanceWorkspaceGenerationForAuthority(
|
|
|
50149
50385
|
error instanceof SandboxWorkspaceMutationFencedError &&
|
|
50150
50386
|
error.code === "capture_in_progress"
|
|
50151
50387
|
) {
|
|
50152
|
-
|
|
50153
|
-
deadline,
|
|
50154
|
-
hardDeadline,
|
|
50155
|
-
error.captureRemainingMs,
|
|
50156
|
-
now,
|
|
50157
|
-
);
|
|
50388
|
+
waitBudget.observeCapture(error.captureRemainingMs, now);
|
|
50158
50389
|
}
|
|
50159
50390
|
if (
|
|
50160
50391
|
!(error instanceof SandboxWorkspaceMutationFencedError) ||
|
|
50161
50392
|
error.code !== "capture_in_progress" ||
|
|
50162
|
-
now >= deadline
|
|
50393
|
+
now >= waitBudget.deadline
|
|
50163
50394
|
) {
|
|
50164
50395
|
throw error;
|
|
50165
50396
|
}
|
|
50166
50397
|
captureWaitStartedAt ??= performance.now();
|
|
50167
50398
|
await waitForSandboxTransition(
|
|
50168
|
-
Math.min(delayMs, Math.max(1, deadline - now)),
|
|
50399
|
+
Math.min(delayMs, Math.max(1, waitBudget.deadline - now)),
|
|
50169
50400
|
waitSignal,
|
|
50170
50401
|
"Sandbox workspace mutation wait cancelled",
|
|
50171
50402
|
);
|
|
@@ -52239,12 +52470,12 @@ export async function claimWorkspaceArchiveCapture(
|
|
|
52239
52470
|
throw new Error("Workspace archive capture timeout is invalid");
|
|
52240
52471
|
}
|
|
52241
52472
|
const operationId = input.operationId ?? input.captureId;
|
|
52242
|
-
//
|
|
52243
|
-
//
|
|
52244
|
-
//
|
|
52245
|
-
//
|
|
52246
|
-
//
|
|
52247
|
-
const providerRequestId =
|
|
52473
|
+
// A fresh admission-fenced capture gets a fresh provider request identity.
|
|
52474
|
+
// After a released failure, writers may re-arm this same lease epoch before
|
|
52475
|
+
// the drain workflow retries. Reusing its operationId could then retrieve an
|
|
52476
|
+
// older provider snapshot as the newer workspace generation. Replacements of
|
|
52477
|
+
// an uninterrupted claim retain this stored identity for late-result adoption.
|
|
52478
|
+
const providerRequestId = randomUUID();
|
|
52248
52479
|
const captureAttempt = input.attempt ?? 1;
|
|
52249
52480
|
const providerReplaySafe = input.providerReplaySafe === true;
|
|
52250
52481
|
const takeoverSafe = input.takeoverSafe === true;
|
|
@@ -53448,13 +53679,30 @@ export async function adoptLegacyModalCheckpointArtifact(
|
|
|
53448
53679
|
}
|
|
53449
53680
|
|
|
53450
53681
|
function publishedWorkspaceArchiveFields(input: {
|
|
53682
|
+
accountId: string;
|
|
53683
|
+
workspaceId: string;
|
|
53684
|
+
sandboxGroupId: string;
|
|
53685
|
+
workspaceArchiveMeta: SandboxArchiveRevision;
|
|
53451
53686
|
workspaceArchive?: string | null;
|
|
53452
53687
|
workspaceArchiveRef?: WorkspaceArchiveObjectRef | null;
|
|
53453
53688
|
}): {
|
|
53454
53689
|
workspaceArchive?: string;
|
|
53455
53690
|
workspaceArchiveRef?: WorkspaceArchiveObjectRef;
|
|
53456
53691
|
} {
|
|
53457
|
-
const ref =
|
|
53692
|
+
const ref =
|
|
53693
|
+
input.workspaceArchiveRef == null
|
|
53694
|
+
? undefined
|
|
53695
|
+
: (validateWorkspaceArchiveObjectRef(input.workspaceArchiveRef, {
|
|
53696
|
+
accountId: input.accountId,
|
|
53697
|
+
workspaceId: input.workspaceId,
|
|
53698
|
+
sandboxGroupId: input.sandboxGroupId,
|
|
53699
|
+
descriptor: input.workspaceArchiveMeta,
|
|
53700
|
+
}) ?? undefined);
|
|
53701
|
+
if (input.workspaceArchiveRef != null && !ref) {
|
|
53702
|
+
throw new Error(
|
|
53703
|
+
"Invalid workspace archive object ref: publication scope or descriptor mismatch",
|
|
53704
|
+
);
|
|
53705
|
+
}
|
|
53458
53706
|
const inline =
|
|
53459
53707
|
typeof input.workspaceArchive === "string" && input.workspaceArchive.length > 0
|
|
53460
53708
|
? input.workspaceArchive
|
|
@@ -53475,6 +53723,30 @@ function publishedWorkspaceArchiveFields(input: {
|
|
|
53475
53723
|
return { workspaceArchive: inline };
|
|
53476
53724
|
}
|
|
53477
53725
|
|
|
53726
|
+
/** Object-candidate outcome from the publication transaction, independent of
|
|
53727
|
+
* lifecycle `wrote`. Absent for inline/provider receipts and pure CAS checks.
|
|
53728
|
+
* `unused` is NOT a retirement receipt: only the owner of a fresh candidate with
|
|
53729
|
+
* no other publication in flight may use it for immediate cleanup. Exceptions
|
|
53730
|
+
* give no disposition (commit may have happened). Legacy/shared keys and evicted
|
|
53731
|
+
* refs need durable no-reattachment evidence before deletion. */
|
|
53732
|
+
export type WorkspaceArchiveCandidateDisposition = "adopted" | "already_referenced" | "unused";
|
|
53733
|
+
|
|
53734
|
+
function workspaceArchiveCandidateFields(
|
|
53735
|
+
candidate: WorkspaceArchiveObjectRef | undefined,
|
|
53736
|
+
lockedResumeState: Record<string, unknown> | null | undefined,
|
|
53737
|
+
): { candidateDisposition?: WorkspaceArchiveCandidateDisposition } {
|
|
53738
|
+
if (!candidate) return {};
|
|
53739
|
+
const sessionState = lockedResumeState?.sessionState as Record<string, unknown> | undefined;
|
|
53740
|
+
const referenced = [
|
|
53741
|
+
sessionState?.workspaceArchiveRef,
|
|
53742
|
+
sessionState?.workspaceArchivePrevRef,
|
|
53743
|
+
].some((value) => {
|
|
53744
|
+
const ref = parseWorkspaceArchiveObjectRef(value);
|
|
53745
|
+
return ref?.key === candidate.key && ref.backend === candidate.backend;
|
|
53746
|
+
});
|
|
53747
|
+
return { candidateDisposition: referenced ? "already_referenced" : "unused" };
|
|
53748
|
+
}
|
|
53749
|
+
|
|
53478
53750
|
export async function persistDrainSnapshot(
|
|
53479
53751
|
db: Database,
|
|
53480
53752
|
input: {
|
|
@@ -53511,12 +53783,14 @@ export async function persistDrainSnapshot(
|
|
|
53511
53783
|
): Promise<{
|
|
53512
53784
|
wrote: boolean;
|
|
53513
53785
|
archiveRevision: string | null;
|
|
53786
|
+
candidateDisposition?: WorkspaceArchiveCandidateDisposition;
|
|
53514
53787
|
}> {
|
|
53515
53788
|
const workspaceArchiveMeta =
|
|
53516
53789
|
input.workspaceArchive === null ? null : parseArchiveRevision(input.workspaceArchiveMeta);
|
|
53517
53790
|
if (input.workspaceArchive !== null && !workspaceArchiveMeta) {
|
|
53518
53791
|
throw new Error("Invalid verified workspace archive descriptor");
|
|
53519
53792
|
}
|
|
53793
|
+
const published = input.workspaceArchive === null ? null : publishedWorkspaceArchiveFields(input);
|
|
53520
53794
|
if (
|
|
53521
53795
|
workspaceArchiveMeta?.version === 2 &&
|
|
53522
53796
|
(workspaceArchiveMeta.provider === "modal_snapshot_filesystem" ||
|
|
@@ -53575,10 +53849,15 @@ export async function persistDrainSnapshot(
|
|
|
53575
53849
|
for update
|
|
53576
53850
|
`);
|
|
53577
53851
|
const row = guard[0];
|
|
53852
|
+
const candidateFields = workspaceArchiveCandidateFields(
|
|
53853
|
+
published?.workspaceArchiveRef,
|
|
53854
|
+
row?.resume_state,
|
|
53855
|
+
);
|
|
53578
53856
|
if (!row) {
|
|
53579
53857
|
return {
|
|
53580
53858
|
wrote: false,
|
|
53581
53859
|
archiveRevision: null,
|
|
53860
|
+
...candidateFields,
|
|
53582
53861
|
};
|
|
53583
53862
|
}
|
|
53584
53863
|
const sourceLeaseMatches =
|
|
@@ -53624,7 +53903,7 @@ export async function persistDrainSnapshot(
|
|
|
53624
53903
|
lateReceipt.providerRequestId === input.providerRequestId &&
|
|
53625
53904
|
!row.unsettled_mutation;
|
|
53626
53905
|
if (!activePublication && !coldLatePublication) {
|
|
53627
|
-
return { wrote: false, archiveRevision: null };
|
|
53906
|
+
return { wrote: false, archiveRevision: null, ...candidateFields };
|
|
53628
53907
|
}
|
|
53629
53908
|
const priorArchive = row.prior_archive ?? null;
|
|
53630
53909
|
const priorArchivePrev = row.prior_archive_prev ?? null;
|
|
@@ -53642,7 +53921,7 @@ export async function persistDrainSnapshot(
|
|
|
53642
53921
|
archiveRevision: null,
|
|
53643
53922
|
};
|
|
53644
53923
|
}
|
|
53645
|
-
|
|
53924
|
+
if (!published) throw new Error("Missing workspace archive publication fields");
|
|
53646
53925
|
const priorMeta = parseArchiveRevision(priorSessionState?.workspaceArchiveMeta);
|
|
53647
53926
|
if (activePublication && row.archive_capture_published_at !== null) {
|
|
53648
53927
|
// A predecessor and its successor may receive the same provider result.
|
|
@@ -53668,7 +53947,7 @@ export async function persistDrainSnapshot(
|
|
|
53668
53947
|
)
|
|
53669
53948
|
`);
|
|
53670
53949
|
}
|
|
53671
|
-
return { wrote: true, archiveRevision: priorMeta.revision };
|
|
53950
|
+
return { wrote: true, archiveRevision: priorMeta.revision, ...candidateFields };
|
|
53672
53951
|
}
|
|
53673
53952
|
const rotation = rotateWorkspaceArchives({
|
|
53674
53953
|
resumeState: row.resume_state,
|
|
@@ -53729,11 +54008,13 @@ export async function persistDrainSnapshot(
|
|
|
53729
54008
|
return {
|
|
53730
54009
|
wrote: false,
|
|
53731
54010
|
archiveRevision: null,
|
|
54011
|
+
...candidateFields,
|
|
53732
54012
|
};
|
|
53733
54013
|
}
|
|
53734
54014
|
return {
|
|
53735
54015
|
wrote: true,
|
|
53736
54016
|
archiveRevision: workspaceArchiveMeta?.revision ?? null,
|
|
54017
|
+
...(published.workspaceArchiveRef ? { candidateDisposition: "adopted" as const } : {}),
|
|
53737
54018
|
};
|
|
53738
54019
|
},
|
|
53739
54020
|
);
|
|
@@ -54132,6 +54413,7 @@ export async function persistWarmSnapshot(
|
|
|
54132
54413
|
throttled: boolean;
|
|
54133
54414
|
superseded: boolean;
|
|
54134
54415
|
archiveRevision: string | null;
|
|
54416
|
+
candidateDisposition?: WorkspaceArchiveCandidateDisposition;
|
|
54135
54417
|
}> {
|
|
54136
54418
|
const capturedAtMs = input.capturedAtMs ?? Date.now();
|
|
54137
54419
|
const workspaceArchiveMeta = parseArchiveRevision(input.workspaceArchiveMeta);
|
|
@@ -54201,6 +54483,26 @@ export async function persistWarmSnapshot(
|
|
|
54201
54483
|
(attempt.outcome === "completed" ||
|
|
54202
54484
|
attempt.outcome === "failed" ||
|
|
54203
54485
|
attempt.outcome === "requires_action"));
|
|
54486
|
+
// Even a replay rejected by the attempt/epoch/capture guards may name
|
|
54487
|
+
// a committed current or previous object. Inspect those slots under the
|
|
54488
|
+
// same lease lock as publication, after the canonical workspace lock.
|
|
54489
|
+
// This conveys no retirement or no-future-reattachment authority.
|
|
54490
|
+
const candidateLease = published.workspaceArchiveRef
|
|
54491
|
+
? await scopedDb.execute<{ resume_state: Record<string, unknown> | null }>(sql`
|
|
54492
|
+
select jsonb_build_object('sessionState', jsonb_build_object(
|
|
54493
|
+
'workspaceArchiveRef', resume_state #> '{sessionState,workspaceArchiveRef}',
|
|
54494
|
+
'workspaceArchivePrevRef', resume_state #> '{sessionState,workspaceArchivePrevRef}'
|
|
54495
|
+
)) as resume_state
|
|
54496
|
+
from sandbox_leases
|
|
54497
|
+
where workspace_id = ${input.workspaceId}
|
|
54498
|
+
and sandbox_group_id = ${input.sandboxGroupId}
|
|
54499
|
+
for update
|
|
54500
|
+
`)
|
|
54501
|
+
: [];
|
|
54502
|
+
const candidateFields = workspaceArchiveCandidateFields(
|
|
54503
|
+
published.workspaceArchiveRef,
|
|
54504
|
+
candidateLease[0]?.resume_state,
|
|
54505
|
+
);
|
|
54204
54506
|
if (
|
|
54205
54507
|
!attempt ||
|
|
54206
54508
|
attempt.accountId !== input.accountId ||
|
|
@@ -54213,6 +54515,7 @@ export async function persistWarmSnapshot(
|
|
|
54213
54515
|
throttled: false,
|
|
54214
54516
|
superseded: true,
|
|
54215
54517
|
archiveRevision: null,
|
|
54518
|
+
...candidateFields,
|
|
54216
54519
|
};
|
|
54217
54520
|
}
|
|
54218
54521
|
const guard = await scopedDb.execute<{
|
|
@@ -54270,6 +54573,7 @@ export async function persistWarmSnapshot(
|
|
|
54270
54573
|
throttled: false,
|
|
54271
54574
|
superseded: false,
|
|
54272
54575
|
archiveRevision: null,
|
|
54576
|
+
...candidateFields,
|
|
54273
54577
|
};
|
|
54274
54578
|
}
|
|
54275
54579
|
const priorArchive = guard[0]!.prior_archive ?? null;
|
|
@@ -54290,6 +54594,7 @@ export async function persistWarmSnapshot(
|
|
|
54290
54594
|
throttled: false,
|
|
54291
54595
|
superseded: true,
|
|
54292
54596
|
archiveRevision: null,
|
|
54597
|
+
...candidateFields,
|
|
54293
54598
|
};
|
|
54294
54599
|
}
|
|
54295
54600
|
if (
|
|
@@ -54302,6 +54607,7 @@ export async function persistWarmSnapshot(
|
|
|
54302
54607
|
throttled: true,
|
|
54303
54608
|
superseded: false,
|
|
54304
54609
|
archiveRevision: null,
|
|
54610
|
+
...candidateFields,
|
|
54305
54611
|
};
|
|
54306
54612
|
}
|
|
54307
54613
|
const rotation = rotateWorkspaceArchives({
|
|
@@ -54323,6 +54629,7 @@ export async function persistWarmSnapshot(
|
|
|
54323
54629
|
throttled: false,
|
|
54324
54630
|
superseded: false,
|
|
54325
54631
|
archiveRevision: null,
|
|
54632
|
+
...candidateFields,
|
|
54326
54633
|
};
|
|
54327
54634
|
}
|
|
54328
54635
|
const livenessGuard: "warm" | "draining" = rowLiveness;
|
|
@@ -54355,6 +54662,7 @@ export async function persistWarmSnapshot(
|
|
|
54355
54662
|
throttled: false,
|
|
54356
54663
|
superseded: false,
|
|
54357
54664
|
archiveRevision: null,
|
|
54665
|
+
...candidateFields,
|
|
54358
54666
|
};
|
|
54359
54667
|
}
|
|
54360
54668
|
return {
|
|
@@ -54362,6 +54670,7 @@ export async function persistWarmSnapshot(
|
|
|
54362
54670
|
throttled: false,
|
|
54363
54671
|
superseded: false,
|
|
54364
54672
|
archiveRevision: workspaceArchiveMeta.revision,
|
|
54673
|
+
...(published.workspaceArchiveRef ? { candidateDisposition: "adopted" as const } : {}),
|
|
54365
54674
|
};
|
|
54366
54675
|
},
|
|
54367
54676
|
);
|
|
@@ -64024,6 +64333,7 @@ export async function claimSessionWorkForAttempt(
|
|
|
64024
64333
|
stage: "session_attempts.claim",
|
|
64025
64334
|
eventTypes: ["session.turn.attempt_claimed"],
|
|
64026
64335
|
maxAttempts: 3,
|
|
64336
|
+
organizationMembershipFence: true,
|
|
64027
64337
|
},
|
|
64028
64338
|
async (scopedDb) =>
|
|
64029
64339
|
await withSessionActivitySavepoint(scopedDb, async (tx) => {
|