@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.
Files changed (64) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-UVBPOGA7.js → chunk-232VC2LW.js} +2 -2
  3. package/dist/{chunk-DVSLOUEI.js → chunk-2ZMTQVTR.js} +2 -2
  4. package/dist/{chunk-AI4NC6XW.js → chunk-6TKL3GQO.js} +47 -1
  5. package/dist/chunk-6TKL3GQO.js.map +1 -0
  6. package/dist/{chunk-F4BIHJ7J.js → chunk-EYF6MH6C.js} +4 -4
  7. package/dist/{chunk-I4ITNMX2.js → chunk-GVZJ7XSR.js} +16 -4
  8. package/dist/{chunk-I4ITNMX2.js.map → chunk-GVZJ7XSR.js.map} +1 -1
  9. package/dist/{chunk-XBXANJQS.js → chunk-M7PSMVFF.js} +2 -2
  10. package/dist/{chunk-PFBUHOI5.js → chunk-QLA3UQNX.js} +3 -3
  11. package/dist/{chunk-YVYBAWBU.js → chunk-THJGMEK5.js} +53 -1
  12. package/dist/chunk-THJGMEK5.js.map +1 -0
  13. package/dist/{chunk-UUPY6IGP.js → chunk-UK57QA2H.js} +2 -2
  14. package/dist/codex-selection-diagnostics.d.ts +12 -0
  15. package/dist/connection-token-resolver.d.ts +2 -0
  16. package/dist/database.d.ts +7 -2
  17. package/dist/editable-artifact-durable-export.js +2 -2
  18. package/dist/editable-artifacts.js +3 -3
  19. package/dist/host-mcp-bindings.d.ts +3 -1
  20. package/dist/index.d.ts +32 -1
  21. package/dist/index.js +345 -75
  22. package/dist/index.js.map +1 -1
  23. package/dist/managed-auth-session-sets.js +3 -3
  24. package/dist/mcp-operations.d.ts +95 -0
  25. package/dist/mcp-operations.js +95 -0
  26. package/dist/mcp-operations.js.map +1 -0
  27. package/dist/plugin-packages.d.ts +1 -0
  28. package/dist/provision-roles.js +1 -1
  29. package/dist/retained-provider-commands.js +2 -2
  30. package/dist/runtime-posture.d.ts +3 -3
  31. package/dist/sandbox-transition-wait.d.ts +10 -0
  32. package/dist/schema.d.ts +517 -0
  33. package/dist/schema.js +3 -1
  34. package/dist/session-background-commands.js +4 -4
  35. package/dist/session-command-output.js +4 -4
  36. package/dist/session-event-slices.js +2 -2
  37. package/dist/session-mcp-progress.d.ts +8 -1
  38. package/dist/session-tenancy.js +3 -3
  39. package/dist/video-generation.js +3 -3
  40. package/dist/workspace-tool-defaults.js +4 -4
  41. package/drizzle/0459_mcp_operations.sql +396 -0
  42. package/drizzle/0460_host_export_message_attribution.sql +177 -0
  43. package/package.json +10 -6
  44. package/src/codex-selection-diagnostics.ts +25 -0
  45. package/src/computer-sessions.ts +5 -1
  46. package/src/connection-token-resolver.ts +2 -0
  47. package/src/database.ts +23 -2
  48. package/src/host-mcp-bindings.ts +14 -1
  49. package/src/index.ts +370 -60
  50. package/src/mcp-operations.ts +189 -0
  51. package/src/plugin-packages.ts +3 -0
  52. package/src/provision-roles.ts +6 -0
  53. package/src/runtime-posture.ts +50 -0
  54. package/src/sandbox-transition-wait.ts +45 -0
  55. package/src/schema.ts +46 -0
  56. package/src/session-mcp-progress.ts +26 -5
  57. package/dist/chunk-AI4NC6XW.js.map +0 -1
  58. package/dist/chunk-YVYBAWBU.js.map +0 -1
  59. /package/dist/{chunk-UVBPOGA7.js.map → chunk-232VC2LW.js.map} +0 -0
  60. /package/dist/{chunk-DVSLOUEI.js.map → chunk-2ZMTQVTR.js.map} +0 -0
  61. /package/dist/{chunk-F4BIHJ7J.js.map → chunk-EYF6MH6C.js.map} +0 -0
  62. /package/dist/{chunk-XBXANJQS.js.map → chunk-M7PSMVFF.js.map} +0 -0
  63. /package/dist/{chunk-PFBUHOI5.js.map → chunk-QLA3UQNX.js.map} +0 -0
  64. /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({ session: schema.sessions, pin: schema.sessionPins })
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
- const [session] = await canonicalSessionRowsFromEventCursors(scopedDb, workspaceId, [
35233
- row.session,
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
- await mapSessionWithControl(
35242
- scopedDb,
35243
- session,
35244
- mcpServers.get(sessionId) ?? [],
35245
- mapSessionPin(row.pin),
35246
- mapSessionAttention(session, row.pin),
35247
- mapSessionArchive(row.pin),
35248
- undefined,
35249
- { subjectId, activated: tenancyActivated },
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, but never
43979
- // beyond the same one-hour lifecycle ceiling.
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 hardDeadline = startedAt + SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS;
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
- deadline = extendSandboxTransitionDeadline(
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 hardDeadline = startedAt + SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS;
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
- deadline = extendSandboxTransitionDeadline(
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
- // captureId owns one DB callback; providerRequestId owns the external
52243
- // operation. Modal's wire contract explicitly defines snapshotId as an
52244
- // idempotency key, so every Temporal retry of one logical capture must reuse
52245
- // it. Other providers still retain the stable lineage for late-result
52246
- // adoption, but may not replay the physical request before its deadline.
52247
- const providerRequestId = operationId;
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 = parseWorkspaceArchiveObjectRef(input.workspaceArchiveRef) ?? undefined;
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
- const published = publishedWorkspaceArchiveFields(input);
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) => {