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
@@ -17,6 +17,9 @@ export const DEFAULT_ADVISORY_PROVIDER_MAX_RPS = 100;
17
17
  /** Cold-start rate for an unknown provider bucket shared across a fleet key. */
18
18
  export const DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS = 10;
19
19
 
20
+ /** Learned-rate floor, independent of the unknown provider's starting guess. */
21
+ export const DEFAULT_UNKNOWN_PROVIDER_MIN_RPS = 2;
22
+
20
23
  /** AIMD state cooldown cap; provider Retry-After holds remain uncapped. */
21
24
  export const PROVIDER_AIMD_COOLDOWN_CAP_MS = 45 * 60 * 1_000;
22
25
 
@@ -52,6 +52,7 @@ export const PLAY_LAUNCH_ENVELOPE_FIELD_ROLES: {
52
52
  playId: 'detail',
53
53
  schedulerSchema: 'detail',
54
54
  requiresRunCreatedProjection: 'detail',
55
+ runtimeLogIdentity: 'detail',
55
56
  runtimeArtifact: 'detail',
56
57
  runtimeProtocolVersion: 'detail',
57
58
  staticPipeline: 'detail',
@@ -5,6 +5,42 @@
5
5
  export type PlayLogContextValue = string | number | boolean;
6
6
  export type PlayLogContext = Readonly<Record<string, PlayLogContextValue>>;
7
7
 
8
+ /** Producer-owned facts for one line, separate from authored context. */
9
+ export type PlayLogMetadata = {
10
+ level: 'debug' | 'info' | 'warn' | 'error';
11
+ occurredAt: number;
12
+ stepId?: string;
13
+ };
14
+
15
+ export function normalizePlayLogMetadata(
16
+ value: unknown,
17
+ ): PlayLogMetadata | null {
18
+ if (value === undefined || value === null) return null;
19
+ if (typeof value !== 'object' || Array.isArray(value)) {
20
+ throw new TypeError('Run log metadata must be an object.');
21
+ }
22
+ const entry = value as Record<string, unknown>;
23
+ if (
24
+ !['debug', 'info', 'warn', 'error'].includes(String(entry.level)) ||
25
+ typeof entry.occurredAt !== 'number' ||
26
+ !Number.isSafeInteger(entry.occurredAt) ||
27
+ entry.occurredAt <= 0 ||
28
+ (entry.stepId !== undefined &&
29
+ (typeof entry.stepId !== 'string' ||
30
+ !entry.stepId.trim() ||
31
+ entry.stepId.length > 256))
32
+ ) {
33
+ throw new TypeError(
34
+ 'Run log metadata requires a severity, timestamp and optional step ID.',
35
+ );
36
+ }
37
+ return {
38
+ level: entry.level as PlayLogMetadata['level'],
39
+ occurredAt: entry.occurredAt,
40
+ ...(typeof entry.stepId === 'string' ? { stepId: entry.stepId } : {}),
41
+ };
42
+ }
43
+
8
44
  export const PLAY_LOG_CONTEXT_LIMITS = {
9
45
  fields: 8,
10
46
  keyBytes: 64,
@@ -117,6 +117,23 @@ export const LOG_PROVENANCE_POLICY: Record<
117
117
  receipt: { watch: false, ui: false, debug: true },
118
118
  };
119
119
 
120
+ /**
121
+ * Shared retention rule for the detached runner's positional log channel.
122
+ * Sender offsets and gateway persistence must count the same events. Host
123
+ * lifecycle output stays in worker logs; bounded performance markers also
124
+ * reach the durable stream for dispatcher throughput diagnostics.
125
+ */
126
+ export function isDurableDirectRunnerLog(event: {
127
+ source?: unknown;
128
+ line?: unknown;
129
+ }): boolean {
130
+ return (
131
+ typeof event.line === 'string' &&
132
+ event.line.length > 0 &&
133
+ (event.source !== 'worker' || event.line.startsWith('[perf-trace] '))
134
+ );
135
+ }
136
+
120
137
  /** True when a line of the given provenance should render on the surface. */
121
138
  export function logProvenanceReaches(
122
139
  provenance: LogProvenance,
@@ -324,7 +341,8 @@ export function classifyLegacyLogLine(line: string): LogProvenance {
324
341
 
325
342
  // Scheduler / sandbox / worker internals.
326
343
  if (
327
- /^\[perf\] runtime (?:map|state)\b/i.test(message) ||
344
+ /^\[perf\]/i.test(message) ||
345
+ /^\[runtime\.(?:phase_summary|logs|log_transport)\]/i.test(message) ||
328
346
  /\[worker\] picked up run\b/.test(message) ||
329
347
  /\[worker\] heartbeat\b/.test(message) ||
330
348
  /\[worker\] progress completedRows=\d+ totalRows=\d+ rowUpdates=\d+/.test(
@@ -39,10 +39,11 @@ export type PlayRunAttemptWorkerTerminalProjectionPorts = {
39
39
  };
40
40
 
41
41
  /**
42
- * Builds the worker projection Module. The ordering is its Interface:
43
- * reconcile durable truth, release failed-attempt leases, then enqueue
44
- * best-effort progress/outbox projection. A stale terminal fence observes the
45
- * winner only; it never emits a second projection sequence.
42
+ * Request delivery of the already committed terminal before awaited
43
+ * reconciliation or cleanup can delay it. Progress finalization still follows
44
+ * durable-truth reconciliation and failed-attempt lease release. A stale
45
+ * terminal fence observes the winner only; it never emits a second projection
46
+ * sequence.
46
47
  */
47
48
  export function createPlayRunAttemptWorkerTerminalProjection(
48
49
  ports: PlayRunAttemptWorkerTerminalProjectionPorts,
@@ -50,6 +51,7 @@ export function createPlayRunAttemptWorkerTerminalProjection(
50
51
  return {
51
52
  observeFenceLost: ports.observeFenceLost,
52
53
  async projectCommitted(input) {
54
+ ports.requestTerminalProjection({ terminal: input.terminal });
53
55
  await ports.reconcileTerminalTruth(input);
54
56
  if (input.terminal.status === 'failed') {
55
57
  await ports.releaseFailedAttempt({
@@ -58,7 +60,6 @@ export function createPlayRunAttemptWorkerTerminalProjection(
58
60
  });
59
61
  }
60
62
  ports.queueProgressFinalization(input);
61
- ports.requestTerminalProjection({ terminal: input.terminal });
62
63
  },
63
64
  };
64
65
  }
@@ -6,6 +6,7 @@ import { fullenrichBatchStrategies } from './fullenrich-batching';
6
6
  import { opensosdataBatchStrategies } from './opensosdata-batching';
7
7
  import { zerobounceBatchStrategies } from './zerobounce-batching';
8
8
  import { testAsyncBatchStrategies } from './test-async-batching';
9
+ import { testAdmissionBatchStrategies } from './test-admission-batching';
9
10
 
10
11
  export const PLAY_RUNTIME_BATCH_OPERATION_REGISTRY: Record<
11
12
  string,
@@ -17,6 +18,7 @@ export const PLAY_RUNTIME_BATCH_OPERATION_REGISTRY: Record<
17
18
  ...fullenrichBatchStrategies,
18
19
  ...opensosdataBatchStrategies,
19
20
  ...testAsyncBatchStrategies,
21
+ ...testAdmissionBatchStrategies,
20
22
  ...zerobounceBatchStrategies,
21
23
  };
22
24
 
@@ -14,7 +14,7 @@ import type {
14
14
  import type { PlayExecutionSuspension } from './suspension';
15
15
  import type { PlayStaticPipeline } from '../plays/static-pipeline';
16
16
  import type { PlayLiveEventSource } from './live-events';
17
- import type { PlayLogContext } from './log-context';
17
+ import type { PlayLogContext, PlayLogMetadata } from './log-context';
18
18
  import type { PreloadedRuntimeDbSession } from './db-session';
19
19
  import type { PacingRule } from './governor/rate-state-backend';
20
20
  import type { GovernanceSnapshot } from './governor/governor';
@@ -185,6 +185,8 @@ export type PlayRunnerBudgetChargeResult = {
185
185
  };
186
186
 
187
187
  export interface PlayRunnerContextConfig extends RetainedPipelineCarrier {
188
+ /** Reader-first logging cutover, frozen at admission; absent stays legacy. */
189
+ runtimeLogIdentity?: 'origin';
188
190
  executorToken?: string;
189
191
  baseUrl?: string;
190
192
  /**
@@ -320,8 +322,11 @@ export type PlayRunnerLogEvent = {
320
322
  at: string;
321
323
  source: PlayLiveEventSource;
322
324
  line: string;
325
+ /** Origin identity, preserved across live and terminal transports. */
326
+ entryId?: string;
323
327
  /** Search-only context; omitted before the compatible Convex projection. */
324
328
  context?: PlayLogContext;
329
+ metadata?: PlayLogMetadata;
325
330
  };
326
331
 
327
332
  export type PlayRunnerEvent =
@@ -411,8 +416,10 @@ export type PlayRunnerResult =
411
416
  outputRowCount?: number;
412
417
  rowOutcomes?: PlayRunnerRowOutcomeSummary;
413
418
  logs: string[];
419
+ logIds?: string[];
414
420
  /** Search-only context aligned to logs; omitted when every entry is null. */
415
421
  logContexts?: Array<PlayLogContext | null>;
422
+ logMetadata?: Array<PlayLogMetadata | null>;
416
423
  stats: Record<string, unknown>;
417
424
  steps: PlayStep[];
418
425
  checkpoint: PlayCheckpoint;
@@ -426,8 +433,10 @@ export type PlayRunnerResult =
426
433
  status: 'suspended';
427
434
  suspension: PlayExecutionSuspension;
428
435
  logs: string[];
436
+ logIds?: string[];
429
437
  /** Search-only context aligned to logs; omitted when every entry is null. */
430
438
  logContexts?: Array<PlayLogContext | null>;
439
+ logMetadata?: Array<PlayLogMetadata | null>;
431
440
  stats: Record<string, unknown>;
432
441
  steps: PlayStep[];
433
442
  checkpoint: PlayCheckpoint;
@@ -442,8 +451,10 @@ export type PlayRunnerResult =
442
451
  error: string;
443
452
  errors?: PlayRunFailureDetails[];
444
453
  logs: string[];
454
+ logIds?: string[];
445
455
  /** Search-only context aligned to logs; omitted when every entry is null. */
446
456
  logContexts?: Array<PlayLogContext | null>;
457
+ logMetadata?: Array<PlayLogMetadata | null>;
447
458
  stats: Record<string, unknown>;
448
459
  steps: PlayStep[];
449
460
  checkpoint?: PlayCheckpoint | null;
@@ -387,6 +387,7 @@ export const RUN_SCHEDULER_OUTBOX_EVENT_TYPE = {
387
387
  runCancelled: RUN_EVENT_KIND.runCancelled,
388
388
  runEvents: 'run.events',
389
389
  runnerLogs: 'runner.logs',
390
+ receiptEvent: 'receipt.event',
390
391
  runWaitTimeout: 'run.wait_timeout',
391
392
  } as const;
392
393
 
@@ -549,6 +550,7 @@ export const RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL = {
549
550
  runCancelled: sqlStringLiteral(RUN_SCHEDULER_OUTBOX_EVENT_TYPE.runCancelled),
550
551
  runEvents: sqlStringLiteral(RUN_SCHEDULER_OUTBOX_EVENT_TYPE.runEvents),
551
552
  runnerLogs: sqlStringLiteral(RUN_SCHEDULER_OUTBOX_EVENT_TYPE.runnerLogs),
553
+ receiptEvent: sqlStringLiteral(RUN_SCHEDULER_OUTBOX_EVENT_TYPE.receiptEvent),
552
554
  runWaitTimeout: sqlStringLiteral(
553
555
  RUN_SCHEDULER_OUTBOX_EVENT_TYPE.runWaitTimeout,
554
556
  ),
@@ -1,6 +1,6 @@
1
+ import { normalizeRunLogIds } from './run-log-buffer';
1
2
  import { RUN_EVENT_KIND } from './run-event-catalog';
2
3
  import type { PlayRunLedgerIngressEvent } from './run-ledger';
3
- import type { PlayLogContext } from './log-context';
4
4
  import type { RunLedgerProjectionChunk } from './run-ledger-projection-contract';
5
5
 
6
6
  const APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES = 128 * 1024;
@@ -29,25 +29,35 @@ function splitRunEventForAppRuntime(
29
29
  : [];
30
30
  if (eventType !== RUN_EVENT_KIND.logAppended || lines.length === 0)
31
31
  return [event];
32
- const payloadOwnsLineContexts =
33
- payload !== null && Object.hasOwn(payload, 'lineContexts');
34
- const eventOwnsLineContexts = Object.hasOwn(event, 'lineContexts');
35
- if (payloadOwnsLineContexts && eventOwnsLineContexts) {
36
- throw new TypeError('Run log contexts must have one event owner.');
37
- }
38
- const rawLineContexts = payloadOwnsLineContexts
39
- ? payload?.lineContexts
40
- : event.lineContexts;
41
- const hasLineContexts = payloadOwnsLineContexts || eventOwnsLineContexts;
42
- if (
43
- hasLineContexts &&
44
- (!Array.isArray(rawLineContexts) || rawLineContexts.length !== lines.length)
45
- ) {
46
- throw new TypeError('Run log contexts must align with their lines.');
47
- }
48
- const lineContexts = hasLineContexts
49
- ? (rawLineContexts as Array<PlayLogContext | null>)
50
- : null;
32
+ const alignedFields = ['lineContexts', 'lineMetadata'] as const;
33
+ const aligned = alignedFields.map((key) => {
34
+ const label = key === 'lineContexts' ? 'contexts' : 'metadata';
35
+ const inPayload = payload !== null && Object.hasOwn(payload, key);
36
+ const inEvent = Object.hasOwn(event, key);
37
+ if (inPayload && inEvent)
38
+ throw new TypeError(`Run log ${label} must have one event owner.`);
39
+ const value = inPayload ? payload?.[key] : event[key];
40
+ if (
41
+ (inPayload || inEvent) &&
42
+ (!Array.isArray(value) || value.length !== lines.length)
43
+ ) {
44
+ throw new TypeError(`Run log ${label} must align with their lines.`);
45
+ }
46
+ return {
47
+ key,
48
+ inPayload,
49
+ inEvent,
50
+ value: Array.isArray(value) ? value : undefined,
51
+ };
52
+ });
53
+ const payloadOwnsLineIds =
54
+ payload !== null && Object.hasOwn(payload, 'lineIds');
55
+ if (payloadOwnsLineIds && Object.hasOwn(event, 'lineIds'))
56
+ throw new TypeError('Run log identities must have one event owner.');
57
+ const lineIds = normalizeRunLogIds(
58
+ payloadOwnsLineIds ? payload?.lineIds : event.lineIds,
59
+ lines,
60
+ );
51
61
  const payloadChannelOffset =
52
62
  typeof payload?.channelOffset === 'number'
53
63
  ? payload.channelOffset
@@ -58,6 +68,8 @@ function splitRunEventForAppRuntime(
58
68
  const {
59
69
  lines: _outerLines,
60
70
  lineContexts: _outerLineContexts,
71
+ lineMetadata: _outerLineMetadata,
72
+ lineIds: _outerLineIds,
61
73
  ...eventWithoutOuterLines
62
74
  } = event;
63
75
  let index = 0;
@@ -65,37 +77,65 @@ function splitRunEventForAppRuntime(
65
77
  const start = index;
66
78
  let bytes = 2;
67
79
  while (index < lines.length) {
68
- const lineBytes = appRuntimeTextEncoder.encode(
69
- JSON.stringify(lines[index]!),
70
- ).length;
80
+ const lineBytes =
81
+ appRuntimeTextEncoder.encode(JSON.stringify(lines[index]!)).length +
82
+ aligned.reduce(
83
+ (total, field) =>
84
+ total +
85
+ (field.value
86
+ ? appRuntimeTextEncoder.encode(JSON.stringify(field.value[index]))
87
+ .length + 1
88
+ : 0),
89
+ 0,
90
+ );
91
+ const metadataBytes = lineIds
92
+ ? appRuntimeTextEncoder.encode(JSON.stringify(lineIds[index])).length + 1
93
+ : 0;
71
94
  if (
72
95
  index > start &&
73
- bytes + lineBytes + 1 > APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES
96
+ bytes + lineBytes + metadataBytes + 1 >
97
+ APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES
74
98
  ) {
75
99
  break;
76
100
  }
77
- bytes += lineBytes + 1;
101
+ bytes += lineBytes + metadataBytes + 1;
78
102
  index += 1;
79
103
  }
80
104
  const batchLines = lines.slice(start, index);
81
- const batchLineContexts = lineContexts?.slice(start, index);
105
+ const batchLineIds = lineIds?.slice(start, index);
106
+ const eventFields = Object.fromEntries(
107
+ aligned
108
+ .filter((field) => field.inEvent)
109
+ .map((field) => [field.key, field.value!.slice(start, index)]),
110
+ );
111
+ const payloadFields = Object.fromEntries(
112
+ aligned
113
+ .filter((field) => field.inPayload)
114
+ .map((field) => [field.key, field.value!.slice(start, index)]),
115
+ );
82
116
  split.push(
83
117
  payload
84
118
  ? {
85
119
  ...eventWithoutOuterLines,
86
- ...(eventOwnsLineContexts && batchLineContexts
87
- ? { lineContexts: batchLineContexts }
88
- : {}),
120
+ ...eventFields,
89
121
  ...(payloadOwnsLines ? {} : { lines: batchLines }),
122
+ ...(!payloadOwnsLineIds && batchLineIds
123
+ ? { lineIds: batchLineIds }
124
+ : {}),
90
125
  payload: {
91
126
  ...Object.fromEntries(
92
127
  Object.entries(payload).filter(
93
- ([key]) => key !== 'lineContexts',
128
+ ([key]) =>
129
+ key !== 'lineIds' &&
130
+ !alignedFields.includes(
131
+ key as (typeof alignedFields)[number],
132
+ ),
94
133
  ),
95
134
  ),
96
135
  ...(payloadOwnsLines ? { lines: batchLines } : {}),
97
- ...(payloadOwnsLineContexts && batchLineContexts
98
- ? { lineContexts: batchLineContexts }
136
+ ...payloadFields,
137
+ ...(payloadOwnsLineIds && batchLineIds
138
+ ? { lineIds: batchLineIds }
99
139
  : {}),
100
140
  ...(payloadChannelOffset === undefined
101
141
  ? {}
@@ -108,7 +148,8 @@ function splitRunEventForAppRuntime(
108
148
  : {
109
149
  ...eventWithoutOuterLines,
110
150
  lines: batchLines,
111
- ...(batchLineContexts ? { lineContexts: batchLineContexts } : {}),
151
+ ...eventFields,
152
+ ...(batchLineIds ? { lineIds: batchLineIds } : {}),
112
153
  ...(eventChannelOffset === undefined
113
154
  ? {}
114
155
  : { channelOffset: eventChannelOffset + start }),
@@ -24,7 +24,7 @@ import {
24
24
  type PlayConditionCounts,
25
25
  } from './cell-provenance';
26
26
  import { isLedgerTerminalResultRef } from './output-size-limits';
27
- import type { PlayLogContext } from './log-context';
27
+ import type { PlayLogContext, PlayLogMetadata } from './log-context';
28
28
  import { RUN_EVENT_KIND, type RunEventSource } from './run-event-catalog';
29
29
 
30
30
  export type PlayRunLedgerStatus =
@@ -100,6 +100,8 @@ export type PlayRunLedgerStepProgress = {
100
100
  completedAt?: number | null;
101
101
  updatedAt?: number | null;
102
102
  nodeIo?: PlayDocflowNodeIoState;
103
+ /** Error reported by this step, distinct from a run stopping other work. */
104
+ error?: string;
103
105
  conditionCounts?: PlayConditionCounts;
104
106
  };
105
107
 
@@ -320,6 +322,8 @@ export type PlayRunLedgerEvent =
320
322
  lines: string[];
321
323
  /** Search-only context aligned to `lines`; stripped before Convex. */
322
324
  lineContexts?: Array<PlayLogContext | null>;
325
+ lineMetadata?: Array<PlayLogMetadata | null>;
326
+ lineIds?: Array<string | null>;
323
327
  /**
324
328
  * Absolute per-run sequence (1-based) of `lines[0]`. Assigned by Run
325
329
  * Log Stream ingestion in Convex only; producers never set it.
@@ -430,6 +434,8 @@ export type PlayRunLedgerStatusPatch = {
430
434
  liveLogs?: readonly string[] | null;
431
435
  /** Search-only context aligned to `liveLogs`; stripped before Convex. */
432
436
  liveLogContexts?: readonly (PlayLogContext | null)[] | null;
437
+ liveLogMetadata?: readonly (PlayLogMetadata | null)[] | null;
438
+ liveLogIds?: readonly (string | null)[] | null;
433
439
  /**
434
440
  * Positional cursor for `liveLogs[0]` on this producer's log channel (the
435
441
  * count of lines emitted before it). Computed by the producer-side cursor
@@ -1381,6 +1387,9 @@ function normalizeStepProgress(
1381
1387
  ...(optionalFiniteNumber(value.supersededRows) !== undefined
1382
1388
  ? { supersededRows: optionalFiniteNumber(value.supersededRows) }
1383
1389
  : {}),
1390
+ ...(optionalString(value.error)
1391
+ ? { error: optionalString(value.error) }
1392
+ : {}),
1384
1393
  ...(optionalString(value.message)
1385
1394
  ? { message: optionalString(value.message) }
1386
1395
  : {}),
@@ -2327,6 +2336,7 @@ function progressSignature(
2327
2336
  completedRows: progress?.completedRows ?? null,
2328
2337
  supersededRows: progress?.supersededRows ?? null,
2329
2338
  message: progress?.message ?? null,
2339
+ error: progress?.error ?? null,
2330
2340
  artifactTableNamespace: progress?.artifactTableNamespace ?? null,
2331
2341
  startedAt: progress?.startedAt ?? null,
2332
2342
  completedAt: progress?.completedAt ?? null,
@@ -2369,12 +2379,16 @@ function terminalEventTypeForStatus(
2369
2379
  export function slicePositionalLogLines(input: {
2370
2380
  bufferLines: readonly string[];
2371
2381
  bufferContexts?: readonly (PlayLogContext | null)[];
2382
+ bufferMetadata?: readonly (PlayLogMetadata | null)[];
2383
+ bufferIds?: readonly (string | null)[];
2372
2384
  bufferTotalCount: number;
2373
2385
  sentCount: number;
2374
2386
  }): {
2375
2387
  lines: string[];
2376
2388
  channelOffset: number;
2377
2389
  lineContexts?: Array<PlayLogContext | null>;
2390
+ lineMetadata?: Array<PlayLogMetadata | null>;
2391
+ lineIds?: Array<string | null>;
2378
2392
  } | null {
2379
2393
  if (
2380
2394
  input.bufferContexts !== undefined &&
@@ -2382,6 +2396,14 @@ export function slicePositionalLogLines(input: {
2382
2396
  ) {
2383
2397
  throw new TypeError('Run log contexts must align with the retained lines.');
2384
2398
  }
2399
+ if (
2400
+ input.bufferMetadata !== undefined &&
2401
+ input.bufferMetadata.length !== input.bufferLines.length
2402
+ )
2403
+ throw new TypeError('Run log metadata must align with lines.');
2404
+ if (input.bufferIds && input.bufferIds.length !== input.bufferLines.length) {
2405
+ throw new TypeError('Run log identities must align with retained lines.');
2406
+ }
2385
2407
  const total = Math.max(input.bufferTotalCount, input.bufferLines.length);
2386
2408
  const sent = Math.max(0, input.sentCount);
2387
2409
  if (total <= sent) {
@@ -2395,10 +2417,13 @@ export function slicePositionalLogLines(input: {
2395
2417
  }
2396
2418
  const start = sendFromOffset - bufferFirstOffset;
2397
2419
  const lineContexts = input.bufferContexts?.slice(start);
2420
+ const lineMetadata = input.bufferMetadata?.slice(start);
2398
2421
  return {
2399
2422
  lines: [...lines],
2400
2423
  channelOffset: sendFromOffset,
2424
+ ...(input.bufferIds ? { lineIds: input.bufferIds.slice(start) } : {}),
2401
2425
  ...(lineContexts?.some(Boolean) ? { lineContexts: [...lineContexts] } : {}),
2426
+ ...(lineMetadata?.some(Boolean) ? { lineMetadata: [...lineMetadata] } : {}),
2402
2427
  };
2403
2428
  }
2404
2429
 
@@ -2427,6 +2452,8 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2427
2452
  runId: string;
2428
2453
  lines: readonly string[];
2429
2454
  lineContexts?: readonly (PlayLogContext | null)[];
2455
+ lineMetadata?: readonly (PlayLogMetadata | null)[];
2456
+ lineIds?: readonly (string | null)[];
2430
2457
  source?: PlayRunLedgerEventSource;
2431
2458
  occurredAt?: number;
2432
2459
  channelOffset?: number | null;
@@ -2448,10 +2475,20 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2448
2475
  ) {
2449
2476
  throw new TypeError('Run log contexts must align with their lines.');
2450
2477
  }
2478
+ if (
2479
+ input.lineMetadata !== undefined &&
2480
+ input.lineMetadata.length !== input.lines.length
2481
+ )
2482
+ throw new TypeError('Run log metadata must align with lines.');
2483
+ if (input.lineIds && input.lineIds.length !== input.lines.length) {
2484
+ throw new TypeError('Run log identities must align with their lines.');
2485
+ }
2451
2486
  const entries = input.lines
2452
2487
  .map((line, index) => ({
2453
2488
  line: positional ? line.trim() || '(blank log line)' : line.trim(),
2454
2489
  context: input.lineContexts?.[index] ?? null,
2490
+ metadata: input.lineMetadata?.[index] ?? null,
2491
+ entryId: input.lineIds?.[index] ?? null,
2455
2492
  }))
2456
2493
  .filter((entry) => positional || entry.line.length > 0);
2457
2494
  const newLines = entries.map((entry) => entry.line);
@@ -2470,6 +2507,13 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2470
2507
  source,
2471
2508
  occurredAt,
2472
2509
  lines: newLines.slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT),
2510
+ ...(input.lineIds
2511
+ ? {
2512
+ lineIds: entries
2513
+ .slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT)
2514
+ .map((entry) => entry.entryId),
2515
+ }
2516
+ : {}),
2473
2517
  ...(entries
2474
2518
  .slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT)
2475
2519
  .some((entry) => entry.context)
@@ -2479,6 +2523,15 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2479
2523
  .map((entry) => entry.context),
2480
2524
  }
2481
2525
  : {}),
2526
+ ...(entries
2527
+ .slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT)
2528
+ .some((entry) => entry.metadata)
2529
+ ? {
2530
+ lineMetadata: entries
2531
+ .slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT)
2532
+ .map((entry) => entry.metadata),
2533
+ }
2534
+ : {}),
2482
2535
  ...(positional ? { channelOffset: input.channelOffset! + index } : {}),
2483
2536
  ...(positional &&
2484
2537
  typeof input.producerAttempt === 'number' &&
@@ -2507,6 +2560,8 @@ export function buildTerminalLogReplayEvents(input: {
2507
2560
  runId: string;
2508
2561
  lines: readonly string[];
2509
2562
  lineContexts?: readonly (PlayLogContext | null)[];
2563
+ lineMetadata?: readonly (PlayLogMetadata | null)[];
2564
+ lineIds?: readonly (string | null)[];
2510
2565
  source?: PlayRunLedgerEventSource;
2511
2566
  occurredAt?: number;
2512
2567
  liveLogTotalCount?: number;
@@ -2521,6 +2576,8 @@ export function buildTerminalLogReplayEvents(input: {
2521
2576
  runId: input.runId,
2522
2577
  lines: input.lines,
2523
2578
  lineContexts: input.lineContexts,
2579
+ lineMetadata: input.lineMetadata,
2580
+ lineIds: input.lineIds,
2524
2581
  source: 'coordinator',
2525
2582
  occurredAt: input.occurredAt,
2526
2583
  channelOffset,
@@ -2562,6 +2619,8 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
2562
2619
  runId: patch.runId,
2563
2620
  lines: patch.liveLogs,
2564
2621
  lineContexts: patch.liveLogContexts ?? undefined,
2622
+ lineMetadata: patch.liveLogMetadata ?? undefined,
2623
+ lineIds: patch.liveLogIds ?? undefined,
2565
2624
  source,
2566
2625
  occurredAt: checkpointAt,
2567
2626
  channelOffset: patch.liveLogsChannelOffset ?? null,
@@ -2643,6 +2702,7 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
2643
2702
  ? { supersededRows: progress.supersededRows }
2644
2703
  : {}),
2645
2704
  ...(progress.message !== undefined ? { message: progress.message } : {}),
2705
+ ...(progress.error !== undefined ? { error: progress.error } : {}),
2646
2706
  ...(progress.artifactTableNamespace !== undefined
2647
2707
  ? { artifactTableNamespace: progress.artifactTableNamespace }
2648
2708
  : {}),
@@ -2669,6 +2729,7 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
2669
2729
  occurredAt: progress.updatedAt ?? checkpointAt,
2670
2730
  stepId,
2671
2731
  status:
2732
+ typeof progress.error === 'string' ||
2672
2733
  typeof progress.nodeIo?.error === 'string'
2673
2734
  ? 'failed'
2674
2735
  : typeof progress.completedAt === 'number'
@@ -2680,9 +2741,18 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
2680
2741
  });
2681
2742
  }
2682
2743
 
2683
- if (progress.completedAt && !previousStep?.completedAt) {
2744
+ const stepError = progress.error ?? progress.nodeIo?.error;
2745
+ if (
2746
+ progress.completedAt &&
2747
+ (!previousStep?.completedAt ||
2748
+ (typeof stepError === 'string' && previousStep.status !== 'failed'))
2749
+ ) {
2684
2750
  events.push({
2685
- type: RUN_EVENT_KIND.stepCompleted,
2751
+ type:
2752
+ typeof stepError === 'string'
2753
+ ? RUN_EVENT_KIND.stepFailed
2754
+ : RUN_EVENT_KIND.stepCompleted,
2755
+ ...(typeof stepError === 'string' ? { error: stepError } : {}),
2686
2756
  runId: patch.runId,
2687
2757
  source,
2688
2758
  occurredAt: progress.completedAt,
@@ -1,5 +1,8 @@
1
- import type { PlayLogContext } from './log-context';
2
- import { normalizePlayLogContext } from './log-context';
1
+ import type { PlayLogContext, PlayLogMetadata } from './log-context';
2
+ import {
3
+ normalizePlayLogContext,
4
+ normalizePlayLogMetadata,
5
+ } from './log-context';
3
6
 
4
7
  /** Limits apply before logs enter runner memory or a transport request. */
5
8
  export const RUNNER_LOG_MAX_CHARS = 2_000;
@@ -25,7 +28,10 @@ export function boundRunLogLine(line: string): string {
25
28
  */
26
29
  export class RunLogBuffer {
27
30
  readonly lines: string[] = [];
31
+ readonly ids: string[] = [];
32
+ private readonly origin = crypto.randomUUID();
28
33
  readonly contexts: Array<PlayLogContext | null> = [];
34
+ readonly metadata: Array<PlayLogMetadata | null> = [];
29
35
  private bytes = 0;
30
36
  private contextBytes = 0;
31
37
  private omittedContexts = 0;
@@ -36,10 +42,12 @@ export class RunLogBuffer {
36
42
  raw: string,
37
43
  terminal = false,
38
44
  context?: PlayLogContext | null,
45
+ metadata?: PlayLogMetadata | null,
39
46
  ): string[] {
40
47
  const line = boundRunLogLine(raw);
41
48
  const bytes = encoder.encode(line).byteLength;
42
49
  const normalizedContext = normalizePlayLogContext(context);
50
+ const normalizedMetadata = normalizePlayLogMetadata(metadata);
43
51
  const contextBytes = normalizedContext
44
52
  ? encoder.encode(JSON.stringify(normalizedContext)).byteLength
45
53
  : 0;
@@ -56,7 +64,9 @@ export class RunLogBuffer {
56
64
  true,
57
65
  );
58
66
  }
67
+ this.ids.push(`${this.origin}:${this.lines.length}`);
59
68
  this.lines.push(line);
69
+ this.metadata.push(normalizedMetadata);
60
70
  const retainContext =
61
71
  normalizedContext !== null &&
62
72
  this.contextBytes + contextBytes <= RUNNER_LOG_CONTEXT_MAX_BYTES;
@@ -80,3 +90,30 @@ export class RunLogBuffer {
80
90
  };
81
91
  }
82
92
  }
93
+
94
+ /** Optional origin identities: old producers omit them; never infer them from text. */
95
+ export function normalizeRunLogIds(
96
+ value: unknown,
97
+ lines: readonly unknown[],
98
+ ): Array<string | null> | undefined {
99
+ if (value === undefined || value === null) return undefined;
100
+ if (
101
+ !Array.isArray(value) ||
102
+ value.length !== lines.length ||
103
+ value.some(
104
+ (id) =>
105
+ id !== null &&
106
+ (typeof id !== 'string' || !/^[A-Za-z0-9:_-]{1,128}$/.test(id)),
107
+ )
108
+ ) {
109
+ throw new TypeError(
110
+ 'Run log identities must be bounded strings aligned with their lines.',
111
+ );
112
+ }
113
+ const ids = value.filter((id): id is string => id !== null);
114
+ if (new Set(ids).size !== ids.length)
115
+ throw new TypeError(
116
+ 'Run log identities must identify distinct occurrences.',
117
+ );
118
+ return value;
119
+ }