deepline 0.3.290 → 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 (46) 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 +11 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +209 -51
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -2
  8. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +36 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +2 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +5 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +54 -41
  12. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +50 -3
  13. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +9 -2
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +35 -7
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +55 -9
  16. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +16 -4
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +140 -4
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +559 -313
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +1 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +116 -24
  21. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +6 -1
  22. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +22 -5
  23. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +35 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -8
  25. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
  26. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
  27. package/dist/cli/index.js +193 -582
  28. package/dist/cli/index.mjs +193 -582
  29. package/dist/{client-FR_u0dxT.d.ts → client-Ct4jmvAg.d.ts} +29 -8
  30. package/dist/{client-phK1Ixcl.d.mts → client-Ct9GKrwu.d.mts} +29 -8
  31. package/dist/{compiler-manifest-9SQtUuKb.d.mts → compiler-manifest-qtTNYvBl.d.mts} +2 -0
  32. package/dist/{compiler-manifest-9SQtUuKb.d.ts → compiler-manifest-qtTNYvBl.d.ts} +2 -0
  33. package/dist/index.d.mts +4 -4
  34. package/dist/index.d.ts +4 -4
  35. package/dist/index.js +12 -5
  36. package/dist/index.mjs +12 -5
  37. package/dist/install-integrity.json +4 -4
  38. package/dist/plays/bundle-play-file.d.mts +2 -2
  39. package/dist/plays/bundle-play-file.d.ts +2 -2
  40. package/dist/plays/run-request.d.mts +2 -2
  41. package/dist/plays/run-request.d.ts +2 -2
  42. package/dist/release.d.mts +1 -1
  43. package/dist/release.d.ts +1 -1
  44. package/dist/release.js +1 -1
  45. package/dist/release.mjs +1 -1
  46. package/package.json +1 -1
@@ -3,7 +3,10 @@ import {
3
3
  type PlayRunnerEvent,
4
4
  type PlayRunnerResult,
5
5
  } from '../protocol';
6
- import type { PlayLogContext } from '../log-context';
6
+ import type {
7
+ PlayLogContext,
8
+ PlayLogMetadata,
9
+ } from '../log-context';
7
10
  import { parseCtxFetchHttpFailureDiagnostic } from '../log-provenance';
