deepline 0.3.174 → 0.3.176

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.
@@ -25,6 +25,13 @@ import {
25
25
  type PhysicalSheetColumnProjection,
26
26
  } from '../play-data-plane/sheet-contract';
27
27
  import { RUNTIME_SHEET_CELL_META_COLUMN } from '../play-data-plane/runtime-sheet-storage-contract';
28
+ import {
29
+ createRuntimeSheetOrm,
30
+ isRuntimeSheetOwnerMetadataConstraintError,
31
+ RUNTIME_SHEET_ATTEMPT_COLUMNS,
32
+ RUNTIME_SHEET_TIMESTAMP_COLUMNS,
33
+ RUNTIME_SHEET_VERSION_SEQUENCE,
34
+ } from '../play-data-plane/runtime-sheet-orm';
28
35
  import type {
29
36
  CreateDbSessionResponse,
30
37
  DbLogicalTable,
@@ -316,13 +323,6 @@ const APPEND_KEY_SUFFIX_LENGTH = 12;
316
323
  // 60-min alignment fixed. A shorter TTL only causes periodic re-mints (cheap;
317
324
  // the tenant runtime role is long-lived), not run failures.
318
325
  const RUNTIME_DB_SESSION_TTL_SECONDS = 10 * 60;
319
- const RUNTIME_SHEET_ATTEMPT_COLUMNS = [
320
- '_attempt_id',
321
- '_attempt_owner_run_id',
322
- '_attempt_seq',
323
- '_attempt_expires_at',
324
- ] as const;
325
- const RUNTIME_SHEET_TIMESTAMP_COLUMNS = ['_created_at', '_updated_at'] as const;
326
326
  const RUNTIME_SHEET_ENSURE_CACHE_TTL_MS = 10 * 60_000;
327
327
  const RUNTIME_SHEET_READY_AFTER_ENSURE_RETRY_DELAYS_MS = [
328
328
  100, 250, 500, 1_000, 2_000,
@@ -519,6 +519,7 @@ type RuntimeApiActionRequest =
519
519
  tableNamespace: string;
520
520
  sheetContract?: PlaySheetContract | null;
521
521
  userEmail?: string | null;
522
+ forceRepair?: boolean;
522
523
  }
523
524
  | {
524
525
  action: 'create_db_session';
@@ -1135,62 +1136,21 @@ async function isRuntimeSheetSchemaReady(
1135
1136
  FROM ${sheetTable(session)}
1136
1137
  LIMIT 0`,
1137
1138
  );
1138
- const attemptColumns = await client.query<Record<string, unknown>>(
1139
- `
1140
- SELECT count(*)::int AS present_count
1141
- FROM information_schema.columns
1142
- WHERE table_schema = $1
1143
- AND table_name = $2
1144
- AND column_name = ANY($3::text[])
1145
- `,
1146
- [
1147
- session.postgres.schema,
1148
- session.postgres.sheetTable,
1149
- [...RUNTIME_SHEET_ATTEMPT_COLUMNS],
1150
- ],
1151
- );
1152
- const timestampColumns = await client.query<Record<string, unknown>>(
1153
- `
1154
- SELECT count(*)::int AS ready_count
1155
- FROM information_schema.columns
1156
- WHERE table_schema = $1
1157
- AND table_name = $2
1158
- AND column_name = ANY($3::text[])
1159
- AND data_type = 'timestamp with time zone'
1160
- AND is_nullable = 'NO'
1161
- AND column_default IS NOT NULL
1162
- `,
1163
- [
1164
- session.postgres.schema,
1165
- session.postgres.sheetTable,
1166
- [...RUNTIME_SHEET_TIMESTAMP_COLUMNS],
1167
- ],
1168
- );
1169
- const cellMetaColumn = await client.query<Record<string, unknown>>(
1170
- `
1171
- SELECT count(*)::int AS ready_count
1172
- FROM information_schema.columns
1173
- WHERE table_schema = $1
1174
- AND table_name = $2
1175
- AND column_name = $3
1176
- AND data_type = $4
1177
- AND is_nullable = 'NO'
1178
- AND column_default IS NOT NULL
1179
- `,
1180
- [
1181
- session.postgres.schema,
1182
- session.postgres.sheetTable,
1183
- RUNTIME_SHEET_CELL_META_COLUMN.name,
1184
- RUNTIME_SHEET_CELL_META_COLUMN.dataType,
1185
- ],
1186
- );
1187
- return (
1188
- Number(attemptColumns.rows[0]?.present_count ?? 0) ===
1189
- RUNTIME_SHEET_ATTEMPT_COLUMNS.length &&
1190
- Number(timestampColumns.rows[0]?.ready_count ?? 0) ===
1191
- RUNTIME_SHEET_TIMESTAMP_COLUMNS.length &&
1192
- Number(cellMetaColumn.rows[0]?.ready_count ?? 0) === 1
1193
- );
1139
+ const orm = createRuntimeSheetOrm({
1140
+ schema: session.postgres.schema,
1141
+ table: session.postgres.sheetTable,
1142
+ query: async <Row = Record<string, unknown>>(
1143
+ sql: string,
1144
+ params?: readonly unknown[],
1145
+ ): Promise<{ rows: Row[] }> => {
1146
+ const result = await client.query<Row & Record<string, unknown>>(
1147
+ sql,
1148
+ params === undefined ? undefined : [...params],
1149
+ );
1150
+ return { rows: result.rows as Row[] };
1151
+ },
1152
+ });
1153
+ return (await orm.readiness()).ready;
1194
1154
  };
1195
1155
  try {
1196
1156
  if (isRuntimeOneShotQueryFactoryRegistered()) {
@@ -1250,15 +1210,31 @@ async function readRuntimeSheetSchemaDiagnostic(
1250
1210
  `${quoteIdentifier(session.postgres.schema)}.${quoteIdentifier(session.postgres.sheetTable)}`,
1251
1211
  ],
1252
1212
  );
1213
+ const orm = createRuntimeSheetOrm({
1214
+ schema: session.postgres.schema,
1215
+ table: session.postgres.sheetTable,
1216
+ query: async <Row = Record<string, unknown>>(
1217
+ sql: string,
1218
+ params?: readonly unknown[],
1219
+ ): Promise<{ rows: Row[] }> => {
1220
+ const result = await client.query<Row & Record<string, unknown>>(
1221
+ sql,
1222
+ params === undefined ? undefined : [...params],
1223
+ );
1224
+ return { rows: result.rows as Row[] };
1225
+ },
1226
+ });
1227
+ const ownerSchema = await orm.inspect();
1228
+ const ownerReadiness = await orm.readiness(ownerSchema);
1253
1229
  const visible = new Set(visibleColumns.rows.map((row) => row.column_name));
1254
1230
  const timestampColumns = new Map(
1255
- visibleColumns.rows
1256
- .filter((row) =>
1257
- RUNTIME_SHEET_TIMESTAMP_COLUMNS.includes(
1258
- row.column_name as (typeof RUNTIME_SHEET_TIMESTAMP_COLUMNS)[number],
1259
- ),
1260
- )
1261
- .map((row) => [row.column_name, row]),
1231
+ [...ownerSchema.columns.entries()].filter(([column]) =>
1232
+ RUNTIME_SHEET_TIMESTAMP_COLUMNS.includes(column),
1233
+ ),
1234
+ );
1235
+ const versionColumn = ownerSchema.columns.get('_version');
1236
+ const cellMetaColumn = ownerSchema.columns.get(
1237
+ RUNTIME_SHEET_CELL_META_COLUMN.name,
1262
1238
  );
1263
1239
  return {
1264
1240
  schema: session.postgres.schema,
@@ -1268,6 +1244,16 @@ async function readRuntimeSheetSchemaDiagnostic(
1268
1244
  canSelect: privileges.rows[0]?.can_select ?? null,
1269
1245
  canInsert: privileges.rows[0]?.can_insert ?? null,
1270
1246
  canUpdate: privileges.rows[0]?.can_update ?? null,
1247
+ ownerSchemaReady: ownerReadiness.ready,
1248
+ ownerSchemaMissingColumns: ownerReadiness.missingColumns,
1249
+ ownerSchemaIncompatibleColumns: ownerReadiness.incompatibleColumns,
1250
+ ownerSchemaMissingDefaults: ownerReadiness.missingDefaults,
1251
+ versionSequenceExists: ownerSchema.sequenceExists,
1252
+ versionColumnState: {
1253
+ dataType: versionColumn?.data_type ?? null,
1254
+ isNullable: versionColumn?.is_nullable ?? null,
1255
+ hasDefault: versionColumn?.column_default != null,
1256
+ },
1271
1257
  visibleColumnCount: visible.size,
1272
1258
  missingAttemptColumns: RUNTIME_SHEET_ATTEMPT_COLUMNS.filter(
1273
1259
  (column) => !visible.has(column),
@@ -1282,14 +1268,10 @@ async function readRuntimeSheetSchemaDiagnostic(
1282
1268
  };
1283
1269
  }),
1284
1270
  cellMetaColumnState: (() => {
1285
- const state = visibleColumns.rows.find(
1286
- (column) =>
1287
- column.column_name === RUNTIME_SHEET_CELL_META_COLUMN.name,
1288
- );
1289
1271
  return {
1290
- dataType: state?.data_type ?? null,
1291
- isNullable: state?.is_nullable ?? null,
1292
- hasDefault: state?.column_default != null,
1272
+ dataType: cellMetaColumn?.data_type ?? null,
1273
+ isNullable: cellMetaColumn?.is_nullable ?? null,
1274
+ hasDefault: cellMetaColumn?.column_default != null,
1293
1275
  };
1294
1276
  })(),
1295
1277
  };
@@ -2483,6 +2465,7 @@ async function withRuntimeSheetProvisioningRetry<T>(
2483
2465
  playName: string;
2484
2466
  tableNamespace: string;
2485
2467
  sheetContract: PlaySheetContract;
2468
+ session: RuntimePostgresSession;
2486
2469
  timings?: RuntimeSheetTiming[];
2487
2470
  },
2488
2471
  operation: () => Promise<T>,
@@ -2505,12 +2488,33 @@ async function withRuntimeSheetProvisioningRetry<T>(
2505
2488
  });
2506
2489
  if (isMissingRelationError(error)) {
2507
2490
  const ensureStartedAt = Date.now();
2508
- await ensureRuntimeSheet(context, input);
2491
+ await ensureRuntimeSheet(context, {
2492
+ playName: input.playName,
2493
+ tableNamespace: input.tableNamespace,
2494
+ sheetContract: input.sheetContract,
2495
+ });
2509
2496
  input.timings?.push({
2510
2497
  phase: 'ensure_sheet_after_provisioning_error',
2511
2498
  ms: Date.now() - ensureStartedAt,
2512
2499
  retried: true,
2513
2500
  });
2501
+ } else if (
2502
+ isRuntimeSheetOwnerMetadataConstraintError(error, {
2503
+ table: input.session.postgres.sheetTable,
2504
+ })
2505
+ ) {
2506
+ const ensureStartedAt = Date.now();
2507
+ await ensureRuntimeSheet(context, {
2508
+ playName: input.playName,
2509
+ tableNamespace: input.tableNamespace,
2510
+ sheetContract: input.sheetContract,
2511
+ forceRepair: true,
2512
+ });
2513
+ input.timings?.push({
2514
+ phase: 'force_repair_sheet_after_metadata_constraint_error',
2515
+ ms: Date.now() - ensureStartedAt,
2516
+ retried: true,
2517
+ });
2514
2518
  } else if (isPostgresPermissionDeniedError(error)) {
2515
2519
  const repairStartedAt = Date.now();
2516
2520
  await repairRuntimeStorageGrants(context, {
@@ -2551,7 +2555,7 @@ async function withRuntimeSheetQueryClient<T>(
2551
2555
  const totalStartedAt = Date.now();
2552
2556
  const result = await withRuntimeSheetProvisioningRetry(
2553
2557
  context,
2554
- input,
2558
+ { ...input, session },
2555
2559
  async () => {
2556
2560
  if (
2557
2561
  !input.transactional &&
@@ -2865,12 +2869,10 @@ async function withRuntimeWorkReceiptClient<T>(
2865
2869
  throw new Error('Runtime work receipt query failed after retries.');
2866
2870
  }
2867
2871
 
2868
- const PLAY_INTERNAL_SHEET_VERSION_SEQUENCE = '_deepline_sheet_version_seq';
2869
-
2870
2872
  function nextRuntimeSheetVersionExpression(
2871
2873
  session: RuntimePostgresSession,
2872
2874
  ): string {
2873
- return `nextval(${quoteLiteral(`${session.postgres.schema}.${PLAY_INTERNAL_SHEET_VERSION_SEQUENCE}`)}::regclass)`;
2875
+ return `nextval(${quoteLiteral(`${session.postgres.schema}.${RUNTIME_SHEET_VERSION_SEQUENCE}`)}::regclass)`;
2874
2876
  }
2875
2877
 
2876
2878
  function missingOutputCellSql(
@@ -3476,6 +3478,23 @@ async function writeRuntimeRows(
3476
3478
  });
3477
3479
  }
3478
3480
 
3481
+ async function writeRuntimeRowsWithProvisioningRetry(
3482
+ context: RuntimeApiContext,
3483
+ session: RuntimePostgresSession,
3484
+ input: Parameters<typeof writeRuntimeRows>[1],
3485
+ ): Promise<RowsWriteResponse> {
3486
+ return await withRuntimeSheetProvisioningRetry(
3487
+ context,
3488
+ {
3489
+ playName: session.playName,
3490
+ tableNamespace: input.tableNamespace,
3491
+ sheetContract: input.sheetContract,
3492
+ session,
3493
+ },
3494
+ () => writeRuntimeRows(session, input),
3495
+ );
3496
+ }
3497
+
3479
3498
  export async function resolveRuntimeReferencedPlay(
3480
3499
  context: RuntimeApiContext,
3481
3500
  playRef: string,
@@ -3509,6 +3528,7 @@ export async function ensureRuntimeSheet(
3509
3528
  playName: string;
3510
3529
  tableNamespace: string;
3511
3530
  sheetContract: PlaySheetContract;
3531
+ forceRepair?: boolean;
3512
3532
  },
3513
3533
  ): Promise<void> {
3514
3534
  const request = {
@@ -3516,6 +3536,7 @@ export async function ensureRuntimeSheet(
3516
3536
  ...input,
3517
3537
  runId: context.runId ?? null,
3518
3538
  userEmail: normalizeRuntimeUserEmail(context.userEmail),
3539
+ ...(input.forceRepair === true ? { forceRepair: true } : {}),
3519
3540
  };
3520
3541
  try {
3521
3542
  await postRuntimeApi<{ ok: true }>(context, request);
@@ -9124,18 +9145,22 @@ export async function upsertRuntimeRows(
9124
9145
  },
9125
9146
  );
9126
9147
  return rowsWriteResponseSchema.parse(
9127
- await writeRuntimeRows(requireRuntimePostgresSession(session), {
9128
- tableNamespace: input.tableNamespace,
9129
- rows: input.rows,
9130
- runId: input.runId,
9131
- sheetContract: input.sheetContract,
9132
- idempotencyKey: createHash('sha1')
9133
- .update(
9134
- `${input.playName}:${input.tableNamespace}:${input.runId}:${JSON.stringify(input.rows)}`,
9135
- )
9136
- .digest('hex'),
9137
- mode: 'upsert',
9138
- }),
9148
+ await writeRuntimeRowsWithProvisioningRetry(
9149
+ context,
9150
+ requireRuntimePostgresSession(session),
9151
+ {
9152
+ tableNamespace: input.tableNamespace,
9153
+ rows: input.rows,
9154
+ runId: input.runId,
9155
+ sheetContract: input.sheetContract,
9156
+ idempotencyKey: createHash('sha1')
9157
+ .update(
9158
+ `${input.playName}:${input.tableNamespace}:${input.runId}:${JSON.stringify(input.rows)}`,
9159
+ )
9160
+ .digest('hex'),
9161
+ mode: 'upsert',
9162
+ },
9163
+ ),
9139
9164
  );
9140
9165
  }
9141
9166
 
@@ -9170,14 +9195,18 @@ export async function appendRuntimeRows(
9170
9195
  },
9171
9196
  );
9172
9197
  return rowsWriteResponseSchema.parse(
9173
- await writeRuntimeRows(requireRuntimePostgresSession(session), {
9174
- tableNamespace: input.tableNamespace,
9175
- rows: input.rows,
9176
- runId: input.runId,
9177
- sheetContract: input.sheetContract,
9178
- idempotencyKey: input.idempotencyKey,
9179
- mode: 'append',
9180
- }),
9198
+ await writeRuntimeRowsWithProvisioningRetry(
9199
+ context,
9200
+ requireRuntimePostgresSession(session),
9201
+ {
9202
+ tableNamespace: input.tableNamespace,
9203
+ rows: input.rows,
9204
+ runId: input.runId,
9205
+ sheetContract: input.sheetContract,
9206
+ idempotencyKey: input.idempotencyKey,
9207
+ mode: 'append',
9208
+ },
9209
+ ),
9181
9210
  );
9182
9211
  }
9183
9212
 
@@ -9212,14 +9241,18 @@ export async function replaceRuntimeRows(
9212
9241
  },
9213
9242
  );
9214
9243
  return rowsWriteResponseSchema.parse(
9215
- await writeRuntimeRows(requireRuntimePostgresSession(session), {
9216
- tableNamespace: input.tableNamespace,
9217
- rows: input.rows,
9218
- runId: input.runId,
9219
- sheetContract: input.sheetContract,
9220
- idempotencyKey: input.idempotencyKey,
9221
- mode: 'replace',
9222
- }),
9244
+ await writeRuntimeRowsWithProvisioningRetry(
9245
+ context,
9246
+ requireRuntimePostgresSession(session),
9247
+ {
9248
+ tableNamespace: input.tableNamespace,
9249
+ rows: input.rows,
9250
+ runId: input.runId,
9251
+ sheetContract: input.sheetContract,
9252
+ idempotencyKey: input.idempotencyKey,
9253
+ mode: 'replace',
9254
+ },
9255
+ ),
9223
9256
  );
9224
9257
  }
9225
9258
 
@@ -14,33 +14,27 @@
14
14
  * Persist the absurd task id
15
15
  * on the run row so a later cancel can target it.
16
16
  * observe/result → reuse the postgres LISTEN/NOTIFY run handle.
17
- * cancel → durable Absurd cancellation lane + bounded postgres terminal
18
- * projection (with worker reconciliation when locks are busy).
17
+ * cancel → one bounded transaction cancels the Absurd task and terminalizes
18
+ * the postgres scheduler run. Success means both states committed.
19
19
  * signal → durable signal row (authoritative payload) + absurd emitEvent
20
20
  * (the wakeup only; payload is re-read from the signal row).
21
21
  */
22
22
  import { recordPerfTrace } from '../perf-trace';
23
23
  import { CTX_RUN_PLAY_INLINE_ONLY } from '../child-execution-strategy';
24
24
  import { PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES } from '../admission-transport-policy';
25
- import {
26
- defaultRuntimeSandboxReconciliation,
27
- runtimeSandboxDeleteSucceeded,
28
- runtimeSandboxRefIdentityKey,
29
- runtimeSandboxRefFromResource,
30
- type RuntimeSandboxRef,
31
- } from '../runner-backends/runtime-sandbox-reconciliation';
32
25
  import {
33
26
  PLAY_SCHEDULER_BACKENDS,
34
- RUNTIME_SCHEDULER_CANCELLATION_PENDING_CODE,
35
27
  type PlaySchedulerBackend,
36
28
  type PlaySchedulerRunHandle,
37
29
  type PlaySchedulerSubmitInput,
38
30
  } from '../scheduler-backend';
39
31
  import {
32
+ cancelPostgresSchedulerRun,
33
+ postgresSchedulerCancellationOutboxLease,
40
34
  postgresSchedulerRunHandle,
41
35
  readPostgresSchedulerLaunch,
42
36
  readPostgresSchedulerAbsurdTaskId,
43
- readPostgresSchedulerRuntimeResourcesForRun,
37
+ readPostgresSchedulerResult,
44
38
  recordPostgresSchedulerSignal,
45
39
  } from './postgres';
46
40
  import {
@@ -57,7 +51,6 @@ import { withRuntimeSchedulerSubmitAdmission } from './submit-admission';
57
51
  import {
58
52
  ABSURD_PLAY_RUN_MAX_ATTEMPTS,
59
53
  ABSURD_PLAY_RUN_RETRY_STRATEGY,
60
- ABSURD_DAYTONA_SANDBOX_CLEANUP_TASK,
61
54
  absurdIntegrationEventName,
62
55
  absurdQueueNameForRelease,
63
56
  absurdReleaseIdForLaunch,
@@ -67,7 +60,6 @@ import {
67
60
  normalizeAbsurdSchedulerOptions,
68
61
  resolveAbsurdPoolOptions,
69
62
  serverlessAdmissionSubmitMaxActive,
70
- type AbsurdDaytonaSandboxCleanupTaskParams,
71
63
  type AbsurdSchedulerOptions,
72
64
  } from './absurd-shared';
73
65
 
@@ -162,168 +154,98 @@ function absurdRunHandle(input: {
162
154
  const runsQueue = absurdQueueNameForRelease(releaseId);
163
155
  const cancellationKey = `${poolOptions.url}::${effectiveOptions.schema ?? ''}::${input.runId}`;
164
156
 
165
- const cancelAbsurdTask = async () => {
166
- const taskId = await withRuntimeSchedulerClient(poolOptions, (client) =>
167
- readPostgresSchedulerAbsurdTaskId(client, input.runId, effectiveOptions),
168
- );
169
- if (!taskId) return;
170
- // Keep cancellation on the same bounded control-plane contract as the
171
- // scheduler terminal write. This is a durable lane transition, not an
172
- // unbounded best-effort query which can pile up behind a worker lock.
173
- await withRuntimeSchedulerTransaction(
157
+ const cancelAbsurdRunSynchronously = async (reason?: string) =>
158
+ withRuntimeSchedulerTransaction(
174
159
  poolOptions,
175
160
  {
176
161
  lockTimeoutMs: RUNTIME_SCHEDULER_CANCEL_LOCK_TIMEOUT_MS,
177
162
  statementTimeoutMs: RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS,
178
163
  },
179
164
  async (client) => {
180
- await client.query(`SELECT absurd.cancel_task($1, $2::uuid)`, [
181
- runsQueue,
182
- taskId,
183
- ]);
184
- // Older v4 Absurd functions do not materialize a manual cancellation
185
- // deadline or reason. Stamp both in the adapter so existing queues
186
- // participate in the same durable reconciliation lane without a
187
- // schema cutover. The explicit marker keeps a user cancel distinct
188
- // from a task cancelled by its configured max_delay sweep.
189
- await client.query(
190
- `UPDATE absurd.${quotePostgresIdentifier(`t_${runsQueue}`)}
165
+ // Resolve the immutable release lane inside the same checkout and
166
+ // transaction that performs cancellation. The stop endpoint therefore
167
+ // has no preliminary lane lookup that can succeed or fail separately.
168
+ const launch = await readPostgresSchedulerLaunch(
169
+ client,
170
+ input.runId,
171
+ effectiveOptions,
172
+ { caller: 'play_run_control' },
173
+ );
174
+ if (!launch) {
175
+ throw new Error(
176
+ `Cannot cancel play run "${input.runId}": scheduler launch is missing.`,
177
+ );
178
+ }
179
+ const cancellationReleaseId = absurdReleaseIdForLaunch(launch);
180
+ const cancellationQueue = absurdQueueNameForRelease(
181
+ cancellationReleaseId,
182
+ );
183
+ const taskId = await readPostgresSchedulerAbsurdTaskId(
184
+ client,
185
+ input.runId,
186
+ effectiveOptions,
187
+ );
188
+ if (taskId) {
189
+ await client.query(`SELECT absurd.cancel_task($1, $2::uuid)`, [
190
+ cancellationQueue,
191
+ taskId,
192
+ ]);
193
+ // Older v4 Absurd functions do not materialize a manual cancellation
194
+ // deadline or reason. Stamp both in the adapter so existing queues
195
+ // participate in the same durable reconciliation lane without a
196
+ // schema cutover. The explicit marker keeps a user cancel distinct
197
+ // from a task cancelled by its configured max_delay sweep.
198
+ await client.query(
199
+ `UPDATE absurd.${quotePostgresIdentifier(`t_${cancellationQueue}`)}
191
200
  SET cancel_at = COALESCE(cancel_at, clock_timestamp()),
192
201
  cancellation = COALESCE(cancellation, '{}'::jsonb)
193
202
  || jsonb_build_object('manual', true)
194
203
  WHERE task_id = $1::uuid
195
204
  AND state = 'cancelled'`,
196
- [taskId],
205
+ [taskId],
206
+ );
207
+ }
208
+ const cancelled = await cancelPostgresSchedulerRun(
209
+ client,
210
+ {
211
+ runId: input.runId,
212
+ reason,
213
+ outboxLease: postgresSchedulerCancellationOutboxLease(input.runId),
214
+ },
215
+ effectiveOptions,
197
216
  );
217
+ // The update returning a row is proof that this transaction wrote the
218
+ // terminal state and outbox event. Only the idempotent retry/race path
219
+ // needs one read to prove another terminal writer already won.
220
+ if (!cancelled) {
221
+ const terminal = await readPostgresSchedulerResult(
222
+ client,
223
+ input.runId,
224
+ effectiveOptions,
225
+ );
226
+ if (!terminal) {
227
+ throw new Error(
228
+ `Cancellation did not terminalize scheduler run "${input.runId}".`,
229
+ );
230
+ }
231
+ }
198
232
  },
199
233
  {
200
234
  runId: input.runId,
201
- phase: 'cancel_engine_task',
235
+ phase: 'cancel_run_synchronously',
202
236
  priority: 'critical',
203
237
  },
204
- { maxAttempts: 1 },
205
238
  );
206
- };
207
-
208
- const cancellationPendingError = (cause: unknown): Error => {
209
- const error = new Error(
210
- `Run cancellation was durably requested, but scheduler terminalization is pending for ${input.runId}.`,
211
- );
212
- Object.assign(error, {
213
- code: RUNTIME_SCHEDULER_CANCELLATION_PENDING_CODE,
214
- cause,
215
- });
216
- return error;
217
- };
218
239
 
