@opengeni/db 0.12.0 → 0.12.6

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/index.js CHANGED
@@ -32,9 +32,11 @@ import {
32
32
  rigChanges,
33
33
  rigVersions,
34
34
  rigs,
35
+ sandboxLeaseHolders,
35
36
  sandboxPtySessions,
36
37
  sandboxRetainedProcesses,
37
38
  sandboxSessionEnvelopes,
39
+ sandboxWorkspaceMutationAdmissions,
38
40
  sandboxes,
39
41
  scheduledTaskRuns,
40
42
  scheduledTasks,
@@ -70,14 +72,27 @@ import {
70
72
  workspaceVariableSetVariables,
71
73
  workspaceVariableSets,
72
74
  workspaces
73
- } from "./chunk-VUKRIBO5.js";
75
+ } from "./chunk-CYA6BHV6.js";
74
76
  import {
75
77
  migrate,
76
78
  runMigrations
77
79
  } from "./chunk-Y5WZZVQK.js";
78
80
  import {
79
- provisionRoles
80
- } from "./chunk-BMFDXFPA.js";
81
+ FORCE_RLS_TABLES,
82
+ NON_RLS_RUNTIME_TABLES,
83
+ PROTECTED_NO_DIRECT_DML_TABLES,
84
+ RUNTIME_DML_TABLES,
85
+ RUNTIME_FULL_DML_TABLES,
86
+ RUNTIME_READ_INSERT_TABLES,
87
+ RUNTIME_READ_ONLY_TABLES,
88
+ RUNTIME_TABLE_PRIVILEGES,
89
+ RuntimeDatabasePostureError,
90
+ assertRuntimeDatabasePosture,
91
+ evaluateRuntimeDatabasePosture,
92
+ inspectRuntimeDatabasePosture,
93
+ provisionRoles,
94
+ runtimeDatabaseReadyCheck
95
+ } from "./chunk-XRVNI2GB.js";
81
96
  import "./chunk-PZ5AY32C.js";
82
97
 
83
98
  // src/index.ts
@@ -284,24 +299,15 @@ function assertKey(key) {
284
299
  }
285
300
 
286
301
  // src/event-payload-sanitizer.ts
287
- import { boundSessionEventPayload } from "@opengeni/contracts";
302
+ import {
303
+ boundSessionEventPayload,
304
+ isCredentialHeaderName,
305
+ isSensitiveFieldName,
306
+ redactSensitiveKey,
307
+ redactSensitiveText
308
+ } from "@opengeni/contracts";
288
309
  var REPLACEMENT = "\uFFFD";
289
310
  var REDACTED = "[redacted]";
290
- var SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
291
- "authorization",
292
- "headers",
293
- "accesstoken",
294
- "refreshtoken",
295
- "idtoken",
296
- "token",
297
- "apikey",
298
- "secret",
299
- "clientsecret",
300
- "credential",
301
- "credentialencrypted",
302
- "encryptedpkceverifier",
303
- "codeverifier"
304
- ]);
305
311
  function sanitizeEventString(value) {
306
312
  let needsWork = false;
307
313
  for (let i = 0; i < value.length; i++) {
@@ -343,7 +349,8 @@ function sanitizeEventPayload(payload, options = {}) {
343
349
  fullEvidence: options.fullEvidence
344
350
  });
345
351
  return sanitizeEventPayloadDeep(
346
- bounded === payload ? removeProducerTruncationMetadata(bounded) : bounded
352
+ bounded === payload ? removeProducerTruncationMetadata(bounded) : bounded,
353
+ options.knownSecrets ?? []
347
354
  );
348
355
  }
