deepline 0.3.289 → 0.3.291

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 (64) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +8 -6
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/runs/overview.ts +16 -6
  4. package/dist/bundling-sources/sdk/src/types.ts +21 -2
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +41 -11
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +317 -63
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -2
  8. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +1 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +36 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +19 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +6 -5
  14. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +12 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/run-event-catalog.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +74 -33
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +73 -3
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +39 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +72 -9
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +2 -3
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +77 -10
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +1 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool-options.ts +145 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +17 -9
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -139
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +19 -5
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +176 -18
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +7 -5
  31. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1404 -478
  32. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +55 -25
  33. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +97 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +34 -15
  35. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +558 -57
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +10 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +6 -1
  38. package/dist/bundling-sources/shared_libs/play-runtime/test-admission-batching.ts +103 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +22 -5
  40. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +35 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -8
  42. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +75 -28
  44. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
  45. package/dist/cli/index.js +195 -582
  46. package/dist/cli/index.mjs +195 -582
  47. package/dist/{client-FR_u0dxT.d.ts → client-Ct4jmvAg.d.ts} +29 -8
  48. package/dist/{client-phK1Ixcl.d.mts → client-Ct9GKrwu.d.mts} +29 -8
  49. package/dist/{compiler-manifest-9SQtUuKb.d.mts → compiler-manifest-qtTNYvBl.d.mts} +2 -0
  50. package/dist/{compiler-manifest-9SQtUuKb.d.ts → compiler-manifest-qtTNYvBl.d.ts} +2 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/index.d.ts +4 -4
  53. package/dist/index.js +14 -5
  54. package/dist/index.mjs +14 -5
  55. package/dist/install-integrity.json +7 -4
  56. package/dist/plays/bundle-play-file.d.mts +2 -2
  57. package/dist/plays/bundle-play-file.d.ts +2 -2
  58. package/dist/plays/run-request.d.mts +2 -2
  59. package/dist/plays/run-request.d.ts +2 -2
  60. package/dist/release.d.mts +1 -1
  61. package/dist/release.d.ts +1 -1
  62. package/dist/release.js +1 -1
  63. package/dist/release.mjs +1 -1
  64. package/package.json +1 -1
@@ -11,7 +11,6 @@ import { RUN_EVENT_KIND } from '../run-event-catalog';
11
11
  * absurd-specific wiring (queue, event names, checkpoint boundary shaping).
12
12
  */
13
13
  import { createHash } from 'node:crypto';