219
240
  const requestCancellation = async (reason?: string) => {
220
241
  await runCancellationSingleFlight(cancellationKey, async () => {
221
- // First move the durable engine task into its cancellation lane. If the
222
- // scheduler tables are briefly locked, the worker reconciler can finish
223
- // the terminal write later without another user request piling up.
224
- await cancelAbsurdTask();
225
- try {
226
- await delegate.cancel(reason);
227
- } catch (error) {
228
- throw cancellationPendingError(error);
229
- }
230
- try {
231
- await cleanupPersistedRuntimeSandboxes();
232
- } catch (error) {
233
- // The scheduler terminal transaction already won. Its cleanup jobs are
234
- // durable, so a best-effort eager cleanup failure must not turn a real
235
- // cancellation into a false 502 or cause the caller to retry the
236
- // terminal write.
237
- console.warn('[absurd.runtime_sandbox_cleanup_deferred]', {
238
- runId: input.runId,
239
- error: error instanceof Error ? error.message : String(error),
240
- });
241
- }
242
+ // Engine cancellation and the scheduler terminal write commit together.
243
+ // A timeout rolls both back, so returning means the run is terminal and
244
+ // throwing means the caller can safely retry one unchanged operation.
245
+ await cancelAbsurdRunSynchronously(reason);
242
246
  });
243
247
  };
