deepline 0.3.235 → 0.3.237

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.
@@ -136,6 +136,8 @@ import {
136
136
  completePostgresSchedulerComputeBillingSessionBootstrap,
137
137
  deferPostgresSchedulerAttemptForCapacity,
138
138
  enqueuePostgresSchedulerSandboxCleanupForAttempt,
139
+ enqueuePostgresSchedulerSandboxCleanupForRun,
140
+ hasPostgresSchedulerRecoverableDetachedRunner,
139
141
  executePostgresSchedulerClaim,
140
142
  ensurePostgresSchedulerTerminalComputeBillingJob,
141
143
  failPostgresSchedulerAttempt,
@@ -221,6 +223,7 @@ import {
221
223
  type RuntimeSchedulerPoolOptions,
222
224
  withRuntimeSchedulerClient,
223
225
  } from './postgres-pool';
226
+ import { runWithAbsurdSchedulerQuerySignal } from './absurd-pool';
224
227
  import {
225
228
  createPostgresSchedulerProgressReporter,
226
229
  type PostgresSchedulerProgressReporter,
@@ -550,7 +553,10 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
550
553
  lastHeartbeatAt = now;
551
554
  heartbeatState.inFlight = (async () => {
552
555
  try {
553
- await input.ctx.heartbeat();
556
+ await runWithAbsurdSchedulerQuerySignal(
557
+ cancellationController.signal,
558
+ () => input.ctx.heartbeat(),
559
+ );
554
560
  } catch (error) {
555
561
  if (!isTransientHeartbeatTimeout(error) || stopped) throw error;
556
562
  console.warn('[absurd-runtime.worker] lease_heartbeat_timeout_retry', {
@@ -559,10 +565,22 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
559
565
  });
560
566
  await sleep(heartbeatTimeoutRetryDelayMs);
561
567
  if (stopped) return;
562
- await input.ctx.heartbeat();
568
+ await runWithAbsurdSchedulerQuerySignal(
569
+ cancellationController.signal,
570
+ () => input.ctx.heartbeat(),
571
+ );
563
572
  }
564
573
  })()
565
574
  .catch((error) => {
575
+ // The handoff deadline owns this cancellation. A cancelled local SQL
576
+ // call does not prove that the engine cancelled the durable task.
577
+ if (
578
+ cancellationController.signal.aborted &&
579
+ error === cancellationController.signal.reason &&
580
+ error instanceof AbsurdExecutionOperationTimeoutError
581
+ ) {
582
+ return;
583
+ }
566
584
  const engineTerminal =
567
585
  error instanceof CancelledTask || error instanceof FailedTask;
568
586
  if (engineTerminal) {
@@ -595,9 +613,22 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
595
613
  timer.unref?.();
596
614
  let operationTimeout: ReturnType<typeof setTimeout> | null = null;
597
615
  try {
598
- const operation = Promise.resolve().then(() =>
599
- input.operation(cancellationController.signal),
600
- );
616
+ // A completed backend still cannot park until its in-flight lease update
617
+ // settles. Keep that drain inside the same supervised handoff deadline.
618
+ const operation = (async () => {
619
+ try {
620
+ operationResult = await Promise.resolve().then(() =>
621
+ input.operation(cancellationController.signal),
622
+ );
623
+ } catch (error) {
624
+ operationError = error;
625
+ } finally {
626
+ stopped = true;
627
+ stopHeartbeatTimer();
628
+ await heartbeatState.inFlight;
629
+ }
630
+ return operationResult;
631
+ })();
601
632
  if (operationTimeoutMs !== null) {
602
633
  operationResult = await Promise.race([
603
634
  operation,
@@ -619,15 +650,11 @@ export async function runWithAbsurdExecutionLeaseHeartbeat<T>(input: {
619
650
  operationResult = await operation;
620
651
  }
621
652
  } catch (error) {
622
- operationError = error;
653
+ operationError ??= error;
623
654
  } finally {
624
655
  if (operationTimeout) clearTimeout(operationTimeout);
625
656
  stopped = true;
626
657
  stopHeartbeatTimer();
627
- const pendingHeartbeat = heartbeatState.inFlight;
628
- if (pendingHeartbeat) {
629
- await pendingHeartbeat.catch(() => undefined);
630
- }
631
658
  }
632
659
  if (heartbeatFailure) {
633
660
  if (
@@ -735,7 +762,7 @@ export type AbsurdRuntimeWorkerDeps = {
735
762
  deleteRuntimeSandboxRef?: (
736
763
  ref: RuntimeSandboxRef,
737
764
  ) => Promise<RuntimeSandboxDeleteOutcome>;
738
- deleteRuntimeSandboxWithOutcome: (
765
+ deleteRuntimeSandboxWithOutcome?: (
739
766
  sandboxId: string,
740
767
  expectedOrganizationId: string | null,
741
768
  ) => Promise<DaytonaSandboxDeleteOutcome>;
@@ -860,40 +887,48 @@ function buildDefaultDeps(absurd: Absurd): AbsurdRuntimeWorkerDeps {
860
887
  * of silently leaving the CLI stale.
861
888
  */
862
889
  async function projectLifecycleOutbox(input: {
890
+ ctx: TaskContext;
863
891
  deps: AbsurdRuntimeWorkerDeps;
864
892
  launch: PlaySchedulerSubmitInput;
865
893
  lifecycle: PostgresSchedulerLifecycleOutboxEvent;
866
894
  options?: Pick<PostgresSchedulerOptions, 'schema'>;
867
895
  transition: 'waiting' | 'resumed';
868
896
  }): Promise<void> {
869
- try {
870
- const result = await input.deps.projectOutbox(
871
- input.launch,
872
- input.lifecycle.runId,
873
- input.options,
874
- input.lifecycle.eventId,
875
- );
876
- if (result && !result.targetDelivered) {
877
- console.error('[run-ledger-outbox.lifecycle_projection_pending]', {
878
- runId: input.lifecycle.runId,
879
- eventId: input.lifecycle.eventId,
880
- transition: input.transition,
881
- projected: result.projected,
882
- failed: result.failed,
883
- failedEventIds: result.failedEventIds,
884
- });
885
- }
886
- } catch (error) {
887
- // Scheduler truth and the outbox row were committed atomically before this
888
- // handoff. Keep the durable continuation live so recovery can heal a
889
- // transient app-runtime failure, but do not hide the visibility gap.
890
- console.error('[run-ledger-outbox.lifecycle_projection_failed]', {
891
- runId: input.lifecycle.runId,
892
- eventId: input.lifecycle.eventId,
893
- transition: input.transition,
894
- error: error instanceof Error ? error.message : String(error),
895
- });
896
- }
897
+ await runWithAbsurdExecutionLeaseHeartbeat({
898
+ ctx: input.ctx,
899
+ runId: input.lifecycle.runId,
900
+ operation: async (signal) => {
901
+ try {
902
+ const result = await input.deps.projectOutbox(
903
+ input.launch,
904
+ input.lifecycle.runId,
905
+ input.options,
906
+ input.lifecycle.eventId,
907
+ signal,
908
+ );
909
+ if (result && !result.targetDelivered) {
910
+ console.error('[run-ledger-outbox.lifecycle_projection_pending]', {
911
+ runId: input.lifecycle.runId,
912
+ eventId: input.lifecycle.eventId,
913
+ transition: input.transition,
914
+ projected: result.projected,
915
+ failed: result.failed,
916
+ failedEventIds: result.failedEventIds,
917
+ });
918
+ }
919
+ } catch (error) {
920
+ // Scheduler truth and the outbox row were committed atomically before this
921
+ // handoff. Keep the durable continuation live so recovery can heal a
922
+ // transient app-runtime failure, but do not hide the visibility gap.
923
+ console.error('[run-ledger-outbox.lifecycle_projection_failed]', {
924
+ runId: input.lifecycle.runId,
925
+ eventId: input.lifecycle.eventId,
926
+ transition: input.transition,
927
+ error: error instanceof Error ? error.message : String(error),
928
+ });
929
+ }
930
+ },
931
+ });
897
932
  }
898
933
 
899
934
  function nonJsonResponseTitle(
@@ -2397,23 +2432,26 @@ async function executeAbsurdAttemptAdapter(
2397
2432
  let executionLaunch = launch;
2398
2433
  try {
2399
2434
  const tokenStartedAt = performance.now();
2400
- const executorToken = await deps
2401
- .mintAttemptExecutorToken({
2402
- launch,
2403
- attempt: generation,
2404
- leaseToken,
2405
- schedulerSchema: options?.schema ?? null,
2406
- })
2407
- .finally(() => {
2408
- console.info(
2409
- '[absurd-runtime.worker.phase] ' +
2410
- JSON.stringify({
2411
- runId,
2412
- phase: 'executor_token_mint',
2413
- totalMs: performance.now() - tokenStartedAt,
2414
- }),
2415
- );
2416
- });
2435
+ const executorToken = await runWithAbsurdExecutionLeaseHeartbeat({
2436
+ ctx,
2437
+ runId,
2438
+ operation: () =>
2439
+ deps.mintAttemptExecutorToken({
2440
+ launch,
2441
+ attempt: generation,
2442
+ leaseToken,
2443
+ schedulerSchema: options?.schema ?? null,
2444
+ }),
2445
+ }).finally(() => {
2446
+ console.info(
2447
+ '[absurd-runtime.worker.phase] ' +
2448
+ JSON.stringify({
2449
+ runId,
2450
+ phase: 'executor_token_mint',
2451
+ totalMs: performance.now() - tokenStartedAt,
2452
+ }),
2453
+ );
2454
+ });
2417
2455
  executionLaunch = {
2418
2456
  ...launch,
2419
2457
  executorToken,
@@ -2434,12 +2472,56 @@ async function executeAbsurdAttemptAdapter(
2434
2472
  runtimeApi,
2435
2473
  });
2436
2474
  } catch (error) {
2437
- if (
2438
- error instanceof SuspendTask ||
2439
- error instanceof CancelledTask ||
2440
- error instanceof FailedTask ||
2441
- error instanceof DeferTask
2442
- ) {
2475
+ if (error instanceof SuspendTask || error instanceof DeferTask) throw error;
2476
+ let detached: Awaited<
2477
+ ReturnType<typeof resumePostgresSchedulerDetachedRunnerAttempt>
2478
+ >;
2479
+ try {
2480
+ detached = await deps.runWithClient((client) =>
2481
+ resumePostgresSchedulerDetachedRunnerAttempt(
2482
+ client,
2483
+ { runId, attempt: generation, leaseToken },
2484
+ options,
2485
+ ),
2486
+ );
2487
+ } catch (recoveryError) {
2488
+ console.error(
2489
+ '[absurd-runtime.worker] detached_runner_recovery_unavailable',
2490
+ JSON.stringify({
2491
+ runId,
2492
+ attempt: generation,
2493
+ engineTaskId: ctx.taskID,
2494
+ error: error instanceof Error ? error.message : String(error),
2495
+ recoveryError:
2496
+ recoveryError instanceof Error
2497
+ ? recoveryError.message
2498
+ : String(recoveryError),
2499
+ cleanupAuthorized: false,
2500
+ }),
2501
+ );
2502
+ throw new AggregateError(
2503
+ [error, recoveryError],
2504
+ 'Scheduler could not prove detached runner cleanup authority.',
2505
+ );
2506
+ }
2507
+ if (detached) {
2508
+ console.error(
2509
+ '[absurd-runtime.worker] detached_runner_handoff_recovered',
2510
+ JSON.stringify({
2511
+ runId,
2512
+ attempt: generation,
2513
+ engineAttempt: ctx.attempt,
2514
+ engineTaskId: ctx.taskID,
2515
+ error: error instanceof Error ? error.message : String(error),
2516
+ cleanupAuthorized: false,
2517
+ }),
2518
+ );
2519
+ throw new DeferTask(
2520
+ `Detached runner ${runId}/${generation} retains execution after scheduler handoff failure.`,
2521
+ 1,
2522
+ );
2523
+ }
2524
+ if (error instanceof CancelledTask || error instanceof FailedTask) {
2443
2525
  // Park/cancel signals belong to the engine; never treat them as leg
2444
2526
  // failures.
2445
2527
  throw error;
@@ -2761,11 +2843,6 @@ async function executeAssignedAbsurdAttempt(input: {
2761
2843
  `absurd runtime worker: suspended runner result for ${runId} carried no suspension.`,
2762
2844
  );
2763
2845
  }
2764
- await input.progressReporter.finalize(input.runnerResult, {
2765
- status: 'running',
2766
- replayAllTerminalLogs: input.fromDetachedRunner,
2767
- terminalLogProducerAttempt: input.terminalLogProducerAttempt,
2768
- });
2769
2846
  const suspendedCheckpoint = absurdCheckpointBase(
2770
2847
  input.runnerResult.checkpoint ?? checkpoint ?? null,
2771
2848
  );
@@ -2812,16 +2889,29 @@ async function executeAssignedAbsurdAttempt(input: {
2812
2889
  store: attemptStore,
2813
2890
  engine,
2814
2891
  lifecycle: {
2815
- waiting: async (lifecycle) =>
2892
+ waiting: async (lifecycle) => {
2893
+ await runWithAbsurdExecutionLeaseHeartbeat({
2894
+ ctx,
2895
+ runId,
2896
+ operation: () =>
2897
+ input.progressReporter.finalize(input.runnerResult, {
2898
+ status: 'running',
2899
+ replayAllTerminalLogs: input.fromDetachedRunner,
2900
+ terminalLogProducerAttempt: input.terminalLogProducerAttempt,
2901
+ }),
2902
+ });
2816
2903
  await projectLifecycleOutbox({
2904
+ ctx,
2817
2905
  deps,
2818
2906
  launch,
2819
2907
  lifecycle,
2820
2908
  options,
2821
2909
  transition: 'waiting',
2822
- }),
2910
+ });
2911
+ },
2823
2912
  resumed: async (lifecycle) =>
2824
2913
  await projectLifecycleOutbox({
2914
+ ctx,
2825
2915
  deps,
2826
2916
  launch,
2827
2917
  lifecycle,
@@ -2916,6 +3006,7 @@ async function executeAssignedAbsurdAttempt(input: {
2916
3006
  return { ok: true, runId, schedulerStatus: 'idle' };
2917
3007
  }
2918
3008
  await projectLifecycleOutbox({
3009
+ ctx,
2919
3010
  deps,
2920
3011
  launch,
2921
3012
  lifecycle: marked,
@@ -2962,6 +3053,7 @@ async function executeAssignedAbsurdAttempt(input: {
2962
3053
  return { ok: true, runId, schedulerStatus: 'idle' };
2963
3054
  }
2964
3055
  await projectLifecycleOutbox({
3056
+ ctx,
2965
3057
  deps,
2966
3058
  launch,
2967
3059
  lifecycle: running,
@@ -3043,6 +3135,16 @@ async function executeAssignedAbsurdAttempt(input: {
3043
3135
  error: unknown,
3044
3136
  runnerConfig: PlayRunnerExecutionConfig,
3045
3137
  ): Promise<void> => {
3138
+ if (
3139
+ await deps.runWithClient((client) =>
3140
+ hasPostgresSchedulerRecoverableDetachedRunner(
3141
+ client,
3142
+ { runId, attempt: generation, leaseToken },
3143
+ options,
3144
+ ),
3145
+ )
3146
+ )
3147
+ return;
3046
3148
  const cleanupTerminalReason =
3047
3149
  error instanceof CancelledTask ? 'lease_expired' : 'runner_failed';
3048
3150
  await deps
@@ -3102,33 +3204,52 @@ async function executeAssignedAbsurdAttempt(input: {
3102
3204
  ),
3103
3205
  )
3104
3206
  .catch(() => attemptResources);
3105
- // Deleting without a durable obligation turns a transient provider failure
3106
- // into an untracked occupied sandbox. Preserve the recorded resource for
3107
- // terminal/retry recovery when the enqueue cannot commit; receipt and
3108
- // sheet release below remain safe and attempt-scoped.
3109
- const immediateSandboxGcAllowed =
3110
- !attemptResourceReadFailed &&
3111
- (attemptResources.length === 0 ||
3112
- (await persistAttemptSandboxCleanupBeforeBestEffortGc({
3113
- deps,
3114
- runId,
3115
- attempt: generation,
3207
+ // All provider deletion goes through the durable cleanup lease, including
3208
+ // retired generations. Never pass a read-only resource snapshot to raw GC.
3209
+ if (!attemptResourceReadFailed) {
3210
+ await deps.runWithClient((client) =>
3211
+ enqueuePostgresSchedulerSandboxCleanupForRun(
3212
+ client,
3213
+ { runId },
3116
3214
  options,
3117
- })));
3215
+ ),
3216
+ );
3217
+ const environments = new Set(
3218
+ allResources.flatMap((resource) => {
3219
+ if (!runtimeSandboxRefFromResource(resource)) return [];
3220
+ const value = resource as {
3221
+ runtimeEnvironment?: string;
3222
+ daytonaEnvironment?: string;
3223
+ };
3224
+ return [
3225
+ value.runtimeEnvironment === 'preview' ||
3226
+ value.daytonaEnvironment === 'preview'
3227
+ ? ('preview' as const)
3228
+ : ('production' as const),
3229
+ ];
3230
+ }),
3231
+ );
3232
+ for (const environment of environments) {
3233
+ await drainPostgresSchedulerSandboxCleanupOnce({
3234
+ runWithClient: deps.runWithClient,
3235
+ deleteRuntimeSandbox: deps.deleteRuntimeSandbox,
3236
+ deleteRuntimeSandboxRef: deps.deleteRuntimeSandboxRef,
3237
+ deleteRuntimeSandboxWithOutcome: deps.deleteRuntimeSandboxWithOutcome,
3238
+ readSandboxCrashDiagnostic: deps.readSandboxCrashDiagnostic,
3239
+ leaseOwner: `attempt-error:${runId}:${generation}`,
3240
+ runId,
3241
+ environment,
3242
+ schedulerOptions: options,
3243
+ timeoutMs: deps.sandboxGcTimeoutMs,
3244
+ });
3245
+ }
3246
+ }
3118
3247
  const runtimeStateCleanup = releaseAbsurdAttemptRuntimeState({
3119
3248
  config: runnerConfig,
3120
3249
  runId,
3121
3250
  runAttempt: generation,
3122
3251
  resources: attemptResources,
3123
- sandboxRefs: runtimeSandboxResourceRefs(allResources),
3124
3252
  reason: cleanupTerminalReason,
3125
- deleteRuntimeSandbox: immediateSandboxGcAllowed
3126
- ? deps.deleteRuntimeSandbox
3127
- : undefined,
3128
- deleteRuntimeSandboxRef: immediateSandboxGcAllowed
3129
- ? deps.deleteRuntimeSandboxRef
3130
- : undefined,
3131
- sandboxGcTimeoutMs: deps.sandboxGcTimeoutMs,
3132
3253
  });
3133
3254
  if (ctx.maxAttempts !== null && ctx.attempt >= ctx.maxAttempts) {
3134
3255
  // A final engine attempt cannot retry. Its durable failed terminal must
@@ -3165,30 +3286,41 @@ async function executeAssignedAbsurdAttempt(input: {
3165
3286
  pendingDetachedRunner = null;
3166
3287
  let runnerResult: PlayRunnerResult;
3167
3288
  try {
3168
- runnerResult = await resolveDetachedRunnerOutcome({
3289
+ runnerResult = await runWithAbsurdExecutionLeaseHeartbeat({
3290
+ ctx,
3169
3291
  runId,
3170
- suspension: detached,
3171
- checkpoint: absurdCheckpointBase(checkpoint),
3172
- readRunnerTerminal,
3173
- recordRunnerTerminal,
3174
- readRunnerLiveness: () =>
3175
- deps.runWithClient((client) =>
3176
- readPostgresSchedulerRunnerLiveness(
3177
- client,
3178
- { runId, attempt: detached.runnerAttempt },
3179
- options,
3180
- ),
3181
- ),
3182
- inspectDetachedRunner: deps.inspectDetachedRunner,
3183
- deleteRuntimeSandbox: deps.deleteRuntimeSandbox,
3184
- deleteRuntimeSandboxRef: deps.deleteRuntimeSandboxRef,
3185
- sandboxGcTimeoutMs: deps.sandboxGcTimeoutMs,
3292
+ operation: () =>
3293
+ resolveDetachedRunnerOutcome({
3294
+ runId,
3295
+ suspension: detached,
3296
+ checkpoint: absurdCheckpointBase(checkpoint),
3297
+ readRunnerTerminal,
3298
+ recordRunnerTerminal,
3299
+ readRunnerLiveness: () =>
3300
+ deps.runWithClient((client) =>
3301
+ readPostgresSchedulerRunnerLiveness(
3302
+ client,
3303
+ { runId, attempt: detached.runnerAttempt },
3304
+ options,
3305
+ ),
3306
+ ),
3307
+ inspectDetachedRunner: deps.inspectDetachedRunner,
3308
+ deleteRuntimeSandbox: deps.deleteRuntimeSandbox,
3309
+ deleteRuntimeSandboxRef: deps.deleteRuntimeSandboxRef,
3310
+ sandboxGcTimeoutMs: deps.sandboxGcTimeoutMs,
3311
+ }),
3186
3312
  });
3187
3313
  } catch (error) {
3188
3314
  // The terminal fast path skips normal runner setup. Build its cleanup
3189
3315
  // context only on failure, then preserve the same pre-retry resource
3190
3316
  // release and sandbox-GC fence as the full execution path.
3191
- const preparedRunnerConfig = await prepareRunnerConfig(claim);
3317
+ const preparedRunnerConfig = await runWithAbsurdExecutionLeaseHeartbeat(
3318
+ {
3319
+ ctx,
3320
+ runId,
3321
+ operation: () => prepareRunnerConfig(claim),
3322
+ },
3323
+ );
3192
3324
  try {
3193
3325
  await cleanupAfterExecutionError(
3194
3326
  error,
@@ -3236,7 +3368,11 @@ async function executeAssignedAbsurdAttempt(input: {
3236
3368
  }
3237
3369
 
3238
3370
  const progressReporter = deps.createProgressReporter(launch, generation);
3239
- const preparedRunnerConfig = await prepareRunnerConfig(claim);
3371
+ const preparedRunnerConfig = await runWithAbsurdExecutionLeaseHeartbeat({
3372
+ ctx,
3373
+ runId,
3374
+ operation: () => prepareRunnerConfig(claim),
3375
+ });
3240
3376
  const runnerConfig = preparedRunnerConfig.runnerConfig;
3241
3377
 
3242
3378
  let runnerResult: PlayRunnerResult;
@@ -5003,6 +5139,9 @@ export function createAbsurdRuntimeTasks(
5003
5139
  if (deps?.deleteRuntimeSandbox && !deps.deleteRuntimeSandboxRef) {
5004
5140
  resolved.deleteRuntimeSandboxRef = undefined;
5005
5141
  }
5142
+ if (deps?.deleteRuntimeSandbox && !deps.deleteRuntimeSandboxWithOutcome) {
5143
+ resolved.deleteRuntimeSandboxWithOutcome = undefined;
5144
+ }
5006
5145
  absurd.registerTask({ name: ABSURD_PLAY_RUN_TASK, queue }, (params, ctx) =>
5007
5146
  executeAbsurdRuntimeLaunch(
5008
5147
  params as AbsurdPlayRunTaskParams,
@@ -5120,11 +5259,15 @@ async function readOpenAbsurdEngineRunId(input: {
5120
5259
  * can win that same terminalization race while a replacement is starting. In
5121
5260
  * that case Absurd rejects `complete_run` because the engine is already
5122
5261
  * terminal; that is an idempotent success, not a reason to flap the new worker.
5262
+ * A receipt can also arrive before its wake is claimed. Retire that pending or
5263
+ * sleeping task with the existing cancellation primitive after scheduler truth
5264
+ * is durable, rather than failing recovery because complete_run needs a claim.
5123
5265
  */
5124
5266
  async function completeAbsurdEngineRunIfStillRunning(input: {
5125
5267
  client: PostgresSchedulerQueryClient;
5126
5268
  queue: string;
5127
5269
  absurdRunId: string;
5270
+ absurdTaskId: string;
5128
5271
  result: unknown;
5129
5272
  }): Promise<boolean> {
5130
5273
  try {
@@ -5153,6 +5296,13 @@ async function completeAbsurdEngineRunIfStillRunning(input: {
5153
5296
  ) {
5154
5297
  return false;
5155
5298
  }
5299
+ if (state === 'pending' || state === 'sleeping') {
5300
+ return await cancelAbsurdEngineTaskAfterSchedulerTerminal({
5301
+ client: input.client,
5302
+ queue: input.queue,
5303
+ absurdTaskId: input.absurdTaskId,
5304
+ });
5305
+ }
5156
5306
  throw error;
5157
5307
  }
5158
5308
  }
@@ -5206,9 +5356,9 @@ async function cancelAbsurdEngineTaskAfterSchedulerTerminal(input: {
5206
5356
  * its eventual lease reclaim would allocate a fresh scheduler generation and
5207
5357
  * re-run a play that has already completed. This recovery uses the original
5208
5358
  * attempt lease and the exact same terminal path as the normal wake leg, then
5209
- * marks the engine run complete. It is deliberately limited to `completed`
5210
- * results: failure and suspension retain the engine's normal retry/park
5211
- * semantics once the scaler brings a recovery worker back for the lane.
5359
+ * closes an engine run when it still exists. Completed and failed receipts
5360
+ * remain authoritative even after the final engine claim expires. Suspended
5361
+ * receipts retain the normal continuation handoff.
5212
5362
  */
5213
5363
  export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
5214
5364
  client: PostgresSchedulerQueryClient;
@@ -5228,6 +5378,7 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
5228
5378
  {
5229
5379
  limit: input.limit,
5230
5380
  absurdReleaseId: input.absurdReleaseId,
5381
+ includeFailed: true,
5231
5382
  },
5232
5383
  options,
5233
5384
  );
@@ -5236,7 +5387,10 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
5236
5387
 
5237
5388
  for (const terminal of terminals) {
5238
5389
  const runnerResult = parseDetachedRunnerResult(terminal.result);
5239
- if (runnerResult?.status !== 'completed') {
5390
+ if (
5391
+ runnerResult?.status !== 'completed' &&
5392
+ runnerResult?.status !== 'failed'
5393
+ ) {
5240
5394
  skipped += 1;
5241
5395
  continue;
5242
5396
  }
@@ -5245,10 +5399,6 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
5245
5399
  queue: input.queue,
5246
5400
  schedulerRunId: terminal.runId,
5247
5401
  });
5248
- if (!absurdRun) {
5249
- skipped += 1;
5250
- continue;
5251
- }
5252
5402
  const launch = await readPostgresSchedulerLaunch(
5253
5403
  input.client,
5254
5404
  terminal.runId,
@@ -5267,6 +5417,16 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
5267
5417
  let schedulerStatus: 'completed' | 'failed';
5268
5418
  let terminalizedByAnotherWorker = false;
5269
5419
  try {
5420
+ await markPostgresSchedulerAttemptRunning(
5421
+ input.client,
5422
+ {
5423
+ runId: terminal.runId,
5424
+ attempt: terminal.attempt,
5425
+ leaseToken: terminal.leaseToken,
5426
+ preserveHeartbeatAt: true,
5427
+ },
5428
+ options,
5429
+ );
5270
5430
  const settled = await executePostgresSchedulerClaim(
5271
5431
  input.client,
5272
5432
  {
@@ -5296,22 +5456,26 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
5296
5456
  terminal.runId,
5297
5457
  options,
5298
5458
  );
5299
- if (observed?.status !== 'completed') throw error;
5300
- schedulerStatus = 'completed';
5459
+ if (observed?.status !== 'completed' && observed?.status !== 'failed')
5460
+ throw error;
5461
+ schedulerStatus = observed.status;
5301
5462
  terminalizedByAnotherWorker = true;
5302
5463
  }
5303
5464
 
5304
- const engineCompleted = await completeAbsurdEngineRunIfStillRunning({
5305
- client: input.client,
5306
- queue: input.queue,
5307
- absurdRunId: absurdRun.runId,
5308
- result: {
5309
- recovered: terminalizedByAnotherWorker
5310
- ? 'completed_runner_terminal_race'
5311
- : 'completed_runner_terminal',
5312
- schedulerStatus,
5313
- },
5314
- });
5465
+ const engineCompleted = absurdRun
5466
+ ? await completeAbsurdEngineRunIfStillRunning({
5467
+ client: input.client,
5468
+ queue: input.queue,
5469
+ absurdRunId: absurdRun.runId,
5470
+ absurdTaskId: absurdRun.taskId,
5471
+ result: {
5472
+ recovered: terminalizedByAnotherWorker
5473
+ ? 'completed_runner_terminal_race'
5474
+ : 'completed_runner_terminal',
5475
+ schedulerStatus,
5476
+ },
5477
+ })
5478
+ : false;
5315
5479
  if (terminalizedByAnotherWorker) {
5316
5480
  if (engineCompleted) recovered += 1;
5317
5481
  else skipped += 1;
@@ -6333,6 +6497,7 @@ function unavailableSandboxCrashDiagnostic(
6333
6497
  async function claimPostgresSchedulerSandboxCleanupBatch(input: {
6334
6498
  runWithClient: AbsurdRuntimeWorkerDeps['runWithClient'];
6335
6499
  leaseOwner: string;
6500
+ runId?: string;
6336
6501
  environment: 'preview' | 'production';
6337
6502
  schedulerOptions: Pick<PostgresSchedulerOptions, 'schema'>;
6338
6503
  limit: number;
@@ -6343,6 +6508,7 @@ async function claimPostgresSchedulerSandboxCleanupBatch(input: {
6343
6508
  client,
6344
6509
  {
6345
6510
  leaseOwner: input.leaseOwner,
6511
+ runId: input.runId,
6346
6512
  environment: input.environment,
6347
6513
  limit: input.limit,
6348
6514
  providers: input.providers,
@@ -6630,7 +6796,8 @@ async function processPostgresSchedulerSandboxCleanupJob(input: {
6630
6796
  provider: job.provider,
6631
6797
  sandboxId: job.sandboxId,
6632
6798
  cleanupAttempt: job.attemptCount,
6633
- terminalStatus: job.runStatusAtClaim,
6799
+ runStatusAtClaim: job.runStatusAtClaim,
6800
+ cleanupAuthority: 'durable_cleanup_claim',
6634
6801
  providerOutcome: outcome.providerOutcome,
6635
6802
  completionOutcome: outcome.completion,
6636
6803
  });
@@ -6656,6 +6823,7 @@ export async function drainPostgresSchedulerSandboxCleanupOnce(input: {
6656
6823
  deleteRuntimeSandboxRef?: AbsurdRuntimeWorkerDeps['deleteRuntimeSandboxRef'];
6657
6824
  deleteRuntimeSandboxWithOutcome?: AbsurdRuntimeWorkerDeps['deleteRuntimeSandboxWithOutcome'];
6658
6825
  leaseOwner: string;
6826
+ runId?: string;
6659
6827
  environment: 'preview' | 'production';
6660
6828
  schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
6661
6829
  limit?: number;
@@ -6677,6 +6845,7 @@ export async function drainPostgresSchedulerSandboxCleanupOnce(input: {
6677
6845
  const jobs = await claimPostgresSchedulerSandboxCleanupBatch({
6678
6846
  runWithClient: input.runWithClient,
6679
6847
  leaseOwner: input.leaseOwner,
6848
+ runId: input.runId,
6680
6849
  environment: input.environment,
6681
6850
  schedulerOptions: options,
6682
6851
  limit,