349
356
  function removeProducerTruncationMetadata(payload) {
@@ -354,33 +361,38 @@ function removeProducerTruncationMetadata(payload) {
354
361
  delete cleaned.truncation;
355
362
  return cleaned;
356
363
  }
357
- function sanitizeEventPayloadDeep(payload) {
364
+ function sanitizeEventPayloadDeep(payload, knownSecrets = []) {
358
365
  if (typeof payload === "string") {
359
- return sanitizeEventString(payload);
366
+ return sanitizeEventString(redactSensitiveText(payload, knownSecrets));
360
367
  }
361
368
  if (Array.isArray(payload)) {
362
- return payload.map((item) => sanitizeEventPayloadDeep(item));
369
+ return payload.map((item) => sanitizeEventPayloadDeep(item, knownSecrets));
363
370
  }
364
371
  if (payload instanceof Date) {
365
372
  return safeDateIso(payload);
366
373
  }
367
374
  if (payload && typeof payload === "object") {
368
- const entries = Object.entries(payload).map(
369
- ([key, value]) => [sanitizeEventString(key), sanitizeSensitiveEventField(key, value)]
370
- );
375
+ const usedKeys = /* @__PURE__ */ new Set();
376
+ const entries = Object.entries(payload).map(([key, value]) => {
377
+ const safeKey = nextUniqueKey(
378
+ sanitizeEventString(redactSensitiveKey(key, knownSecrets)),
379
+ usedKeys
380
+ );
381
+ return [safeKey, sanitizeSensitiveEventField(key, value, knownSecrets)];
382
+ });
371
383
  return Object.fromEntries(entries);
372
384
  }
373
385
  return payload;
374
386
  }
375
- function sanitizeModelPayload(payload) {
376
- return sanitizeModelPayloadDeep(payload, /* @__PURE__ */ new WeakSet(), 0);
387
+ function sanitizeModelPayload(payload, knownSecrets = []) {
388
+ return sanitizeModelPayloadDeep(payload, /* @__PURE__ */ new WeakSet(), 0, knownSecrets);
377
389
  }
378
390
  var MODEL_PAYLOAD_SANITIZE_MAX_DEPTH = 64;
379
391
  var MODEL_PAYLOAD_CYCLE_MARKER = "[OpenGeni omitted cyclic model payload]";
380
392
  var MODEL_PAYLOAD_DEPTH_MARKER = "[OpenGeni omitted model payload beyond database-safety depth]";
381
- function sanitizeModelPayloadDeep(payload, seen, depth) {
393
+ function sanitizeModelPayloadDeep(payload, seen, depth, knownSecrets = []) {
382
394
  if (typeof payload === "string") {
383
- return sanitizeEventString(payload);
395
+ return sanitizeEventString(redactSensitiveText(payload, knownSecrets));
384
396
  }
385
397
  if (!payload || typeof payload !== "object") return payload;
386
398
  if (payload instanceof Date) {
@@ -393,13 +405,25 @@ function sanitizeModelPayloadDeep(payload, seen, depth) {
393
405
  seen.add(payload);
394
406
  try {
395
407
  if (Array.isArray(payload)) {
396
- return payload.map((item) => sanitizeModelPayloadDeep(item, seen, depth + 1));
408
+ return payload.map(
409
+ (item) => sanitizeModelPayloadDeep(item, seen, depth + 1, knownSecrets)
410
+ );
397
411
  }
412
+ const usedKeys = /* @__PURE__ */ new Set();
398
413
  return Object.fromEntries(
399
- Object.entries(payload).map(([key, value]) => [
400
- sanitizeEventString(key),
401
- sanitizeModelPayloadDeep(value, seen, depth + 1)
402
- ])
414
+ Object.entries(payload).map(([key, value]) => {
415
+ const safeKey = nextUniqueKey(
416
+ sanitizeEventString(redactSensitiveKey(key, knownSecrets)),
417
+ usedKeys
418
+ );
419
+ if (isSensitiveFieldName(key)) {
420
+ return [safeKey, REDACTED];
421
+ }
422
+ if (normalizeFieldName(key) === "headers") {
423
+ return [safeKey, sanitizeModelHeaders(value, seen, depth + 1, knownSecrets)];
424
+ }
425
+ return [safeKey, sanitizeModelPayloadDeep(value, seen, depth + 1, knownSecrets)];
426
+ })
403
427
  );
404
428
  } finally {
405
429
  seen.delete(payload);
@@ -413,57 +437,118 @@ function safeDateIso(value) {
413
437
  return null;
414
438
  }
415
439
  }
416
- function sanitizeSensitiveEventField(key, value) {
440
+ function sanitizeSensitiveEventField(key, value, knownSecrets) {
417
441
  if (key === "mcpServers") {
418
- return sanitizeSessionMcpServerList(value);
442
+ return sanitizeSessionMcpServerList(value, knownSecrets);
419
443
  }
420
444
  if (key === "mcpCredentialUpdates") {
421
- return sanitizeMcpCredentialUpdateList(value);
445
+ return sanitizeMcpCredentialUpdateList(value, knownSecrets);
422
446
  }
423
- if (SENSITIVE_FIELD_NAMES.has(normalizeFieldName(key))) {
447
+ if (normalizeFieldName(key) === "headers") {
448
+ return sanitizeEventHeaders(value, knownSecrets);
449
+ }
450
+ if (isSensitiveFieldName(key)) {
424
451
  return REDACTED;
425
452
  }
426
- return sanitizeEventPayloadDeep(value);
453
+ return sanitizeEventPayloadDeep(value, knownSecrets);
454
+ }
455
+ function sanitizeEventHeaders(value, knownSecrets) {
456
+ if (!isPlainObject(value)) {
457
+ return sanitizeEventPayloadDeep(value, knownSecrets);
458
+ }
459
+ const usedKeys = /* @__PURE__ */ new Set();
460
+ return Object.fromEntries(
461
+ Object.entries(value).map(([key, child]) => {
462
+ const safeKey = nextUniqueKey(
463
+ sanitizeEventString(redactSensitiveKey(key, knownSecrets)),
464
+ usedKeys
465
+ );
466
+ return [
467
+ safeKey,
468
+ isCredentialHeaderName(key) ? REDACTED : sanitizeEventPayloadDeep(child, knownSecrets)
469
+ ];
470
+ })
471
+ );
472
+ }
473
+ function sanitizeModelHeaders(value, seen, depth, knownSecrets) {
474
+ if (!isPlainObject(value)) {
475
+ return sanitizeModelPayloadDeep(value, seen, depth, knownSecrets);
476
+ }
477
+ if (depth >= MODEL_PAYLOAD_SANITIZE_MAX_DEPTH) {
478
+ return MODEL_PAYLOAD_DEPTH_MARKER;
479
+ }
480
+ if (seen.has(value)) return MODEL_PAYLOAD_CYCLE_MARKER;
481
+ seen.add(value);
482
+ try {
483
+ const usedKeys = /* @__PURE__ */ new Set();
484
+ return Object.fromEntries(
485
+ Object.entries(value).map(([key, child]) => {
486
+ const safeKey = nextUniqueKey(
487
+ sanitizeEventString(redactSensitiveKey(key, knownSecrets)),
488
+ usedKeys
489
+ );
490
+ return [
491
+ safeKey,
492
+ isCredentialHeaderName(key) ? REDACTED : sanitizeModelPayloadDeep(child, seen, depth + 1, knownSecrets)
493
+ ];
494
+ })
495
+ );
496
+ } finally {
497
+ seen.delete(value);
498
+ }
427
499
  }
428
- function sanitizeSessionMcpServerList(value) {
500
+ function sanitizeSessionMcpServerList(value, knownSecrets) {
429
501
  if (!Array.isArray(value)) {
430
- return sanitizeEventPayloadDeep(value);
502
+ return sanitizeEventPayloadDeep(value, knownSecrets);
431
503
  }
432
504
  return value.map((item) => {
433
505
  if (!isPlainObject(item)) {
434
- return sanitizeEventPayloadDeep(item);
506
+ return sanitizeEventPayloadDeep(item, knownSecrets);
435
507
  }
436
508
  const { headers, headersEncrypted, ...rest } = item;
437
- const cleaned = sanitizeEventPayloadDeep(rest);
438
- const headerNames = safeHeaderNames(headers) ?? safeHeaderNames(headersEncrypted);
509
+ const cleaned = sanitizeEventPayloadDeep(rest, knownSecrets);
510
+ const headerNames = safeHeaderNames(headers, knownSecrets) ?? safeHeaderNames(headersEncrypted, knownSecrets);
439
511
  if (headerNames) {
440
512
  cleaned.headerNames = headerNames;
441
513
  }
442
514
  return cleaned;
443
515
  });
444
516
  }
445
- function sanitizeMcpCredentialUpdateList(value) {
517
+ function sanitizeMcpCredentialUpdateList(value, knownSecrets) {
446
518
  if (!Array.isArray(value)) {
447
- return sanitizeEventPayloadDeep(value);
519
+ return sanitizeEventPayloadDeep(value, knownSecrets);
448
520
  }
449
521
  return value.map((item) => {
450
522
  if (!isPlainObject(item)) {
451
- return sanitizeEventPayloadDeep(item);
523
+ return sanitizeEventPayloadDeep(item, knownSecrets);
452
524
  }
453
525
  const { headers, headersEncrypted, ...rest } = item;
454
- const cleaned = sanitizeEventPayloadDeep(rest);
455
- const headerNames = safeHeaderNames(headers) ?? safeHeaderNames(headersEncrypted);
526
+ const cleaned = sanitizeEventPayloadDeep(rest, knownSecrets);
527
+ const headerNames = safeHeaderNames(headers, knownSecrets) ?? safeHeaderNames(headersEncrypted, knownSecrets);
456
528
  if (headerNames) {
457
529
  cleaned.headerNames = headerNames;
458
530
  }
459
531
  return cleaned;
460
532
  });
461
533
  }
462
- function safeHeaderNames(value) {
534
+ function safeHeaderNames(value, knownSecrets) {
463
535
  if (!isPlainObject(value)) {
464
536
  return null;
465
537
  }
466
- return Object.keys(value).map(sanitizeEventString).sort();
538
+ const usedKeys = /* @__PURE__ */ new Set();
539
+ return Object.keys(value).map(
540
+ (key) => nextUniqueKey(sanitizeEventString(redactSensitiveKey(key, knownSecrets)), usedKeys)
541
+ ).sort();
542
+ }
543
+ function nextUniqueKey(base, usedKeys) {
544
+ let candidate = base;
545
+ let suffix = 2;
546
+ while (usedKeys.has(candidate)) {
547
+ candidate = `${base}#${suffix}`;
548
+ suffix += 1;
549
+ }
550
+ usedKeys.add(candidate);
551
+ return candidate;
467
552
  }
468
553
  function isPlainObject(value) {
469
554
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -487,6 +572,18 @@ var NewSessionDraftAccessError = class extends Error {
487
572
  super("New-session draft access changed");
488
573
  }
489
574
  };
575
+ function storedOptions(options, toolsProvided) {
576
+ return { ...options, toolsProvided };
577
+ }
578
+ function newSessionDraftToolsProvided(row) {
579
+ const options = row.sessionOptions;
580
+ return options.toolsProvided === true || !Object.hasOwn(options, "toolsProvided");
581
+ }
582
+ function publicNewSessionDraftOptions(row) {
583
+ const options = { ...row.sessionOptions };
584
+ delete options.toolsProvided;
585
+ return options;
586
+ }
490
587
  async function getNewSessionDraftInTransaction(db, input) {
491
588
  const query = db.select().from(newSessionDrafts).where(
492
589
  and2(
@@ -523,7 +620,9 @@ async function saveNewSessionDraftInTransaction(db, input) {
523
620
  tools: input.tools,
524
621
  model: input.model,
525
622
  reasoningEffort: input.reasoningEffort,
526
- sessionOptions: input.options,
623
+ // Keep the explicit/omitted policy in the existing JSONB extension point;
624
+ // adding a column here would turn a client preference into a migration.
625
+ sessionOptions: storedOptions(input.options, input.toolsProvided),
527
626
  updatedAt: /* @__PURE__ */ new Date()
528
627
  };
529
628
  if (current) {
@@ -538,17 +637,69 @@ async function saveNewSessionDraftInTransaction(db, input) {
538
637
  const raced = await getNewSessionDraftInTransaction(db, { ...input, lock: true });
539
638
  throw new NewSessionDraftConflictError(raced?.revision ?? 0);
540
639
  }
541
- async function consumeNewSessionDraftInTransaction(db, input) {
640
+ function safeRepositoryResource(resource) {
641
+ return {
642
+ kind: "repository",
643
+ uri: resource.uri,
644
+ ref: resource.ref,
645
+ ...resource.mountPath ? { mountPath: resource.mountPath } : {},
646
+ ...resource.subpath ? { subpath: resource.subpath } : {},
647
+ ...resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {},
648
+ ...resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}
649
+ };
650
+ }
651
+ function safeWorkingDir(value, targetSandboxId) {
652
+ if (!targetSandboxId || typeof value !== "string") return void 0;
653
+ const trimmed = value.trim();
654
+ if (!trimmed || trimmed === "." || trimmed === ".." || trimmed.includes("\0") || trimmed.startsWith("/") || trimmed.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.split(/[\\/]+/).some((part) => part === "..")) {
655
+ return void 0;
656
+ }
657
+ return trimmed;
658
+ }
659
+ async function seedNewSessionDraftInTransaction(db, input) {
542
660
  if (input.expectedRevision === 0) return false;
543
- const deleted = await db.delete(newSessionDrafts).where(
661
+ const [current] = await db.select().from(newSessionDrafts).where(
544
662
  and2(
545
663
  eq2(newSessionDrafts.workspaceId, input.workspaceId),
546
- eq2(newSessionDrafts.subjectId, input.subjectId),
664
+ eq2(newSessionDrafts.subjectId, input.subjectId)
665
+ )
666
+ ).for("update").limit(1);
667
+ if (!current || current.revision !== input.expectedRevision) return false;
668
+ const options = current.sessionOptions;
669
+ const targetSandboxId = typeof options.targetSandboxId === "string" ? options.targetSandboxId : void 0;
670
+ const safeOptions = {
671
+ ...options.sandboxBackend ? { sandboxBackend: options.sandboxBackend } : {},
672
+ ...targetSandboxId ? { targetSandboxId } : {},
673
+ ...safeWorkingDir(options.workingDir, targetSandboxId) ? { workingDir: safeWorkingDir(options.workingDir, targetSandboxId) } : {},
674
+ ...options.variableSetId ? { variableSetId: options.variableSetId } : {},
675
+ ...options.rigId ? { rigId: options.rigId } : {}
676
+ };
677
+ const resources = (Array.isArray(current.resources) ? current.resources : []).flatMap((raw) => {
678
+ if (!raw || typeof raw !== "object" || raw.kind !== "repository") {
679
+ return [];
680
+ }
681
+ return [safeRepositoryResource(raw)];
682
+ });
683
+ const [seeded] = await db.update(newSessionDrafts).set({
684
+ revision: current.revision + 1,
685
+ text: "",
686
+ resources,
687
+ // The explicit array is retained only when the caller explicitly pinned
688
+ // tools. Omitted workspace-default policy is represented by [] + false.
689
+ tools: newSessionDraftToolsProvided(current) ? current.tools : [],
690
+ model: current.model,
691
+ reasoningEffort: current.reasoningEffort,
692
+ sessionOptions: storedOptions(safeOptions, newSessionDraftToolsProvided(current)),
693
+ updatedAt: /* @__PURE__ */ new Date()
694
+ }).where(
695
+ and2(
696
+ eq2(newSessionDrafts.id, current.id),
547
697
  eq2(newSessionDrafts.revision, input.expectedRevision)
548
698
  )
549
699
  ).returning({ id: newSessionDrafts.id });
550
- return deleted.length > 0;
700
+ return Boolean(seeded);
551
701
  }
702
+ var consumeNewSessionDraftInTransaction = seedNewSessionDraftInTransaction;
552
703
 
553
704
  // src/persistence-errors.ts
554
705
  var SQLSTATE_KEYS = ["sqlState", "sqlstate", "code"];
@@ -5020,6 +5171,14 @@ async function cancelResponseBody(response) {
5020
5171
  }
5021
5172
 
5022
5173
  // src/index.ts
5174
+ var SessionToolPolicyVersionConflictError = class extends Error {
5175
+ constructor(currentVersion) {
5176
+ super("The session tool policy changed in another client");
5177
+ this.currentVersion = currentVersion;
5178
+ this.name = "SessionToolPolicyVersionConflictError";
5179
+ }
5180
+ code = "SESSION_TOOL_POLICY_CONFLICT";
5181
+ };
5023
5182
  var SessionSpawnDeniedDbError = class extends Error {
5024
5183
  constructor(denial) {
5025
5184
  super(denial.code);
@@ -5367,14 +5526,23 @@ async function withRlsContext(db, context, fn, transactionConfig) {
5367
5526
  const scoped = tx;
5368
5527
  await setRlsContext(scoped, context);
5369
5528
  const applied = await tx.execute(
5370
- sql4`select current_setting('opengeni.account_id', true) as account_id`
5529
+ sql4`select
5530
+ current_setting('opengeni.account_id', true) as account_id,
5531
+ current_setting('opengeni.workspace_id', true) as workspace_id`
5371
5532
  );
5372
5533
  const appliedAccountId = applied[0]?.account_id ?? "";
5534
+ const expectedWorkspaceId = context.workspaceId ?? "";
5535
+ const appliedWorkspaceId = applied[0]?.workspace_id ?? "";
5373
5536
  if (appliedAccountId !== context.accountId) {
5374
5537
  throw new Error(
5375
5538
  `RLS context not applied on the active backend: expected account ${context.accountId}, got "${appliedAccountId}"`
5376
5539
  );
5377
5540
  }
5541
+ if (appliedWorkspaceId !== expectedWorkspaceId) {
5542
+ throw new Error(
5543
+ `RLS context not applied on the active backend: expected workspace "${expectedWorkspaceId}", got "${appliedWorkspaceId}"`
5544
+ );
5545
+ }
5378
5546
  return await fn(scoped);
5379
5547
  }, transactionConfig);
5380
5548
  }
@@ -6000,6 +6168,21 @@ async function findActiveApiKeyByHash(db, keyHash) {
6000
6168
  return mapApiKey({ ...row, lastUsedAt: now });
6001
6169
  });
6002
6170
  }
6171
+ var GitHubInstallationAuthorityCommitError = class extends Error {
6172
+ constructor() {
6173
+ super("GitHub installation authority expired before the binding transaction completed");
6174
+ }
6175
+ };
6176
+ var githubInstallationAuthorityMaxAgeMs = 10 * 6e4;
6177
+ function hasAuditableGitHubInstallationAuthority(installation) {
6178
+ const checkedAt = installation.authorityCheckedAt ? Date.parse(installation.authorityCheckedAt) : Number.NaN;
6179
+ const expiresAt = installation.authorityExpiresAt ? Date.parse(installation.authorityExpiresAt) : Number.NaN;
6180
+ const common = installation.repositoryScope === "selected" && installation.repositoryIds.length > 0 && installation.repositoryIds.every((id) => Number.isSafeInteger(id) && id > 0) && installation.githubAccountId !== null && Number.isSafeInteger(installation.githubAccountId) && installation.githubAccountId > 0 && Boolean(installation.accountLogin) && installation.githubActorId !== null && Number.isSafeInteger(installation.githubActorId) && installation.githubActorId > 0 && Boolean(installation.githubActorLogin) && Boolean(installation.linkedBySubjectId) && Boolean(installation.authorityNonce) && Number.isFinite(checkedAt) && Number.isFinite(expiresAt) && checkedAt < expiresAt;
6181
+ if (!common) {
6182
+ return false;
6183
+ }
6184
+ return installation.authorityKind === "personal_owner" ? installation.accountType === "User" && installation.githubActorId === installation.githubAccountId : installation.authorityKind === "organization_owner" && installation.accountType === "Organization";
6185
+ }
6003
6186
  async function upsertGitHubInstallation(db, input) {
6004
6187
  return await withRlsContext(
6005
6188
  db,
@@ -6023,7 +6206,10 @@ async function upsertGitHubInstallation(db, input) {
6023
6206
  accountType: input.accountType ?? null,
6024
6207
  ...input.linkedBySubjectId !== void 0 ? { linkedBySubjectId: input.linkedBySubjectId } : {},
6025
6208
  updatedAt: /* @__PURE__ */ new Date()
6026
- }
6209
+ },
6210
+ // This legacy metadata helper cannot mutate an owner-authorized row.
6211
+ // New bindings use bindAuthorizedGitHubInstallationRepositories.
6212
+ setWhere: isNull(githubInstallations.authorityNonce)
6027
6213
  }).returning();
6028
6214
  if (!row) {
6029
6215
  throw new Error("Failed to upsert GitHub installation");
@@ -6064,6 +6250,114 @@ async function bindGitHubInstallationRepositories(db, input) {
6064
6250
  repositoryScope: "selected",
6065
6251
  linkedBySubjectId: input.linkedBySubjectId,
6066
6252
  updatedAt: /* @__PURE__ */ new Date()
6253
+ },
6254
+ // Preserve the immutable authority + allowlist audit boundary.
6255
+ setWhere: isNull(githubInstallations.authorityNonce)
6256
+ }).returning();
6257
+ if (!row) {
6258
+ throw new Error("Failed to bind GitHub installation");
6259
+ }
6260
+ await tx.delete(githubInstallationRepositories).where(
6261
+ and6(
6262
+ eq6(githubInstallationRepositories.workspaceId, input.workspaceId),
6263
+ eq6(githubInstallationRepositories.installationId, input.installationId)
6264
+ )
6265
+ );
6266
+ for (let offset = 0; offset < repositoryIds.length; offset += 1e3) {
6267
+ await tx.insert(githubInstallationRepositories).values(
6268
+ repositoryIds.slice(offset, offset + 1e3).map((repositoryId) => ({
6269
+ accountId: input.accountId,
6270
+ workspaceId: input.workspaceId,
6271
+ installationId: input.installationId,
6272
+ repositoryId
6273
+ }))
6274
+ );
6275
+ }
6276
+ return { ...mapGitHubInstallation(row), repositoryIds };
6277
+ })
6278
+ );
6279
+ }
6280
+ async function bindAuthorizedGitHubInstallationRepositories(db, input) {
6281
+ if (!Number.isSafeInteger(input.installationId) || input.installationId <= 0) {
6282
+ throw new Error("GitHub installation id must be a positive safe integer");
6283
+ }
6284
+ if (!Number.isSafeInteger(input.githubAccountId) || input.githubAccountId <= 0) {
6285
+ throw new Error("GitHub account id must be a positive safe integer");
6286
+ }
6287
+ if (!Number.isSafeInteger(input.githubActorId) || input.githubActorId <= 0) {
6288
+ throw new Error("GitHub actor id must be a positive safe integer");
6289
+ }
6290
+ const authorityCheckedAtMs = input.authorityCheckedAt.getTime();
6291
+ const authorityExpiresAtMs = input.authorityExpiresAt.getTime();
6292
+ if (!Number.isFinite(authorityCheckedAtMs) || !Number.isFinite(authorityExpiresAtMs) || authorityCheckedAtMs >= authorityExpiresAtMs || authorityExpiresAtMs - authorityCheckedAtMs > githubInstallationAuthorityMaxAgeMs || !input.authorityNonce || !input.githubActorLogin.trim() || !input.accountLogin?.trim() || !input.linkedBySubjectId.trim() || input.authorityKind !== "personal_owner" && input.authorityKind !== "organization_owner" || input.authorityKind === "personal_owner" && (input.accountType !== "User" || input.githubActorId !== input.githubAccountId) || input.authorityKind === "organization_owner" && input.accountType !== "Organization") {
6293
+ throw new Error("GitHub installation authority proof is invalid or expired");
6294
+ }
6295
+ const repositoryIds = [...new Set(input.repositoryIds)];
6296
+ if (repositoryIds.length === 0 || repositoryIds.length !== input.repositoryIds.length || repositoryIds.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
6297
+ throw new Error(
6298
+ "GitHub repository ids must be a nonempty, unique list of positive safe integers"
6299
+ );
6300
+ }
6301
+ return await withRlsContext(
6302
+ db,
6303
+ { accountId: input.accountId, workspaceId: input.workspaceId },
6304
+ async (scopedDb) => await scopedDb.transaction(async (tx) => {
6305
+ await assertGitHubAuthorityWindowOpen(
6306
+ tx,
6307
+ input.authorityCheckedAt,
6308
+ input.authorityExpiresAt
6309
+ );
6310
+ await tx.delete(integrationOauthStateNonces).where(
6311
+ and6(
6312
+ eq6(integrationOauthStateNonces.workspaceId, input.workspaceId),
6313
+ lt(integrationOauthStateNonces.expiresAt, input.authorityCheckedAt)
6314
+ )
6315
+ );
6316
+ const consumed = await tx.insert(integrationOauthStateNonces).values({
6317
+ accountId: input.accountId,
6318
+ workspaceId: input.workspaceId,
6319
+ subjectId: input.linkedBySubjectId,
6320
+ nonce: input.authorityNonce,
6321
+ expiresAt: input.authorityExpiresAt,
6322
+ usedAt: input.authorityCheckedAt
6323
+ }).onConflictDoNothing({ target: integrationOauthStateNonces.nonce }).returning({ nonce: integrationOauthStateNonces.nonce });
6324
+ if (consumed.length === 0) {
6325
+ return null;
6326
+ }
6327
+ const [row] = await tx.insert(githubInstallations).values({
6328
+ accountId: input.accountId,
6329
+ workspaceId: input.workspaceId,
6330
+ installationId: input.installationId,
6331
+ githubAccountId: input.githubAccountId,
6332
+ accountLogin: input.accountLogin,
6333
+ accountType: input.accountType,
6334
+ repositoryScope: "selected",
6335
+ linkedBySubjectId: input.linkedBySubjectId,
6336
+ githubActorId: input.githubActorId,
6337
+ githubActorLogin: input.githubActorLogin,
6338
+ authorityKind: input.authorityKind,
6339
+ authorityCheckedAt: input.authorityCheckedAt,
6340
+ authorityExpiresAt: input.authorityExpiresAt,
6341
+ authorityNonce: input.authorityNonce
6342
+ }).onConflictDoUpdate({
6343
+ target: [
6344
+ githubInstallations.workspaceId,
6345
+ githubInstallations.installationId
6346
+ ],
6347
+ set: {
6348
+ accountId: input.accountId,
6349
+ githubAccountId: input.githubAccountId,
6350
+ accountLogin: input.accountLogin,
6351
+ accountType: input.accountType,
6352
+ repositoryScope: "selected",
6353
+ linkedBySubjectId: input.linkedBySubjectId,
6354
+ githubActorId: input.githubActorId,
6355
+ githubActorLogin: input.githubActorLogin,
6356
+ authorityKind: input.authorityKind,
6357
+ authorityCheckedAt: input.authorityCheckedAt,
6358
+ authorityExpiresAt: input.authorityExpiresAt,
6359
+ authorityNonce: input.authorityNonce,
6360
+ updatedAt: input.authorityCheckedAt
6067
6361
  }
6068
6362
  }).returning();
6069
6363
  if (!row) {
@@ -6085,10 +6379,29 @@ async function bindGitHubInstallationRepositories(db, input) {
6085
6379
  }))
6086
6380
  );
6087
6381
  }
6382
+ await assertGitHubAuthorityWindowOpen(
6383
+ tx,
6384
+ input.authorityCheckedAt,
6385
+ input.authorityExpiresAt
6386
+ );
6088
6387
  return { ...mapGitHubInstallation(row), repositoryIds };
6089
6388
  })
6090
6389
  );
6091
6390
  }
6391
+ async function assertGitHubAuthorityWindowOpen(tx, checkedAt, expiresAt) {
6392
+ const checkedAtIso = checkedAt.toISOString();
6393
+ const expiresAtIso = expiresAt.toISOString();
6394
+ const result = await tx.execute(sql4`
6395
+ select (
6396
+ ${checkedAtIso}::timestamptz <= clock_timestamp()
6397
+ and clock_timestamp() < ${expiresAtIso}::timestamptz
6398
+ and ${expiresAtIso}::timestamptz <= ${checkedAtIso}::timestamptz + interval '10 minutes'
6399
+ ) as valid
6400
+ `);
6401
+ if (result[0]?.valid !== true) {
6402
+ throw new GitHubInstallationAuthorityCommitError();
6403
+ }
6404
+ }
6092
6405
  async function listGitHubInstallationsForWorkspace(db, workspaceId) {
6093
6406
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
6094
6407
  const rows = await scopedDb.select().from(githubInstallations).where(eq6(githubInstallations.workspaceId, workspaceId)).orderBy(desc(githubInstallations.updatedAt));
@@ -6128,7 +6441,7 @@ async function areGitHubRepositoriesAllowedForWorkspace(db, workspaceId, install
6128
6441
  return false;
6129
6442
  }
6130
6443
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
6131
- const [installation] = await scopedDb.select({ repositoryScope: githubInstallations.repositoryScope }).from(githubInstallations).where(
6444
+ const [installation] = await scopedDb.select().from(githubInstallations).where(
6132
6445
  and6(
6133
6446
  eq6(githubInstallations.workspaceId, workspaceId),
6134
6447
  eq6(githubInstallations.installationId, installationId)
@@ -6137,9 +6450,6 @@ async function areGitHubRepositoriesAllowedForWorkspace(db, workspaceId, install
6137
6450
  if (!installation) {
6138
6451
  return false;
6139
6452
  }
6140
- if (installation.repositoryScope === "all") {
6141
- return true;
6142
- }
6143
6453
  const allowed = await scopedDb.select({
6144
6454
  repositoryId: githubInstallationRepositories.repositoryId
6145
6455
  }).from(githubInstallationRepositories).where(
@@ -6149,7 +6459,13 @@ async function areGitHubRepositoriesAllowedForWorkspace(db, workspaceId, install
6149
6459
  inArray3(githubInstallationRepositories.repositoryId, requestedIds)
6150
6460
  )
6151
6461
  );
6152
- return allowed.length === requestedIds.length;
6462
+ if (allowed.length !== requestedIds.length) {
6463
+ return false;
6464
+ }
6465
+ return hasAuditableGitHubInstallationAuthority({
6466
+ ...mapGitHubInstallation(installation),
6467
+ repositoryIds: allowed.map((row) => row.repositoryId)
6468
+ });
6153
6469
  });
6154
6470
  }
6155
6471
  async function deleteGitHubInstallationBinding(db, input) {
@@ -10365,6 +10681,7 @@ function mapCodexCapacityWaiter(row) {
10365
10681
  sessionId: row.sessionId,
10366
10682
  goalId: row.goalId,
10367
10683
  blockedTurnId: row.blockedTurnId,
10684
+ blockedTurnGeneration: row.blockedTurnGeneration,
10368
10685
  workflowId: row.workflowId,
10369
10686
  generation: row.generation,
10370
10687
  status: row.status,
@@ -10411,6 +10728,11 @@ function nextCodexCapacityCheckAt(earliestResetAt, resetKind, refreshAttempt, no
10411
10728
  }
10412
10729
  async function armCodexCapacityWait(db, input) {
10413
10730
  const now = input.now ?? /* @__PURE__ */ new Date();
10731
+ const goalId = input.goalId ?? null;
10732
+ const goalVersion = input.goalVersion ?? null;
10733
+ if (goalId === null !== (goalVersion === null) || goalVersion !== null && (!Number.isSafeInteger(goalVersion) || goalVersion < 1)) {
10734
+ throw new Error("Codex capacity goal fence must be absent or contain a positive version");
10735
+ }
10414
10736
  return await withRlsContext(
10415
10737
  db,
10416
10738
  { accountId: input.accountId, workspaceId: input.workspaceId },
@@ -10433,13 +10755,13 @@ async function armCodexCapacityWait(db, input) {
10433
10755
  const effectiveControl = session ? await evaluateSessionControl(tx, input.workspaceId, input.sessionId, {
10434
10756
  workspaceControl: locks.control ?? void 0
10435
10757
  }) : null;
10436
- const [goal] = await tx.select().from(sessionGoals).where(
10758
+ const [goal] = goalId ? await tx.select().from(sessionGoals).where(
10437
10759
  and6(
10438
10760
  eq6(sessionGoals.workspaceId, input.workspaceId),
10439
- eq6(sessionGoals.id, input.goalId),
10761
+ eq6(sessionGoals.id, goalId),
10440
10762
  eq6(sessionGoals.sessionId, input.sessionId)
10441
10763
  )
10442
- ).for("update").limit(1);
10764
+ ).for("update").limit(1) : [];
10443
10765
  const leaseRows = input.leaseFence ? await tx.execute(sql4`
10444
10766
  select holder_id, generation
10445
10767
  from codex_credential_leases
@@ -10463,7 +10785,7 @@ async function armCodexCapacityWait(db, input) {
10463
10785
  events: []
10464
10786
  };
10465
10787
  }
10466
- if (existing?.status === "waiting" && existing.blockedTurnId === input.turnId && turn?.status === "failed") {
10788
+ if (existing?.status === "waiting" && existing.blockedTurnId === input.turnId && existing.blockedTurnGeneration === turn?.executionGeneration && turn?.status === "waiting_capacity" && session?.status === "waiting_capacity" && session.activeTurnId === input.turnId) {
10467
10789
  return {
10468
10790
  action: "waiting",
10469
10791
  waiter: mapCodexCapacityWaiter(existing),
@@ -10474,7 +10796,7 @@ async function armCodexCapacityWait(db, input) {
10474
10796
  const currentRedispatches = Number(turn?.metadata?.workerDeathRedispatches ?? 0);
10475
10797
  const lease = leaseRows[0];
10476
10798
  const leaseFenceValid = !input.leaseFence || lease?.holder_id === input.leaseFence.holderId && Number(lease.generation) === input.leaseFence.generation && currentRedispatches === (input.expectedRedispatches ?? currentRedispatches);
10477
- if (!goal || effectiveControl?.state !== "active" || effectiveControl.settlement !== null || session.activeTurnId !== input.turnId || session.status !== "running" || goal.status !== "active" || goal.version !== input.goalVersion || turn.status !== "running" || turn.activeAttemptId !== input.attemptId || !leaseFenceValid || codexCapacityPolicyHashFromTurnMetadata(turn.metadata) !== policyHash) {
10799
+ if (!session || !turn || effectiveControl?.state !== "active" || effectiveControl.settlement !== null || session.activeTurnId !== input.turnId || session.status !== "running" || goalId !== null && (!goal || goal.status !== "active" || goal.version !== goalVersion) || turn.status !== "running" || turn.activeAttemptId !== input.attemptId || !leaseFenceValid || codexCapacityPolicyHashFromTurnMetadata(turn.metadata) !== policyHash) {
10478
10800
  return {
10479
10801
  action: "stale",
10480
10802
  waiter: existing ? mapCodexCapacityWaiter(existing) : null,
@@ -10488,7 +10810,7 @@ async function armCodexCapacityWait(db, input) {
10488
10810
  sessionId: input.sessionId,
10489
10811
  turnId: input.turnId,
10490
10812
  executionGeneration: turn.executionGeneration,
10491
- outcome: "failed",
10813
+ outcome: "waiting_capacity",
10492
10814
  closedAt: now
10493
10815
  });
10494
10816
  const generation = (existing?.generation ?? 0) + 1;
@@ -10503,12 +10825,13 @@ async function armCodexCapacityWait(db, input) {
10503
10825
  accountId: input.accountId,
10504
10826
  workspaceId: input.workspaceId,
10505
10827
  sessionId: input.sessionId,
10506
- goalId: input.goalId,
10828
+ goalId,
10507
10829
  blockedTurnId: input.turnId,
10830
+ blockedTurnGeneration: turn.executionGeneration,
10508
10831
  workflowId: input.workflowId,
10509
10832
  generation,
10510
10833
  status: "waiting",
10511
- goalVersion: input.goalVersion,
10834
+ goalVersion,
10512
10835
  policyHash,
10513
10836
  earliestResetAt: input.earliestResetAt,
10514
10837
  nextCheckAt,
@@ -10544,32 +10867,17 @@ async function armCodexCapacityWait(db, input) {
10544
10867
  workspaceId: input.workspaceId,
10545
10868
  sessionId: input.sessionId,
10546
10869
  sequence: ++sequence,
10547
- type: "turn.failed",
10870
+ type: "codex.capacity.waiting",
10548
10871
  payload: sanitizeEventPayload({
10549
10872
  ...input.failurePayload,
10550
10873
  recovery: "codex_capacity",
10551
- retryable: false,
10874
+ retryable: true,
10552
10875
  rotated: true,
10553
- capacityWaiterId: waiterRow.id,
10554
- capacityWaitGeneration: waiterRow.generation
10555
- }),
10556
- turnId: input.turnId,
10557
- turnGeneration: turn.executionGeneration,
10558
- turnAttemptId: input.attemptId,
10559
- turnAssociation: "current",
10560
- occurredAt: now
10561
- },
10562
- {
10563
- accountId: input.accountId,
10564
- workspaceId: input.workspaceId,
10565
- sessionId: input.sessionId,
10566
- sequence: ++sequence,
10567
- type: "codex.capacity.waiting",
10568
- payload: sanitizeEventPayload({
10569
10876
  waiterId: waiterRow.id,
10570
10877
  generation: waiterRow.generation,
10571
- goalId: input.goalId,
10572
- goalVersion: input.goalVersion,
10878
+ goalId,
10879
+ goalVersion,
10880
+ blockedTurnGeneration: turn.executionGeneration,
10573
10881
  policyHash,
10574
10882
  resetKind: input.resetKind,
10575
10883
  earliestResetAt: input.earliestResetAt?.toISOString() ?? null,
@@ -10587,7 +10895,7 @@ async function armCodexCapacityWait(db, input) {
10587
10895
  sessionId: input.sessionId,
10588
10896
  sequence: ++sequence,
10589
10897
  type: "session.status.changed",
10590
- payload: { status: "idle", reason: "codex_capacity" },
10898
+ payload: { status: "waiting_capacity", reason: "codex_capacity" },
10591
10899
  turnId: input.turnId,
10592
10900
  turnGeneration: turn.executionGeneration,
10593
10901
  turnAttemptId: input.attemptId,
@@ -10595,37 +10903,50 @@ async function armCodexCapacityWait(db, input) {
10595
10903
  occurredAt: now
10596
10904
  }
10597
10905
  ]).returning();
10598
- await tx.update(sessionTurns).set({
10599
- status: "failed",
10906
+ const [waitingTurn] = await tx.update(sessionTurns).set({
10907
+ status: "waiting_capacity",
10600
10908
  activeAttemptId: null,
10909
+ metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
10601
10910
  version: turn.version + 1,
10602
- finishedAt: now,
10911
+ finishedAt: null,
10603
10912
  updatedAt: now
10604
10913
  }).where(
10605
10914
  and6(
10606
10915
  eq6(sessionTurns.workspaceId, input.workspaceId),
10607
10916
  eq6(sessionTurns.id, input.turnId),
10608
- eq6(sessionTurns.status, "running")
10917
+ eq6(sessionTurns.status, "running"),
10918
+ eq6(sessionTurns.activeAttemptId, input.attemptId)
10609
10919
  )
10610
- );
10611
- await tx.update(sessions).set({
10612
- status: "idle",
10613
- activeTurnId: null,
10920
+ ).returning({ id: sessionTurns.id });
10921
+ if (!waitingTurn) {
10922
+ throw new Error("Codex capacity blocked turn changed during atomic arm");
10923
+ }
10924
+ const [waitingSession] = await tx.update(sessions).set({
10925
+ status: "waiting_capacity",
10926
+ activeTurnId: input.turnId,
10614
10927
  lastSequence: sequence,
10615
10928
  updatedAt: now
10616
10929
  }).where(
10617
10930
  and6(
10618
10931
  eq6(sessions.workspaceId, input.workspaceId),
10619
10932
  eq6(sessions.id, input.sessionId),
10933
+ eq6(sessions.status, "running"),
10620
10934
  eq6(sessions.activeTurnId, input.turnId)
10621
10935
  )
10622
- );
10623
- await tx.execute(sql4`
10624
- delete from codex_credential_leases
10625
- where account_id = ${input.accountId}
10626
- and workspace_id = ${input.workspaceId}
10627
- and turn_id = ${input.turnId}
10628
- `);
10936
+ ).returning({ id: sessions.id });
10937
+ if (!waitingSession) {
10938
+ throw new Error("Codex capacity session changed during atomic arm");
10939
+ }
10940
+ if (input.leaseFence) {
10941
+ await tx.execute(sql4`
10942
+ delete from codex_credential_leases
10943
+ where account_id = ${input.accountId}
10944
+ and workspace_id = ${input.workspaceId}
10945
+ and turn_id = ${input.turnId}
10946
+ and holder_id = ${input.leaseFence.holderId}
10947
+ and generation = ${input.leaseFence.generation}
10948
+ `);
10949
+ }
10629
10950
  return {
10630
10951
  action: "waiting",
10631
10952
  waiter: mapCodexCapacityWaiter(waiterRow),
@@ -10742,26 +11063,87 @@ async function supersedeCodexCapacityWaitInTransaction(tx, input) {
10742
11063
  if (!updated) {
10743
11064
  return { waiter: mapCodexCapacityWaiter(input.waiter), events: [] };
10744
11065
  }
10745
- const inserted = await tx.insert(sessionEvents).values({
10746
- accountId: input.session.accountId,
10747
- workspaceId: input.session.workspaceId,
10748
- sessionId: input.session.id,
10749
- sequence: input.session.lastSequence + 1,
10750
- type: "codex.capacity.superseded",
10751
- payload: sanitizeEventPayload({
10752
- waiterId: updated.id,
10753
- generation: updated.generation,
10754
- reason: input.reason
10755
- }),
10756
- turnId: updated.blockedTurnId,
10757
- occurredAt: input.now
10758
- }).returning();
10759
- await tx.update(sessions).set({ lastSequence: input.session.lastSequence + 1, updatedAt: input.now }).where(
11066
+ const turnWasCurrent = input.session.activeTurnId === input.blockedTurn.id;
11067
+ const turnStillWaiting = input.blockedTurn.status === "waiting_capacity";
11068
+ const terminalTurnStatus = input.session.status === "cancelled" ? "cancelled" : "superseded";
11069
+ if (turnStillWaiting) {
11070
+ const [supersededTurn] = await tx.update(sessionTurns).set({
11071
+ status: terminalTurnStatus,
11072
+ activeAttemptId: null,
11073
+ cancelledBy: "codex_capacity_reconcile",
11074
+ cancelReason: input.reason,
11075
+ version: input.blockedTurn.version + 1,
11076
+ finishedAt: input.now,
11077
+ updatedAt: input.now
11078
+ }).where(
11079
+ and6(
11080
+ eq6(sessionTurns.workspaceId, input.session.workspaceId),
11081
+ eq6(sessionTurns.id, input.blockedTurn.id),
11082
+ eq6(sessionTurns.status, "waiting_capacity"),
11083
+ isNull(sessionTurns.activeAttemptId),
11084
+ eq6(sessionTurns.executionGeneration, input.waiter.blockedTurnGeneration)
11085
+ )
11086
+ ).returning({ id: sessionTurns.id });
11087
+ if (!supersededTurn) {
11088
+ throw new Error("Codex capacity blocked turn changed during atomic supersession");
11089
+ }
11090
+ }
11091
+ const [queued] = turnWasCurrent ? await tx.select({ id: sessionTurns.id }).from(sessionTurns).where(
11092
+ and6(
11093
+ eq6(sessionTurns.workspaceId, input.session.workspaceId),
11094
+ eq6(sessionTurns.sessionId, input.session.id),
11095
+ eq6(sessionTurns.status, "queued")
11096
+ )
11097
+ ).limit(1) : [];
11098
+ const nextSessionStatus = input.session.status === "cancelled" ? "cancelled" : queued ? "queued" : "idle";
11099
+ const eventValues = [
11100
+ {
11101
+ accountId: input.session.accountId,
11102
+ workspaceId: input.session.workspaceId,
11103
+ sessionId: input.session.id,
11104
+ sequence: input.session.lastSequence + 1,
11105
+ type: "codex.capacity.superseded",
11106
+ payload: sanitizeEventPayload({
11107
+ waiterId: updated.id,
11108
+ generation: updated.generation,
11109
+ reason: input.reason
11110
+ }),
11111
+ turnId: updated.blockedTurnId,
11112
+ turnGeneration: input.blockedTurn.executionGeneration,
11113
+ ...turnWasCurrent ? { turnAssociation: "current" } : {},
11114
+ occurredAt: input.now
11115
+ }
11116
+ ];
11117
+ if (turnWasCurrent && input.session.status !== nextSessionStatus) {
11118
+ eventValues.push({
11119
+ accountId: input.session.accountId,
11120
+ workspaceId: input.session.workspaceId,
11121
+ sessionId: input.session.id,
11122
+ sequence: input.session.lastSequence + 2,
11123
+ type: "session.status.changed",
11124
+ payload: { status: nextSessionStatus, reason: input.reason },
11125
+ turnId: updated.blockedTurnId,
11126
+ turnGeneration: input.blockedTurn.executionGeneration,
11127
+ turnAssociation: "current",
11128
+ occurredAt: input.now
11129
+ });
11130
+ }
11131
+ const inserted = await tx.insert(sessionEvents).values(eventValues).returning();
11132
+ const lastSequence = input.session.lastSequence + inserted.length;
11133
+ const [updatedSession] = await tx.update(sessions).set({
11134
+ ...turnWasCurrent ? { status: nextSessionStatus, activeTurnId: null } : {},
11135
+ lastSequence,
11136
+ updatedAt: input.now
11137
+ }).where(
10760
11138
  and6(
10761
11139
  eq6(sessions.workspaceId, input.session.workspaceId),
10762
- eq6(sessions.id, input.session.id)
11140
+ eq6(sessions.id, input.session.id),
11141
+ ...turnWasCurrent ? [eq6(sessions.activeTurnId, input.blockedTurn.id)] : []
10763
11142
  )
10764
- );
11143
+ ).returning({ id: sessions.id });
11144
+ if (!updatedSession) {
11145
+ throw new Error("Codex capacity session changed during atomic supersession");
11146
+ }
10765
11147
  return {
10766
11148
  waiter: mapCodexCapacityWaiter(updated),
10767
11149
  events: inserted.map(mapEvent2)
@@ -10808,61 +11190,47 @@ async function reconcileCodexCapacityWait(db, input, decide, policy) {
10808
11190
  const effectiveControl = session ? await evaluateSessionControl(tx, input.workspaceId, input.sessionId, {
10809
11191
  workspaceControl: prefix.control ?? void 0
10810
11192
  }) : null;
10811
- const [goal] = await tx.select().from(sessionGoals).where(
11193
+ const [goal] = waiterRead.goalId ? await tx.select().from(sessionGoals).where(
10812
11194
  and6(
10813
11195
  eq6(sessionGoals.workspaceId, input.workspaceId),
10814
11196
  eq6(sessionGoals.id, waiterRead.goalId),
10815
11197
  eq6(sessionGoals.sessionId, input.sessionId)
10816
11198
  )
10817
- ).for("update").limit(1);
11199
+ ).for("update").limit(1) : [];
10818
11200
  const [waiter] = await tx.select().from(codexCapacityWaiters).where(eq6(codexCapacityWaiters.id, input.waiterId)).for("update").limit(1);
10819
- if (!session || !goal || !blockedTurn || !waiter || session.accountId !== input.accountId || blockedTurn.accountId !== input.accountId || blockedTurn.sessionId !== input.sessionId || waiter.accountId !== input.accountId || waiter.workspaceId !== input.workspaceId || waiter.sessionId !== input.sessionId || waiter.blockedTurnId !== blockedTurn.id || waiter.generation !== input.generation || waiter.status !== "waiting") {
11201
+ if (!session || !blockedTurn || !waiter || session.accountId !== input.accountId || blockedTurn.accountId !== input.accountId || blockedTurn.sessionId !== input.sessionId || waiter.accountId !== input.accountId || waiter.workspaceId !== input.workspaceId || waiter.sessionId !== input.sessionId || waiter.blockedTurnId !== blockedTurn.id || waiter.generation !== input.generation || waiter.status !== "waiting") {
10820
11202
  return {
10821
11203
  action: "stale",
10822
11204
  waiter: waiter ? mapCodexCapacityWaiter(waiter) : null,
10823
11205
  events: []
10824
11206
  };
10825
11207
  }
10826
- const [pending] = await tx.select({ id: sessionTurns.id }).from(sessionTurns).where(
10827
- and6(
10828
- eq6(sessionTurns.workspaceId, input.workspaceId),
10829
- eq6(sessionTurns.sessionId, input.sessionId),
10830
- inArray3(sessionTurns.status, [
10831
- "queued",
10832
- "running",
10833
- "requires_action",
10834
- "recovering",
10835
- "waiting_capacity"
10836
- ])
10837
- )
10838
- ).limit(1);
10839
- const [laterTurn] = await tx.select({ id: sessionTurns.id }).from(sessionTurns).where(
10840
- and6(
10841
- eq6(sessionTurns.workspaceId, input.workspaceId),
10842
- eq6(sessionTurns.sessionId, input.sessionId),
10843
- gt(sessionTurns.position, blockedTurn.position)
10844
- )
10845
- ).limit(1);
11208
+ if (effectiveControl?.state !== "active" || effectiveControl.settlement !== null) {
11209
+ return {
11210
+ action: "paused",
11211
+ waiter: mapCodexCapacityWaiter(waiter),
11212
+ events: []
11213
+ };
11214
+ }
10846
11215
  const currentPolicyHash = codexCapacityPolicyHashFromTurnMetadata(blockedTurn.metadata);
10847
11216
  let supersedeReason = null;
10848
- if (effectiveControl?.state !== "active" || effectiveControl.settlement !== null) {
10849
- supersedeReason = "control_changed";
10850
- } else if (goal.status !== "active" || goal.version !== waiter.goalVersion) {
11217
+ if (session.status === "cancelled") {
11218
+ supersedeReason = "session_cancelled";
11219
+ } else if (waiter.goalId !== null && (!goal || goal.status !== "active" || goal.version !== waiter.goalVersion)) {
10851
11220
  supersedeReason = "goal_changed";
10852
11221
  } else if (currentPolicyHash !== waiter.policyHash) {
10853
11222
  supersedeReason = "credential_policy_changed";
10854
- } else if (session.status !== "idle" || session.activeTurnId !== null) {
10855
- supersedeReason = "session_not_capacity_idle";
10856
- } else if (blockedTurn.status !== "failed") {
11223
+ } else if (session.activeTurnId !== blockedTurn.id) {
11224
+ supersedeReason = "active_turn_changed";
11225
+ } else if (session.status !== "waiting_capacity") {
11226
+ supersedeReason = "session_not_waiting_capacity";
11227
+ } else if (blockedTurn.status !== "waiting_capacity" || blockedTurn.activeAttemptId !== null || blockedTurn.executionGeneration !== waiter.blockedTurnGeneration) {
10857
11228
  supersedeReason = "blocked_turn_changed";
10858
- } else if (pending) {
10859
- supersedeReason = "pending_work_exists";
10860
- } else if (laterTurn) {
10861
- supersedeReason = "newer_turn_exists";
10862
11229
  }
10863
11230
  if (supersedeReason) {
10864
11231
  const superseded = await supersedeCodexCapacityWaitInTransaction(tx, {
10865
11232
  session,
11233
+ blockedTurn,
10866
11234
  waiter,
10867
11235
  reason: supersedeReason,
10868
11236
  now
@@ -10927,77 +11295,26 @@ async function reconcileCodexCapacityWait(db, input, decide, policy) {
10927
11295
  events: []
10928
11296
  };
10929
11297
  }
10930
- const prompt = [
10931
- "[CODEX CAPACITY RESUME] Codex subscription capacity is available again.",
10932
- `Continue the existing active goal from durable conversation history: ${goal.text}`,
10933
- `Success criteria: ${goal.successCriteria ?? "none specified"}.`,
10934
- "Do not replay completed tool side effects; verify any ambiguous in-flight effect before repeating it.",
10935
- "If the goal is complete, call opengeni__goal_complete. If blocked for another reason, call opengeni__goal_pause."
10936
- ].join("\n");
10937
- const [update] = await tx.insert(sessionSystemUpdates).values({
10938
- accountId: input.accountId,
10939
- workspaceId: input.workspaceId,
10940
- sessionId: input.sessionId,
10941
- kind: "goal_continuation",
10942
- classification: "info",
10943
- sourceId: goal.id,
10944
- dedupeKey: `codex-capacity-resume:${waiter.id}:${waiter.generation}`,
10945
- summary: prompt,
10946
- payload: {
10947
- type: "goal_continuation",
10948
- goalId: goal.id,
10949
- goalVersion: goal.version,
10950
- prompt,
10951
- reason: "codex_capacity",
10952
- capacityWaiterId: waiter.id,
10953
- capacityWaitGeneration: waiter.generation,
10954
- policy: {
10955
- model: blockedTurn.model,
10956
- reasoningEffort: blockedTurn.reasoningEffort,
10957
- tools: blockedTurn.tools,
10958
- sandboxBackend: blockedTurn.sandboxBackend
10959
- }
10960
- },
10961
- lineage: {
10962
- goalId: goal.id,
10963
- blockedTurnId: blockedTurn.id,
10964
- capacityWaiterId: waiter.id
10965
- },
10966
- state: "pending"
10967
- }).returning();
10968
- if (!update) {
10969
- throw new Error("Codex capacity resume did not create an internal update");
10970
- }
10971
- await tx.insert(usageEvents).values({
10972
- accountId: input.accountId,
10973
- workspaceId: input.workspaceId,
10974
- eventType: "agent_run.created",
10975
- quantity: 1,
10976
- unit: "run",
10977
- sourceResourceType: "session_system_update",
10978
- sourceResourceId: update.id,
10979
- sessionId: input.sessionId,
10980
- initiatorKind: "service",
10981
- initiatorSubjectId: "goal-continuation",
10982
- initiatorContext: { goalId: goal.id, reason: "codex_capacity" },
10983
- origin: "goal",
10984
- idempotencyKey: `agent_run.created:codex-capacity:${input.workspaceId}:${update.id}`,
10985
- occurredAt: now
10986
- }).onConflictDoNothing({ target: usageEvents.idempotencyKey });
10987
11298
  const events = await tx.insert(sessionEvents).values([
10988
11299
  {
10989
11300
  accountId: input.accountId,
10990
11301
  workspaceId: input.workspaceId,
10991
11302
  sessionId: input.sessionId,
10992
11303
  sequence: session.lastSequence + 1,
10993
- type: "system.update.pending",
11304
+ type: "codex.capacity.resumed",
10994
11305
  payload: sanitizeEventPayload({
10995
- updateId: update.id,
10996
- kind: update.kind,
10997
- classification: update.classification,
10998
- sourceId: update.sourceId,
10999
- summary: update.summary
11306
+ waiterId: waiter.id,
11307
+ generation: waiter.generation,
11308
+ wakeRevision: waiter.wakeRevision,
11309
+ goalId: waiter.goalId,
11310
+ goalVersion: waiter.goalVersion,
11311
+ blockedTurnGeneration: waiter.blockedTurnGeneration,
11312
+ policyHash: waiter.policyHash,
11313
+ diagnostic: decision.diagnostic ?? null
11000
11314
  }),
11315
+ turnId: blockedTurn.id,
11316
+ turnGeneration: blockedTurn.executionGeneration,
11317
+ turnAssociation: "current",
11001
11318
  occurredAt: now
11002
11319
  },
11003
11320
  {
@@ -11005,24 +11322,17 @@ async function reconcileCodexCapacityWait(db, input, decide, policy) {
11005
11322
  workspaceId: input.workspaceId,
11006
11323
  sessionId: input.sessionId,
11007
11324
  sequence: session.lastSequence + 2,
11008
- type: "codex.capacity.resumed",
11009
- payload: sanitizeEventPayload({
11010
- waiterId: waiter.id,
11011
- generation: waiter.generation,
11012
- wakeRevision: waiter.wakeRevision,
11013
- goalId: goal.id,
11014
- goalVersion: goal.version,
11015
- policyHash: waiter.policyHash,
11016
- diagnostic: decision.diagnostic ?? null,
11017
- updateId: update.id
11018
- }),
11325
+ type: "session.status.changed",
11326
+ payload: { status: "recovering", reason: "codex_capacity" },
11019
11327
  turnId: blockedTurn.id,
11328
+ turnGeneration: blockedTurn.executionGeneration,
11329
+ turnAssociation: "current",
11020
11330
  occurredAt: now
11021
11331
  }
11022
11332
  ]).returning();
11023
11333
  const [updatedWaiter] = await tx.update(codexCapacityWaiters).set({
11024
11334
  status: "resumed",
11025
- resumedUpdateId: update.id,
11335
+ resumedUpdateId: null,
11026
11336
  observedWakeRevision: waiter.wakeRevision,
11027
11337
  lastWakeReason: "capacity_available",
11028
11338
  updatedAt: now
@@ -11036,22 +11346,44 @@ async function reconcileCodexCapacityWait(db, input, decide, policy) {
11036
11346
  if (!updatedWaiter) {
11037
11347
  throw new Error("Codex capacity waiter changed during atomic resume");
11038
11348
  }
11039
- await tx.update(sessions).set({
11040
- status: "queued",
11041
- activeTurnId: null,
11349
+ const [recoveringTurn] = await tx.update(sessionTurns).set({
11350
+ status: "recovering",
11351
+ activeAttemptId: null,
11352
+ metadata: metadataWithoutTurnDispatchAttempt(blockedTurn.metadata),
11353
+ version: blockedTurn.version + 1,
11354
+ finishedAt: null,
11355
+ updatedAt: now
11356
+ }).where(
11357
+ and6(
11358
+ eq6(sessionTurns.workspaceId, input.workspaceId),
11359
+ eq6(sessionTurns.id, blockedTurn.id),
11360
+ eq6(sessionTurns.status, "waiting_capacity"),
11361
+ isNull(sessionTurns.activeAttemptId),
11362
+ eq6(sessionTurns.executionGeneration, waiter.blockedTurnGeneration)
11363
+ )
11364
+ ).returning({ id: sessionTurns.id });
11365
+ if (!recoveringTurn) {
11366
+ throw new Error("Codex capacity blocked turn changed during atomic resume");
11367
+ }
11368
+ const [recoveringSession] = await tx.update(sessions).set({
11369
+ status: "recovering",
11370
+ activeTurnId: blockedTurn.id,
11042
11371
  lastSequence: session.lastSequence + 2,
11043
11372
  updatedAt: now
11044
11373
  }).where(
11045
11374
  and6(
11046
11375
  eq6(sessions.workspaceId, input.workspaceId),
11047
11376
  eq6(sessions.id, input.sessionId),
11048
- isNull(sessions.activeTurnId)
11377
+ eq6(sessions.status, "waiting_capacity"),
11378
+ eq6(sessions.activeTurnId, blockedTurn.id)
11049
11379
  )
11050
- );
11380
+ ).returning({ id: sessions.id });
11381
+ if (!recoveringSession) {
11382
+ throw new Error("Codex capacity session changed during atomic resume");
11383
+ }
11051
11384
  return {
11052
11385
  action: "resumed",
11053
11386
  waiter: mapCodexCapacityWaiter(updatedWaiter),
11054
- update: mapSessionSystemUpdate(update),
11055
11387
  events: events.map(mapEvent2)
11056
11388
  };
11057
11389
  })
@@ -13349,17 +13681,19 @@ async function listSessionsForSubject(db, workspaceId, options) {
13349
13681
  if (ordinaryIds.length > limit) {
13350
13682
  let snapshot = reusableSnapshot;
13351
13683
  if (!snapshot) {
13352
- const activeSnapshots = await tx.select({ id: sessionListSnapshots.id }).from(sessionListSnapshots).where(
13353
- and6(
13354
- eq6(sessionListSnapshots.workspaceId, workspaceId),
13355
- eq6(sessionListSnapshots.subjectId, options.subjectId)
13356
- )
13357
- ).limit(SESSION_LIST_SNAPSHOT_MAX_ACTIVE_PER_SUBJECT);
13358
- if (activeSnapshots.length >= SESSION_LIST_SNAPSHOT_MAX_ACTIVE_PER_SUBJECT) {
13359
- throw new SessionListSnapshotLimitError(
13360
- "too many active session list snapshots; retry after an existing cursor expires"
13361
- );
13362
- }
13684
+ await tx.execute(sql4`
13685
+ delete from ${sessionListSnapshots} snapshot
13686
+ where snapshot.workspace_id = ${workspaceId}
13687
+ and snapshot.subject_id = ${options.subjectId}
13688
+ and snapshot.id in (
13689
+ select evicted.id
13690
+ from ${sessionListSnapshots} evicted
13691
+ where evicted.workspace_id = ${workspaceId}
13692
+ and evicted.subject_id = ${options.subjectId}
13693
+ order by evicted.created_at desc, evicted.id desc
13694
+ offset ${SESSION_LIST_SNAPSHOT_MAX_ACTIVE_PER_SUBJECT - 1}
13695
+ )
13696
+ `);
13363
13697
  const [workspace] = await tx.select({ accountId: workspaces.accountId }).from(workspaces).where(eq6(workspaces.id, workspaceId)).limit(1);
13364
13698
  if (!workspace) {
13365
13699
  throw new Error("session list workspace disappeared while creating a snapshot");
@@ -16163,7 +16497,7 @@ async function acquireLease(db, input) {
16163
16497
  }
16164
16498
  if (liveness === "cold") {
16165
16499
  const recovery = recoveryStateFromLeaseRow(row);
16166
- if (recovery.restore.status === "degraded" || recovery.restore.status === "unrecoverable") {
16500
+ if (recovery.restore.status === "degraded" && recovery.restore.retryable !== true || recovery.restore.status === "unrecoverable") {
16167
16501
  return {
16168
16502
  role: "blocked",
16169
16503
  code: recovery.restore.status === "degraded" ? "restore_degraded" : "restore_unrecoverable",
@@ -16642,12 +16976,148 @@ async function recordWarmingSandboxCreated(db, input) {
16642
16976
  })
16643
16977
  );
16644
16978
  }
16979
+ var LOST_PROVIDER_PROCESS_REASON = "provider_instance_lost";
16980
+ async function lockExactLostProviderWorkspaceBlockersTx(tx, input) {
16981
+ await tx.execute(sql4`
16982
+ select id from sandbox_retained_processes
16983
+ where account_id = ${input.accountId}
16984
+ and workspace_id = ${input.workspaceId}
16985
+ and lease_id = ${input.leaseId}
16986
+ and sandbox_group_id = ${input.sandboxGroupId}
16987
+ and lease_epoch = ${input.lostEpoch}
16988
+ and provider_instance_id = ${input.lostInstanceId}
16989
+ and state = 'active'
16990
+ order by id
16991
+ for update
16992
+ `);
16993
+ await tx.execute(sql4`
16994
+ select id from sandbox_workspace_mutation_admissions
16995
+ where account_id = ${input.accountId}
16996
+ and workspace_id = ${input.workspaceId}
16997
+ and lease_id = ${input.leaseId}
16998
+ and sandbox_group_id = ${input.sandboxGroupId}
16999
+ and lease_epoch = ${input.lostEpoch}
17000
+ and provider_instance_id = ${input.lostInstanceId}
17001
+ and settled_at is null
17002
+ order by id
17003
+ for update
17004
+ `);
17005
+ await tx.execute(sql4`
17006
+ select id from sandbox_pty_sessions
17007
+ where account_id = ${input.accountId}
17008
+ and workspace_id = ${input.workspaceId}
17009
+ and lease_id = ${input.leaseId}
17010
+ and sandbox_group_id = ${input.sandboxGroupId}
17011
+ and lease_epoch = ${input.lostEpoch}
17012
+ and provider_instance_id = ${input.lostInstanceId}
17013
+ and status = 'open'
17014
+ order by id
17015
+ for update
17016
+ `);
17017
+ }
17018
+ async function settleExactLostProviderWorkspaceBlockersTx(tx, input) {
17019
+ const lostProcesses = await tx.update(sandboxRetainedProcesses).set({
17020
+ state: "lost",
17021
+ exitCode: null,
17022
+ settlementReason: LOST_PROVIDER_PROCESS_REASON,
17023
+ settledAt: /* @__PURE__ */ new Date()
17024
+ }).where(
17025
+ and6(
17026
+ eq6(sandboxRetainedProcesses.accountId, input.accountId),
17027
+ eq6(sandboxRetainedProcesses.workspaceId, input.workspaceId),
17028
+ eq6(sandboxRetainedProcesses.leaseId, input.leaseId),
17029
+ eq6(sandboxRetainedProcesses.sandboxGroupId, input.sandboxGroupId),
17030
+ eq6(sandboxRetainedProcesses.leaseEpoch, input.lostEpoch),
17031
+ eq6(sandboxRetainedProcesses.providerInstanceId, input.lostInstanceId),
17032
+ eq6(sandboxRetainedProcesses.state, "active")
17033
+ )
17034
+ ).returning({
17035
+ id: sandboxRetainedProcesses.id,
17036
+ holderId: sandboxRetainedProcesses.holderId
17037
+ });
17038
+ const rejectedAdmissions = await tx.update(sandboxWorkspaceMutationAdmissions).set({ providerOutcome: "rejected", settledAt: /* @__PURE__ */ new Date() }).where(
17039
+ and6(
17040
+ eq6(sandboxWorkspaceMutationAdmissions.accountId, input.accountId),
17041
+ eq6(sandboxWorkspaceMutationAdmissions.workspaceId, input.workspaceId),
17042
+ eq6(sandboxWorkspaceMutationAdmissions.leaseId, input.leaseId),
17043
+ eq6(sandboxWorkspaceMutationAdmissions.sandboxGroupId, input.sandboxGroupId),
17044
+ eq6(sandboxWorkspaceMutationAdmissions.leaseEpoch, input.lostEpoch),
17045
+ eq6(sandboxWorkspaceMutationAdmissions.providerInstanceId, input.lostInstanceId),
17046
+ isNull(sandboxWorkspaceMutationAdmissions.settledAt)
17047
+ )
17048
+ ).returning({ id: sandboxWorkspaceMutationAdmissions.id });
17049
+ const closedPtys = await tx.update(sandboxPtySessions).set({ status: "closed", closedAt: /* @__PURE__ */ new Date() }).where(
17050
+ and6(
17051
+ eq6(sandboxPtySessions.accountId, input.accountId),
17052
+ eq6(sandboxPtySessions.workspaceId, input.workspaceId),
17053
+ eq6(sandboxPtySessions.leaseId, input.leaseId),
17054
+ eq6(sandboxPtySessions.sandboxGroupId, input.sandboxGroupId),
17055
+ eq6(sandboxPtySessions.leaseEpoch, input.lostEpoch),
17056
+ eq6(sandboxPtySessions.providerInstanceId, input.lostInstanceId),
17057
+ eq6(sandboxPtySessions.status, "open")
17058
+ )
17059
+ ).returning({ id: sandboxPtySessions.id });
17060
+ const deletedHolders = lostProcesses.length === 0 ? [] : await tx.delete(sandboxLeaseHolders).where(
17061
+ and6(
17062
+ eq6(sandboxLeaseHolders.accountId, input.accountId),
17063
+ eq6(sandboxLeaseHolders.workspaceId, input.workspaceId),
17064
+ eq6(sandboxLeaseHolders.leaseId, input.leaseId),
17065
+ eq6(sandboxLeaseHolders.kind, "process"),
17066
+ inArray3(
17067
+ sandboxLeaseHolders.holderId,
17068
+ lostProcesses.map((process) => process.holderId)
17069
+ )
17070
+ )
17071
+ ).returning({ holderId: sandboxLeaseHolders.holderId });
17072
+ await tx.execute(sql4`
17073
+ update sandbox_leases as lease set
17074
+ refcount = counts.total,
17075
+ turn_holders = counts.turns,
17076
+ viewer_holders = counts.viewers,
17077
+ updated_at = now()
17078
+ from (
17079
+ select count(*)::int as total,
17080
+ count(*) filter (where kind = 'turn')::int as turns,
17081
+ count(*) filter (where kind = 'viewer')::int as viewers
17082
+ from sandbox_lease_holders
17083
+ where lease_id = ${input.leaseId}
17084
+ ) as counts
17085
+ where lease.id = ${input.leaseId}
17086
+ `);
17087
+ return {
17088
+ processesLost: lostProcesses.length,
17089
+ admissionsRejected: rejectedAdmissions.length,
17090
+ ptysClosed: closedPtys.length,
17091
+ processHoldersDeleted: deletedHolders.length
17092
+ };
17093
+ }
16645
17094
  async function markWarmLeaseInstanceLost(db, input) {
16646
17095
  return await withRlsContext(
16647
17096
  db,
16648
17097
  { accountId: input.accountId, workspaceId: input.workspaceId },
16649
17098
  async (scopedDb) => await scopedDb.transaction(async (txRaw) => {
16650
17099
  const tx = txRaw;
17100
+ const observedRows = await tx.execute(sql4`
17101
+ select * from sandbox_leases
17102
+ where workspace_id = ${input.workspaceId}
17103
+ and sandbox_group_id = ${input.sandboxGroupId}
17104
+ `);
17105
+ const observed = observedRows[0];
17106
+ if (!observed || observed.liveness !== "warm" || Number(observed.lease_epoch) !== input.expectedEpoch || observed.instance_id !== input.expectedInstanceId) {
17107
+ return {
17108
+ status: "stale",
17109
+ lease: observed ? mapLeaseRow(observed) : null
17110
+ };
17111
+ }
17112
+ const blockerScope = {
17113
+ accountId: input.accountId,
17114
+ workspaceId: input.workspaceId,
17115
+ leaseId: observed.id,
17116
+ sandboxGroupId: input.sandboxGroupId,
17117
+ lostEpoch: input.expectedEpoch,
17118
+ lostInstanceId: input.expectedInstanceId
17119
+ };
17120
+ await lockExactLostProviderWorkspaceBlockersTx(tx, blockerScope);
16651
17121
  const currentRows = await tx.execute(sql4`
16652
17122
  select * from sandbox_leases
16653
17123
  where workspace_id = ${input.workspaceId}
@@ -16655,12 +17125,13 @@ async function markWarmLeaseInstanceLost(db, input) {
16655
17125
  for update
16656
17126
  `);
16657
17127
  const current = currentRows[0];
16658
- if (!current || current.liveness !== "warm" || Number(current.lease_epoch) !== input.expectedEpoch || current.instance_id !== input.expectedInstanceId) {
17128
+ if (!current || current.id !== observed.id || current.liveness !== "warm" || Number(current.lease_epoch) !== input.expectedEpoch || current.instance_id !== input.expectedInstanceId) {
16659
17129
  return {
16660
17130
  status: "stale",
16661
17131
  lease: current ? mapLeaseRow(current) : null
16662
17132
  };
16663
17133
  }
17134
+ const settlement = await settleExactLostProviderWorkspaceBlockersTx(tx, blockerScope);
16664
17135
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
16665
17136
  const before = recoveryStateFromLeaseRow(current);
16666
17137
  const archiveStatus = before.archive.status;
@@ -16711,7 +17182,66 @@ async function markWarmLeaseInstanceLost(db, input) {
16711
17182
  if (!updated) {
16712
17183
  throw new Error(`Warm sandbox lease vanished while retiring instance ${current.id}`);
16713
17184
  }
16714
- return { status: "marked", lease: mapLeaseRow(updated) };
17185
+ return { status: "marked", lease: mapLeaseRow(updated), settlement };
17186
+ })
17187
+ );
17188
+ }
17189
+ async function reconcileColdLostLeaseInstanceBlockers(db, input) {
17190
+ if (!Number.isSafeInteger(input.expectedCurrentEpoch) || !Number.isSafeInteger(input.expectedLostEpoch) || input.expectedCurrentEpoch !== input.expectedLostEpoch + 1) {
17191
+ throw new Error("Cold lost-provider reconciliation requires currentEpoch = lostEpoch + 1");
17192
+ }
17193
+ return await withRlsContext(
17194
+ db,
17195
+ { accountId: input.accountId, workspaceId: input.workspaceId },
17196
+ async (scopedDb) => await scopedDb.transaction(async (txRaw) => {
17197
+ const tx = txRaw;
17198
+ const observedRows = await tx.execute(sql4`
17199
+ select * from sandbox_leases
17200
+ where workspace_id = ${input.workspaceId}
17201
+ and sandbox_group_id = ${input.sandboxGroupId}
17202
+ `);
17203
+ const observed = observedRows[0];
17204
+ const observedRecovery = observed ? recoveryStateFromLeaseRow(observed) : null;
17205
+ if (!observed || observed.liveness !== "cold" || observed.instance_id !== null || Number(observed.lease_epoch) !== input.expectedCurrentEpoch || Number(observed.workspace_generation) !== input.expectedWorkspaceGeneration || (observed.archive_generation === null ? null : Number(observed.archive_generation)) !== input.expectedArchiveGeneration || hasCompleteWorkspaceArchive(observed) !== input.expectedArchiveComplete || observedRecovery?.provider.status !== "missing" || observedRecovery.provider.instanceId !== input.expectedLostInstanceId) {
17206
+ return {
17207
+ status: "stale",
17208
+ lease: observed ? mapLeaseRow(observed) : null
17209
+ };
17210
+ }
17211
+ const blockerScope = {
17212
+ accountId: input.accountId,
17213
+ workspaceId: input.workspaceId,
17214
+ leaseId: observed.id,
17215
+ sandboxGroupId: input.sandboxGroupId,
17216
+ lostEpoch: input.expectedLostEpoch,
17217
+ lostInstanceId: input.expectedLostInstanceId
17218
+ };
17219
+ await lockExactLostProviderWorkspaceBlockersTx(tx, blockerScope);
17220
+ const currentRows = await tx.execute(sql4`
17221
+ select * from sandbox_leases
17222
+ where workspace_id = ${input.workspaceId}
17223
+ and sandbox_group_id = ${input.sandboxGroupId}
17224
+ for update
17225
+ `);
17226
+ const current = currentRows[0];
17227
+ const recovery = current ? recoveryStateFromLeaseRow(current) : null;
17228
+ if (!current || current.id !== observed.id || current.liveness !== "cold" || current.instance_id !== null || Number(current.lease_epoch) !== input.expectedCurrentEpoch || Number(current.workspace_generation) !== input.expectedWorkspaceGeneration || (current.archive_generation === null ? null : Number(current.archive_generation)) !== input.expectedArchiveGeneration || hasCompleteWorkspaceArchive(current) !== input.expectedArchiveComplete || recovery?.provider.status !== "missing" || recovery.provider.instanceId !== input.expectedLostInstanceId) {
17229
+ return {
17230
+ status: "stale",
17231
+ lease: current ? mapLeaseRow(current) : null
17232
+ };
17233
+ }
17234
+ const settlement = await settleExactLostProviderWorkspaceBlockersTx(tx, blockerScope);
17235
+ const refreshedRows = await tx.execute(sql4`
17236
+ select * from sandbox_leases where id = ${current.id}
17237
+ `);
17238
+ const refreshed = refreshedRows[0];
17239
+ if (!refreshed) throw new Error("Cold sandbox lease vanished during reconciliation");
17240
+ return {
17241
+ status: "reconciled",
17242
+ lease: mapLeaseRow(refreshed),
17243
+ settlement
17244
+ };
16715
17245
  })
16716
17246
  );
16717
17247
  }
@@ -17204,6 +17734,45 @@ var SandboxWorkspaceMutationFencedError = class extends Error {
17204
17734
  }
17205
17735
  name = "SandboxWorkspaceMutationFencedError";
17206
17736
  };
17737
+ function retainedProcessSettlementIdentity(process) {
17738
+ return {
17739
+ leaseId: process.leaseId,
17740
+ sandboxGroupId: process.sandboxGroupId,
17741
+ parentAdmissionId: process.parentAdmissionId,
17742
+ holderId: process.holderId,
17743
+ leaseEpoch: process.leaseEpoch,
17744
+ providerBackend: process.providerBackend,
17745
+ providerInstanceId: process.providerInstanceId,
17746
+ routeKind: process.routeKind,
17747
+ routeTargetId: process.routeTargetId,
17748
+ routeEpoch: process.routeEpoch,
17749
+ providerSessionId: process.providerSessionId
17750
+ };
17751
+ }
17752
+ function retainedProcessReconciliationProof(process) {
17753
+ if (process.reconcileProofOutcome === "exited" && process.reconcileProofReason === "provider_exit_banner" && process.reconcileProofExitCode !== null) {
17754
+ return {
17755
+ outcome: "exited",
17756
+ exitCode: process.reconcileProofExitCode,
17757
+ reason: "provider_exit_banner"
17758
+ };
17759
+ }
17760
+ if (process.reconcileProofOutcome === "lost" && (process.reconcileProofReason === "provider_session_lost_banner" || process.reconcileProofReason === "provider_instance_not_found")) {
17761
+ return {
17762
+ outcome: "lost",
17763
+ exitCode: null,
17764
+ reason: process.reconcileProofReason
17765
+ };
17766
+ }
17767
+ return null;
17768
+ }
17769
+ var SandboxRetainedProcessPromotionFencedError = class extends SandboxWorkspaceMutationFencedError {
17770
+ constructor(code, message, process) {
17771
+ super(code, message);
17772
+ this.process = process;
17773
+ }
17774
+ name = "SandboxRetainedProcessPromotionFencedError";
17775
+ };
17207
17776
  function normalizeWorkspaceMutationOperation(operation) {
17208
17777
  const normalized = operation.trim();
17209
17778
  const byteLength = new TextEncoder().encode(normalized).byteLength;
@@ -17233,6 +17802,17 @@ function normalizeRetainedProcessSettlementReason(reason) {
17233
17802
  }
17234
17803
  return bounded;
17235
17804
  }
17805
+ function normalizeRetainedProcessReconciliationOutcome(outcome) {
17806
+ const normalized = outcome.trim();
17807
+ const byteLength = Buffer.byteLength(normalized, "utf8");
17808
+ if (byteLength < 1 || byteLength > 64) {
17809
+ throw new SandboxWorkspaceMutationFencedError(
17810
+ "process_fenced",
17811
+ "Retained process reconciliation outcome must contain between 1 and 64 UTF-8 bytes"
17812
+ );
17813
+ }
17814
+ return normalized;
17815
+ }
17236
17816
  function mapWorkspaceMutationAdmission(row) {
17237
17817
  return {
17238
17818
  id: row.id,
@@ -17278,9 +17858,21 @@ function mapRetainedProcess(row) {
17278
17858
  exitCode: row.exitCode ?? null,
17279
17859
  settlementReason: row.settlementReason ?? null,
17280
17860
  startedAt: row.startedAt.toISOString(),
17281
- settledAt: row.settledAt?.toISOString() ?? null
17861
+ settledAt: row.settledAt?.toISOString() ?? null,
17862
+ reconcileAfter: row.reconcileAfter.toISOString(),
17863
+ reconcileClaimId: row.reconcileClaimId ?? null,
17864
+ reconcileClaimedAt: row.reconcileClaimedAt?.toISOString() ?? null,
17865
+ reconcileAttempts: row.reconcileAttempts,
17866
+ lastReconcileOutcome: row.lastReconcileOutcome ?? null,
17867
+ reconcileProofOutcome: row.reconcileProofOutcome ?? null,
17868
+ reconcileProofExitCode: row.reconcileProofExitCode ?? null,
17869
+ reconcileProofReason: row.reconcileProofReason ?? null,
17870
+ reconcileProofObservedAt: row.reconcileProofObservedAt?.toISOString() ?? null
17282
17871
  };
17283
17872
  }
17873
+ function retainedProcessMatchesSettlementIdentity(process, expected) {
17874
+ return process.leaseId === expected.leaseId && process.sandboxGroupId === expected.sandboxGroupId && process.parentAdmissionId === expected.parentAdmissionId && process.holderId === expected.holderId && process.leaseEpoch === expected.leaseEpoch && process.providerBackend === expected.providerBackend && process.providerInstanceId === expected.providerInstanceId && process.routeKind === expected.routeKind && (process.routeTargetId ?? null) === expected.routeTargetId && process.routeEpoch === expected.routeEpoch && process.providerSessionId === expected.providerSessionId;
17875
+ }
17284
17876
  async function lockWorkspaceMutationSessionTx(tx, workspaceId, sessionId) {
17285
17877
  const locks = await lockSessionEventWriteRows(tx, {
17286
17878
  workspaceId,
@@ -17901,7 +18493,11 @@ async function retainWorkspaceMutationProcess(db, input) {
17901
18493
  })
17902
18494
  );
17903
18495
  if (result.failure.failure !== null) {
17904
- throw new SandboxWorkspaceMutationFencedError(result.failure.failure, result.failure.detail);
18496
+ throw new SandboxRetainedProcessPromotionFencedError(
18497
+ result.failure.failure,
18498
+ result.failure.detail,
18499
+ result.process
18500
+ );
17905
18501
  }
17906
18502
  return result.process;
17907
18503
  }
@@ -17917,6 +18513,186 @@ async function getRetainedProcess(db, input) {
17917
18513
  return row ? mapRetainedProcess(row) : null;
17918
18514
  });
17919
18515
  }
18516
+ async function claimTerminalRetainedProcesses(db, input) {
18517
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100) {
18518
+ throw new Error("Retained process reconciliation limit must be between 1 and 100");
18519
+ }
18520
+ if (!Number.isSafeInteger(input.claimTtlMs) || input.claimTtlMs < 0 || input.claimTtlMs > 36e5) {
18521
+ throw new Error("Retained process reconciliation claim TTL is invalid");
18522
+ }
18523
+ const rows = await rawRows(
18524
+ db,
18525
+ sql4`
18526
+ select account_id, workspace_id, session_id, process_id, claim_id,
18527
+ owner_state, owner_attempt_outcome
18528
+ from opengeni_private.claim_terminal_retained_processes(
18529
+ ${input.claimId}::uuid, ${input.limit}::integer, ${input.claimTtlMs}::bigint
18530
+ )
18531
+ `
18532
+ );
18533
+ const claims = [];
18534
+ for (const row of rows) {
18535
+ const process = await getRetainedProcess(db, {
18536
+ workspaceId: row.workspace_id,
18537
+ sessionId: row.session_id,
18538
+ processId: row.process_id
18539
+ });
18540
+ if (!process || process.accountId !== row.account_id || process.state !== "active" || process.reconcileClaimId !== row.claim_id) {
18541
+ continue;
18542
+ }
18543
+ claims.push({
18544
+ process,
18545
+ claimId: row.claim_id,
18546
+ ownerState: row.owner_state,
18547
+ ownerAttemptOutcome: row.owner_attempt_outcome
18548
+ });
18549
+ }
18550
+ return claims;
18551
+ }
18552
+ async function recordRetainedProcessReconciliationProof(db, input) {
18553
+ if (input.proof.outcome === "exited" && (!Number.isSafeInteger(input.proof.exitCode) || input.proof.exitCode === null)) {
18554
+ throw new SandboxWorkspaceMutationFencedError(
18555
+ "process_fenced",
18556
+ "Retained process reconciliation exit proof requires a safe integer exit code"
18557
+ );
18558
+ }
18559
+ return await withRlsContext(
18560
+ db,
18561
+ { accountId: input.accountId, workspaceId: input.workspaceId },
18562
+ async (scopedDb) => await scopedDb.transaction(async (txRaw) => {
18563
+ const tx = txRaw;
18564
+ const [process] = await tx.select().from(sandboxRetainedProcesses).where(
18565
+ and6(
18566
+ eq6(sandboxRetainedProcesses.accountId, input.accountId),
18567
+ eq6(sandboxRetainedProcesses.workspaceId, input.workspaceId),
18568
+ eq6(sandboxRetainedProcesses.sessionId, input.sessionId),
18569
+ eq6(sandboxRetainedProcesses.id, input.processId)
18570
+ )
18571
+ ).for("update").limit(1);
18572
+ if (!process || process.state !== "active" || !retainedProcessMatchesSettlementIdentity(process, input.expected)) {
18573
+ throw new SandboxWorkspaceMutationFencedError(
18574
+ "process_fenced",
18575
+ "Retained process proof did not match an active copied durable identity"
18576
+ );
18577
+ }
18578
+ if (process.reconcileClaimId !== input.claimId) {
18579
+ throw new SandboxWorkspaceMutationFencedError(
18580
+ "process_fenced",
18581
+ "Retained process proof claim was lost or superseded"
18582
+ );
18583
+ }
18584
+ const existing = mapRetainedProcess(process);
18585
+ const existingProof = retainedProcessReconciliationProof(existing);
18586
+ if (existingProof) {
18587
+ if (existingProof.outcome !== input.proof.outcome || existingProof.exitCode !== input.proof.exitCode || existingProof.reason !== input.proof.reason) {
18588
+ throw new SandboxWorkspaceMutationFencedError(
18589
+ "process_fenced",
18590
+ "Retained process already carries different provider proof"
18591
+ );
18592
+ }
18593
+ return existing;
18594
+ }
18595
+ const [updated] = await tx.update(sandboxRetainedProcesses).set({
18596
+ reconcileProofOutcome: input.proof.outcome,
18597
+ reconcileProofExitCode: input.proof.exitCode,
18598
+ reconcileProofReason: input.proof.reason,
18599
+ reconcileProofObservedAt: /* @__PURE__ */ new Date(),
18600
+ lastReconcileOutcome: `proof_${input.proof.outcome}`
18601
+ }).where(
18602
+ and6(
18603
+ eq6(sandboxRetainedProcesses.id, process.id),
18604
+ eq6(sandboxRetainedProcesses.state, "active"),
18605
+ eq6(sandboxRetainedProcesses.reconcileClaimId, input.claimId)
18606
+ )
18607
+ ).returning();
18608
+ if (!updated) {
18609
+ throw new SandboxWorkspaceMutationFencedError(
18610
+ "process_fenced",
18611
+ "Retained process changed while checkpointing provider proof"
18612
+ );
18613
+ }
18614
+ return mapRetainedProcess(updated);
18615
+ })
18616
+ );
18617
+ }
18618
+ async function deferRetainedProcessReconciliation(db, input) {
18619
+ const outcome = normalizeRetainedProcessReconciliationOutcome(input.outcome);
18620
+ if (!Number.isSafeInteger(input.retryAfterMs) || input.retryAfterMs < 0 || input.retryAfterMs > 864e5) {
18621
+ throw new SandboxWorkspaceMutationFencedError(
18622
+ "process_fenced",
18623
+ "Retained process reconciliation retry delay is invalid"
18624
+ );
18625
+ }
18626
+ return await withRlsContext(
18627
+ db,
18628
+ { accountId: input.accountId, workspaceId: input.workspaceId },
18629
+ async (scopedDb) => await scopedDb.transaction(async (txRaw) => {
18630
+ const tx = txRaw;
18631
+ const [process] = await tx.select().from(sandboxRetainedProcesses).where(
18632
+ and6(
18633
+ eq6(sandboxRetainedProcesses.accountId, input.accountId),
18634
+ eq6(sandboxRetainedProcesses.workspaceId, input.workspaceId),
18635
+ eq6(sandboxRetainedProcesses.sessionId, input.sessionId),
18636
+ eq6(sandboxRetainedProcesses.id, input.processId)
18637
+ )
18638
+ ).for("update").limit(1);
18639
+ if (!process || !retainedProcessMatchesSettlementIdentity(process, input.expected)) {
18640
+ throw new SandboxWorkspaceMutationFencedError(
18641
+ "process_fenced",
18642
+ "Retained process reconciliation did not match the copied durable identity"
18643
+ );
18644
+ }
18645
+ if (process.state !== "active") return false;
18646
+ if (process.reconcileClaimId !== input.claimId) {
18647
+ throw new SandboxWorkspaceMutationFencedError(
18648
+ "process_fenced",
18649
+ "Retained process reconciliation claim was lost or superseded"
18650
+ );
18651
+ }
18652
+ const [updated] = await tx.update(sandboxRetainedProcesses).set({
18653
+ reconcileAfter: new Date(Date.now() + input.retryAfterMs),
18654
+ reconcileClaimId: null,
18655
+ reconcileClaimedAt: null,
18656
+ lastReconcileOutcome: outcome
18657
+ }).where(
18658
+ and6(
18659
+ eq6(sandboxRetainedProcesses.id, process.id),
18660
+ eq6(sandboxRetainedProcesses.state, "active"),
18661
+ eq6(sandboxRetainedProcesses.reconcileClaimId, input.claimId)
18662
+ )
18663
+ ).returning({ id: sandboxRetainedProcesses.id });
18664
+ return Boolean(updated);
18665
+ })
18666
+ );
18667
+ }
18668
+ async function countActiveRetainedProcessesByOwnerState(db) {
18669
+ const rows = await rawRows(
18670
+ db,
18671
+ sql4`
18672
+ select owner_state, active_count, terminal_owner_count
18673
+ from opengeni_private.count_active_retained_processes_by_owner_state()
18674
+ `
18675
+ );
18676
+ return rows.map((row) => ({
18677
+ ownerState: row.owner_state,
18678
+ activeCount: Number(row.active_count),
18679
+ terminalOwnerCount: Number(row.terminal_owner_count)
18680
+ }));
18681
+ }
18682
+ async function countExpiredDrainingSandboxLeases(db) {
18683
+ const rows = await rawRows(
18684
+ db,
18685
+ sql4`
18686
+ select backend, age_bucket, count
18687
+ from opengeni_private.count_expired_draining_sandbox_leases()
18688
+ `
18689
+ );
18690
+ return rows.map((row) => ({
18691
+ backend: row.backend,
18692
+ ageBucket: row.age_bucket,
18693
+ count: Number(row.count)
18694
+ }));
18695
+ }
17920
18696
  async function settleRetainedProcess(db, input) {
17921
18697
  const reason = normalizeRetainedProcessSettlementReason(input.reason);
17922
18698
  const exitCode = input.outcome === "exited" ? input.exitCode ?? null : null;
@@ -17946,6 +18722,12 @@ async function settleRetainedProcess(db, input) {
17946
18722
  "Retained process settlement did not match a durable process"
17947
18723
  );
17948
18724
  }
18725
+ if (!retainedProcessMatchesSettlementIdentity(process, input.expected)) {
18726
+ throw new SandboxWorkspaceMutationFencedError(
18727
+ "process_fenced",
18728
+ "Retained process settlement did not match the copied durable identity"
18729
+ );
18730
+ }
17949
18731
  if (process.state !== "active") {
17950
18732
  if (process.state !== input.outcome || process.exitCode !== exitCode || process.settlementReason !== reason) {
17951
18733
  throw new SandboxWorkspaceMutationFencedError(
@@ -17961,6 +18743,19 @@ async function settleRetainedProcess(db, input) {
17961
18743
  );
17962
18744
  return { settled: false, process: mapRetainedProcess(process) };
17963
18745
  }
18746
+ if (input.reconciliationClaimId !== void 0 && process.reconcileClaimId !== input.reconciliationClaimId) {
18747
+ throw new SandboxWorkspaceMutationFencedError(
18748
+ "process_fenced",
18749
+ "Retained process settlement reconciliation claim was lost or superseded"
18750
+ );
18751
+ }
18752
+ const durableProof = retainedProcessReconciliationProof(mapRetainedProcess(process));
18753
+ if (durableProof && (durableProof.outcome !== input.outcome || durableProof.exitCode !== exitCode || durableProof.reason !== reason)) {
18754
+ throw new SandboxWorkspaceMutationFencedError(
18755
+ "process_fenced",
18756
+ "Retained process settlement conflicts with checkpointed provider proof"
18757
+ );
18758
+ }
17964
18759
  const admissions = await tx.execute(sql4`
17965
18760
  select * from sandbox_workspace_mutation_admissions
17966
18761
  where id = ${process.parentAdmissionId}
@@ -17968,6 +18763,13 @@ async function settleRetainedProcess(db, input) {
17968
18763
  and workspace_id = ${input.workspaceId}
17969
18764
  and session_id = ${input.sessionId}
17970
18765
  and lease_id = ${process.leaseId}
18766
+ and sandbox_group_id = ${process.sandboxGroupId}
18767
+ and lease_epoch = ${process.leaseEpoch}
18768
+ and provider_backend = ${process.providerBackend}
18769
+ and provider_instance_id = ${process.providerInstanceId}
18770
+ and route_kind = ${process.routeKind}
18771
+ and route_target_id is not distinct from ${process.routeTargetId}
18772
+ and route_epoch = ${process.routeEpoch}
17971
18773
  and provider_outcome = 'retained'
17972
18774
  and settled_at is null
17973
18775
  for update
@@ -17978,6 +18780,23 @@ async function settleRetainedProcess(db, input) {
17978
18780
  "Retained process parent admission is not open"
17979
18781
  );
17980
18782
  }
18783
+ const leases = await tx.execute(sql4`
18784
+ select * from sandbox_leases
18785
+ where id = ${process.leaseId}
18786
+ and account_id = ${input.accountId}
18787
+ and workspace_id = ${input.workspaceId}
18788
+ and sandbox_group_id = ${process.sandboxGroupId}
18789
+ and lease_epoch = ${process.leaseEpoch}
18790
+ and backend = ${process.providerBackend}
18791
+ and instance_id = ${process.providerInstanceId}
18792
+ for update
18793
+ `);
18794
+ if (!leases[0]) {
18795
+ throw new SandboxWorkspaceMutationFencedError(
18796
+ "lease_fenced",
18797
+ "Retained process settlement cannot mutate a successor lease identity"
18798
+ );
18799
+ }
17981
18800
  await tx.update(sandboxPtySessions).set({ status: "closed", closedAt: /* @__PURE__ */ new Date() }).where(
17982
18801
  and6(
17983
18802
  eq6(sandboxPtySessions.retainedProcessId, process.id),
@@ -17988,7 +18807,10 @@ async function settleRetainedProcess(db, input) {
17988
18807
  state: input.outcome,
17989
18808
  exitCode,
17990
18809
  settlementReason: reason,
17991
- settledAt: /* @__PURE__ */ new Date()
18810
+ settledAt: /* @__PURE__ */ new Date(),
18811
+ reconcileClaimId: null,
18812
+ reconcileClaimedAt: null,
18813
+ lastReconcileOutcome: input.reconciliationClaimId === void 0 ? `owner_settled_${input.outcome}` : `reconciled_${input.outcome}`
17992
18814
  }).where(
17993
18815
  and6(
17994
18816
  eq6(sandboxRetainedProcesses.id, process.id),
@@ -18011,6 +18833,8 @@ async function settleRetainedProcess(db, input) {
18011
18833
  await tx.execute(sql4`
18012
18834
  delete from sandbox_lease_holders
18013
18835
  where lease_id = ${process.leaseId}
18836
+ and account_id = ${input.accountId}
18837
+ and workspace_id = ${input.workspaceId}
18014
18838
  and kind = 'process' and holder_id = ${process.holderId}
18015
18839
  `);
18016
18840
  const [counts] = await tx.execute(sql4`
@@ -18031,6 +18855,10 @@ async function settleRetainedProcess(db, input) {
18031
18855
  else expires_at end,` : sql4``}
18032
18856
  updated_at = now()
18033
18857
  where id = ${process.leaseId}
18858
+ and sandbox_group_id = ${process.sandboxGroupId}
18859
+ and lease_epoch = ${process.leaseEpoch}
18860
+ and backend = ${process.providerBackend}
18861
+ and instance_id = ${process.providerInstanceId}
18034
18862
  `);
18035
18863
  return { settled: true, process: mapRetainedProcess(updated) };
18036
18864
  })
@@ -20840,13 +21668,14 @@ async function initializeSessionStartAtomically(db, input) {
20840
21668
  }).returning();
20841
21669
  if (!goal) throw new Error("Failed to create initial session goal");
20842
21670
  }
20843
- let [userEvent] = await tx.select().from(sessionEvents).where(
21671
+ const existingUserEvents = await tx.select().from(sessionEvents).where(
20844
21672
  and6(
20845
21673
  eq6(sessionEvents.workspaceId, input.workspaceId),
20846
21674
  eq6(sessionEvents.sessionId, session.id),
20847
21675
  eq6(sessionEvents.type, "user.message")
20848
21676
  )
20849
21677
  ).orderBy(asc3(sessionEvents.sequence)).limit(1);
21678
+ let userEvent = existingUserEvents[0];
20850
21679
  let sequence = session.lastSequence;
20851
21680
  const insertedEvents = [];
20852
21681
  const runnable = effectiveControl.state === "active";
@@ -21013,7 +21842,7 @@ async function initializeSessionStartAtomically(db, input) {
21013
21842
  tx,
21014
21843
  input.consumeNewSessionDraft.subjectId
21015
21844
  );
21016
- await consumeNewSessionDraftInTransaction(tx, {
21845
+ await seedNewSessionDraftInTransaction(tx, {
21017
21846
  workspaceId: input.workspaceId,
21018
21847
  subjectId: input.consumeNewSessionDraft.subjectId,
21019
21848
  expectedRevision: input.consumeNewSessionDraft.expectedRevision
@@ -22324,7 +23153,7 @@ async function peekSessionWork(db, workspaceId, sessionId) {
22324
23153
  workspaceId,
22325
23154
  sessionId
22326
23155
  );
22327
- if (latestInterruption && latestInterruption.quiescedAt === null && ["settled", "rejected_stale"].includes(latestInterruption.interruptionState)) {
23156
+ if (latestInterruption && latestInterruption.quiescedAt === null && latestInterruption.interruptionState === "settled") {
22328
23157
  return {
22329
23158
  kind: "cancellation-wait",
22330
23159
  attemptId: latestInterruption.attemptId
@@ -24992,17 +25821,28 @@ async function appendSessionEventsAndUpdateSession(db, workspaceId, sessionId, i
24992
25821
  })
24993
25822
  );
24994
25823
  }
24995
- async function appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, build) {
25824
+ async function appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, build, options = {}) {
24996
25825
  return await withWorkspaceRls(
24997
25826
  db,
24998
25827
  workspaceId,
24999
25828
  async (scopedDb) => await scopedDb.transaction(async (tx) => {
25000
- const locks = await lockSessionEventWriteRows(tx, {
25829
+ const firstLocks = await lockSessionEventWriteRows(tx, {
25001
25830
  workspaceId,
25002
25831
  controlLock: "share",
25003
25832
  sessionIds: [sessionId]
25004
25833
  });
25005
- const sessionRow = locks.sessions[0];
25834
+ const firstSessionRow = firstLocks.sessions.find((row) => row.id === sessionId);
25835
+ if (!firstSessionRow) {
25836
+ throw new Error(`Session not found: ${sessionId}`);
25837
+ }
25838
+ const lockSessionIds = options.lockParentSession ? [sessionId, firstSessionRow.parentSessionId].filter((id) => Boolean(id)) : [sessionId];
25839
+ const locks = lockSessionIds.length === 1 ? firstLocks : await lockSessionEventWriteRows(tx, {
25840
+ workspaceId,
25841
+ controlLock: "already_locked",
25842
+ workspaceLock: "already_locked",
25843
+ sessionIds: lockSessionIds
25844
+ });
25845
+ const sessionRow = locks.sessions.find((row) => row.id === sessionId);
25006
25846
  if (!sessionRow) {
25007
25847
  throw new Error(`Session not found: ${sessionId}`);
25008
25848
  }
@@ -25034,6 +25874,16 @@ async function appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessi
25034
25874
  )
25035
25875
  ).orderBy(asc3(sessionTurns.position), asc3(sessionTurns.createdAt));
25036
25876
  return rows.map(mapSessionTurn);
25877
+ },
25878
+ getLockedSession: async (lockedSessionId) => {
25879
+ const row = locks.sessions.find((candidate) => candidate.id === lockedSessionId);
25880
+ return row ? await mapSessionWithControl(
25881
+ tx,
25882
+ row,
25883
+ [],
25884
+ void 0,
25885
+ locks.control ?? void 0
25886
+ ) : null;
25037
25887
  }
25038
25888
  });
25039
25889
  if (built.events.length === 0) {
@@ -25067,18 +25917,32 @@ async function appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessi
25067
25917
  const inserted = await tx.insert(sessionEvents).values(values).returning();
25068
25918
  const update = built.update ?? {};
25069
25919
  const advancesActivity = sessionMutationAdvancesActivity(update) || sessionEventTypesAdvanceActivity(values);
25070
- await tx.update(sessions).set({
25920
+ const updated = await tx.update(sessions).set({
25071
25921
  lastSequence: sequence,
25072
25922
  ...update.resources !== void 0 ? { resources: update.resources } : {},
25073
25923
  ...update.tools !== void 0 ? { tools: update.tools } : {},
25924
+ ...update.toolPolicy !== void 0 ? { toolPolicy: update.toolPolicy } : {},
25925
+ ...update.toolPolicyVersion !== void 0 ? { toolPolicyVersion: update.toolPolicyVersion } : {},
25074
25926
  ...update.model !== void 0 ? { model: update.model } : {},
25075
25927
  ...update.metadata !== void 0 ? { metadata: update.metadata } : {},
25076
25928
  ...update.status !== void 0 ? { status: update.status } : {},
25077
25929
  ...update.activeTurnId !== void 0 ? { activeTurnId: update.activeTurnId } : {},
25078
25930
  ...advancesActivity ? { updatedAt: now } : {}
25079
25931
  }).where(
25080
- and6(eq6(sessions.workspaceId, workspaceId), eq6(sessions.id, sessionId))
25081
- );
25932
+ and6(
25933
+ eq6(sessions.workspaceId, workspaceId),
25934
+ eq6(sessions.id, sessionId),
25935
+ ...update.expectedToolPolicyVersion !== void 0 ? [eq6(sessions.toolPolicyVersion, update.expectedToolPolicyVersion)] : []
25936
+ )
25937
+ ).returning({ id: sessions.id });
25938
+ if (updated.length === 0) {
25939
+ const [current] = await tx.select({ toolPolicyVersion: sessions.toolPolicyVersion }).from(sessions).where(
25940
+ and6(eq6(sessions.workspaceId, workspaceId), eq6(sessions.id, sessionId))
25941
+ ).limit(1);
25942
+ throw new SessionToolPolicyVersionConflictError(
25943
+ Number(current?.toolPolicyVersion ?? update.expectedToolPolicyVersion ?? 1)
25944
+ );
25945
+ }
25082
25946
  return inserted.map(mapEvent2);
25083
25947
  })
25084
25948
  );
@@ -25119,6 +25983,7 @@ function mapSession(row, effectiveControl, mcpServers = [], pin = mapSessionPin(
25119
25983
  mode: "legacy",
25120
25984
  inheritedFromSessionId: null
25121
25985
  },
25986
+ toolPolicyVersion: Number(row.toolPolicyVersion ?? 1),
25122
25987
  metadata: row.metadata,
25123
25988
  createdBy: initiatorFromStorage(
25124
25989
  row.createdByKind,
@@ -25598,10 +26463,17 @@ function mapGitHubInstallation(row) {
25598
26463
  accountId: row.accountId,
25599
26464
  workspaceId: row.workspaceId,
25600
26465
  installationId: row.installationId,
26466
+ githubAccountId: row.githubAccountId,
25601
26467
  accountLogin: row.accountLogin,
25602
26468
  accountType: row.accountType,
25603
26469
  repositoryScope: row.repositoryScope,
25604
26470
  linkedBySubjectId: row.linkedBySubjectId,
26471
+ githubActorId: row.githubActorId,
26472
+ githubActorLogin: row.githubActorLogin,
26473
+ authorityKind: row.authorityKind,
26474
+ authorityCheckedAt: row.authorityCheckedAt?.toISOString() ?? null,
26475
+ authorityExpiresAt: row.authorityExpiresAt?.toISOString() ?? null,
26476
+ authorityNonce: row.authorityNonce,
25605
26477
  createdAt: row.createdAt.toISOString(),
25606
26478
  updatedAt: row.updatedAt.toISOString()
25607
26479
  };
@@ -25758,6 +26630,8 @@ export {
25758
26630
  CODEX_RESET_REDEMPTION_OUTCOMES,
25759
26631
  CODEX_ROTATION_STRATEGIES,
25760
26632
  ConnectionRefreshHttpError,
26633
+ FORCE_RLS_TABLES,
26634
+ GitHubInstallationAuthorityCommitError,
25761
26635
  HostExportPayloadError,
25762
26636
  HostMcpCredentialBindingError,
25763
26637
  HostMcpCredentialScopeError,
@@ -25779,12 +26653,20 @@ export {
25779
26653
  MEMORY_SEARCH_TOOL_DESCRIPTION,
25780
26654
  MEMORY_TEXT_MAX_CHARS,
25781
26655
  MEMORY_VISIBLE_RECORD_CAP,
26656
+ NON_RLS_RUNTIME_TABLES,
25782
26657
  NewSessionDraftAccessError,
25783
26658
  NewSessionDraftConflictError,
26659
+ PROTECTED_NO_DIRECT_DML_TABLES,
25784
26660
  QueueCommandConflictError,
26661
+ RUNTIME_DML_TABLES,
26662
+ RUNTIME_FULL_DML_TABLES,
26663
+ RUNTIME_READ_INSERT_TABLES,
26664
+ RUNTIME_READ_ONLY_TABLES,
26665
+ RUNTIME_TABLE_PRIVILEGES,
25785
26666
  RigActiveVersionChangedError,
25786
26667
  RigChangeAlreadyVerifyingError,
25787
26668
  RigChangeTransitionError,
26669
+ RuntimeDatabasePostureError,
25788
26670
  SESSION_ANCESTRY_LIMIT,
25789
26671
  SESSION_DISCOVERY_CONTROL_TARGET_LIMIT,
25790
26672
  SESSION_DISCOVERY_CONTROL_TITLE_MAX_CHARS,
@@ -25796,6 +26678,7 @@ export {
25796
26678
  SandboxImageConflictError,
25797
26679
  SandboxLeaseRecoveryBlockedError,
25798
26680
  SandboxLeaseSupersededError,
26681
+ SandboxRetainedProcessPromotionFencedError,
25799
26682
  SandboxRigConflictError,
25800
26683
  SandboxWorkspaceMutationFencedError,
25801
26684
  SanitizedDatabasePersistenceCause,
@@ -25812,6 +26695,7 @@ export {
25812
26695
  SessionPinAccessError,
25813
26696
  SessionPinVersionConflictError,
25814
26697
  SessionSpawnDeniedDbError,
26698
+ SessionToolPolicyVersionConflictError,
25815
26699
  WORKSPACE_MEMORY_BLOCK_EMPTY,
25816
26700
  WORKSPACE_MEMORY_BLOCK_HEADER_POPULATED,
25817
26701
  WORKSPACE_MEMORY_BLOCK_TOKEN_BUDGET,
@@ -25847,9 +26731,11 @@ export {
25847
26731
  areGitHubRepositoriesAllowedForWorkspace,
25848
26732
  armCodexCapacityWait,
25849
26733
  assertAgentCommandAuthorityInTransaction,
26734
+ assertRuntimeDatabasePosture,
25850
26735
  autoResumeSessionBranchInTransaction,
25851
26736
  beginRigChangeVerificationAttempt,
25852
26737
  beginSandboxRematerialization,
26738
+ bindAuthorizedGitHubInstallationRepositories,
25853
26739
  bindGitHubInstallationRepositories,
25854
26740
  bootstrapWorkspace,
25855
26741
  buildChildCompletionDigest,
@@ -25864,6 +26750,7 @@ export {
25864
26750
  claimPendingSessionSystemUpdateOutbox,
25865
26751
  claimPendingSessionWorkflowWakes,
25866
26752
  claimSessionWorkForAttempt,
26753
+ claimTerminalRetainedProcesses,
25867
26754
  clearDurablePendingSessionToolCalls,
25868
26755
  clearEnrollmentWentOffline,
25869
26756
  clearPendingSessionToolspaceCall,
@@ -25885,11 +26772,13 @@ export {
25885
26772
  consumeNewSessionDraftInTransaction,
25886
26773
  correctWorkspaceMemory,
25887
26774
  countActiveApiKeysForWorkspace,
26775
+ countActiveRetainedProcessesByOwnerState,
25888
26776
  countActiveSessionHistoryItems,
25889
26777
  countActiveSessionsForWorkspace,
25890
26778
  countActiveSessionsUsingEnvironment,
25891
26779
  countActiveSessionsUsingVariableSet,
25892
26780
  countConsecutiveReactiveRotations,
26781
+ countExpiredDrainingSandboxLeases,
25893
26782
  countQueuedTurns,
25894
26783
  countRigs,
25895
26784
  countSandboxLeasesByLiveness,
@@ -25933,6 +26822,7 @@ export {
25933
26822
  decryptEnvironmentValue,
25934
26823
  decryptEnvironmentValue as decryptVariableSetValue,
25935
26824
  decryptedCapabilityHeaders,
26825
+ deferRetainedProcessReconciliation,
25936
26826
  deleteGitHubInstallationBinding,
25937
26827
  deleteRecording,
25938
26828
  deleteRig,
@@ -25965,6 +26855,7 @@ export {
25965
26855
  ensureManagedAccessForUser,
25966
26856
  estimateMemoryTokens,
25967
26857
  evaluateGoalContinuation,
26858
+ evaluateRuntimeDatabasePosture,
25968
26859
  evaluateSessionControl,
25969
26860
  evaluateSessionControls,
25970
26861
  evaluateSessionDiscoveryControls,
@@ -26057,6 +26948,7 @@ export {
26057
26948
  getWorkspaceModelPolicy,
26058
26949
  getWorkspacePack,
26059
26950
  grantWorkspaceAccess,
26951
+ hasAuditableGitHubInstallationAuthority,
26060
26952
  hasCreditLedgerEntry,
26061
26953
  hashMemoryText,
26062
26954
  heartbeatCodexCredentialLease,
@@ -26069,6 +26961,7 @@ export {
26069
26961
  insertPtySession,
26070
26962
  insertRecording,
26071
26963
  insertWorkspaceCapture,
26964
+ inspectRuntimeDatabasePosture,
26072
26965
  installOrReadTurnExecutionPolicyForAttempt,
26073
26966
  interruptedToolCallResult,
26074
26967
  isCodexBilledModel2 as isCodexBilledModel,
@@ -26162,6 +27055,7 @@ export {
26162
27055
  mutateSessionControlInTransaction,
26163
27056
  mutateWorkspaceControlInTransaction,
26164
27057
  nestedPostgresSqlState,
27058
+ newSessionDraftToolsProvided,
26165
27059
  nextSessionHistoryPosition,
26166
27060
  normalizeBearerScheme,
26167
27061
  normalizeMemoryText,
@@ -26174,6 +27068,7 @@ export {
26174
27068
  projectSessionForRelatedAccess,
26175
27069
  provisionRoles,
26176
27070
  pruneHostExportOutbox,
27071
+ publicNewSessionDraftOptions,
26177
27072
  quarantineCodexCredentialForLease,
26178
27073
  reArmDrainingLease,
26179
27074
  readActiveSandbox,
@@ -26186,6 +27081,7 @@ export {
26186
27081
  reapStaleLeaseHolders,
26187
27082
  reapStaleLeaseHoldersGlobal,
26188
27083
  reconcileCodexCapacityWait,
27084
+ reconcileColdLostLeaseInstanceBlockers,
26189
27085
  recordAuditEvent,
26190
27086
  recordCodexAccountConnectors,
26191
27087
  recordCodexAccountUsage,
@@ -26196,6 +27092,7 @@ export {
26196
27092
  recordLeaseDataPlaneUrl,
26197
27093
  recordLeaseTerminalDataPlaneUrl,
26198
27094
  recordPendingSessionToolCallResult,
27095
+ recordRetainedProcessReconciliationProof,
26199
27096
  recordSessionActiveCodexCredential,
26200
27097
  recordSkippedContextCompaction,
26201
27098
  recordStreamAcknowledgment,
@@ -26230,6 +27127,8 @@ export {
26230
27127
  resolveWorkspaceMemoryBlock,
26231
27128
  resumeHostExportConsumer,
26232
27129
  retainWorkspaceMutationProcess,
27130
+ retainedProcessReconciliationProof,
27131
+ retainedProcessSettlementIdentity,
26233
27132
  retireHostExportConsumer,
26234
27133
  revokeApiKey,
26235
27134
  revokeConnection,
@@ -26241,6 +27140,7 @@ export {
26241
27140
  rotateWorkspaceArchives,
26242
27141
  runIdempotentPersistenceTransaction,
26243
27142
  runMigrations,
27143
+ runtimeDatabaseReadyCheck,
26244
27144
  safeDatabaseErrorFacts,
26245
27145
  sanitizeEventPayload,
26246
27146
  sanitizeEventString,
@@ -26251,6 +27151,7 @@ export {
26251
27151
  saveRunState,
26252
27152
  saveWorkspaceMemory,
26253
27153
  searchWorkspaceMemories,
27154
+ seedNewSessionDraftInTransaction,
26254
27155
  sendAgentMessageInTransaction,
26255
27156
  serializeEffectiveSessionControl,
26256
27157
  sessionAuthorizationScopeFilter,