244
248
 
245
- const cleanupPersistedRuntimeSandboxes = async () => {
246
- const resources = await withRuntimeSchedulerClient(poolOptions, (client) =>
247
- readPostgresSchedulerRuntimeResourcesForRun(
248
- client,
249
- { runId: input.runId },
250
- effectiveOptions,
251
- ),
252
- );
253
- const productionRefs = new Map<string, RuntimeSandboxRef>();
254
- const workerOwnedRefs = new Map<string, RuntimeSandboxRef>();
255
- for (const resource of resources) {
256
- if (
257
- !resource ||
258
- typeof resource !== 'object' ||
259
- Array.isArray(resource)
260
- ) {
261
- continue;
262
- }
263
- const record = resource as Record<string, unknown>;
264
- const ref = runtimeSandboxRefFromResource(record);
265
- if (!ref) continue;
266
- const refKey = runtimeSandboxRefIdentityKey(ref);
267
- // Modal credentials and all preview provider credentials are worker-only.
268
- // This explicit task keeps deletion on the owning release lane. Legacy
269
- // Daytona records predate runtimeEnvironment, so retain their marker.
270
- const runtimeEnvironment =
271
- record.runtimeEnvironment === 'preview' ||
272
- record.daytonaEnvironment === 'preview'
273
- ? 'preview'
274
- : 'production';
275
- if (runtimeEnvironment === 'preview' || ref.provider === 'modal') {
276
- workerOwnedRefs.set(refKey, ref);
277
- continue;
278
- }
279
- productionRefs.set(refKey, ref);
280
- }
281
- if (workerOwnedRefs.size > 0) {
282
- const absurd = createAbsurdRuntimeClient(poolOptions, runsQueue);
283
- await Promise.all(
284
- [...workerOwnedRefs.values()].map((runtimeSandboxRef) => {
285
- // sandboxId remains in the payload for workers deployed before the
286
- // provider-scoped cleanup cutover. New workers use the versioned ref.
287
- const params: AbsurdDaytonaSandboxCleanupTaskParams & {
288
- runtimeSandboxRef: RuntimeSandboxRef;
289
- } = {
290
- runId: input.runId,
291
- sandboxId: runtimeSandboxRef.resourceId,
292
- runtimeSandboxRef,
293
- };
294
- return absurd.spawn(ABSURD_DAYTONA_SANDBOX_CLEANUP_TASK, params, {
295
- queue: runsQueue,
296
- idempotencyKey: `sandbox-cleanup:${input.runId}:${runtimeSandboxRefIdentityKey(runtimeSandboxRef)}`,
297
- maxAttempts: ABSURD_PLAY_RUN_MAX_ATTEMPTS,
298
- retryStrategy: ABSURD_PLAY_RUN_RETRY_STRATEGY,
299
- });
300
- }),
301
- );
302
- }
303
- if (productionRefs.size === 0) return;
304
- const cleanupResults = await Promise.allSettled(
305
- [...productionRefs.values()].map(async (ref) => {
306
- const outcome = await defaultRuntimeSandboxReconciliation.delete(ref);
307
- if (!runtimeSandboxDeleteSucceeded(outcome)) {
308
- throw new Error(
309
- `${ref.provider} sandbox cleanup did not delete ${ref.resourceId}.`,
310
- );
311
- }
312
- }),
313
- );
314
- for (const result of cleanupResults) {
315
- if (result.status === 'rejected') {
316
- console.warn('[absurd.runtime_sandbox_cleanup_failed]', {
317
- runId: input.runId,
318
- error:
319
- result.reason instanceof Error
320
- ? result.reason.message
321
- : String(result.reason),
322
- });
323
- }
324
- }
325
- };
326
-
327
249
  return {
328
250
  ...delegate,
329
251
  runId: input.runId,