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
@@ -84,6 +84,7 @@ import {
84
84
  type PlayRunLedgerIngressEvent,
85
85
  } from '../run-ledger';
86
86
  import { stableStringify } from '../../plays/row-identity';
87
+ import { runtimeWorkCountersFromResources } from '../runtime-work-counters';
87
88
  import {
88
89
  isCtxFetchHttpFailureDiagnostic,
89
90
  parseCtxFetchHttpFailureDiagnostic,
@@ -8774,13 +8775,29 @@ export async function resolvePostgresSchedulerAmbiguousModalCreateIntentFromProv
8774
8775
  ): Promise<boolean> {
8775
8776
  const resolved = await client.query<{ run_id: string }>(
8776
8777
  `
8777
- UPDATE ${tableName('ambiguous_modal_creates', options)}
8778
- SET status = 'resolved', resolved_at = clock_timestamp()
8779
- WHERE run_id = $1
8780
- AND attempt = $2
8781
- AND modal_app_id = $3
8782
- AND status = 'pending'
8783
- RETURNING run_id
8778
+ WITH resolved AS (
8779
+ UPDATE ${tableName('ambiguous_modal_creates', options)}
8780
+ SET status = 'resolved', resolved_at = clock_timestamp()
8781
+ WHERE run_id = $1
8782
+ AND attempt = $2
8783
+ AND modal_app_id = $3
8784
+ AND status = 'pending'
8785
+ RETURNING run_id, sandbox_capacity_lease_id
8786
+ ), abandoned AS (
8787
+ UPDATE ${tableName('runtime_sandbox_operations', options)} AS operation
8788
+ SET state = 'abandoned', updated_at = clock_timestamp()
8789
+ FROM resolved
8790
+ WHERE operation.operation_key = resolved.sandbox_capacity_lease_id
8791
+ AND operation.state = 'reserved'
8792
+ RETURNING operation.operation_key
8793
+ ), released AS (
8794
+ UPDATE ${tableName('runtime_sandbox_capacity_leases', options)} AS lease
8795
+ SET released_at = clock_timestamp()
8796
+ FROM abandoned
8797
+ WHERE lease.lease_id = abandoned.operation_key
8798
+ AND lease.released_at IS NULL
8799
+ )
8800
+ SELECT run_id FROM resolved
8784
8801
  `,
8785
8802
  [input.runId, input.attempt, input.modalAppId],
8786
8803
  );
@@ -10587,6 +10604,131 @@ export async function enqueuePostgresSchedulerSandboxCleanupForAttempt(
10587
10604
  return enqueued.rows.length;
10588
10605
  }
10589
10606
 
10607
+ /** Queue every known sandbox before a requested cancellation can terminalize. */
10608
+ export async function enqueuePostgresSchedulerSandboxCleanupForRun(
10609
+ client: PostgresSchedulerQueryClient,
10610
+ input: { runId: string },
10611
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
10612
+ ): Promise<void> {
10613
+ await client.query(
10614
+ `
10615
+ INSERT INTO ${tableName('sandbox_cleanup_jobs_v2', options)} (
10616
+ provider, sandbox_id, run_id, attempt, environment, routing_domain
10617
+ )
10618
+ SELECT DISTINCT
10619
+ CASE resource->>'kind'
10620
+ WHEN 'modal_sandbox' THEN 'modal'
10621
+ ELSE 'daytona'
10622
+ END,
10623
+ resource->>'sandboxId',
10624
+ attempt.run_id,
10625
+ attempt.attempt,
10626
+ COALESCE(
10627
+ NULLIF(resource->>'runtimeEnvironment', ''),
10628
+ NULLIF(resource->>'daytonaEnvironment', ''),
10629
+ 'production'
10630
+ ),
10631
+ CASE resource->>'kind'
10632
+ WHEN 'modal_sandbox' THEN NULLIF(resource->>'modalAppId', '')
10633
+ ELSE NULLIF(resource->>'daytonaOrganizationId', '')
10634
+ END
10635
+ FROM ${tableName('attempts', options)} AS attempt
10636
+ CROSS JOIN LATERAL jsonb_array_elements(
10637
+ CASE
10638
+ WHEN jsonb_typeof(attempt.resource_json) = 'array'
10639
+ THEN attempt.resource_json
10640
+ ELSE jsonb_build_array(attempt.resource_json)
10641
+ END
10642
+ ) AS resource
10643
+ WHERE attempt.run_id = $1
10644
+ AND attempt.resource_json IS NOT NULL
10645
+ AND resource->>'kind' IN ('daytona_sandbox', 'modal_sandbox')
10646
+ AND NULLIF(resource->>'sandboxId', '') IS NOT NULL
10647
+ UNION ALL
10648
+ SELECT operation.provider, operation.sandbox_id, operation.run_id,
10649
+ operation.attempt, operation.environment, operation.routing_domain
10650
+ FROM ${tableName('runtime_sandbox_operations', options)} AS operation
10651
+ WHERE operation.run_id = $1
10652
+ AND operation.state IN ('live', 'deleting')
10653
+ AND operation.sandbox_id IS NOT NULL
10654
+ ON CONFLICT (provider, sandbox_id, routing_domain_key) DO NOTHING
10655
+ `,
10656
+ [input.runId],
10657
+ );
10658
+ }
10659
+
10660
+ /** Modal cannot create without a durable intent; an old reservation with no
10661
+ * pending selector or recorded resource is safe to release after recovery time. */
10662
+ export async function abandonUnstartedModalCancellationReservations(
10663
+ client: PostgresSchedulerQueryClient,
10664
+ input: { runId: string },
10665
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
10666
+ ): Promise<void> {
10667
+ await client.query(
10668
+ `WITH abandoned AS (
10669
+ UPDATE ${tableName('runtime_sandbox_operations', options)} AS operation
10670
+ SET state = 'abandoned', updated_at = clock_timestamp()
10671
+ WHERE operation.run_id = $1
10672
+ AND operation.provider = 'modal'
10673
+ AND operation.state = 'reserved'
10674
+ AND operation.created_at <= clock_timestamp()
10675
+ - ($2::double precision * interval '1 millisecond')
10676
+ AND NOT EXISTS (
10677
+ SELECT 1 FROM ${tableName('ambiguous_modal_creates', options)} AS intent
10678
+ WHERE intent.sandbox_capacity_lease_id = operation.operation_key
10679
+ AND intent.status = 'pending'
10680
+ )
10681
+ AND NOT EXISTS (
10682
+ SELECT 1 FROM ${tableName('attempts', options)} AS attempt
10683
+ CROSS JOIN LATERAL jsonb_array_elements(
10684
+ CASE
10685
+ WHEN jsonb_typeof(attempt.resource_json) = 'array'
10686
+ THEN attempt.resource_json
10687
+ WHEN attempt.resource_json IS NULL THEN '[]'::jsonb
10688
+ ELSE jsonb_build_array(attempt.resource_json)
10689
+ END
10690
+ ) AS resource
10691
+ WHERE attempt.run_id = operation.run_id
10692
+ AND resource->>'sandboxCapacityLeaseId' = operation.operation_key
10693
+ )
10694
+ RETURNING operation.operation_key
10695
+ )
10696
+ UPDATE ${tableName('runtime_sandbox_capacity_leases', options)} AS lease
10697
+ SET released_at = clock_timestamp()
10698
+ FROM abandoned
10699
+ WHERE lease.lease_id = abandoned.operation_key
10700
+ AND lease.released_at IS NULL`,
10701
+ [input.runId, RUNTIME_RELIABILITY_POLICY.recovery.horizonMs],
10702
+ );
10703
+ }
10704
+
10705
+ /** A reserved create or unconfirmed deletion keeps cancellation pending. */
10706
+ export async function hasPendingPostgresSchedulerSandboxCleanupForRun(
10707
+ client: PostgresSchedulerQueryClient,
10708
+ input: { runId: string },
10709
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
10710
+ ): Promise<boolean> {
10711
+ const pending = await client.query<{ pending: boolean }>(
10712
+ `
10713
+ SELECT EXISTS (
10714
+ SELECT 1
10715
+ FROM ${tableName('sandbox_cleanup_jobs_v2', options)}
10716
+ WHERE run_id = $1 AND status <> 'deleted'
10717
+ ) OR EXISTS (
10718
+ SELECT 1
10719
+ FROM ${tableName('runtime_sandbox_operations', options)}
10720
+ WHERE run_id = $1 AND state = 'reserved'
10721
+ ) OR EXISTS (
10722
+ SELECT 1
10723
+ FROM ${tableName('ambiguous_modal_creates', options)}
10724
+ WHERE run_id = $1 AND status = 'pending'
10725
+ ) AS pending
10726
+ `,
10727
+ [input.runId],
10728
+ );
10729
+ return pending.rows[0]?.pending ?? true;
10730
+ }
10731
+
10590
10732
  export async function claimPostgresSchedulerSandboxCleanupJobs(
10591
10733
  client: PostgresSchedulerQueryClient,
10592
10734
  input: {
@@ -12278,7 +12420,26 @@ export async function cancelPostgresSchedulerRun(
12278
12420
  WHEN elem->>'kind' IN ('daytona_sandbox', 'modal_sandbox')
12279
12421
  AND (elem->'billingEndedAt' IS NULL OR elem->'billingEndedAt' = 'null'::jsonb)
12280
12422
  THEN elem || jsonb_build_object(
12281
- 'billingEndedAt', extract(epoch FROM clock_timestamp()) * 1000,
12423
+ 'billingEndedAt', COALESCE(
12424
+ (
12425
+ SELECT extract(epoch FROM cleanup.deleted_at) * 1000
12426
+ FROM ${tableName('sandbox_cleanup_jobs_v2', options)} AS cleanup
12427
+ WHERE cleanup.provider = CASE elem->>'kind'
12428
+ WHEN 'modal_sandbox' THEN 'modal'
12429
+ ELSE 'daytona'
12430
+ END
12431
+ AND cleanup.sandbox_id = elem->>'sandboxId'
12432
+ AND cleanup.routing_domain_key = COALESCE(
12433
+ CASE elem->>'kind'
12434
+ WHEN 'modal_sandbox' THEN NULLIF(elem->>'modalAppId', '')
12435
+ ELSE NULLIF(elem->>'daytonaOrganizationId', '')
12436
+ END,
12437
+ ''
12438
+ )
12439
+ AND cleanup.status = 'deleted'
12440
+ ),
12441
+ extract(epoch FROM clock_timestamp()) * 1000
12442
+ ),
12282
12443
  'terminalAt', extract(epoch FROM clock_timestamp()) * 1000,
12283
12444
  'terminalReason', 'cancelled'
12284
12445
  )
@@ -12885,7 +13046,7 @@ function terminalLogLinesForOutbox(
12885
13046
  .slice(-LOG_TAIL_LIMIT);
12886
13047
  }
12887
13048
 
12888
- function attachRunnerWorkProgressMetadata(
13049
+ export function attachRunnerWorkProgressMetadata(
12889
13050
  output: unknown,
12890
13051
  runnerResult: unknown,
12891
13052
  options: {
@@ -12937,6 +13098,13 @@ function attachRunnerWorkProgressMetadata(
12937
13098
  const runtimeResources = metadata?.runtimeResources
12938
13099
  ? null
12939
13100
  : boundedRuntimeResourcesMetadata(runnerResult);
13101
+ // Keep the two scalar counts when the detailed runtimeResources record
13102
+ // exceeds its metadata budget. Compact status must not scan a call log to
13103
+ // reconstruct physical work.
13104
+ const runtimeCounters = runtimeWorkCountersFromResources(
13105
+ (runnerResult as { stats?: { runtimeResources?: unknown } }).stats
13106
+ ?.runtimeResources,
13107
+ );
12940
13108
  const outputWarnings = Array.isArray(
12941
13109
  (runnerResult as { outputWarnings?: unknown }).outputWarnings,
12942
13110
  )
@@ -12979,6 +13147,8 @@ function attachRunnerWorkProgressMetadata(
12979
13147
  (total === null || skipped === null || metadata?.workProgress) &&
12980
13148
  !runLogTail &&
12981
13149
  !runtimeResources &&
13150
+ !runtimeCounters &&
13151
+ !metadata?.runtimeCounters &&
12982
13152
  !rowOutcomes &&
12983
13153
  mergedOutputWarnings.length === 0
12984
13154
  ) {
@@ -13010,6 +13180,9 @@ function attachRunnerWorkProgressMetadata(
13010
13180
  ...record,
13011
13181
  _metadata: {
13012
13182
  ...(metadata ?? {}),
13183
+ // A previous attempt's scalar evidence cannot describe this terminal
13184
+ // attempt. Undefined removes it from persisted JSON when unavailable.
13185
+ runtimeCounters: runtimeCounters ?? undefined,
13013
13186
  ...(workProgress ? { workProgress } : {}),
13014
13187
  ...(rowOutcomes ? { rowOutcomes } : {}),
13015
13188
  ...(runLogTail ? { runLogTail } : {}),
@@ -15507,8 +15680,63 @@ export async function readPostgresSchedulerResult(
15507
15680
  export async function readPostgresSchedulerRunStatus(
15508
15681
  client: PostgresSchedulerQueryClient,
15509
15682
  runId: string,
15510
- options?: Pick<PostgresSchedulerOptions, 'schema'>,
15683
+ options?: Pick<PostgresSchedulerOptions, 'schema'> & {
15684
+ includeDetails?: boolean;
15685
+ },
15511
15686
  ): Promise<PostgresSchedulerRunStatusSnapshot | null> {
15687
+ if (options?.includeDetails === false) {
15688
+ // An admission projection gap needs lifecycle truth only. Launch, result,
15689
+ // suspension and error JSON may contain row/call-sized detail: never even
15690
+ // select them for status polling. Missing progress remains unconfirmed.
15691
+ const result = await client.query<
15692
+ Pick<
15693
+ RunStatusRow,
15694
+ | 'run_id'
15695
+ | 'org_id'
15696
+ | 'status'
15697
+ | 'play_name'
15698
+ | 'graph_hash'
15699
+ | 'runtime_profile'
15700
+ | 'created_at'
15701
+ | 'updated_at'
15702
+ | 'started_at'
15703
+ | 'finished_at'
15704
+ >
15705
+ >(
15706
+ `
15707
+ SELECT run_id, org_id, status, play_name, graph_hash, runtime_profile,
15708
+ created_at, updated_at, started_at, finished_at
15709
+ FROM ${tableName('runs', options)}
15710
+ WHERE run_id = $1
15711
+ `,
15712
+ [runId],
15713
+ );
15714
+ const row = result.rows[0];
15715
+ if (!row) return null;
15716
+ return {
15717
+ runId: row.run_id,
15718
+ orgId: row.org_id ?? '',
15719
+ status: row.status,
15720
+ playName: row.play_name,
15721
+ graphHash: row.graph_hash,
15722
+ runtimeBackend: null,
15723
+ schedulerBackend: 'absurd',
15724
+ executionProfile: row.runtime_profile,
15725
+ createdAt: row.created_at?.getTime() ?? null,
15726
+ updatedAt: row.updated_at?.getTime() ?? null,
15727
+ startedAt: row.started_at?.getTime() ?? null,
15728
+ finishedAt: row.finished_at?.getTime() ?? null,
15729
+ durationMs:
15730
+ row.finished_at && row.started_at
15731
+ ? Math.max(0, row.finished_at.getTime() - row.started_at.getTime())
15732
+ : null,
15733
+ runtimeTiming: null,
15734
+ result: null,
15735
+ error: null,
15736
+ suspension: null,
15737
+ staticPipeline: null,
15738
+ };
15739
+ }
15512
15740
  const result = await client.query<RunStatusRow>(
15513
15741
  `
15514
15742
  SELECT run_id, org_id, status, play_name, graph_hash, runtime_profile,
@@ -15723,6 +15951,10 @@ export async function assertPostgresRunLedgerDelivery(
15723
15951
  runId: string;
15724
15952
  idempotencyKey: string;
15725
15953
  leaseOwner?: string;
15954
+ projectedEvents?: readonly AcceptedPlayRunLedgerEvent[];
15955
+ normalizeProjectedEvents?: (
15956
+ events: readonly PlayRunLedgerIngressEvent[],
15957
+ ) => readonly AcceptedPlayRunLedgerEvent[];
15726
15958
  },
15727
15959
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
15728
15960
  ): Promise<void> {
@@ -15733,6 +15965,8 @@ export async function assertPostgresRunLedgerDelivery(
15733
15965
  lease_live: boolean;
15734
15966
  due: boolean;
15735
15967
  blocked: boolean;
15968
+ payload_json: unknown;
15969
+ created_at: Date;
15736
15970
  }>(
15737
15971
  `
15738
15972
  SELECT outbox.delivery_key,
@@ -15740,7 +15974,9 @@ export async function assertPostgresRunLedgerDelivery(
15740
15974
  outbox.lease_owner,
15741
15975
  outbox.lease_expires_at > now() AS lease_live,
15742
15976
  coalesce(outbox.next_attempt_at, outbox.created_at) <= now() AS due,
15743
- outbox.failed_at IS NOT NULL AS blocked
15977
+ outbox.failed_at IS NOT NULL AS blocked,
15978
+ outbox.payload_json,
15979
+ outbox.created_at
15744
15980
  FROM ${tableName('outbox', options)} AS outbox
15745
15981
  JOIN ${tableName('runs', options)} AS runs ON runs.run_id = outbox.run_id
15746
15982
  WHERE outbox.run_id = $1 AND runs.org_id = $2
@@ -15755,12 +15991,9 @@ export async function assertPostgresRunLedgerDelivery(
15755
15991
  throw new RunLedgerProjectionLeaseLostError();
15756
15992
  }
15757
15993
 
15758
- // A normal projector leases only the batch it is about to send. Cancellation
15759
- // intentionally transfers the entire pending causal prefix to one stable
15760
- // owner before synchronously draining it, while run.events are still claimed
15761
- // one row at a time. Validate the submitted key against a *contiguous due
15762
- // prefix*, not every row carrying that owner; otherwise a valid cancellation
15763
- // head is rejected whenever later facts share the cancellation lease.
15994
+ // Cancellation may own several pending facts at once. Validate the exact
15995
+ // contiguous, due prefix named by the receipt instead of all rows sharing
15996
+ // that cancellation lease.
15764
15997
  const prefix: typeof rows = [];
15765
15998
  for (const row of rows) {
15766
15999
  if (
@@ -15780,6 +16013,9 @@ export async function assertPostgresRunLedgerDelivery(
15780
16013
  candidate.event_type,
15781
16014
  ),
15782
16015
  );
16016
+ const prefixDigest = createHash('sha256')
16017
+ .update(prefix.map((candidate) => candidate.delivery_key).join('\n'))
16018
+ .digest('hex');
15783
16019
  let expectedKey: string;
15784
16020
  if (terminal) {
15785
16021
  expectedKey = buildRunLedgerTerminalIdempotencyKey({
@@ -15795,29 +16031,58 @@ export async function assertPostgresRunLedgerDelivery(
15795
16031
  deliveryKey: first.delivery_key,
15796
16032
  });
15797
16033
  } else {
15798
- const digest = createHash('sha256')
15799
- .update(prefix.map((candidate) => candidate.delivery_key).join('\n'))
15800
- .digest('hex');
15801
16034
  expectedKey = buildRunLedgerEventsIdempotencyKey({
15802
16035
  runId: input.runId,
15803
- deliveryDigest: digest,
16036
+ deliveryDigest: prefixDigest,
15804
16037
  });
15805
16038
  }
15806
- if (input.idempotencyKey === expectedKey) return;
15807
16039
 
15808
- // The pre-batch projector used run-terminal:<delivery-key> for a single
15809
- // nonterminal fact. Retain that wire-compatibility key for the current
15810
- // singleton head, but never let it authorize a later row in the prefix.
15811
- if (
16040
+ // Older batch dispatchers may use the digest receipt even when the batch
16041
+ // contains a terminal fact. Keep accepting it for this exact prefix.
16042
+ const batchKey = buildRunLedgerEventsIdempotencyKey({
16043
+ runId: input.runId,
16044
+ deliveryDigest: prefixDigest,
16045
+ });
16046
+
16047
+ // Preserve the old singleton key for non-start, nonterminal facts.
16048
+ const legacySingletonKey =
15812
16049
  prefix.length === 1 &&
15813
16050
  !['run.created', 'run.started'].includes(first.event_type) &&
15814
16051
  input.idempotencyKey ===
15815
16052
  buildRunLedgerTerminalIdempotencyKey({
15816
16053
  runId: input.runId,
15817
16054
  deliveryKey: first.delivery_key,
15818
- })
16055
+ });
16056
+ if (
16057
+ input.idempotencyKey === expectedKey ||
16058
+ input.idempotencyKey === batchKey ||
16059
+ legacySingletonKey
15819
16060
  ) {
15820
- return;
16061
+ if (input.projectedEvents === undefined) return;
16062
+ const prefixEvents: PlayRunLedgerIngressEvent[] = [];
16063
+ for (const prefixRow of prefix) {
16064
+ prefixEvents.push(
16065
+ ...buildRunLedgerEventsFromPostgresSchedulerOutbox({
16066
+ eventId: prefixRow.delivery_key,
16067
+ runId: input.runId,
16068
+ eventType: prefixRow.event_type,
16069
+ payload: prefixRow.payload_json,
16070
+ createdAt: new Date(prefixRow.created_at),
16071
+ leaseOwner: prefixRow.lease_owner ?? undefined,
16072
+ deliveryKey: prefixRow.delivery_key,
16073
+ }),
16074
+ );
16075
+ }
16076
+ const comparablePrefixEvents = input.normalizeProjectedEvents
16077
+ ? input.normalizeProjectedEvents(prefixEvents)
16078
+ : prefixEvents;
16079
+ if (
16080
+ stableStringify(input.projectedEvents) ===
16081
+ stableStringify(comparablePrefixEvents)
16082
+ ) {
16083
+ return;
16084
+ }
16085
+ throw new RunLedgerProjectionLeaseLostError();
15821
16086
  }
15822
16087
 
15823
16088
  if (
@@ -15997,7 +16262,7 @@ export async function readPendingPostgresSchedulerOutboxEvents(
15997
16262
  )
15998
16263
  )
15999
16264
  )
16000
- ORDER BY outbox.created_at, outbox.event_id
16265
+ ORDER BY ${runLedgerOrderSql('outbox')}
16001
16266
  FOR UPDATE OF outbox
16002
16267
  LIMIT $1
16003
16268
  ), terminal_batch AS (
@@ -16033,7 +16298,7 @@ export async function readPendingPostgresSchedulerOutboxEvents(
16033
16298
  SELECT event_id, run_id, event_type, payload_json, created_at, lease_owner,
16034
16299
  delivery_key
16035
16300
  FROM claimed
16036
- ORDER BY created_at, event_id
16301
+ ORDER BY ${runLedgerOrderSql('claimed')}
16037
16302
  `,
16038
16303
  [limit, runId, leaseOwner, leaseSeconds],
16039
16304
  );
@@ -16127,7 +16392,9 @@ export async function readPostgresSchedulerOutboxNextDueAt(
16127
16392
  export async function readPostgresSchedulerOutboxDeliveryStatus(
16128
16393
  client: PostgresSchedulerQueryClient,
16129
16394
  input: { runId: string; eventId: string },
16130
- options?: Pick<PostgresSchedulerOptions, 'schema'>,
16395
+ options?: Pick<PostgresSchedulerOptions, 'schema'> & {
16396
+ includeBlocked?: boolean;
16397
+ },
16131
16398
  ): Promise<PostgresSchedulerOutboxDeliveryStatus> {
16132
16399
  const runId = input.runId.trim();
16133
16400
  const eventId = input.eventId.trim();
@@ -16138,13 +16405,17 @@ export async function readPostgresSchedulerOutboxDeliveryStatus(
16138
16405
  }
16139
16406
  const result = await client.query<{ delivered: boolean; blocked: boolean }>(
16140
16407
  `SELECT target.delivered_at IS NOT NULL AS delivered,
16141
- target.failed_at IS NOT NULL OR EXISTS (
16408
+ ${
16409
+ options?.includeBlocked === false
16410
+ ? 'false'
16411
+ : `target.failed_at IS NOT NULL OR EXISTS (
16142
16412
  SELECT 1
16143
16413
  FROM ${tableName('outbox', options)} predecessor
16144
16414
  WHERE predecessor.run_id = target.run_id
16145
16415
  AND predecessor.failed_at IS NOT NULL
16146
16416
  AND ${runLedgerPrecedesSql('predecessor', 'target', true)}
16147
- ) AS blocked
16417
+ )`
16418
+ } AS blocked
16148
16419
  FROM ${tableName('outbox', options)} target
16149
16420
  WHERE target.run_id = $1 AND target.event_id = $2`,
16150
16421
  [runId, eventId],
@@ -66,6 +66,7 @@ import {
66
66
  createRuntimeReceiptValuePersister,
67
67
  createRuntimeReceiptValueReader,
68
68
  RUNTIME_RECEIPT_VALUE_INLINE_MAX_BYTES,
69
+ RuntimeValueStoreUnavailableError,
69
70
  type RuntimeReceiptValueBlobStore,
70
71
  type RuntimeReceiptValuePersister,
71
72
  type RuntimeReceiptValueReader,
@@ -250,6 +251,26 @@ export function receiptGatewayFailureResponse(
250
251
  },
251
252
  );
252
253
  }
254
+ if (error instanceof RuntimeValueStoreUnavailableError) {
255
+ return receiptGatewayJsonResponse(
256
+ {
257
+ error: `${operationLabel} object store is temporarily unavailable.`,
258
+ code: 'runtime_value_store_unavailable',
259
+ retryable: true,
260
+ retry_after_ms: 1_000,
261
+ detail: error.message,
262
+ ...(context.action ? { action: context.action } : {}),
263
+ },
264
+ {
265
+ status: 503,
266
+ headers: {
267
+ 'retry-after': '1',
268
+ 'x-deepline-error-code': 'runtime_value_store_unavailable',
269
+ 'x-deepline-error-detail': error.message,
270
+ },
271
+ },
272
+ );
273
+ }
253
274
  if (isRuntimeSchedulerCapacityError(error)) {
254
275
  const retryAfterMs = runtimeSchedulerCapacityRetryAfterMs(error);
255
276
  return receiptGatewayJsonResponse(
@@ -25,6 +25,40 @@ export type RuntimeValueBlobStore = {
25
25
  /** Backward-compatible receipt-specific name for the shared object store. */
26
26
  export type RuntimeReceiptValueBlobStore = RuntimeValueBlobStore;
27
27
 
28
+ export class RuntimeValueStoreUnavailableError extends Error {
29
+ constructor(
30
+ readonly operation: 'put' | 'head',
31
+ readonly status: number,
32
+ cause: unknown,
33
+ ) {
34
+ super(`Runtime value object store ${operation} returned HTTP ${status}.`, {
35
+ cause,
36
+ });
37
+ this.name = 'RuntimeValueStoreUnavailableError';
38
+ }
39
+ }
40
+
41
+ function transientStoreStatus(error: unknown): number | null {
42
+ if (!error || typeof error !== 'object') return null;
43
+ const metadata = (error as { $metadata?: { httpStatusCode?: unknown } })
44
+ .$metadata;
45
+ const status = metadata?.httpStatusCode;
46
+ return typeof status === 'number' &&
47
+ (status === 408 || status === 429 || status >= 500)
48
+ ? status
49
+ : null;
50
+ }
51
+
52
+ function transientStoreError(
53
+ operation: 'put' | 'head',
54
+ error: unknown,
55
+ ): RuntimeValueStoreUnavailableError | null {
56
+ const status = transientStoreStatus(error);
57
+ return status === null
58
+ ? null
59
+ : new RuntimeValueStoreUnavailableError(operation, status, error);
60
+ }
61
+
28
62
  /**
29
63
  * Foundation owns value references and bounded I/O admission; the deployment
30
64
  * injects its object-store adapter. This keeps R2 credentials and transport
@@ -185,7 +219,13 @@ export async function persistRuntimeReceiptValue(
185
219
  // An immutable deterministic object that is observable after a failed PUT
186
220
  // is the same attempted publication. Treat it as a recovered write, never
187
221
  // as permission to call the provider again.
188
- if (!(await store.exists(outputRef.key))) throw error;
222
+ let exists: boolean;
223
+ try {
224
+ exists = await store.exists(outputRef.key);
225
+ } catch (probeError) {
226
+ throw transientStoreError('head', probeError) ?? probeError;
227
+ }
228
+ if (!exists) throw transientStoreError('put', error) ?? error;
189
229
  }
190
230
  return { kind: 'reference', outputJson: null, outputRef };
191
231
  }
@@ -209,11 +249,10 @@ export function createRuntimeReceiptValuePersister(
209
249
  serialized: string;
210
250
  inlineFields?: RuntimeReceiptValueInlineFields;
211
251
  };
212
- const writes = new BoundedBatchFunction<
213
- Write,
214
- PersistedRuntimeReceiptValue,
215
- 'r2'
216
- >({
252
+ type WriteOutcome =
253
+ | { ok: true; value: PersistedRuntimeReceiptValue }
254
+ | { ok: false; error: unknown };
255
+ const writes = new BoundedBatchFunction<Write, WriteOutcome, 'r2'>({
217
256
  keyId: () => 'r2',
218
257
  estimateBytes: (value) => Buffer.byteLength(value.serialized, 'utf8'),
219
258
  maxBatchItems: 4,
@@ -224,16 +263,24 @@ export function createRuntimeReceiptValuePersister(
224
263
  maxRetainedUnits: 32,
225
264
  maxInFlightBatches: 2,
226
265
  flushMs: 0,
266
+ // R2 has no multi-PUT. One failed object must not reject the other
267
+ // independent receipts admitted alongside it.
227
268
  execute: async ({ values }) =>
228
269
  await Promise.all(
229
- values.map(
230
- async (value) =>
231
- await persistRuntimeReceiptValue({
232
- ...value,
233
- inlineMaxBytes,
234
- ...(input.store ? { store: input.store } : {}),
235
- }),
236
- ),
270
+ values.map(async (value): Promise<WriteOutcome> => {
271
+ try {
272
+ return {
273
+ ok: true,
274
+ value: await persistRuntimeReceiptValue({
275
+ ...value,
276
+ inlineMaxBytes,
277
+ ...(input.store ? { store: input.store } : {}),
278
+ }),
279
+ };
280
+ } catch (error) {
281
+ return { ok: false, error };
282
+ }
283
+ }),
237
284
  ),
238
285
  });
239
286
  return async (value) => {
@@ -244,7 +291,9 @@ export function createRuntimeReceiptValuePersister(
244
291
  ...(input.store ? { store: input.store } : {}),
245
292
  });
246
293
  }
247
- return await writes.submit({ key: 'r2', value });
294
+ const outcome = await writes.submit({ key: 'r2', value });
295
+ if (!outcome.ok) throw outcome.error;
296
+ return outcome.value;
248
297
  };
249
298
  }
250
299