deepline 0.3.200 → 0.3.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +62 -1
  2. package/dist/bundling-sources/sdk/src/http.ts +55 -6
  3. package/dist/bundling-sources/sdk/src/index.ts +1 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/types.ts +10 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
  10. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-deadline-reconciliation.ts +160 -58
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +111 -6
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +51 -46
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +304 -33
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +133 -5
  26. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
  30. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
  31. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
  32. package/dist/cli/index.js +364 -39
  33. package/dist/cli/index.mjs +367 -41
  34. package/dist/index.d.mts +131 -3
  35. package/dist/index.d.ts +131 -3
  36. package/dist/index.js +64 -7
  37. package/dist/index.mjs +64 -7
  38. package/dist/install-integrity.json +3 -0
  39. package/dist/release.d.mts +1 -1
  40. package/dist/release.d.ts +1 -1
  41. package/dist/release.js +1 -1
  42. package/dist/release.mjs +1 -1
  43. package/package.json +1 -1
@@ -1,4 +1,10 @@
1
- import { cancelPostgresSchedulerRun } from './postgres';
1
+ import {
2
+ abandonUnstartedModalCancellationReservations,
3
+ cancelPostgresSchedulerRun,
4
+ enqueuePostgresSchedulerSandboxCleanupForRun,
5
+ hasPendingPostgresSchedulerSandboxCleanupForRun,
6
+ readPostgresSchedulerResult,
7
+ } from './postgres';
2
8
  import type {
3
9
  PostgresSchedulerOptions,
4
10
  PostgresSchedulerQueryClient,
@@ -14,31 +20,100 @@ import { resolveRuntimeSchedulerSchemaEnv } from './runtime-scheduler-schema';
14
20
 
15
21
  /** Bound deadline reconciliation just like Absurd's own cancellation sweep. */
16
22
  export const ABSURD_CANCELLED_DEADLINE_RECONCILE_LIMIT = 100;
23
+ const QUEUED_CANCELLATION_RECONCILE_LIMIT = 20;
17
24
 
18
25
  function quoteAbsurdTable(table: string): string {
19
26
  return `"${table.replaceAll('"', '""')}"`;
20
27
  }
21
28
 
22
- /**
23
- * Project a play task cancelled by Absurd into the scheduler's authoritative
24
- * run state. This covers both indexed `max_delay` cancellation and the manual
25
- * cancellation lane. Manual cancellation stamps `cancel_at` in the adapter so
26
- * existing Absurd v4 queues are discoverable without a schema cutover.
27
- *
28
- * The engine can cancel an overdue task before or after a worker claims its
29
- * handler. A cancelled task has no handler path left to write the normal
30
- * terminal state. This reconciler
31
- * closes that gap: `cancelPostgresSchedulerRun` atomically writes the runtime
32
- * terminal and its run-ledger outbox event, and is idempotent if a user cancel
33
- * or a racing worker already terminalized the run. The next claim/maintenance
34
- * pass invokes it after Absurd performs its bounded cancellation sweep.
35
- */
29
+ async function finalizeCancelledRunAfterCleanup(input: {
30
+ client: PostgresSchedulerQueryClient;
31
+ runId: string;
32
+ reason: string;
33
+ schedulerSchema: string;
34
+ schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
35
+ controlKey: string;
36
+ }): Promise<'pending' | 'cancelled' | 'already_terminal'> {
37
+ await input.client.query('BEGIN');
38
+ try {
39
+ await input.client.query(
40
+ `SELECT set_config('lock_timeout', $1, true),
41
+ set_config('statement_timeout', $2, true)`,
42
+ [
43
+ `${RUNTIME_SCHEDULER_CANCEL_LOCK_TIMEOUT_MS}ms`,
44
+ `${RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS}ms`,
45
+ ],
46
+ );
47
+ // Sandbox admission locks the run before reserving an operation; resource
48
+ // writers lock attempts. Hold both through the final cleanup check.
49
+ await input.client.query(
50
+ `SELECT run_id FROM ${input.schedulerSchema}.runs
51
+ WHERE run_id = $1 FOR UPDATE`,
52
+ [input.runId],
53
+ );
54
+ await input.client.query(
55
+ `SELECT attempt FROM ${input.schedulerSchema}.attempts
56
+ WHERE run_id = $1 FOR UPDATE`,
57
+ [input.runId],
58
+ );
59
+ await abandonUnstartedModalCancellationReservations(
60
+ input.client,
61
+ { runId: input.runId },
62
+ input.schedulerOptions,
63
+ );
64
+ await enqueuePostgresSchedulerSandboxCleanupForRun(
65
+ input.client,
66
+ { runId: input.runId },
67
+ input.schedulerOptions,
68
+ );
69
+ if (
70
+ await hasPendingPostgresSchedulerSandboxCleanupForRun(
71
+ input.client,
72
+ { runId: input.runId },
73
+ input.schedulerOptions,
74
+ )
75
+ ) {
76
+ await input.client.query('COMMIT');
77
+ return 'pending';
78
+ }
79
+ const cancelled = await cancelPostgresSchedulerRun(
80
+ input.client,
81
+ { runId: input.runId, reason: input.reason },
82
+ input.schedulerOptions,
83
+ );
84
+ if (
85
+ !cancelled &&
86
+ !(await readPostgresSchedulerResult(
87
+ input.client,
88
+ input.runId,
89
+ input.schedulerOptions,
90
+ ))
91
+ ) {
92
+ throw new Error(
93
+ `Cancellation did not terminalize scheduler run "${input.runId}".`,
94
+ );
95
+ }
96
+ await input.client.query(
97
+ `DELETE FROM ${input.schedulerSchema}.control WHERE key = $1`,
98
+ [input.controlKey],
99
+ );
100
+ await input.client.query('COMMIT');
101
+ return cancelled ? 'cancelled' : 'already_terminal';
102
+ } catch (error) {
103
+ await input.client.query('ROLLBACK').catch(() => undefined);
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ /** Put legacy manual and deadline cancellations into the same durable queue
109
+ * as caller requests. A queued task is excluded from later scans, so one
110
+ * pending sandbox cannot hide other cancelled tasks behind the batch limit. */
36
111
  export async function reconcileAbsurdRuntimeCancellations(input: {
37
112
  client: PostgresSchedulerQueryClient;
38
113
  queue: string;
39
114
  schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
40
115
  limit?: number;
41
- }): Promise<{ reconciled: number; skipped: number }> {
116
+ }): Promise<{ queued: number }> {
42
117
  const requestedLimit = Math.floor(
43
118
  input.limit ?? ABSURD_CANCELLED_DEADLINE_RECONCILE_LIMIT,
44
119
  );
@@ -54,16 +129,21 @@ export async function reconcileAbsurdRuntimeCancellations(input: {
54
129
  resolveRuntimeSchedulerSchemaEnv() ||
55
130
  'runtime_scheduler',
56
131
  );
57
- const cancelled = await input.client.query<{
58
- run_id: string | null;
59
- cancellation_kind: 'manual' | 'deadline';
60
- }>(
132
+ const queued = await input.client.query<{ key: string }>(
61
133
  `
62
- SELECT run.run_id,
63
- CASE
64
- WHEN task.cancellation ? 'manual' THEN 'manual'
65
- ELSE 'deadline'
66
- END AS cancellation_kind
134
+ INSERT INTO ${schedulerSchema}.control (key, value)
135
+ SELECT 'runtime_cancel:' || run.run_id,
136
+ jsonb_build_object(
137
+ 'reason', CASE
138
+ WHEN task.cancellation ? 'manual'
139
+ THEN COALESCE(
140
+ NULLIF(task.cancellation->>'reason', ''),
141
+ 'Run cancellation was requested by the caller.'
142
+ )
143
+ ELSE 'Run did not start within its configured scheduler queue deadline.'
144
+ END,
145
+ 'requestedAt', clock_timestamp()
146
+ )
67
147
  FROM ${schedulerSchema}.runs AS run
68
148
  JOIN absurd.${taskTable} AS task
69
149
  ON task.task_id::text = run.absurd_task_id
@@ -78,57 +158,79 @@ export async function reconcileAbsurdRuntimeCancellations(input: {
78
158
  AND task.cancelled_at >= task.cancel_at
79
159
  )
80
160
  )
161
+ AND NOT EXISTS (
162
+ SELECT 1 FROM ${schedulerSchema}.control AS pending
163
+ WHERE pending.key = 'runtime_cancel:' || run.run_id
164
+ )
81
165
  ORDER BY task.cancelled_at ASC NULLS LAST, task.task_id ASC
82
166
  LIMIT $2
167
+ ON CONFLICT (key) DO NOTHING
168
+ RETURNING key
83
169
  `,
84
170
  [ABSURD_PLAY_RUN_TASK, limit],
85
171
  );
172
+ return { queued: queued.rows.length };
173
+ }
86
174
 
175
+ /** Drain caller-requested cancellations independently of their release lane. */
176
+ export async function reconcileQueuedAbsurdRuntimeCancellations(input: {
177
+ client: PostgresSchedulerQueryClient;
178
+ schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
179
+ limit?: number;
180
+ }): Promise<{ reconciled: number; skipped: number }> {
181
+ const schedulerSchema = quoteAbsurdTable(
182
+ input.schedulerOptions?.schema?.trim() ||
183
+ resolveRuntimeSchedulerSchemaEnv() ||
184
+ 'runtime_scheduler',
185
+ );
186
+ const requestedLimit = Math.floor(
187
+ input.limit ?? QUEUED_CANCELLATION_RECONCILE_LIMIT,
188
+ );
189
+ const limit = Number.isFinite(requestedLimit)
190
+ ? Math.max(1, Math.min(QUEUED_CANCELLATION_RECONCILE_LIMIT, requestedLimit))
191
+ : QUEUED_CANCELLATION_RECONCILE_LIMIT;
192
+ const queued = await input.client.query<{
193
+ key: string;
194
+ reason: string | null;
195
+ }>(
196
+ `WITH selected AS (
197
+ SELECT key FROM ${schedulerSchema}.control
198
+ WHERE starts_with(key, 'runtime_cancel:')
199
+ ORDER BY updated_at ASC, key ASC
200
+ LIMIT $1
201
+ FOR UPDATE SKIP LOCKED
202
+ )
203
+ UPDATE ${schedulerSchema}.control AS request
204
+ SET updated_at = clock_timestamp()
205
+ FROM selected
206
+ WHERE request.key = selected.key
207
+ RETURNING request.key, request.value->>'reason' AS reason`,
208
+ [limit],
209
+ );
87
210
  let reconciled = 0;
88
211
  let skipped = 0;
89
- for (const row of cancelled.rows) {
90
- const runId = row.run_id?.trim();
212
+ for (const row of queued.rows) {
213
+ const runId = row.key.slice('runtime_cancel:'.length);
91
214
  if (!runId) {
92
- skipped += 1;
93
- continue;
215
+ throw new Error(`Malformed runtime cancellation request "${row.key}".`);
94
216
  }
95
- await input.client.query('BEGIN');
96
217
  try {
97
- await input.client.query(
98
- `SELECT set_config('lock_timeout', $1, true),
99
- set_config('statement_timeout', $2, true)`,
100
- [
101
- `${RUNTIME_SCHEDULER_CANCEL_LOCK_TIMEOUT_MS}ms`,
102
- `${RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS}ms`,
103
- ],
104
- );
105
- const didCancel = await cancelPostgresSchedulerRun(
106
- input.client,
107
- {
108
- runId,
109
- reason:
110
- row.cancellation_kind === 'manual'
111
- ? 'Run cancellation was requested by the caller.'
112
- : 'Run did not start within its configured scheduler queue deadline.',
113
- },
114
- input.schedulerOptions,
115
- );
116
- await input.client.query('COMMIT');
117
- if (didCancel) reconciled += 1;
218
+ const outcome = await finalizeCancelledRunAfterCleanup({
219
+ client: input.client,
220
+ runId,
221
+ reason: row.reason || 'Cancelled play run.',
222
+ schedulerSchema,
223
+ schedulerOptions: input.schedulerOptions,
224
+ controlKey: row.key,
225
+ });
226
+ if (outcome === 'cancelled') reconciled += 1;
118
227
  else skipped += 1;
119
228
  } catch (error) {
120
- await input.client.query('ROLLBACK').catch(() => undefined);
121
- // A worker-held lock is expected to be retried by the next lane sweep;
122
- // never let one blocked cancellation stall the claim loop or create a
123
- // second unbounded cancellation pile.
124
229
  if (
125
230
  isRuntimeSchedulerQueryCancellationError(error) ||
126
231
  isRuntimeSchedulerRetryableTransactionError(error)
127
232
  ) {
128
233
  skipped += 1;
129
- // The hardened pool destroys a client after a statement timeout or a
130
- // retryable transaction error. Do not issue the next candidate on that
131
- // same socket; the next worker sweep acquires a fresh client.
132
234
  break;
133
235
  }
134
236
  throw error;
@@ -262,10 +262,13 @@ import { shouldTerminalizeSandboxCapacityRejection } from '../launch-source-poli
262
262
  import {
263
263
  ABSURD_CANCELLED_DEADLINE_RECONCILE_LIMIT,
264
264
  reconcileAbsurdRuntimeCancellations,
265
+ reconcileQueuedAbsurdRuntimeCancellations,
265
266
  } from './absurd-deadline-reconciliation';
266
267
  import {
267
268
  abandonRuntimeSandboxOperationReservation,
268
269
  bindRuntimeSandboxCapacityLease,
270
+ readReservedDaytonaCancellationOperation,
271
+ recordRecoveredDaytonaCancellationSandbox,
269
272
  reserveRuntimeSandboxOperation,
270
273
  runtimeSandboxCapacityAdmissionMode,
271
274
  RuntimeSandboxCapacityLimitError,
@@ -373,8 +376,8 @@ export const ABSURD_ADOPTED_LANE_POLL_INTERVAL_MS =
373
376
  const HEARTBEAT_THROTTLE_MS = (HEARTBEAT_CLAIM_TIMEOUT_SECONDS * 1000) / 3;
374
377
  const HEARTBEAT_TIMEOUT_RETRY_DELAY_MS = 1_000;
375
378
  /**
376
- * Sandbox deletion is best-effort cleanup after a durable terminal write. The
377
- * Daytona SDK may hang before its own delete timeout (for example while
379
+ * Sandbox deletion is a durable obligation and may precede a cancellation's
380
+ * terminal write. The Daytona SDK may hang before its own delete timeout (for example while
378
381
  * looking up an already-autostopped sandbox), so never let that external call
379
382
  * occupy an Absurd worker slot indefinitely.
380
383
  */
@@ -6644,6 +6647,75 @@ export async function reconcilePostgresSchedulerAmbiguousModalCreatesOnce(input:
6644
6647
  return { observed: intents.length, resourcesRecorded, resolvedAbsent };
6645
6648
  }
6646
6649
 
6650
+ async function reconcileReservedDaytonaCancellationOnce(input: {
6651
+ runWithClient: AbsurdRuntimeWorkerDeps['runWithClient'];
6652
+ environment: 'preview' | 'production';
6653
+ schedulerOptions: Pick<PostgresSchedulerOptions, 'schema'>;
6654
+ }): Promise<void> {
6655
+ const operation = await input.runWithClient((client) =>
6656
+ readReservedDaytonaCancellationOperation(
6657
+ client,
6658
+ input.environment,
6659
+ input.schedulerOptions,
6660
+ ),
6661
+ );
6662
+ if (!operation) return;
6663
+ const [{ Daytona }, { loadDaytonaRequiredConfig }] = await Promise.all([
6664
+ import('@daytonaio/sdk'),
6665
+ import('../daytona-runtime-config'),
6666
+ ]);
6667
+ const daytona = new Daytona(
6668
+ loadDaytonaRequiredConfig({ environment: input.environment }).clientOptions,
6669
+ );
6670
+ const sandboxName = `dl-${operation.providerOperationToken}`;
6671
+ let sandbox: Awaited<ReturnType<typeof daytona.get>>;
6672
+ try {
6673
+ sandbox = await deleteRuntimeSandboxBounded({
6674
+ deleteRuntimeSandbox: (name) => daytona.get(name),
6675
+ sandboxId: sandboxName,
6676
+ timeoutMs: ABSURD_SANDBOX_GC_TIMEOUT_MS,
6677
+ });
6678
+ } catch (error) {
6679
+ const message = error instanceof Error ? error.message : String(error);
6680
+ if (!/\b(?:sandbox )?not found\b|\b404\b/i.test(message)) throw error;
6681
+ if (
6682
+ Date.now() - operation.createdAtMs <
6683
+ RUNTIME_RELIABILITY_POLICY.recovery.horizonMs
6684
+ ) {
6685
+ return;
6686
+ }
6687
+ await input.runWithClient((client) =>
6688
+ abandonRuntimeSandboxOperationReservation(
6689
+ client,
6690
+ { operationKey: operation.operationKey },
6691
+ input.schedulerOptions,
6692
+ ),
6693
+ );
6694
+ return;
6695
+ }
6696
+ if (sandbox.labels?.operationKey !== operation.providerOperationToken) {
6697
+ throw new Error(
6698
+ `Daytona cancellation lookup ${sandboxName} lacks its exact operation label.`,
6699
+ );
6700
+ }
6701
+ if (!sandbox.organizationId) {
6702
+ throw new Error(
6703
+ `Daytona cancellation lookup ${sandboxName} lacks its routing organization.`,
6704
+ );
6705
+ }
6706
+ await input.runWithClient((client) =>
6707
+ recordRecoveredDaytonaCancellationSandbox(
6708
+ client,
6709
+ {
6710
+ operationKey: operation.operationKey,
6711
+ sandboxId: sandbox.id,
6712
+ routingDomain: sandbox.organizationId,
6713
+ },
6714
+ input.schedulerOptions,
6715
+ ),
6716
+ );
6717
+ }
6718
+
6647
6719
  export async function runPostgresSchedulerSandboxCleanupController(input: {
6648
6720
  runWithClient: AbsurdRuntimeWorkerDeps['runWithClient'];
6649
6721
  deleteRuntimeSandbox: AbsurdRuntimeWorkerDeps['deleteRuntimeSandbox'];
@@ -6681,6 +6753,8 @@ export async function runPostgresSchedulerSandboxCleanupController(input: {
6681
6753
  const reportReconciliationPollFailure = createRateLimitedRuntimeLogger({
6682
6754
  log: (...args) => console.warn(...args),
6683
6755
  });
6756
+ let reservedRecovery: Promise<void> | null = null;
6757
+ let nextReservedRecoveryAt = 0;
6684
6758
 
6685
6759
  const startJob = (job: PostgresSchedulerSandboxCleanupJob): void => {
6686
6760
  const task = processPostgresSchedulerSandboxCleanupJob({
@@ -6726,6 +6800,37 @@ export async function runPostgresSchedulerSandboxCleanupController(input: {
6726
6800
  };
6727
6801
 
6728
6802
  while (!input.shouldStop()) {
6803
+ if (
6804
+ (!input.providers || input.providers.includes('daytona')) &&
6805
+ !reservedRecovery &&
6806
+ Date.now() >= nextReservedRecoveryAt
6807
+ ) {
6808
+ nextReservedRecoveryAt = Date.now() + 60_000;
6809
+ const recovery = reconcileReservedDaytonaCancellationOnce(input)
6810
+ .catch((error) => {
6811
+ reportReconciliationPollFailure(
6812
+ '[absurd-runtime.worker] daytona_cancellation_reservation_recovery_failed',
6813
+ { error: error instanceof Error ? error.message : String(error) },
6814
+ );
6815
+ })
6816
+ .finally(() => {
6817
+ if (reservedRecovery === recovery) reservedRecovery = null;
6818
+ });
6819
+ reservedRecovery = recovery;
6820
+ }
6821
+ try {
6822
+ await input.runWithClient((client) =>
6823
+ reconcileQueuedAbsurdRuntimeCancellations({
6824
+ client,
6825
+ schedulerOptions: input.schedulerOptions,
6826
+ }),
6827
+ );
6828
+ } catch (error) {
6829
+ reportReconciliationPollFailure(
6830
+ '[absurd-runtime.worker] cancellation_reconciliation_poll_failed',
6831
+ { error: error instanceof Error ? error.message : String(error) },
6832
+ );
6833
+ }
6729
6834
  if (input.providers?.includes('modal')) {
6730
6835
  try {
6731
6836
  const reconciliation =
@@ -7082,9 +7187,9 @@ export async function startRuntimeWorkerHost(
7082
7187
  queue,
7083
7188
  schedulerOptions,
7084
7189
  });
7085
- if (cancellations.reconciled > 0 || cancellations.skipped > 0) {
7190
+ if (cancellations.queued > 0) {
7086
7191
  reportCancellationProgress(
7087
- '[absurd-runtime.worker] cancellations_reconciled',
7192
+ '[absurd-runtime.worker] cancellations_queued',
7088
7193
  {
7089
7194
  queue,
7090
7195
  releaseId,
@@ -7244,9 +7349,9 @@ export async function startRuntimeWorkerHost(
7244
7349
  queue: adoptedQueue,
7245
7350
  schedulerOptions,
7246
7351
  });
7247
- if (cancellations.reconciled > 0 || cancellations.skipped > 0) {
7352
+ if (cancellations.queued > 0) {
7248
7353
  reportAdoptedCancellationProgress(
7249
- '[absurd-runtime.worker] cancellations_reconciled',
7354
+ '[absurd-runtime.worker] cancellations_queued',
7250
7355
  {
7251
7356
  queue: adoptedQueue,
7252
7357
  releaseId,
@@ -14,8 +14,8 @@
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 → one bounded transaction cancels the Absurd task and terminalizes
18
- * the postgres scheduler run. Success means both states committed.
17
+ * cancel → commit the Absurd stop and a durable cancellation request;
18
+ * the worker deletes sandboxes before the final run write.
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
  */
@@ -29,8 +29,6 @@ import {
29
29
  type PlaySchedulerSubmitInput,
30
30
  } from '../scheduler-backend';
31
31
  import {
32
- cancelPostgresSchedulerRun,
33
- postgresSchedulerCancellationOutboxLease,
34
32
  postgresSchedulerRunHandle,
35
33
  readPostgresSchedulerLaunch,
36
34
  readPostgresSchedulerAbsurdTaskId,
@@ -48,6 +46,8 @@ import {
48
46
  withRuntimeSchedulerClient,
49
47
  withRuntimeSchedulerTransaction,
50
48
  } from './postgres-pool';
49
+ import { RUNTIME_SCHEDULER_CANCELLATION_PENDING_CODE } from '../scheduler-backend';
50
+ import { resolveRuntimeSchedulerSchemaEnv } from './runtime-scheduler-schema';
51
51
  import { withRuntimeSchedulerSubmitAdmission } from './submit-admission';
52
52
  import {
53
53
  ABSURD_PLAY_RUN_MAX_ATTEMPTS,
@@ -200,7 +200,7 @@ function absurdRunHandle(input: {
200
200
  const runsQueue = absurdQueueNameForRelease(releaseId);
201
201
  const cancellationKey = `${poolOptions.url}::${effectiveOptions.schema ?? ''}::${input.runId}`;
202
202
 
203
- const cancelAbsurdRunSynchronously = async (reason?: string) =>
203
+ const queueCancellation = async (reason?: string) =>
204
204
  withRuntimeSchedulerTransaction(
205
205
  poolOptions,
206
206
  {
@@ -208,9 +208,8 @@ function absurdRunHandle(input: {
208
208
  statementTimeoutMs: RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS,
209
209
  },
210
210
  async (client) => {
211
- // Resolve the immutable release lane inside the same checkout and
212
- // transaction that performs cancellation. The stop endpoint therefore
213
- // has no preliminary lane lookup that can succeed or fail separately.
211
+ // Resolve the immutable release lane in the transaction that records
212
+ // the request. A failed final run write cannot roll this marker back.
214
213
  const launch = await readPostgresSchedulerLaunch(
215
214
  client,
216
215
  input.runId,
@@ -231,67 +230,73 @@ function absurdRunHandle(input: {
231
230
  input.runId,
232
231
  effectiveOptions,
233
232
  );
233
+ if (
234
+ await readPostgresSchedulerResult(
235
+ client,
236
+ input.runId,
237
+ effectiveOptions,
238
+ )
239
+ ) {
240
+ return false;
241
+ }
234
242
  if (taskId) {
235
- const engineTaskCancelled = await cancelAbsurdTaskIfPresent(client, {
243
+ const engineTaskPresent = await cancelAbsurdTaskIfPresent(client, {
236
244
  queue: cancellationQueue,
237
245
  taskId,
238
246
  runId: input.runId,
239
247
  });
240
- // Older v4 Absurd functions do not materialize a manual cancellation
241
- // deadline or reason. Stamp both in the adapter so existing queues
242
- // participate in the same durable reconciliation lane without a
243
- // schema cutover. The explicit marker keeps a user cancel distinct
244
- // from a task cancelled by its configured max_delay sweep.
245
- if (engineTaskCancelled) {
248
+ if (engineTaskPresent) {
249
+ // Older workers only reconcile `manual` or `max_delay`. This
250
+ // marker is diagnostic; the control row is the cross-lane queue.
246
251
  await client.query(
247
252
  `UPDATE absurd.${quotePostgresIdentifier(`t_${cancellationQueue}`)}
248
253
  SET cancel_at = COALESCE(cancel_at, clock_timestamp()),
249
254
  cancellation = COALESCE(cancellation, '{}'::jsonb)
250
- || jsonb_build_object('manual', true)
251
- WHERE task_id = $1::uuid
252
- AND state = 'cancelled'`,
255
+ || jsonb_build_object('sandboxFirst', true)
256
+ WHERE task_id = $1::uuid`,
253
257
  [taskId],
254
258
  );
255
259
  }
256
260
  }
257
- const cancelled = await cancelPostgresSchedulerRun(
258
- client,
259
- {
260
- runId: input.runId,
261
- reason,
262
- outboxLease: postgresSchedulerCancellationOutboxLease(input.runId),
263
- },
264
- effectiveOptions,
261
+ const schedulerSchema = quotePostgresIdentifier(
262
+ effectiveOptions.schema?.trim() ||
263
+ resolveRuntimeSchedulerSchemaEnv() ||
264
+ 'runtime_scheduler',
265
265
  );
266
- // The update returning a row is proof that this transaction wrote the
267
- // terminal state and outbox event. Only the idempotent retry/race path
268
- // needs one read to prove another terminal writer already won.
269
- if (!cancelled) {
270
- const terminal = await readPostgresSchedulerResult(
271
- client,
272
- input.runId,
273
- effectiveOptions,
274
- );
275
- if (!terminal) {
276
- throw new Error(
277
- `Cancellation did not terminalize scheduler run "${input.runId}".`,
278
- );
279
- }
280
- }
266
+ await client.query(
267
+ `INSERT INTO ${schedulerSchema}.control (key, value)
268
+ VALUES ($1, jsonb_build_object(
269
+ 'reason', left($2::text, 4000),
270
+ 'requestedAt', clock_timestamp()
271
+ ))
272
+ ON CONFLICT (key) DO NOTHING`,
273
+ [
274
+ `runtime_cancel:${input.runId}`,
275
+ reason?.trim() || 'Cancelled play run.',
276
+ ],
277
+ );
278
+ return true;
281
279
  },
282
280
  {
283
281
  runId: input.runId,
284
- phase: 'cancel_run_synchronously',
282
+ phase: 'queue_run_cancellation',
285
283
  priority: 'critical',
286
284
  },
285
+ { maxAttempts: 1 },
287
286
  );
288
287
 
289
288
  const requestCancellation = async (reason?: string) => {
290
289
  await runCancellationSingleFlight(cancellationKey, async () => {
291
- // Engine cancellation and the scheduler terminal write commit together.
292
- // A timeout rolls both back, so returning means the run is terminal and
293
- // throwing means the caller can safely retry one unchanged operation.
294
- await cancelAbsurdRunSynchronously(reason);
290
+ const queued = await queueCancellation(reason);
291
+ if (queued) {
292
+ const error = new Error(
293
+ `Cancellation queued for ${input.runId}; sandbox cleanup and final run update are pending.`,
294
+ );
295
+ Object.assign(error, {
296
+ code: RUNTIME_SCHEDULER_CANCELLATION_PENDING_CODE,
297
+ });
298
+ throw error;
299
+ }
295
300
  });
296
301
  };
297
302
 
@@ -3080,6 +3080,13 @@ async function penalizeRateStateStatements(
3080
3080
  GREATEST(h.retry_at_ms, COALESCE($5::bigint, nv.now_ms + $2::bigint))
3081
3081
  - COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
3082
3082
  END) AS cycle_ms,
3083
+ CASE
3084
+ WHEN $5::bigint IS NOT NULL AND h.rate_successes <= 0 THEN
3085
+ CASE WHEN h.seen_backpressure THEN
3086
+ LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
3087
+ ELSE 1000 END
3088
+ ELSE NULL
3089
+ END AS no_progress_probe_interval_ms,
3083
3090
  nv.now_ms
3084
3091
  FROM held h CROSS JOIN now_value nv
3085
3092
  ), throttle_ceiling AS (
@@ -3087,12 +3094,12 @@ async function penalizeRateStateStatements(
3087
3094
  GREATEST(${MIN_ADAPTIVE_RPS_FLOOR},
3088
3095
  CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
3089
3096
  CASE WHEN $5::bigint IS NOT NULL
3090
- -- An explicit Retry-After is only the provider's next safe
3091
- -- dispatch deadline. It is not a rate measurement; with no
3092
- -- successful-call evidence, use one conservative probe per
3093
- -- second after that deadline instead of deriving a permanent
3094
- -- RPM limit from the length of the hold.
3095
- THEN GREATEST(e.refill_before, 1000.0 / e.cycle_ms)
3097
+ -- A fresh provider deadline is not a throughput measurement.
3098
+ -- Repeated probes with no successful work are evidence that
3099
+ -- the scope is still unavailable, however. Space subsequent
3100
+ -- probes exponentially (up to one per minute) across every
3101
+ -- run sharing this provider credential.
3102
+ THEN 1000.0 / e.no_progress_probe_interval_ms
3096
3103
  ELSE e.refill_before END
3097
3104
  ELSE e.refill_before / 2 END
3098
3105
  ) AS next_ceiling_rps
@@ -3115,7 +3122,9 @@ async function penalizeRateStateStatements(
3115
3122
  rate_throttle_ceiling_rps = CASE WHEN e.new_episode
3116
3123
  THEN e.next_ceiling_rps ELSE e.rate_throttle_ceiling_rps END,
3117
3124
  rate_observation_started_at_ms = CASE WHEN e.new_episode THEN NULL ELSE e.rate_observation_started_at_ms END,
3118
- rate_recovery_interval_ms = CASE WHEN e.new_episode THEN e.cycle_ms ELSE e.rate_recovery_interval_ms END,
3125
+ rate_recovery_interval_ms = CASE WHEN e.new_episode
3126
+ THEN COALESCE(e.no_progress_probe_interval_ms, e.cycle_ms)
3127
+ ELSE e.rate_recovery_interval_ms END,
3119
3128
  last_ramp_at_ms = CASE WHEN e.new_episode THEN e.deadline ELSE e.last_ramp_at_ms END,
3120
3129
  -- All pre-429 permits are revalidated against this durable hold
3121
3130
  -- before dispatch. Their obsolete schedule debt must not add a