14
- import { AsyncLocalStorage } from 'node:async_hooks';
15
14
  import type {
16
15
  PlayCheckpoint,
17
16
  PlayExecutionEvent,
@@ -22,11 +21,6 @@ import type { PlayRunLedgerEvent } from '../run-ledger';
22
21
  import type { PlaySchedulerSubmitInput } from '../scheduler-backend';
23
22
  import type { WorkerRuntimeApiContext } from '../app-runtime-api';
24
23
  import { RUNTIME_CAPACITY_POLICY } from '../runtime-capacity-policy';
25
- import {
26
- PLAY_ADMISSION_MAX_ACQUIRE_QUEUE_WAIT_MS,
27
- PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES,
28
- PLAY_ADMISSION_MAX_PARALLELISM,
29
- } from '../admission-transport-policy';
30
24
  import { assertKnownResolvedBoundary } from '../checkpoint-boundaries';
31
25
  import type { Absurd, Log, RetryStrategy } from '@deepline/absurd/sdk';
32
26
  import { isIsolatedRuntimeSchedulerSchema } from '../runtime-scheduler-topology';
@@ -37,8 +31,6 @@ import {
37
31
  } from './postgres';
38
32
  import { createAbsurdForRuntimeScheduler } from './absurd-pool';
39
33
  import {
40
- RUNTIME_SCHEDULER_CONTROL_PLANE_MAX_CLIENTS,
41
- resolveWorkflowRuntimePostgresUrl,
42
34
  withRuntimeSchedulerClient,
43
35
  type RuntimeSchedulerPoolOptions,
44
36
  } from './postgres-pool';
@@ -548,137 +540,14 @@ export type AbsurdRuntimeResult = {
548
540
  error?: string;
549
541
  };
550
542
 
551
- // All queues in one worker share this single process pool. Scheduler queries
552
- // are short and batched; execution concurrency belongs to Daytona/Fly slots,
553
- // not to a growing set of database clients. Keeping this at two makes a
554
- // horizontally scaled fleet predictable against the shared PgBouncer budget.
555
- export const ABSURD_RUNTIME_POOL_MAX_CLIENTS =
556
- RUNTIME_SCHEDULER_CONTROL_PLANE_MAX_CLIENTS;
557
-
558
- /**
559
- * Vercel Fluid invocations share a process and its scheduler pool. A single
560
- * client serializes unrelated launches, reads, and critical lease lookups;
561
- * one held connection can make every other request hit its acquire timeout.
562
- * Use the shared two-client control-plane budget so another request can make
563
- * progress while keeping each instance's PgBouncer footprint bounded.
564
- */
565
- export const ABSURD_SERVERLESS_POOL_MAX_CLIENTS =
566
- RUNTIME_SCHEDULER_CONTROL_PLANE_MAX_CLIENTS;
567
-
568
- /**
569
- * A Vercel isolate may survive long after a webhook has returned. Keep its
570
- * scheduler clients only long enough to reuse them for an immediate follow-up,
571
- * then return idle PgBouncer clients before the source's delayed Workpool
572
- * recovery is due. The worker fleet keeps the ordinary 30-second idle lifetime.
573
- */
574
- export const ABSURD_SERVERLESS_POOL_IDLE_TIMEOUT_MS = 1_000;
575
-
576
- // Ordinary Vercel requests share the bounded control-plane pool above. The
577
- // private batch adapter is different: Workpool elects one global up-to-1,024-
578
- // source collection before entering this context, so one batch request may use
579
- // its larger bounded scheduler capacity without multiplying it per source.
580
- // Other Workpool adapters can still overlap during rollout.
581
- type ServerlessAdmissionPoolContext = {
582
- maxClients: number;
583
- maxAcquireQueueDepth: number;
584
- maxAcquireQueueWaitMs: number;
585
- };
586
-
587
- const serverlessAdmissionPoolContext =
588
- new AsyncLocalStorage<ServerlessAdmissionPoolContext>();
589
-
590
- export async function withServerlessAdmissionPoolMaxClients<T>(input: {
591
- maxClients: number;
592
- maxAcquireQueueDepth?: number;
593
- maxAcquireQueueWaitMs?: number;
594
- fn: () => Promise<T>;
595
- }): Promise<T> {
596
- if (
597
- !Number.isInteger(input.maxClients) ||
598
- input.maxClients < 1 ||
599
- input.maxClients > PLAY_ADMISSION_MAX_PARALLELISM
600
- ) {
601
- throw new Error(
602
- 'Serverless admission pool capacity is outside the supported range.',
603
- );
604
- }
605
- const maxAcquireQueueDepth =
606
- input.maxAcquireQueueDepth ?? PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES;
607
- const maxAcquireQueueWaitMs =
608
- input.maxAcquireQueueWaitMs ?? PLAY_ADMISSION_MAX_ACQUIRE_QUEUE_WAIT_MS;
609
- if (
610
- !Number.isSafeInteger(maxAcquireQueueDepth) ||
611
- maxAcquireQueueDepth < 1 ||
612
- maxAcquireQueueDepth > PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES
613
- ) {
614
- throw new Error(
615
- 'Serverless admission acquire queue capacity is outside the supported range.',
616
- );
617
- }
618
- if (
619
- !Number.isSafeInteger(maxAcquireQueueWaitMs) ||
620
- maxAcquireQueueWaitMs < 5_000 ||
621
- maxAcquireQueueWaitMs > PLAY_ADMISSION_MAX_ACQUIRE_QUEUE_WAIT_MS
622
- ) {
623
- throw new Error(
624
- 'Serverless admission acquire queue wait is outside the supported range.',
625
- );
626
- }
627
- return await serverlessAdmissionPoolContext.run(
628
- {
629
- maxClients: input.maxClients,
630
- maxAcquireQueueDepth,
631
- maxAcquireQueueWaitMs,
632
- },
633
- input.fn,
634
- );
635
- }
636
-
637
- /**
638
- * The private batch adapter owns this request-local scheduler pool. Reserve
639
- * one client for executor-token and other critical control-plane traffic; the
640
- * rest may submit the independently idempotent batch sources in parallel.
641
- */
642
- export function serverlessAdmissionSubmitMaxActive(): number | null {
643
- const context = serverlessAdmissionPoolContext.getStore();
644
- return context === undefined ? null : Math.max(1, context.maxClients - 1);
645
- }
646
-
647
- /**
648
- * Resolve the runtime scheduler Postgres pool options for the absurd control
649
- * plane. Loud failure over silent fallback: the absurd worker and scheduler
650
- * only run under Node where `WORKFLOW_RUNTIME_POSTGRES` is configured.
651
- */
652
- export function resolveAbsurdPoolOptions(
653
- explicitUrl?: string | null,
654
- ): RuntimeSchedulerPoolOptions {
655
- const url = explicitUrl?.trim() || resolveWorkflowRuntimePostgresUrl();
656
- if (!url) {
657
- throw new Error(
658
- 'absurd scheduler backend requires WORKFLOW_RUNTIME_POSTGRES (runtime ' +
659
- 'scheduler Postgres URL). Refusing to fall back to local execution.',
660
- );
661
- }
662
- const serverless = process.env.VERCEL?.trim();
663
- const context = serverlessAdmissionPoolContext.getStore();
664
- return {
665
- url,
666
- applicationName: 'deepline-absurd-runtime',
667
- maxClients:
668
- serverless || context !== undefined
669
- ? (context?.maxClients ?? ABSURD_SERVERLESS_POOL_MAX_CLIENTS)
670
- : ABSURD_RUNTIME_POOL_MAX_CLIENTS,
671
- ...(serverless || context !== undefined
672
- ? { idleTimeoutMs: ABSURD_SERVERLESS_POOL_IDLE_TIMEOUT_MS }
673
- : {}),
674
- ...(context
675
- ? {
676
- maxAcquireQueueDepth: context.maxAcquireQueueDepth,
677
- maxAcquireQueueWaitMs: context.maxAcquireQueueWaitMs,
678
- }
679
- : {}),
680
- };
681
- }
543
+ export {
544
+ ABSURD_RUNTIME_POOL_MAX_CLIENTS,
545
+ ABSURD_SERVERLESS_POOL_MAX_CLIENTS,
546
+ ABSURD_SERVERLESS_POOL_IDLE_TIMEOUT_MS,
547
+ withServerlessAdmissionPoolMaxClients,
548
+ serverlessAdmissionSubmitMaxActive,
549
+ resolveAbsurdPoolOptions,
550
+ } from './absurd-pool-options';
682
551
 
683
552
  export function normalizeAbsurdSchedulerOptions(
684
553
  options: { url?: string | null; schema?: string | null } = {},
@@ -22,6 +22,7 @@
22
22
  * connection. Notifications are strictly a wakeup hint — the tables remain
23
23
  * the source of truth and every waiter still reads the DB.
24
24
  */
25
+ import { attachDatabasePool } from '@vercel/functions/db-connections';
25
26
  import { Client, Pool } from 'pg';
26
27
  import {
27
28
  RuntimeSchedulerPgBouncerCapacityError,
@@ -175,6 +176,8 @@ export const RUNTIME_SCHEDULER_POOL_DEFAULTS = {
175
176
  keepAliveIntervalMs: 60_000,
176
177
  } as const;
177
178
 
179
+ export const RUNTIME_SCHEDULER_SERVERLESS_IDLE_TIMEOUT_MS = 1_000;
180
+
178
181
  /**
179
182
  * A control-plane process only needs a few short scheduler transactions at
180
183
  * once. This shared cap keeps horizontally scaled workers and the receipt
@@ -197,8 +200,8 @@ export type RuntimeSchedulerPoolOptions = {
197
200
  maxClients?: number;
198
201
  /**
199
202
  * Serverless launchers must not pin a scarce PgBouncer client between
200
- * requests. Long-lived workers retain the default; Vercel callers opt into
201
- * a short idle lifetime through the shared Absurd pool resolver.
203
+ * requests. Vercel pools share one short idle lifetime; this override only
204
+ * applies to long-lived processes.
202
205
  */
203
206
  idleTimeoutMs?: number;
204
207
  /**
@@ -447,6 +450,7 @@ export class RuntimeSchedulerPoolAdmission {
447
450
 
448
451
  type PoolEntry = {
449
452
  pool: Pool;
453
+ databasePoolAttached?: boolean;
450
454
  maxClients: number;
451
455
  maxAcquireQueueDepth: number;
452
456
  admission: RuntimeSchedulerPoolAdmission;
@@ -723,7 +727,7 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
723
727
  assertRuntimeSchedulerHotPathUrl(options.url);
724
728
  const maxClients =
725
729
  options.maxClients ?? RUNTIME_SCHEDULER_POOL_DEFAULTS.maxClients;
726
- const idleTimeoutMs =
730
+ const requestedIdleTimeoutMs =
727
731
  options.idleTimeoutMs ?? RUNTIME_SCHEDULER_POOL_DEFAULTS.idleTimeoutMs;
728
732
  const maxAcquireQueueDepth =
729
733
  options.maxAcquireQueueDepth ??
@@ -738,11 +742,17 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
738
742
  'instead of widening a single process pool.',
739
743
  );
740
744
  }
741
- if (!Number.isSafeInteger(idleTimeoutMs) || idleTimeoutMs < 0) {
745
+ if (
746
+ !Number.isSafeInteger(requestedIdleTimeoutMs) ||
747
+ requestedIdleTimeoutMs < 0
748
+ ) {
742
749
  throw new Error(
743
- `Runtime scheduler pool idleTimeoutMs=${idleTimeoutMs} must be a non-negative integer.`,
750
+ `Runtime scheduler pool idleTimeoutMs=${requestedIdleTimeoutMs} must be a non-negative integer.`,
744
751
  );
745
752
  }
753
+ const idleTimeoutMs = process.env.VERCEL?.trim()
754
+ ? RUNTIME_SCHEDULER_SERVERLESS_IDLE_TIMEOUT_MS
755
+ : requestedIdleTimeoutMs;
746
756
  if (
747
757
  !Number.isSafeInteger(maxAcquireQueueDepth) ||
748
758
  maxAcquireQueueDepth < 1 ||
@@ -1049,6 +1059,10 @@ export async function withRuntimeSchedulerClient<T>(
1049
1059
  let queryCount = 0;
1050
1060
  let queryMs = 0;
1051
1061
  try {
1062
+ if (!entry.databasePoolAttached) {
1063
+ attachDatabasePool(entry.pool);
1064
+ entry.databasePoolAttached = true;
1065
+ }
1052
1066
  throwIfSchedulerAborted(signal);
1053
1067
  const result = await fn({
1054
1068
  query: async (sql, params) => {
@@ -14,6 +14,7 @@ import { RUN_EVENT_KIND } from '../run-event-catalog';
14
14
  * Runtime Sheet row durability belongs to the context-level row writer.
15
15
  */
16
16
  import { PlayStepLifecycleTracker } from '../step-lifecycle-tracker';
17
+ import { isDurableDirectRunnerLog } from '../log-provenance';
17
18
  import {
18
19
  updateRunStatusViaAppRuntime,
19
20
  type RuntimeStatusUpdate,
@@ -23,6 +24,8 @@ import type { PlayExecutionEvent } from '../ctx-types';
23
24
  import type { PlayActivityObservation } from '../activity-observation';
24
25
  import {
25
26
  normalizePlayLogContext,
27
+ normalizePlayLogMetadata,
28
+ type PlayLogMetadata,
26
29
  type PlayLogContext,
27
30
  } from '../log-context';
28
31
  import type {
@@ -61,6 +64,7 @@ const PLAY_TOOL_BATCH_LOG_PATTERN =
61
64
  /(?:^|\]\s*)Executing tool batch\s+([A-Za-z0-9_:-]+):\s+(\d+)\s+calls\b/i;
62
65
  const CTX_LOG_TIMESTAMP_PREFIX_RE = /^\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\]\s+/;
63
66
  const LIVE_LOG_TAIL_LIMIT = 1_000;
67
+ const STEP_FAILURE_PROGRESS_DEBOUNCE_MS = 250;
64
68
 
65
69
  function docflowInvocationSequence(invocationId?: string): number {
66
70
  if (!invocationId) return -1;
@@ -256,6 +260,11 @@ export type PostgresSchedulerProgressReporter = {
256
260
  skipTerminalLogReplay?: boolean;
257
261
  },
258
262
  ): Promise<void>;
263
+ /** Fold observed step failures into the terminal outbox instead of a
264
+ * separate, immediately-preceding progress write. */
265
+ collectTerminalFailureProgressEvents?(): PlayRunLedgerEvent[];
266
+ /** Cancel deferred failure progress once the terminal transaction owns it. */
267
+ markTerminalFailureProgressPersisted?(): void;
259
268
  };
260
269
 
261
270
  export type PostgresSchedulerDurableLogMode =
@@ -268,18 +277,7 @@ function shouldPublishDurableLogEvent(
268
277
  | Pick<PlayRunnerLogEvent, 'source' | 'line'>
269
278
  | { source?: unknown; line?: unknown },
270
279
  ): boolean {
271
- // Daytona host lifecycle logs use source "worker" (sandbox
272
- // create/upload/execute). Keep those in worker logs while still storing
273
- // play-authored ctx.log events in the durable Run Log Stream. The one
274
- // exception is the structured performance marker: it is bounded (one
275
- // terminal phase observation per runner phase), run-scoped, and is the
276
- // canonical black-box regression signal for dispatcher throughput. Do not
277
- // broaden this to arbitrary worker output.
278
- return (
279
- mode !== 'direct_worker' ||
280
- event.source !== 'worker' ||
281
- (typeof event.line === 'string' && event.line.startsWith('[perf-trace] '))
282
- );
280
+ return mode !== 'direct_worker' || isDurableDirectRunnerLog(event);
283
281
  }
284
282
 
285
283
  function shouldPublishTerminalResultLog(
@@ -305,14 +303,33 @@ type TerminalLogReplayPayload = NonNullable<
305
303
  function terminalReplayEntriesForMode(input: {
306
304
  resultLogs: readonly string[];
307
305
  resultLogContexts?: readonly (PlayLogContext | null)[];
306
+ resultLogMetadata?: readonly (PlayLogMetadata | null)[];
307
+ resultLogIds?: readonly string[];
308
308
  durableLogMode: PostgresSchedulerDurableLogMode;
309
- }): Array<{ line: string; context: PlayLogContext | null }> {
309
+ }): Array<{
310
+ line: string;
311
+ context: PlayLogContext | null;
312
+ metadata: PlayLogMetadata | null;
313
+ entryId: string | null;
314
+ }> {
310
315
  if (
311
316
  input.resultLogContexts !== undefined &&
312
317
  input.resultLogContexts.length !== input.resultLogs.length
313
318
  ) {
314
319
  throw new TypeError('Terminal log contexts must align with result logs.');
315
320
  }
321
+ if (
322
+ input.resultLogMetadata !== undefined &&
323
+ input.resultLogMetadata.length !== input.resultLogs.length
324
+ ) {
325
+ throw new TypeError('Terminal log metadata must align with result logs.');
326
+ }
327
+ if (
328
+ input.resultLogIds &&
329
+ input.resultLogIds.length !== input.resultLogs.length
330
+ ) {
331
+ throw new TypeError('Terminal log identities must align with result logs.');
332
+ }
316
333
  return input.resultLogs.flatMap((rawLine, index) => {
317
334
  const line = rawLine.trim();
318
335
  if (
@@ -325,6 +342,8 @@ function terminalReplayEntriesForMode(input: {
325
342
  {
326
343
  line,
327
344
  context: normalizePlayLogContext(input.resultLogContexts?.[index]),
345
+ metadata: normalizePlayLogMetadata(input.resultLogMetadata?.[index]),
346
+ entryId: input.resultLogIds?.[index] ?? null,
328
347
  },
329
348
  ];
330
349
  });
@@ -333,6 +352,8 @@ function terminalReplayEntriesForMode(input: {
333
352
  function terminalReplayPayloadForMode(input: {
334
353
  resultLogs: readonly string[];
335
354
  resultLogContexts?: readonly (PlayLogContext | null)[];
355
+ resultLogMetadata?: readonly (PlayLogMetadata | null)[];
356
+ resultLogIds?: readonly string[];
336
357
  durableLogMode: PostgresSchedulerDurableLogMode;
337
358
  liveLogs: readonly string[];
338
359
  liveLogContexts: readonly (PlayLogContext | null)[];
@@ -351,6 +372,7 @@ function terminalReplayPayloadForMode(input: {
351
372
  let totalCount = entries.length;
352
373
  if (
353
374
  input.durableLogMode === 'direct_worker' &&
375
+ !input.resultLogIds &&
354
376
  !input.replayAllTerminalLogs
355
377
  ) {
356
378
  const observedCounts = new Map<string, number>();
@@ -377,9 +399,14 @@ function terminalReplayPayloadForMode(input: {
377
399
  }
378
400
  const lines = entries.map((entry) => entry.line);
379
401
  const lineContexts = entries.map((entry) => entry.context);
402
+ const lineMetadata = entries.map((entry) => entry.metadata);
380
403
  return {
381
404
  lines,
382
405
  ...(lineContexts.some(Boolean) ? { lineContexts } : {}),
406
+ ...(lineMetadata.some(Boolean) ? { lineMetadata } : {}),
407
+ ...(input.resultLogIds
408
+ ? { lineIds: entries.map((entry) => entry.entryId) }
409
+ : {}),
383
410
  ...(input.replayAllTerminalLogs ? { dedupeMode: 'exact' as const } : {}),
384
411
  ...(typeof input.producerAttempt === 'number'
385
412
  ? { producerAttempt: input.producerAttempt }
@@ -439,6 +466,8 @@ export function createPostgresSchedulerProgressReporter(input: {
439
466
  let nodeProgressSealed = false;
440
467
  let liveLogs: string[] = [];
441
468
  let liveLogContexts: Array<PlayLogContext | null> = [];
469
+ let liveLogMetadata: Array<PlayLogMetadata | null> = [];
470
+ let liveLogIds: Array<string | null> = [];
442
471
  let liveLogsTotalCount = input.initialLiveLogTotalCount ?? 0;
443
472
  let liveLogsDirty = false;
444
473
  let workCounters: RuntimeStatusUpdate['workCounters'];
@@ -479,6 +508,9 @@ export function createPostgresSchedulerProgressReporter(input: {
479
508
 
480
509
  let activeProgressUpload: Promise<void> | null = null;
481
510
  let pendingProgressUpload: ProgressFlushRequest | null = null;
511
+ let deferredStepFailureFlushTimer: ReturnType<typeof setTimeout> | null =
512
+ null;
513
+ let terminalFailureProgressPersisted = false;
482
514
 
483
515
  const mergeProgressFlushRequest = (
484
516
  current: ProgressFlushRequest | null,
@@ -541,6 +573,8 @@ export function createPostgresSchedulerProgressReporter(input: {
541
573
  : undefined;
542
574
  const nextLogs = liveLogs.slice();
543
575
  const nextLogContexts = liveLogContexts.slice();
576
+ const nextLogMetadata = liveLogMetadata.slice();
577
+ const nextLogIds = liveLogIds.slice();
544
578
  const nextLogTotalCount = liveLogsTotalCount;
545
579
  const nextDatasetLifecycleEvents = includeProgress
546
580
  ? pendingDatasetLifecycleEvents
@@ -563,10 +597,47 @@ export function createPostgresSchedulerProgressReporter(input: {
563
597
  await publishStatusUpdate({
564
598
  playId: launch.runId,
565
599
  status: request.status,
566
- liveNodeProgress: nodeProgressSealed ? {} : nextProgress,
600
+ // On failure only observed causal errors may precede the
601
+ // terminal. Partial map counters cannot prove completion.
602
+ liveNodeProgress: nodeProgressSealed
603
+ ? terminalFailureProgressPersisted
604
+ ? {}
605
+ : Object.fromEntries(
606
+ Object.entries(nextProgress ?? {})
607
+ .filter(
608
+ ([, progress]) =>
609
+ progress.error || progress.nodeIo?.error,
610
+ )
611
+ .map(([id, progress]) => [
612
+ id,
613
+ {
614
+ startedAt: progress.startedAt,
615
+ completedAt: progress.completedAt,
616
+ updatedAt: progress.updatedAt,
617
+ error:
618
+ progress.error ??
619
+ progress.nodeIo?.error ??
620
+ undefined,
621
+ nodeIo: progress.nodeIo,
622
+ },
623
+ ]),
624
+ )
625
+ : terminalFailureProgressPersisted
626
+ ? Object.fromEntries(
627
+ Object.entries(nextProgress ?? {}).filter(
628
+ ([, progress]) =>
629
+ !progress.error && !progress.nodeIo?.error,
630
+ ),
631
+ )
632
+ : nextProgress,
567
633
  liveLogs: nextLogs,
568
634
  liveLogContexts: nextLogContexts,
635
+ liveLogMetadata: nextLogMetadata,
636
+ liveLogIds: nextLogIds,
569
637
  liveLogTotalCount: nextLogTotalCount,
638
+ ...(input.initialLiveLogTotalCount !== undefined
639
+ ? { liveLogBatchOffset: nextLogTotalCount - nextLogs.length }
640
+ : {}),
570
641
  liveLogProducerAttempt: producerAttempt,
571
642
  datasetLifecycleEvents: nextDatasetLifecycleEvents,
572
643
  activityObservations: nextActivityObservations,
@@ -667,7 +738,12 @@ export function createPostgresSchedulerProgressReporter(input: {
667
738
  void flush({ force }).catch(() => undefined);
668
739
  };
669
740
 
670
- const appendLiveLog = (line: string, context?: unknown) => {
741
+ const appendLiveLog = (
742
+ line: string,
743
+ context?: unknown,
744
+ entryId?: unknown,
745
+ metadata?: unknown,
746
+ ) => {
671
747
  const trimmed = line.trim();
672
748
  if (!trimmed) {
673
749
  return;
@@ -675,8 +751,12 @@ export function createPostgresSchedulerProgressReporter(input: {
675
751
  liveLogsTotalCount += 1;
676
752
  liveLogs = [...liveLogs, trimmed];
677
753
  liveLogContexts = [...liveLogContexts, normalizePlayLogContext(context)];
754
+ liveLogMetadata = [...liveLogMetadata, normalizePlayLogMetadata(metadata)];
755
+ liveLogIds = [...liveLogIds, typeof entryId === 'string' ? entryId : null];
678
756
  if (liveLogs.length > LIVE_LOG_TAIL_LIMIT) {
679
757
  liveLogs = liveLogs.slice(liveLogs.length - LIVE_LOG_TAIL_LIMIT);
758
+ liveLogMetadata = liveLogMetadata.slice(-LIVE_LOG_TAIL_LIMIT);
759
+ liveLogIds = liveLogIds.slice(liveLogIds.length - LIVE_LOG_TAIL_LIMIT);
680
760
  liveLogContexts = liveLogContexts.slice(
681
761
  liveLogContexts.length - LIVE_LOG_TAIL_LIMIT,
682
762
  );
@@ -742,6 +822,7 @@ export function createPostgresSchedulerProgressReporter(input: {
742
822
  ...(current.completedAt !== undefined && !clearCompletedAt
743
823
  ? { completedAt: current.completedAt }
744
824
  : {}),
825
+ ...(current.error !== undefined ? { error: current.error } : {}),
745
826
  ...(current.nodeIo !== undefined ? { nodeIo: current.nodeIo } : {}),
746
827
  ...(current.conditionCounts !== undefined
747
828
  ? { conditionCounts: current.conditionCounts }
@@ -791,6 +872,7 @@ export function createPostgresSchedulerProgressReporter(input: {
791
872
  ...(patch.completedAt !== undefined
792
873
  ? { completedAt: patch.completedAt }
793
874
  : {}),
875
+ ...(patch.error !== undefined ? { error: patch.error } : {}),
794
876
  ...(patch.nodeIo !== undefined ? { nodeIo: patch.nodeIo } : {}),
795
877
  ...(patch.conditionCounts !== undefined
796
878
  ? { conditionCounts: patch.conditionCounts }
@@ -975,6 +1057,34 @@ export function createPostgresSchedulerProgressReporter(input: {
975
1057
  };
976
1058
 
977
1059
  const applyExecutionEvent = (event: PlayExecutionEvent) => {
1060
+ if (
1061
+ event.type === 'step.started' ||
1062
+ event.type === 'step.completed' ||
1063
+ event.type === 'step.failed'
1064
+ ) {
1065
+ setNodeProgress(event.stepId, {
1066
+ ...(event.type === 'step.started'
1067
+ ? { startedAt: event.at }
1068
+ : { completedAt: event.at }),
1069
+ ...(event.type === 'step.failed' ? { error: event.error } : {}),
1070
+ });
1071
+ if (event.type === 'step.failed') {
1072
+ // A failing step is often followed immediately by run.failed. Give
1073
+ // that terminal transaction a chance to carry the error in its own
1074
+ // ordered outbox row, avoiding a redundant Convex append ahead of it.
1075
+ if (deferredStepFailureFlushTimer !== null) {
1076
+ clearTimeout(deferredStepFailureFlushTimer);
1077
+ }
1078
+ deferredStepFailureFlushTimer = setTimeout(() => {
1079
+ deferredStepFailureFlushTimer = null;
1080
+ if (!terminalFailureProgressPersisted) scheduleFlush(true);
1081
+ }, STEP_FAILURE_PROGRESS_DEBOUNCE_MS);
1082
+ deferredStepFailureFlushTimer.unref?.();
1083
+ } else {
1084
+ scheduleFlush(true);
1085
+ }
1086
+ return;
1087
+ }
978
1088
  if (event.type === 'docflow.node.hit') {
979
1089
  setNodeProgress(event.nodeId, {
980
1090
  startedAt: event.at,
@@ -1321,13 +1431,20 @@ export function createPostgresSchedulerProgressReporter(input: {
1321
1431
  line?: unknown;
1322
1432
  source?: unknown;
1323
1433
  context?: unknown;
1434
+ metadata?: unknown;
1435
+ entryId?: unknown;
1324
1436
  };
1325
1437
  const rawLine = logEvent.line;
1326
1438
  if (typeof rawLine !== 'string' || !rawLine) {
1327
1439
  return;
1328
1440
  }
1329
1441
  if (shouldPublishDurableLogEvent(durableLogMode, logEvent)) {
1330
- appendLiveLog(rawLine, logEvent.context);
1442
+ appendLiveLog(
1443
+ rawLine,
1444
+ logEvent.context,
1445
+ logEvent.entryId,
1446
+ logEvent.metadata,
1447
+ );
1331
1448
  }
1332
1449
  markMapStartedFromLog(rawLine);
1333
1450
  markMapStartedFromPerfLog(rawLine);
@@ -1593,13 +1710,14 @@ export function createPostgresSchedulerProgressReporter(input: {
1593
1710
  // A failed result's terminal is already decided: every caller that
1594
1711
  // finalizes one does so after (or while) the scheduler commits
1595
1712
  // `run.failed`, and the ledger freezes a terminal run's steps. Its step
1596
- // facts from here are projections of this reporter's own partial view — a
1713
+ // counts from here are projections of this reporter's own partial view — a
1597
1714
  // parked worker's fold saw page-sized map totals and never saw the
1598
1715
  // runner's failures — so they used to land after `run.failed` claiming
1599
1716
  // docflow nodes were "running" and a 50,000-row map had "completed" 2,000.
1600
1717
  // The settled counts that ARE true travel in the terminal outbox row
1601
1718
  // (`buildFailedTerminalSettledProgressEvents`). Logs, work counters,
1602
- // dataset and activity facts still flush.
1719
+ // dataset and activity still flush; causal step failures already ride the
1720
+ // terminal outbox row when one was committed.
1603
1721
  if (result?.status === 'failed') {
1604
1722
  nodeProgressSealed = true;
1605
1723
  } else if (result?.status === 'completed') {
@@ -1629,6 +1747,8 @@ export function createPostgresSchedulerProgressReporter(input: {
1629
1747
  : terminalReplayPayloadForMode({
1630
1748
  resultLogs: result.logs,
1631
1749
  resultLogContexts: result.logContexts,
1750
+ resultLogMetadata: result.logMetadata,
1751
+ resultLogIds: result.logIds,
1632
1752
  durableLogMode,
1633
1753
  liveLogs,
1634
1754
  liveLogContexts,
@@ -1640,11 +1760,47 @@ export function createPostgresSchedulerProgressReporter(input: {
1640
1760
  });
1641
1761
  };
1642
1762
 
1763
+ const collectTerminalFailureProgressEvents = (): PlayRunLedgerEvent[] => {
1764
+ const failedProgress = Object.fromEntries(
1765
+ Object.entries(liveNodeProgress).filter(
1766
+ ([, progress]) => progress.error || progress.nodeIo?.error,
1767
+ ),
1768
+ );
1769
+ if (Object.keys(failedProgress).length === 0) return [];
1770
+ return buildPlayRunLedgerEventsFromStatusPatch({
1771
+ patch: {
1772
+ runId: launch.runId,
1773
+ status: 'running',
1774
+ lastCheckpointAt: Date.now(),
1775
+ liveNodeProgress: failedProgress,
1776
+ },
1777
+ previousSnapshot: createEmptyPlayRunLedgerSnapshot({
1778
+ runId: launch.runId,
1779
+ status: 'queued',
1780
+ }),
1781
+ source: 'worker',
1782
+ }).filter(
1783
+ (event) =>
1784
+ event.type === RUN_EVENT_KIND.stepProgress ||
1785
+ event.type === RUN_EVENT_KIND.stepFailed,
1786
+ );
1787
+ };
1788
+
1789
+ const markTerminalFailureProgressPersisted = (): void => {
1790
+ terminalFailureProgressPersisted = true;
1791
+ if (deferredStepFailureFlushTimer !== null) {
1792
+ clearTimeout(deferredStepFailureFlushTimer);
1793
+ deferredStepFailureFlushTimer = null;
1794
+ }
1795
+ };
1796
+
1643
1797
  return {
1644
1798
  onRunnerEvent,
1645
1799
  applyDurableObservations,
1646
1800
  applyTerminalSheetOutcomes,
1647
1801
  finalize,
1802
+ collectTerminalFailureProgressEvents,
1803
+ markTerminalFailureProgressPersisted,
1648
1804
  };
1649
1805
  }
1650
1806
 
@@ -1832,6 +1988,8 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1832
1988
  liveNodeProgress: finalUpdate.liveNodeProgress,
1833
1989
  liveLogs,
1834
1990
  liveLogContexts: finalUpdate.liveLogContexts,
1991
+ liveLogMetadata: finalUpdate.liveLogMetadata,
1992
+ liveLogIds: finalUpdate.liveLogIds,
1835
1993
  liveLogsChannelOffset: Math.max(0, liveLogTotalCount - liveLogs.length),
1836
1994
  liveLogsProducerAttempt:
1837
1995
  finalUpdate.liveLogProducerAttempt ?? input.attempt,
@@ -10,6 +10,7 @@ import type {
10
10
  import {
11
11
  DEFAULT_ADVISORY_PROVIDER_MAX_RPS,
12
12
  DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS,
13
+ DEFAULT_UNKNOWN_PROVIDER_MIN_RPS,
13
14
  PROVIDER_AIMD_COOLDOWN_CAP_MS,
14
15
  type PacingRule,
15
16
  } from '../governor/rate-state-backend';
@@ -431,7 +432,8 @@ function normalizeRules(
431
432
  * per second; `capacity` is one window of burst. The adaptive `[min,max]` range
432
433
  * is derived value-first so the same wire shape drives both declared and
433
434
  * default-pacing buckets:
434
- * - min_rps = min(5/60, max)
435
+ * - min_rps = 2 for the unknown shared-provider scope
436
+ * - min_rps = min(5/60, max) for declared rules
435
437
  * (a lower explicit adaptive maximum remains authoritative)
436
438
  * - max_rps = max(base, 100) (catalogued-rate recommendation)
437
439
  * - max_rps = explicit adaptiveMaxRps when supplied
@@ -454,11 +456,11 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
454
456
  rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > 0
455
457
  ? Math.max(rule.adaptiveMaxRps, base)
456
458
  : Math.max(base, DEFAULT_ADVISORY_PROVIDER_MAX_RPS);
457
- // Undeclared providers keep their existing 10-RPS fallback. Other provider
458
- // scopes use the five-RPM floor, capped by an explicit adaptive maximum.
459
+ // The unknown-provider seed is a guess, not a quota floor. Declared provider
460
+ // scopes keep the five-RPM floor, capped by an explicit adaptive maximum.
459
461
  const minRps =
460
462
  rule.ruleId === 'default:provider-scope'
461
- ? base
463
+ ? DEFAULT_UNKNOWN_PROVIDER_MIN_RPS
462
464
  : Math.min(MIN_ADAPTIVE_RPS_FLOOR, maxRps);
463
465
  return {
464
466
  rule_id: rule.ruleId,
@@ -472,7 +474,7 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
472
474
 
473
475
  function minimumRateRpsForRules(rules: readonly AcquirePacingRule[]): number {
474
476
  if (rules.some((rule) => rule.ruleId === 'default:provider-scope')) {
475
- return DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS;
477
+ return DEFAULT_UNKNOWN_PROVIDER_MIN_RPS;
476
478
  }
477
479
  const explicitAdaptiveMaxima = rules.flatMap((rule) => {
478
480
  if (rule.adaptiveMaxRps == null || rule.adaptiveMaxRps <= 0) return [];