8
11
  import {
9
12
  RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES,
@@ -201,9 +204,14 @@ function retainArrayWithinJsonByteLimit<T>(
201
204
  * Retain the normal newest log tail, but reserve space for the canonical
202
205
  * ctx.fetch failure diagnostics first. `runtimeDiagnostics` powers the final
203
206
  * warning, while the matching raw log line is the durable evidence customers
204
- * see through `runs logs --debug` after progress events could not be pushed.
207
+ * see through `runs logs` after progress events could not be pushed.
205
208
  */
206
- function retainTerminalLogTailWithRuntimeDiagnostics(logs: readonly string[]): {
209
+ function retainTerminalLogTailWithRuntimeDiagnostics(
210
+ logs: readonly string[],
211
+ logContexts?: readonly (PlayLogContext | null)[],
212
+ logMetadata?: readonly (PlayLogMetadata | null)[],
213
+ logIds?: readonly string[],
214
+ ): {
207
215
  values: string[];
208
216
  indexes: number[];
209
217
  truncated: boolean;
@@ -215,8 +223,16 @@ function retainTerminalLogTailWithRuntimeDiagnostics(logs: readonly string[]): {
215
223
  // array. Re-serializing the retained 128 KiB tail for every historical
216
224
  // line made large completed runs exhaust their terminal-flush deadline.
217
225
  const separatorBytes = retainedIndexes.size ? 1 : 0;
218
- const measurement = jsonByteLengthUpTo(
226
+ // The aligned arrays cross this same transport. Counting their bytes
227
+ // preserves the existing diagnostic allowance and customer-output headroom.
228
+ const entry = [
219
229
  logs[index]!,
230
+ ...(logContexts ? [logContexts[index] ?? null] : []),
231
+ ...(logMetadata ? [logMetadata[index] ?? null] : []),
232
+ ...(logIds ? [logIds[index]] : []),
233
+ ];
234
+ const measurement = jsonByteLengthUpTo(
235
+ entry.length === 1 ? entry[0] : entry,
220
236
  TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES - retainedBytes - separatorBytes,
221
237
  );
222
238
  if (measurement.exceeded) return;
@@ -270,20 +286,29 @@ function appendTerminalDiagnosticsMarker(
270
286
  truncatedFields: string[],
271
287
  logContexts?: Array<PlayLogContext | null>,
272
288
  logIds?: string[],
289
+ logMetadata?: Array<PlayLogMetadata | null>,
273
290
  ): {
274
291
  logs: string[];
275
292
  logContexts?: Array<PlayLogContext | null>;
276
293
  logIds?: string[];
294
+ logMetadata?: Array<PlayLogMetadata | null>;
277
295
  } {
278
296
  const marker =
279
297
  `[deepline] terminal diagnostic metadata was bounded (${truncatedFields.join(', ')}); ` +
280
298
  'the authored play output is complete.';
281
299
  const marked = [marker, ...logs];
282
300
  const markedContexts = logContexts ? [null, ...logContexts] : undefined;
301
+ const markedMetadata = logMetadata ? [null, ...logMetadata] : undefined;
283
302
  const markedIds = logIds ? [crypto.randomUUID(), ...logIds] : undefined;
303
+ const logPayload = () => ({
304
+ logs: marked,
305
+ ...(markedContexts ? { logContexts: markedContexts } : {}),
306
+ ...(markedMetadata ? { logMetadata: markedMetadata } : {}),
307
+ ...(markedIds ? { logIds: markedIds } : {}),
308
+ });
284
309
  while (
285
310
  marked.length > 1 &&
286
- !fitsJsonByteLimit(marked, TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES)
311
+ !fitsJsonByteLimit(logPayload(), TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES)
287
312
  ) {
288
313
  // Preserve canonical ctx.fetch evidence when a marker itself needs room.
289
314
  // Other tail lines are diagnostic context, but this line is the durable
@@ -295,16 +320,20 @@ function appendTerminalDiagnosticsMarker(
295
320
  const removeAt = removableIndex === -1 ? 1 : removableIndex;
296
321
  marked.splice(removeAt, 1);
297
322
  markedContexts?.splice(removeAt, 1);
323
+ markedMetadata?.splice(removeAt, 1);
298
324
  markedIds?.splice(removeAt, 1);
299
325
  }
300
326
  const logsFit = fitsJsonByteLimit(
301
- marked,
327
+ logPayload(),
302
328
  TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES,
303
329
  );
304
330
  const retainedLogs = logsFit ? marked : [marker];
305
331
  const retainedContexts = logsFit ? markedContexts : [null];
306
332
  return {
307
333
  logs: retainedLogs,
334
+ ...(markedMetadata?.some(Boolean)
335
+ ? { logMetadata: logsFit ? markedMetadata : [null] }
336
+ : {}),
308
337
  ...(markedIds
309
338
  ? { logIds: logsFit ? markedIds : markedIds.slice(0, 1) }
310
339
  : {}),
@@ -323,18 +352,29 @@ function appendTerminalDiagnosticsMarker(
323
352
  function boundSettledTerminalTransportDiagnostics(
324
353
  result: SettledRunnerTerminalTransportResult,
325
354
  ): SettledRunnerTerminalTransportResult {
326
- const logProjection = retainTerminalLogTailWithRuntimeDiagnostics(
327
- result.logs,
328
- );
329
355
  if (
330
356
  result.logContexts !== undefined &&
331
357
  result.logContexts.length !== result.logs.length
332
358
  ) {
333
359
  throw new TypeError('Terminal log contexts must align with result logs.');
334
360
  }
361
+ if (
362
+ result.logMetadata !== undefined &&
363
+ result.logMetadata.length !== result.logs.length
364
+ )
365
+ throw new TypeError('Terminal log metadata must align with logs.');
366
+ const logProjection = retainTerminalLogTailWithRuntimeDiagnostics(
367
+ result.logs,
368
+ result.logContexts,
369
+ result.logMetadata,
370
+ result.logIds,
371
+ );
335
372
  const logContexts = result.logContexts?.length
336
373
  ? logProjection.indexes.map((index) => result.logContexts![index] ?? null)
337
374
  : undefined;
375
+ const logMetadata = result.logMetadata?.length
376
+ ? logProjection.indexes.map((index) => result.logMetadata![index] ?? null)
377
+ : undefined;
338
378
  if (result.logIds && result.logIds.length !== result.logs.length)
339
379
  throw new TypeError('Terminal log identities must align with result logs.');
340
380
  const logIds = result.logIds
@@ -384,16 +424,19 @@ function boundSettledTerminalTransportDiagnostics(
384
424
  truncatedFields,
385
425
  logContexts,
386
426
  logIds,
427
+ logMetadata,
387
428
  )
388
429
  : {
389
430
  logs: logProjection.values,
390
431
  ...(logIds ? { logIds } : {}),
391
432
  ...(logContexts?.some(Boolean) ? { logContexts } : {}),
433
+ ...(logMetadata?.some(Boolean) ? { logMetadata } : {}),
392
434
  };
393
435
  return {
394
436
  ...result,
395
437
  logs: boundedLogs.logs,
396
438
  logContexts: boundedLogs.logContexts,
439
+ logMetadata: boundedLogs.logMetadata,
397
440
  logIds: boundedLogs.logIds,
398
441
  stats: statsProjection.value,
399
442
  steps: stepsProjection.values,
@@ -427,17 +470,20 @@ function boundSettledTerminalTransportDiagnostics(
427
470
  truncatedFields,
428
471
  logContexts,
429
472
  logIds,
473
+ logMetadata,
430
474
  )
431
475
  : {
432
476
  logs: logProjection.values,
433
477
  ...(logIds ? { logIds } : {}),
434
478
  ...(logContexts?.some(Boolean) ? { logContexts } : {}),
479
+ ...(logMetadata?.some(Boolean) ? { logMetadata } : {}),
435
480
  };
436
481
  return {
437
482
  ...result,
438
483
  error: error.value,
439
484
  logs: boundedLogs.logs,
440
485
  logContexts: boundedLogs.logContexts,
486
+ logMetadata: boundedLogs.logMetadata,
441
487
  logIds: boundedLogs.logIds,
442
488
  stats: statsProjection.value,
443
489
  steps: stepsProjection.values,
@@ -3040,8 +3040,14 @@ async function executeAssignedAbsurdAttempt(input: {
3040
3040
  });
3041
3041
  }
3042
3042
 
3043
- // A failed run's settled counts must reach the ledger BEFORE its terminal
3044
- // makes the steps immutable, so they ride the terminal outbox row.
3043
+ // Terminal step errors and failed-map settled counts ride the existing
3044
+ // terminal outbox row. They no longer need a progress write immediately
3045
+ // ahead of the terminal in the same ordered run stream.
3046
+ const observedTerminalProgressEvents =
3047
+ progressReporter.collectTerminalFailureProgressEvents?.() ?? [];
3048
+ if (observedTerminalProgressEvents.length > 0) {
3049
+ progressReporter.markTerminalFailureProgressPersisted?.();
3050
+ }
3045
3051
  const failedTerminalProgressEvents =
3046
3052
  runnerResult.status === 'failed'
3047
3053
  ? await collectFailedTerminalSettledProgressEvents({
@@ -3059,8 +3065,14 @@ async function executeAssignedAbsurdAttempt(input: {
3059
3065
  ...options,
3060
3066
  terminalLogOutbox: input.fromDetachedRunner ? 'ctx_log_tail' : 'omit',
3061
3067
  sandboxCrashDiagnostic,
3062
- ...(failedTerminalProgressEvents.length > 0
3063
- ? { failedTerminalProgressEvents }
3068
+ ...(observedTerminalProgressEvents.length > 0 ||
3069
+ failedTerminalProgressEvents.length > 0
3070
+ ? {
3071
+ terminalProgressEvents: [
3072
+ ...observedTerminalProgressEvents,
3073
+ ...failedTerminalProgressEvents,
3074
+ ],
3075
+ }
3064
3076
  : {}),
3065
3077
  },
3066
3078
  ),
@@ -24,6 +24,8 @@ import type { PlayExecutionEvent } from '../ctx-types';
24
24
  import type { PlayActivityObservation } from '../activity-observation';
25
25
  import {
26
26
  normalizePlayLogContext,
27
+ normalizePlayLogMetadata,
28
+ type PlayLogMetadata,
27
29
  type PlayLogContext,
28
30
  } from '../log-context';
29
31
  import type {
@@ -62,6 +64,7 @@ const PLAY_TOOL_BATCH_LOG_PATTERN =
62
64
  /(?:^|\]\s*)Executing tool batch\s+([A-Za-z0-9_:-]+):\s+(\d+)\s+calls\b/i;
63
65
  const CTX_LOG_TIMESTAMP_PREFIX_RE = /^\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\]\s+/;
64
66
  const LIVE_LOG_TAIL_LIMIT = 1_000;
67
+ const STEP_FAILURE_PROGRESS_DEBOUNCE_MS = 250;
65
68
 
66
69
  function docflowInvocationSequence(invocationId?: string): number {
67
70
  if (!invocationId) return -1;
@@ -257,6 +260,11 @@ export type PostgresSchedulerProgressReporter = {
257
260
  skipTerminalLogReplay?: boolean;
258
261
  },
259
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;
260
268
  };
261
269
 
262
270
  export type PostgresSchedulerDurableLogMode =
@@ -295,11 +303,13 @@ type TerminalLogReplayPayload = NonNullable<
295
303
  function terminalReplayEntriesForMode(input: {
296
304
  resultLogs: readonly string[];
297
305
  resultLogContexts?: readonly (PlayLogContext | null)[];
306
+ resultLogMetadata?: readonly (PlayLogMetadata | null)[];
298
307
  resultLogIds?: readonly string[];
299
308
  durableLogMode: PostgresSchedulerDurableLogMode;
300
309
  }): Array<{
301
310
  line: string;
302
311
  context: PlayLogContext | null;
312
+ metadata: PlayLogMetadata | null;
303
313
  entryId: string | null;
304
314
  }> {
305
315
  if (
@@ -308,6 +318,12 @@ function terminalReplayEntriesForMode(input: {
308
318
  ) {
309
319
  throw new TypeError('Terminal log contexts must align with result logs.');
310
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
+ }
311
327
  if (
312
328
  input.resultLogIds &&
313
329
  input.resultLogIds.length !== input.resultLogs.length
@@ -326,6 +342,7 @@ function terminalReplayEntriesForMode(input: {
326
342
  {
327
343
  line,
328
344
  context: normalizePlayLogContext(input.resultLogContexts?.[index]),
345
+ metadata: normalizePlayLogMetadata(input.resultLogMetadata?.[index]),
329
346
  entryId: input.resultLogIds?.[index] ?? null,
330
347
  },
331
348
  ];
@@ -335,6 +352,7 @@ function terminalReplayEntriesForMode(input: {
335
352
  function terminalReplayPayloadForMode(input: {
336
353
  resultLogs: readonly string[];
337
354
  resultLogContexts?: readonly (PlayLogContext | null)[];
355
+ resultLogMetadata?: readonly (PlayLogMetadata | null)[];
338
356
  resultLogIds?: readonly string[];
339
357
  durableLogMode: PostgresSchedulerDurableLogMode;
340
358
  liveLogs: readonly string[];
@@ -381,9 +399,11 @@ function terminalReplayPayloadForMode(input: {
381
399
  }
382
400
  const lines = entries.map((entry) => entry.line);
383
401
  const lineContexts = entries.map((entry) => entry.context);
402
+ const lineMetadata = entries.map((entry) => entry.metadata);
384
403
  return {
385
404
  lines,
386
405
  ...(lineContexts.some(Boolean) ? { lineContexts } : {}),
406
+ ...(lineMetadata.some(Boolean) ? { lineMetadata } : {}),
387
407
  ...(input.resultLogIds
388
408
  ? { lineIds: entries.map((entry) => entry.entryId) }
389
409
  : {}),
@@ -446,6 +466,7 @@ export function createPostgresSchedulerProgressReporter(input: {
446
466
  let nodeProgressSealed = false;
447
467
  let liveLogs: string[] = [];
448
468
  let liveLogContexts: Array<PlayLogContext | null> = [];
469
+ let liveLogMetadata: Array<PlayLogMetadata | null> = [];
449
470
  let liveLogIds: Array<string | null> = [];
450
471
  let liveLogsTotalCount = input.initialLiveLogTotalCount ?? 0;
451
472
  let liveLogsDirty = false;
@@ -487,6 +508,9 @@ export function createPostgresSchedulerProgressReporter(input: {
487
508
 
488
509
  let activeProgressUpload: Promise<void> | null = null;
489
510
  let pendingProgressUpload: ProgressFlushRequest | null = null;
511
+ let deferredStepFailureFlushTimer: ReturnType<typeof setTimeout> | null =
512
+ null;
513
+ let terminalFailureProgressPersisted = false;
490
514
 
491
515
  const mergeProgressFlushRequest = (
492
516
  current: ProgressFlushRequest | null,
@@ -549,6 +573,7 @@ export function createPostgresSchedulerProgressReporter(input: {
549
573
  : undefined;
550
574
  const nextLogs = liveLogs.slice();
551
575
  const nextLogContexts = liveLogContexts.slice();
576
+ const nextLogMetadata = liveLogMetadata.slice();
552
577
  const nextLogIds = liveLogIds.slice();
553
578
  const nextLogTotalCount = liveLogsTotalCount;
554
579
  const nextDatasetLifecycleEvents = includeProgress
@@ -572,9 +597,42 @@ export function createPostgresSchedulerProgressReporter(input: {
572
597
  await publishStatusUpdate({
573
598
  playId: launch.runId,
574
599
  status: request.status,
575
- 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,
576
633
  liveLogs: nextLogs,
577
634
  liveLogContexts: nextLogContexts,
635
+ liveLogMetadata: nextLogMetadata,
578
636
  liveLogIds: nextLogIds,
579
637
  liveLogTotalCount: nextLogTotalCount,
580
638
  ...(input.initialLiveLogTotalCount !== undefined
@@ -684,6 +742,7 @@ export function createPostgresSchedulerProgressReporter(input: {
684
742
  line: string,
685
743
  context?: unknown,
686
744
  entryId?: unknown,
745
+ metadata?: unknown,
687
746
  ) => {
688
747
  const trimmed = line.trim();
689
748
  if (!trimmed) {
@@ -692,9 +751,11 @@ export function createPostgresSchedulerProgressReporter(input: {
692
751
  liveLogsTotalCount += 1;
693
752
  liveLogs = [...liveLogs, trimmed];
694
753
  liveLogContexts = [...liveLogContexts, normalizePlayLogContext(context)];
754
+ liveLogMetadata = [...liveLogMetadata, normalizePlayLogMetadata(metadata)];
695
755
  liveLogIds = [...liveLogIds, typeof entryId === 'string' ? entryId : null];
696
756
  if (liveLogs.length > LIVE_LOG_TAIL_LIMIT) {
697
757
  liveLogs = liveLogs.slice(liveLogs.length - LIVE_LOG_TAIL_LIMIT);
758
+ liveLogMetadata = liveLogMetadata.slice(-LIVE_LOG_TAIL_LIMIT);
698
759
  liveLogIds = liveLogIds.slice(liveLogIds.length - LIVE_LOG_TAIL_LIMIT);
699
760
  liveLogContexts = liveLogContexts.slice(
700
761
  liveLogContexts.length - LIVE_LOG_TAIL_LIMIT,
@@ -761,6 +822,7 @@ export function createPostgresSchedulerProgressReporter(input: {
761
822
  ...(current.completedAt !== undefined && !clearCompletedAt
762
823
  ? { completedAt: current.completedAt }
763
824
  : {}),
825
+ ...(current.error !== undefined ? { error: current.error } : {}),
764
826
  ...(current.nodeIo !== undefined ? { nodeIo: current.nodeIo } : {}),
765
827
  ...(current.conditionCounts !== undefined
766
828
  ? { conditionCounts: current.conditionCounts }
@@ -810,6 +872,7 @@ export function createPostgresSchedulerProgressReporter(input: {
810
872
  ...(patch.completedAt !== undefined
811
873
  ? { completedAt: patch.completedAt }
812
874
  : {}),
875
+ ...(patch.error !== undefined ? { error: patch.error } : {}),
813
876
  ...(patch.nodeIo !== undefined ? { nodeIo: patch.nodeIo } : {}),
814
877
  ...(patch.conditionCounts !== undefined
815
878
  ? { conditionCounts: patch.conditionCounts }
@@ -994,6 +1057,34 @@ export function createPostgresSchedulerProgressReporter(input: {
994
1057
  };
995
1058
 
996
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
+ }
997
1088
  if (event.type === 'docflow.node.hit') {
998
1089
  setNodeProgress(event.nodeId, {
999
1090
  startedAt: event.at,
@@ -1340,6 +1431,7 @@ export function createPostgresSchedulerProgressReporter(input: {
1340
1431
  line?: unknown;
1341
1432
  source?: unknown;
1342
1433
  context?: unknown;
1434
+ metadata?: unknown;
1343
1435
  entryId?: unknown;
1344
1436
  };
1345
1437
  const rawLine = logEvent.line;
@@ -1347,7 +1439,12 @@ export function createPostgresSchedulerProgressReporter(input: {
1347
1439
  return;
1348
1440
  }
1349
1441
  if (shouldPublishDurableLogEvent(durableLogMode, logEvent)) {
1350
- appendLiveLog(rawLine, logEvent.context, logEvent.entryId);
1442
+ appendLiveLog(
1443
+ rawLine,
1444
+ logEvent.context,
1445
+ logEvent.entryId,
1446
+ logEvent.metadata,
1447
+ );
1351
1448
  }
1352
1449
  markMapStartedFromLog(rawLine);
1353
1450
  markMapStartedFromPerfLog(rawLine);
@@ -1613,13 +1710,14 @@ export function createPostgresSchedulerProgressReporter(input: {
1613
1710
  // A failed result's terminal is already decided: every caller that
1614
1711
  // finalizes one does so after (or while) the scheduler commits
1615
1712
  // `run.failed`, and the ledger freezes a terminal run's steps. Its step
1616
- // 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
1617
1714
  // parked worker's fold saw page-sized map totals and never saw the
1618
1715
  // runner's failures — so they used to land after `run.failed` claiming
1619
1716
  // docflow nodes were "running" and a 50,000-row map had "completed" 2,000.
1620
1717
  // The settled counts that ARE true travel in the terminal outbox row
1621
1718
  // (`buildFailedTerminalSettledProgressEvents`). Logs, work counters,
1622
- // 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.
1623
1721
  if (result?.status === 'failed') {
1624
1722
  nodeProgressSealed = true;
1625
1723
  } else if (result?.status === 'completed') {
@@ -1649,6 +1747,7 @@ export function createPostgresSchedulerProgressReporter(input: {
1649
1747
  : terminalReplayPayloadForMode({
1650
1748
  resultLogs: result.logs,
1651
1749
  resultLogContexts: result.logContexts,
1750
+ resultLogMetadata: result.logMetadata,
1652
1751
  resultLogIds: result.logIds,
1653
1752
  durableLogMode,
1654
1753
  liveLogs,
@@ -1661,11 +1760,47 @@ export function createPostgresSchedulerProgressReporter(input: {
1661
1760
  });
1662
1761
  };
1663
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
+
1664
1797
  return {
1665
1798
  onRunnerEvent,
1666
1799
  applyDurableObservations,
1667
1800
  applyTerminalSheetOutcomes,
1668
1801
  finalize,
1802
+ collectTerminalFailureProgressEvents,
1803
+ markTerminalFailureProgressPersisted,
1669
1804
  };
1670
1805
  }
1671
1806
 
@@ -1853,6 +1988,7 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1853
1988
  liveNodeProgress: finalUpdate.liveNodeProgress,
1854
1989
  liveLogs,
1855
1990
  liveLogContexts: finalUpdate.liveLogContexts,
1991
+ liveLogMetadata: finalUpdate.liveLogMetadata,
1856
1992
  liveLogIds: finalUpdate.liveLogIds,
1857
1993
  liveLogsChannelOffset: Math.max(0, liveLogTotalCount - liveLogs.length),
1858
1994
  liveLogsProducerAttempt: