deepline 0.1.224 → 0.1.226

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.
@@ -7836,6 +7836,9 @@ async function executeRunRequest(
7836
7836
  occurredAt: event.at,
7837
7837
  stepId: event.nodeId,
7838
7838
  kind: event.type,
7839
+ ...(event.transition === 'failed' && event.error
7840
+ ? { error: event.error }
7841
+ : {}),
7839
7842
  },
7840
7843
  ];
7841
7844
  flushLedgerEvents(false);
@@ -8317,7 +8320,8 @@ async function executeRunRequest(
8317
8320
  durationMs: nowMs() - startedAt,
8318
8321
  };
8319
8322
  } catch (error) {
8320
- stepLifecycle?.markStartedFailed(nowMs());
8323
+ const failure = normalizePlayRunFailure(error);
8324
+ stepLifecycle?.markStartedFailed(nowMs(), failure.message);
8321
8325
  // A runtime-limit abort is a timeout failure, not a user cancellation, so
8322
8326
  // it should be reported as run.failed with the limit message rather than
8323
8327
  // run.cancelled.
@@ -8329,7 +8333,6 @@ async function executeRunRequest(
8329
8333
  error instanceof Error ? error.message : 'Play run cancelled.',
8330
8334
  );
8331
8335
  }
8332
- const failure = normalizePlayRunFailure(error);
8333
8336
  const message = failure.message;
8334
8337
  // Controlled run-fatal teardown: release the runtime-sheet row leases and
8335
8338
  // work-receipt leases this attempt still holds so an immediate rerun is not
@@ -8369,6 +8372,9 @@ async function executeRunRequest(
8369
8372
  retryable: failure.retryable,
8370
8373
  ...(errorBilling ? { billing: errorBilling } : {}),
8371
8374
  ...(failure.cause ? { cause: failure.cause } : {}),
8375
+ ...(failure.name ? { name: failure.name } : {}),
8376
+ ...(failure.stack ? { stack: failure.stack } : {}),
8377
+ ...(failure.causes ? { causes: failure.causes } : {}),
8372
8378
  },
8373
8379
  ],
8374
8380
  });
@@ -108,10 +108,10 @@ export const SDK_RELEASE = {
108
108
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
109
109
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
110
110
  // automatically without blocking their current command.
111
- version: '0.1.224',
111
+ version: '0.1.226',
112
112
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
113
113
  supportPolicy: {
114
- latest: '0.1.224',
114
+ latest: '0.1.226',
115
115
  minimumSupported: '0.1.53',
116
116
  deprecatedBelow: '0.1.219',
117
117
  commandMinimumSupported: [
@@ -1,4 +1,5 @@
1
1
  import {
2
+ INVALID_SPAN_CONTEXT,
2
3
  SpanKind,
3
4
  SpanStatusCode,
4
5
  trace,
@@ -10,6 +11,97 @@ type PrimitiveAttribute = string | number | boolean | null | undefined;
10
11
 
11
12
  type TraceAttributes = Record<string, PrimitiveAttribute>;
12
13
 
14
+ const NOOP_SPAN: Span = {
15
+ spanContext: () => INVALID_SPAN_CONTEXT,
16
+ setAttribute: () => NOOP_SPAN,
17
+ setAttributes: () => NOOP_SPAN,
18
+ addEvent: () => NOOP_SPAN,
19
+ addLink: () => NOOP_SPAN,
20
+ addLinks: () => NOOP_SPAN,
21
+ setStatus: () => NOOP_SPAN,
22
+ updateName: () => NOOP_SPAN,
23
+ end: () => undefined,
24
+ isRecording: () => false,
25
+ recordException: () => undefined,
26
+ };
27
+
28
+ function discardTelemetryResult(invoke: () => unknown): void {
29
+ try {
30
+ const result = invoke();
31
+ if (
32
+ result === null ||
33
+ (typeof result !== 'object' && typeof result !== 'function')
34
+ ) {
35
+ return;
36
+ }
37
+ let then: unknown;
38
+ try {
39
+ then = Reflect.get(result, 'then');
40
+ } catch {
41
+ return;
42
+ }
43
+ if (typeof then === 'function') {
44
+ void Promise.resolve(result).catch(() => undefined);
45
+ }
46
+ } catch {}
47
+ }
48
+
49
+ function nonBlockingSpan(span: Span): Span {
50
+ const safeSpan: Span = {
51
+ spanContext() {
52
+ try {
53
+ return span.spanContext();
54
+ } catch {
55
+ return INVALID_SPAN_CONTEXT;
56
+ }
57
+ },
58
+ setAttribute(key, value) {
59
+ discardTelemetryResult(() => span.setAttribute(key, value));
60
+ return safeSpan;
61
+ },
62
+ setAttributes(attributes) {
63
+ discardTelemetryResult(() => span.setAttributes(attributes));
64
+ return safeSpan;
65
+ },
66
+ addEvent(name, attributesOrStartTime, startTime) {
67
+ discardTelemetryResult(() =>
68
+ span.addEvent(name, attributesOrStartTime, startTime),
69
+ );
70
+ return safeSpan;
71
+ },
72
+ addLink(link) {
73
+ discardTelemetryResult(() => span.addLink(link));
74
+ return safeSpan;
75
+ },
76
+ addLinks(links) {
77
+ discardTelemetryResult(() => span.addLinks(links));
78
+ return safeSpan;
79
+ },
80
+ setStatus(status) {
81
+ discardTelemetryResult(() => span.setStatus(status));
82
+ return safeSpan;
83
+ },
84
+ updateName(name) {
85
+ discardTelemetryResult(() => span.updateName(name));
86
+ return safeSpan;
87
+ },
88
+ end(endTime) {
89
+ discardTelemetryResult(() => span.end(endTime));
90
+ },
91
+ isRecording() {
92
+ try {
93
+ return span.isRecording();
94
+ } catch {
95
+ return false;
96
+ }
97
+ },
98
+ recordException(exception, time) {
99
+ discardTelemetryResult(() => span.recordException(exception, time));
100
+ },
101
+ };
102
+ return safeSpan;
103
+ }
104
+
13
105
  function normalizeAttributes(
14
106
  attributes: TraceAttributes | undefined,
15
107
  ): Attributes | undefined {
@@ -60,34 +152,67 @@ export async function withActiveSpan<T>(
60
152
  },
61
153
  fn: (span: Span) => Promise<T> | T,
62
154
  ): Promise<T> {
63
- const tracer = trace.getTracer(options.tracer ?? 'deepline');
64
- return await tracer.startActiveSpan(
65
- name,
66
- {
67
- kind: options.kind,
68
- attributes: normalizeAttributes(options.attributes),
69
- },
70
- async (span) => {
155
+ let operationPromise: Promise<T> | null = null;
156
+ const runOperation = async (span: Span): Promise<T> => {
157
+ const safeSpan = nonBlockingSpan(span);
158
+ try {
159
+ const result = await fn(safeSpan);
160
+ safeSpan.setStatus({ code: SpanStatusCode.OK });
161
+ return result;
162
+ } catch (error) {
71
163
  try {
72
- const result = await fn(span);
73
- span.setStatus({ code: SpanStatusCode.OK });
74
- return result;
75
- } catch (error) {
76
- recordError(span, error);
77
- throw error;
78
- } finally {
79
- span.end();
164
+ recordError(safeSpan, error);
165
+ } catch {
166
+ // Error telemetry must not replace the operation's real error.
80
167
  }
81
- },
82
- );
168
+ throw error;
169
+ } finally {
170
+ safeSpan.end();
171
+ }
172
+ };
173
+ const startOperation = (span: Span): Promise<T> => {
174
+ if (operationPromise) return operationPromise;
175
+ let resolveOperation!: (value: T | PromiseLike<T>) => void;
176
+ let rejectOperation!: (reason?: unknown) => void;
177
+ operationPromise = new Promise<T>((resolve, reject) => {
178
+ resolveOperation = resolve;
179
+ rejectOperation = reject;
180
+ });
181
+ void runOperation(span).then(resolveOperation, rejectOperation);
182
+ return operationPromise;
183
+ };
184
+
185
+ let tracerResult: unknown;
186
+ try {
187
+ const tracer = trace.getTracer(options.tracer ?? 'deepline');
188
+ tracerResult = tracer.startActiveSpan(
189
+ name,
190
+ {
191
+ kind: options.kind,
192
+ attributes: normalizeAttributes(options.attributes),
193
+ },
194
+ (span) => startOperation(span),
195
+ );
196
+ } catch {}
197
+
198
+ // A non-conforming exporter may return work unrelated to the callback.
199
+ // Drain its rejection without awaiting it; only execution owns latency and
200
+ // the returned success/failure contract.
201
+ discardTelemetryResult(() => tracerResult);
202
+
203
+ // A broken tracer may return without invoking the callback. Execution is
204
+ // authoritative, so run it once without telemetry in that case.
205
+ return await startOperation(NOOP_SPAN);
83
206
  }
84
207
 
85
208
  export function setSpanAttributes(
86
209
  span: Span,
87
210
  attributes: TraceAttributes,
88
211
  ): void {
89
- const normalized = normalizeAttributes(attributes);
90
- if (normalized) {
91
- span.setAttributes(normalized);
92
- }
212
+ try {
213
+ const normalized = normalizeAttributes(attributes);
214
+ if (normalized) {
215
+ discardTelemetryResult(() => span.setAttributes(normalized));
216
+ }
217
+ } catch {}
93
218
  }
@@ -33,6 +33,8 @@ import { createRuntimeReceiptHeartbeatSupervisor } from './receipt-heartbeat-sup
33
33
  import { dispatchBoundedSettled } from './bounded-dispatch';
34
34
  import type { PlayQueueHint } from './governor/rate-state-backend';
35
35
  import type { MapRowOutcome } from './durability-store';
36
+ import { stringifyPostgresJson } from './postgres-json';
37
+ import { RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE } from './runtime-sheet-row-transition';
36
38
  import type { WorkReceiptFailureKind } from './work-receipts';
37
39
  import {
38
40
  completedMapRowOutcome,
@@ -3090,9 +3092,19 @@ export class PlayContextImpl {
3090
3092
  // are no-ops for any value that is not a tool result.
3091
3093
 
3092
3094
  private serializeCellValue(value: unknown): unknown {
3093
- return isToolExecuteResult(value)
3095
+ const serialized = isToolExecuteResult(value)
3094
3096
  ? serializeToolExecuteResult(value)
3095
3097
  : value;
3098
+ try {
3099
+ stringifyPostgresJson(serialized);
3100
+ } catch (error) {
3101
+ throw new Error(
3102
+ `${RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE}: Dataset cell output is not JSON-serializable: ${
3103
+ error instanceof Error ? error.message : String(error)
3104
+ }`,
3105
+ );
3106
+ }
3107
+ return serialized;
3096
3108
  }
3097
3109
 
3098
3110
  private rehydrateCellValue(value: unknown): unknown {
@@ -5391,6 +5403,7 @@ export class PlayContextImpl {
5391
5403
  });
5392
5404
 
5393
5405
  let value: unknown;
5406
+ let cellValue: unknown;
5394
5407
  try {
5395
5408
  value = await rowContext.run(
5396
5409
  {
@@ -5411,6 +5424,7 @@ export class PlayContextImpl {
5411
5424
  this.previousCellForField(baseRow, fieldName),
5412
5425
  ),
5413
5426
  );
5427
+ cellValue = this.serializeCellValue(value);
5414
5428
  } catch (error) {
5415
5429
  if (
5416
5430
  isPlayRowExecutionSuspendedError(error) ||
@@ -5482,7 +5496,6 @@ export class PlayContextImpl {
5482
5496
  });
5483
5497
  return FAILED_ROW;
5484
5498
  }
5485
- const cellValue = this.serializeCellValue(value);
5486
5499
  computedFields[fieldName] = cellValue;
5487
5500
  this.rowStates.get(idx)?.results.set(fieldName, cellValue);
5488
5501
  const currentCellMeta =
@@ -35,6 +35,7 @@ export type PlayVisualNodeProgressSnapshot = {
35
35
  export type PlayVisualNodeStateSnapshot = {
36
36
  nodeId: string;
37
37
  status: PlayVisualNodeStatus;
38
+ error?: string | null;
38
39
  artifactTableNamespace?: string | null;
39
40
  progress?: PlayVisualNodeProgressSnapshot | null;
40
41
  startedAt?: number | null;
@@ -1,9 +1,71 @@
1
- const POSTGRES_UNSUPPORTED_NUL_RE = /\u0000/g;
1
+ const POSTGRES_JSON_REPLACEMENT_CHARACTER = '\uFFFD';
2
+ const POSTGRES_JSON_SUSPECT_CODE_UNIT_RE = /[\u0000\uD800-\uDFFF]/;
3
+
4
+ function sanitizePostgresJsonString(value: string): string {
5
+ if (!POSTGRES_JSON_SUSPECT_CODE_UNIT_RE.test(value)) return value;
6
+
7
+ let sanitized = '';
8
+ let changed = false;
9
+
10
+ for (let index = 0; index < value.length; index += 1) {
11
+ const codeUnit = value.charCodeAt(index);
12
+ if (codeUnit === 0) {
13
+ changed = true;
14
+ continue;
15
+ }
16
+
17
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
18
+ const nextCodeUnit = value.charCodeAt(index + 1);
19
+ if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
20
+ sanitized += value[index] + value[index + 1];
21
+ index += 1;
22
+ continue;
23
+ }
24
+ sanitized += POSTGRES_JSON_REPLACEMENT_CHARACTER;
25
+ changed = true;
26
+ continue;
27
+ }
28
+
29
+ if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
30
+ sanitized += POSTGRES_JSON_REPLACEMENT_CHARACTER;
31
+ changed = true;
32
+ continue;
33
+ }
34
+
35
+ sanitized += value[index];
36
+ }
37
+
38
+ return changed ? sanitized : value;
39
+ }
2
40
 
3
41
  function postgresJsonReplacer(_key: string, value: unknown): unknown {
4
- return typeof value === 'string'
5
- ? value.replace(POSTGRES_UNSUPPORTED_NUL_RE, '')
6
- : value;
42
+ if (typeof value === 'string') {
43
+ return sanitizePostgresJsonString(value);
44
+ }
45
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
46
+ return value;
47
+ }
48
+
49
+ const keys = Object.keys(value);
50
+ if (!keys.some((key) => sanitizePostgresJsonString(key) !== key)) {
51
+ return value;
52
+ }
53
+ const sanitizedEntries: Array<[string, unknown]> = [];
54
+ const sanitizedKeys = new Set<string>();
55
+ for (const key of keys) {
56
+ const sanitizedKey = sanitizePostgresJsonString(key);
57
+ if (sanitizedKeys.has(sanitizedKey)) {
58
+ throw new TypeError(
59
+ 'Postgres JSON key normalization produced a duplicate object key.',
60
+ );
61
+ }
62
+ sanitizedKeys.add(sanitizedKey);
63
+ sanitizedEntries.push([
64
+ sanitizedKey,
65
+ (value as Record<string, unknown>)[key],
66
+ ]);
67
+ }
68
+ return Object.fromEntries(sanitizedEntries);
7
69
  }
8
70
 
9
71
  export function sanitizePostgresJsonValue<T>(value: T): T {
@@ -12,6 +12,7 @@ import type { PreloadedRuntimeDbSession } from './db-session';
12
12
  import type { PacingRule } from './governor/rate-state-backend';
13
13
  import type { GovernanceSnapshot } from './governor/governor';
14
14
  import type { PlayRunInputPayload } from './play-input';
15
+ import type { PlayRunFailureDetails } from './run-failure';
15
16
 
16
17
  export type PlayRunnerRateStateBackendConfig =
17
18
  | {
@@ -165,6 +166,7 @@ export interface PlayRunnerExecutionConfig {
165
166
  artifactTransport?: {
166
167
  bundledCodePath?: string | null;
167
168
  bundledCodeEncoding?: 'utf8' | 'gzip';
169
+ sourceMapPath?: string | null;
168
170
  } | null;
169
171
  input: PlayRunInputPayload;
170
172
  checkpoint?: PlayCheckpoint | null;
@@ -241,6 +243,7 @@ export type PlayRunnerResult =
241
243
  | {
242
244
  status: 'failed';
243
245
  error: string;
246
+ errors?: PlayRunFailureDetails[];
244
247
  logs: string[];
245
248
  stats: Record<string, unknown>;
246
249
  steps: PlayStep[];
@@ -6,6 +6,7 @@ const CLOUDFLARE_WORKFLOW_ENDPOINT_UNAVAILABLE_RE =
6
6
  /The requested endpoint could not be found, or you don't have access to it\. Please check the provided ID and try again\./i;
7
7
  const VERCEL_RUNTIME_API_DEPLOYMENT_MISSING_RE =
8
8
  /runtime API 404:[\s\S]*(?:DeploymentNotFound|DEPLOYMENT_NOT_FOUND|requested deployment .*exist|deployment could not be found on Vercel)/i;
9
+ const RUNTIME_SANDBOX_LOST_RE = /\bRUNTIME_SANDBOX_LOST\b/i;
9
10
 
10
11
  export const PLATFORM_DEPLOY_INTERRUPTED_MESSAGE =
11
12
  'Run interrupted by a platform deploy. Deepline retries this automatically when possible; if this error is still visible, re-run the same command.';
@@ -16,6 +17,8 @@ export const PLATFORM_WORKFLOW_ENDPOINT_INTERRUPTED_MESSAGE =
16
17
 
17
18
  export const INTERNAL_RUNTIME_STORAGE_ERROR_MESSAGE =
18
19
  'Internal play runtime storage failed. Please retry the run; if this keeps happening, contact Deepline support with the run ID.';
20
+ export const RUNTIME_SANDBOX_LOST_MESSAGE =
21
+ 'The execution sandbox became unreachable before returning a terminal result. Re-run the same command; if this keeps happening, contact Deepline support with the run ID.';
19
22
 
20
23
  export const WORKSPACE_STORAGE_NOT_READY_CODE = 'WORKSPACE_STORAGE_NOT_READY';
21
24
 
@@ -90,11 +93,7 @@ export class ProviderExhaustedError extends Error {
90
93
  readonly provider: string;
91
94
  /** ISO 8601 retry deadline (from the server Retry-After / cooldown). */
92
95
  readonly retryAt: string;
93
- constructor(input: {
94
- provider: string;
95
- retryAtMs: number;
96
- cause?: unknown;
97
- }) {
96
+ constructor(input: { provider: string; retryAtMs: number; cause?: unknown }) {
98
97
  // Prefix the code token so `String(error)` and any persisted error string
99
98
  // carry it across the serialization boundary back to the CLI, exactly like
100
99
  // WorkspaceStorageNotReadyError.
@@ -124,8 +123,7 @@ export class ProviderExhaustedError extends Error {
124
123
  */
125
124
  export function providerNameFromBucketId(bucketId: string): string {
126
125
  const separator = bucketId.indexOf(':');
127
- const provider =
128
- separator >= 0 ? bucketId.slice(separator + 1) : bucketId;
126
+ const provider = separator >= 0 ? bucketId.slice(separator + 1) : bucketId;
129
127
  return provider.startsWith('tool:')
130
128
  ? provider.slice('tool:'.length)
131
129
  : provider;
@@ -137,8 +135,66 @@ export type PlayRunFailureDetails = {
137
135
  message: string;
138
136
  retryable: boolean | null;
139
137
  cause?: string;
138
+ name?: string;
139
+ stack?: string;
140
+ causes?: string[];
140
141
  };
141
142
 
143
+ const PUBLIC_FAILURE_STACK_LINE_LIMIT = 12;
144
+ const PUBLIC_FAILURE_TEXT_BYTE_LIMIT = 64 * 1024;
145
+ const PUBLIC_FAILURE_CAUSE_LIMIT = 8;
146
+ const PUBLIC_FAILURE_CAUSE_LENGTH_LIMIT = 1_000;
147
+
148
+ function boundedFailureText(value: string): string {
149
+ const encoder = new TextEncoder();
150
+ if (encoder.encode(value).byteLength <= PUBLIC_FAILURE_TEXT_BYTE_LIMIT) {
151
+ return value;
152
+ }
153
+ let bytes = 0;
154
+ let output = '';
155
+ for (const character of value) {
156
+ const characterBytes = encoder.encode(character).byteLength;
157
+ if (bytes + characterBytes > PUBLIC_FAILURE_TEXT_BYTE_LIMIT) break;
158
+ output += character;
159
+ bytes += characterBytes;
160
+ }
161
+ return `${output}\n[truncated]`;
162
+ }
163
+
164
+ function boundedFailureStack(error: Error): string | undefined {
165
+ if (typeof error.stack !== 'string' || !error.stack.trim()) return undefined;
166
+ return boundedFailureText(
167
+ error.stack.split('\n').slice(0, PUBLIC_FAILURE_STACK_LINE_LIMIT).join('\n'),
168
+ );
169
+ }
170
+
171
+ function failureCauseTexts(error: Error): string[] {
172
+ const queue: unknown[] = [];
173
+ const seen = new Set<unknown>([error]);
174
+ const causes: string[] = [];
175
+ const withCause = error as Error & { cause?: unknown };
176
+ if (withCause.cause !== undefined) queue.push(withCause.cause);
177
+ if (error instanceof AggregateError) queue.push(...error.errors);
178
+
179
+ while (queue.length > 0 && causes.length < PUBLIC_FAILURE_CAUSE_LIMIT) {
180
+ const current = queue.shift();
181
+ if (current === undefined || current === null || seen.has(current)) {
182
+ continue;
183
+ }
184
+ seen.add(current);
185
+ const text = toErrorText(current).trim();
186
+ if (text && text !== error.message && !causes.includes(text)) {
187
+ causes.push(text.slice(0, PUBLIC_FAILURE_CAUSE_LENGTH_LIMIT));
188
+ }
189
+ if (current instanceof Error) {
190
+ const nested = current as Error & { cause?: unknown };
191
+ if (nested.cause !== undefined) queue.push(nested.cause);
192
+ if (current instanceof AggregateError) queue.push(...current.errors);
193
+ }
194
+ }
195
+ return causes;
196
+ }
197
+
142
198
  function toErrorText(error: unknown): string {
143
199
  if (error instanceof Error) {
144
200
  return error.message;
@@ -147,8 +203,23 @@ function toErrorText(error: unknown): string {
147
203
  }
148
204
 
149
205
  export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
150
- const cause = toErrorText(error);
151
- if (CLOUDFLARE_DURABLE_OBJECT_RESET_RE.test(cause)) {
206
+ const rawCause = toErrorText(error);
207
+ const cause = boundedFailureText(rawCause);
208
+ if (RUNTIME_SANDBOX_LOST_RE.test(rawCause)) {
209
+ const stack = error instanceof Error ? boundedFailureStack(error) : null;
210
+ const causes = error instanceof Error ? failureCauseTexts(error) : [];
211
+ return {
212
+ code: 'RUNTIME_SANDBOX_LOST',
213
+ phase: 'infrastructure',
214
+ message: RUNTIME_SANDBOX_LOST_MESSAGE,
215
+ retryable: true,
216
+ cause,
217
+ ...(error instanceof Error ? { name: error.name || 'Error' } : {}),
218
+ ...(stack ? { stack } : {}),
219
+ ...(causes.length > 0 ? { causes } : {}),
220
+ };
221
+ }
222
+ if (CLOUDFLARE_DURABLE_OBJECT_RESET_RE.test(rawCause)) {
152
223
  return {
153
224
  code: 'PLATFORM_DEPLOY_INTERRUPTED',
154
225
  phase: 'runtime',
@@ -157,7 +228,7 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
157
228
  cause,
158
229
  };
159
230
  }
160
- if (VERCEL_RUNTIME_API_DEPLOYMENT_MISSING_RE.test(cause)) {
231
+ if (VERCEL_RUNTIME_API_DEPLOYMENT_MISSING_RE.test(rawCause)) {
161
232
  return {
162
233
  code: 'PLATFORM_DEPLOY_INTERRUPTED',
163
234
  phase: 'runtime',
@@ -166,7 +237,7 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
166
237
  cause,
167
238
  };
168
239
  }
169
- if (CLOUDFLARE_WORKER_SUBREQUEST_LIMIT_RE.test(cause)) {
240
+ if (CLOUDFLARE_WORKER_SUBREQUEST_LIMIT_RE.test(rawCause)) {
170
241
  return {
171
242
  code: 'PLATFORM_WORKER_SUBREQUEST_INTERRUPTED',
172
243
  phase: 'runtime',
@@ -175,7 +246,7 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
175
246
  cause,
176
247
  };
177
248
  }
178
- if (CLOUDFLARE_WORKFLOW_ENDPOINT_UNAVAILABLE_RE.test(cause)) {
249
+ if (CLOUDFLARE_WORKFLOW_ENDPOINT_UNAVAILABLE_RE.test(rawCause)) {
179
250
  return {
180
251
  code: 'PLATFORM_WORKFLOW_ENDPOINT_INTERRUPTED',
181
252
  phase: 'runtime',
@@ -184,7 +255,7 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
184
255
  cause,
185
256
  };
186
257
  }
187
- const playDepthBudgetMatch = cause.match(
258
+ const playDepthBudgetMatch = rawCause.match(
188
259
  /Play execution playDepth budget exceeded \((\d+)\/(\d+)\)\./,
189
260
  );
190
261
  if (playDepthBudgetMatch) {
@@ -198,7 +269,7 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
198
269
  }
199
270
  if (
200
271
  error instanceof ProviderExhaustedError ||
201
- /\bPROVIDER_EXHAUSTED\b/.test(cause)
272
+ /\bPROVIDER_EXHAUSTED\b/.test(rawCause)
202
273
  ) {
203
274
  // The human message (provider + retry time) already lives in `cause` after
204
275
  // the code token prefix; surface it verbatim minus the token so the run
@@ -218,7 +289,7 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
218
289
  }
219
290
  if (
220
291
  error instanceof WorkspaceStorageNotReadyError ||
221
- /\bWORKSPACE_STORAGE_NOT_READY\b/.test(cause)
292
+ /\bWORKSPACE_STORAGE_NOT_READY\b/.test(rawCause)
222
293
  ) {
223
294
  return {
224
295
  code: WORKSPACE_STORAGE_NOT_READY_CODE,
@@ -242,10 +313,27 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
242
313
  retryable: true,
243
314
  };
244
315
  }
316
+ const diagnostics =
317
+ error instanceof Error
318
+ ? {
319
+ name: error.name || 'Error',
320
+ stack: boundedFailureStack(error),
321
+ causes: failureCauseTexts(error),
322
+ }
323
+ : null;
245
324
  return {
246
325
  code: 'RUN_FAILED',
247
326
  phase: 'runtime',
248
327
  message: cause,
249
328
  retryable: null,
329
+ ...(diagnostics
330
+ ? {
331
+ name: diagnostics.name,
332
+ ...(diagnostics.stack ? { stack: diagnostics.stack } : {}),
333
+ ...(diagnostics.causes.length > 0
334
+ ? { causes: diagnostics.causes }
335
+ : {}),
336
+ }
337
+ : {}),
250
338
  };
251
339
  }
@@ -78,6 +78,7 @@ export type PlayRunLedgerStepSnapshot = {
78
78
  label?: string;
79
79
  kind?: string;
80
80
  status: PlayRunLedgerStepStatus;
81
+ error?: string | null;
81
82
  artifactTableNamespace?: string | null;
82
83
  startedAt?: number | null;
83
84
  completedAt?: number | null;
@@ -480,6 +481,7 @@ export function normalizePlayRunLedgerSnapshot(
480
481
  label: optionalString(rawStep.label),
481
482
  kind: optionalString(rawStep.kind),
482
483
  status,
484
+ error: optionalNullableString(rawStep.error),
483
485
  artifactTableNamespace: optionalNullableString(
484
486
  rawStep.artifactTableNamespace,
485
487
  ),
@@ -1255,6 +1257,7 @@ export function reducePlayRunLedgerEvent(
1255
1257
  label: event.label ?? current?.label,
1256
1258
  kind: event.kind ?? current?.kind,
1257
1259
  status,
1260
+ error: current?.error ?? null,
1258
1261
  artifactTableNamespace:
1259
1262
  progress?.artifactTableNamespace ??
1260
1263
  current?.artifactTableNamespace ??
@@ -1308,6 +1311,12 @@ export function reducePlayRunLedgerEvent(
1308
1311
  label: event.label ?? current?.label,
1309
1312
  kind: event.kind ?? current?.kind,
1310
1313
  status,
1314
+ error:
1315
+ event.type === 'step.failed'
1316
+ ? (event.error ?? current?.error ?? null)
1317
+ : preserveFailedStatus
1318
+ ? (current?.error ?? null)
1319
+ : null,
1311
1320
  artifactTableNamespace:
1312
1321
  event.artifactTableNamespace ??
1313
1322
  current?.artifactTableNamespace ??
@@ -46,6 +46,7 @@ export type PlayRunStreamNodeProgress = {
46
46
  export type PlayRunStreamNodeState = {
47
47
  nodeId: string;
48
48
  status: 'idle' | 'running' | 'completed' | 'failed' | 'skipped';
49
+ error?: string | null;
49
50
  artifactTableNamespace?: string | null;
50
51
  progress?: PlayRunStreamNodeProgress | null;
51
52
  startedAt?: number | null;
@@ -167,6 +168,7 @@ function buildSnapshotFromLedger(
167
168
  .map((step) => ({
168
169
  nodeId: step.stepId,
169
170
  status: step.status,
171
+ error: step.error ?? null,
170
172
  artifactTableNamespace: step.artifactTableNamespace ?? null,
171
173
  progress: step.progress
172
174
  ? {
@@ -19,6 +19,7 @@ type DaytonaPayloadEnvelope = {
19
19
  schemaVersion: 1;
20
20
  runnerCode: string;
21
21
  artifactBundledCode: string;
22
+ artifactSourceMap: string;
22
23
  config: PlayRunnerExecutionConfig;
23
24
  /** Crash-containment epilogue script (see buildDaytonaCrashTerminalPusherSource). */
24
25
  crashPusherCode: string;
@@ -50,10 +51,11 @@ function nodeMaterializePayloadCommand(input: {
50
51
  runnerPath: string;
51
52
  configPath: string;
52
53
  artifactCodePath: string;
54
+ artifactSourceMapPath: string;
53
55
  crashPusherPath: string;
54
56
  }): string {
55
57
  const script =
56
- "const fs=require('node:fs');const zlib=require('node:zlib');const p=JSON.parse(zlib.gunzipSync(fs.readFileSync(process.argv[1])));if(p.schemaVersion!==1)throw new Error('Unsupported Daytona payload envelope');fs.writeFileSync(process.argv[2],p.runnerCode);fs.writeFileSync(process.argv[3],JSON.stringify(p.config));fs.writeFileSync(process.argv[4],p.artifactBundledCode);fs.writeFileSync(process.argv[5],p.crashPusherCode);";
58
+ "const fs=require('node:fs');const zlib=require('node:zlib');const p=JSON.parse(zlib.gunzipSync(fs.readFileSync(process.argv[1])));if(p.schemaVersion!==1)throw new Error('Unsupported Daytona payload envelope');fs.writeFileSync(process.argv[2],p.runnerCode);fs.writeFileSync(process.argv[3],JSON.stringify(p.config));fs.writeFileSync(process.argv[4],p.artifactBundledCode);fs.writeFileSync(process.argv[5],p.artifactSourceMap);fs.writeFileSync(process.argv[6],p.crashPusherCode);";
57
59
  return [
58
60
  'node',
59
61
  '-e',
@@ -62,6 +64,7 @@ function nodeMaterializePayloadCommand(input: {
62
64
  shellQuote(input.runnerPath),
63
65
  shellQuote(input.configPath),
64
66
  shellQuote(input.artifactCodePath),
67
+ shellQuote(input.artifactSourceMapPath),
65
68
  shellQuote(input.crashPusherPath),
66
69
  ].join(' ');
67
70
  }
@@ -236,6 +239,7 @@ export async function stageDaytonaRunnerPayload(input: {
236
239
  const envelopePath = `${workDir}/deepline-play-payload-${randomUUID()}.json.gz`;
237
240
  const configPath = `${workDir}/deepline-play-config-${randomUUID()}.json`;
238
241
  const artifactCodePath = `${workDir}/deepline-play-artifact-${randomUUID()}.cjs`;
242
+ const artifactSourceMapPath = `${artifactCodePath}.map`;
239
243
  const crashPusherPath = `${workDir}/deepline-play-crash-terminal-${randomUUID()}.cjs`;
240
244
 
241
245
  const remoteMaterializedFiles: Record<string, string> = {};
@@ -306,6 +310,7 @@ export async function stageDaytonaRunnerPayload(input: {
306
310
  artifactTransport: {
307
311
  bundledCodePath: artifactCodePath,
308
312
  bundledCodeEncoding: 'utf8',
313
+ sourceMapPath: artifactSourceMapPath,
309
314
  },
310
315
  workspaceRoot: input.workDir,
311
316
  csvSourcePath: hasInlineCsv
@@ -330,6 +335,7 @@ export async function stageDaytonaRunnerPayload(input: {
330
335
  schemaVersion: 1,
331
336
  runnerCode: bundle,
332
337
  artifactBundledCode: compactedArtifact.bundledCode,
338
+ artifactSourceMap: input.config.artifact.sourceMap,
333
339
  config: remoteConfig,
334
340
  crashPusherCode: buildDaytonaCrashTerminalPusherSource(),
335
341
  }),
@@ -355,6 +361,7 @@ export async function stageDaytonaRunnerPayload(input: {
355
361
  runnerPath,
356
362
  configPath,
357
363
  artifactCodePath,
364
+ artifactSourceMapPath,
358
365
  crashPusherPath,
359
366
  })} && DEEPLINE_PLAY_RUNNER_READY_PATH=${shellQuote(readyPath)} DEEPLINE_PLAY_RUNNER_PROGRESS_EVENT_PATH=${shellQuote(progressEventPath)} DEEPLINE_PLAY_RUNNER_STDOUT_EVENT_MODE=compact ${runnerTraceEnv}node ${shellQuote(runnerPath)} ${shellQuote(configPath)}`;
360
367
  // Crash-containment epilogue: runs UNCONDITIONALLY after the runner exits and
@@ -80,6 +80,7 @@ import {
80
80
  import type { MapRowOutcome } from './durability-store';
81
81
  import {
82
82
  normalizeRuntimeMapInputIndex,
83
+ prepareRuntimeSheetRowsForJsonTransport,
83
84
  prepareRuntimeSheetRowTransitions,
84
85
  type RuntimePreparedCompletedRow,
85
86
  type RuntimePreparedFailedRow,
@@ -614,10 +615,22 @@ async function postRuntimeApi<TResponse>(
614
615
  typeof parsed?.error === 'string'
615
616
  ? parsed.error
616
617
  : `Runtime API request failed with status ${response.status}.`;
618
+ const errorCode = typeof parsed?.code === 'string' ? parsed.code : null;
619
+ const errorAction =
620
+ typeof parsed?.action === 'string' ? parsed.action : body.action;
621
+ const requestId = response.headers.get('x-deepline-request-id');
622
+ const diagnosticContext = [
623
+ `status ${response.status}`,
624
+ errorCode ? `code=${errorCode}` : null,
625
+ errorAction ? `action=${errorAction}` : null,
626
+ requestId ? `request_id=${requestId}` : null,
627
+ ]
628
+ .filter((value): value is string => Boolean(value))
629
+ .join(', ');
617
630
  throw new Error(
618
631
  details
619
- ? `${errorMessage} (status ${response.status}): ${details}`
620
- : `${errorMessage} (status ${response.status}).`,
632
+ ? `${errorMessage} (${diagnosticContext}): ${details}`
633
+ : `${errorMessage} (${diagnosticContext}).`,
621
634
  );
622
635
  }
623
636
 
@@ -6151,12 +6164,8 @@ async function completeRuntimeMapRowChunks(
6151
6164
  for (const chunk of input.chunks) {
6152
6165
  const chunkKeys = chunk.map((row) => row.key);
6153
6166
  const chunkInputIndexes = chunk.map((row) => row.input_index);
6154
- const chunkDataPatches = chunk.map((row) =>
6155
- stringifyPostgresJson(row.data_patch),
6156
- );
6157
- const chunkCellMetaPatches = chunk.map((row) =>
6158
- stringifyPostgresJson(row.cell_meta_patch),
6159
- );
6167
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6168
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6160
6169
  const targetChangedPatchedCellSql = changedPatchedCellSql(
6161
6170
  'target',
6162
6171
  'updates.data_patch',
@@ -6418,12 +6427,8 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
6418
6427
  for (const chunk of input.chunks) {
6419
6428
  const chunkKeys = chunk.map((row) => row.key);
6420
6429
  const chunkInputIndexes = chunk.map((row) => row.input_index);
6421
- const chunkDataPatches = chunk.map((row) =>
6422
- stringifyPostgresJson(row.data_patch),
6423
- );
6424
- const chunkCellMetaPatches = chunk.map((row) =>
6425
- stringifyPostgresJson(row.cell_meta_patch),
6426
- );
6430
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6431
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6427
6432
  const targetChangedPatchedCellSql = changedPatchedCellSql(
6428
6433
  'target',
6429
6434
  'updates.data_patch',
@@ -6661,12 +6666,8 @@ async function insertMissingCompletedMapRowChunks(
6661
6666
  for (const chunk of input.chunks) {
6662
6667
  const chunkKeys = chunk.map((row) => row.key);
6663
6668
  const chunkInputIndexes = chunk.map((row) => row.input_index);
6664
- const chunkDataPatches = chunk.map((row) =>
6665
- stringifyPostgresJson(row.data_patch),
6666
- );
6667
- const chunkCellMetaPatches = chunk.map((row) =>
6668
- stringifyPostgresJson(row.cell_meta_patch),
6669
- );
6669
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6670
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6670
6671
  const { rows } = await client.query<{ inserted: number }>(
6671
6672
  `WITH input_rows AS (
6672
6673
  SELECT key_values._key,
@@ -6833,12 +6834,8 @@ async function failRuntimeMapRowChunks(
6833
6834
  for (const chunk of input.chunks) {
6834
6835
  const chunkKeys = chunk.map((row) => row.key);
6835
6836
  const chunkInputIndexes = chunk.map((row) => row.input_index);
6836
- const chunkDataPatches = chunk.map((row) =>
6837
- stringifyPostgresJson(row.data_patch),
6838
- );
6839
- const chunkCellMetaPatches = chunk.map((row) =>
6840
- stringifyPostgresJson(row.cell_meta_patch),
6841
- );
6837
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6838
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6842
6839
  const chunkErrors = chunk.map((row) => row.error);
6843
6840
  // Per-field failed-cell counts for the column summary, computed from the
6844
6841
  // cell meta patches (a failed row records exactly which cell failed).
@@ -7064,9 +7061,21 @@ export async function completeRuntimeMapRows(
7064
7061
  },
7065
7062
  ): Promise<RuntimeMapRowsWriteResult> {
7066
7063
  if (context.dbSessionStrategy === 'gateway_only') {
7064
+ const keyedRows = input.rows.map((row) => {
7065
+ if (row.key) return row;
7066
+ const key = resolveMapRowOutcomeKey(
7067
+ row as unknown as Record<string, unknown>,
7068
+ );
7069
+ return key ? { ...row, key } : row;
7070
+ });
7071
+ const rows = prepareRuntimeSheetRowsForJsonTransport({
7072
+ rows: keyedRows,
7073
+ runId: input.runId,
7074
+ outputFields: input.outputFields ?? [],
7075
+ });
7067
7076
  return await postRuntimeApi<RuntimeMapRowsWriteResult>(context, {
7068
7077
  action: 'runtime_sheet_complete_map_rows',
7069
- input,
7078
+ input: { ...input, rows },
7070
7079
  });
7071
7080
  }
7072
7081
  if (input.rows.length === 0) {
@@ -1,10 +1,22 @@
1
1
  import type { MapRowOutcome } from './durability-store';
2
+ import { stringifyPostgresJson } from './postgres-json';
3
+
4
+ export const RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE =
5
+ 'RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE';
6
+
7
+ const RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE_MESSAGE =
8
+ `${RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE}: Row output is not JSON-serializable. ` +
9
+ 'Remove BigInt values, circular references, and throwing toJSON implementations.';
2
10
 
3
11
  export type RuntimePreparedCompletedRow = {
4
12
  key: string;
5
13
  input_index: number | null;
6
14
  data_patch: Record<string, unknown>;
7
15
  cell_meta_patch: Record<string, unknown>;
16
+ /** Serialized once at the row boundary so SQL batching cannot re-run toJSON. */
17
+ data_patch_json: string;
18
+ /** Serialized once at the row boundary so SQL batching cannot re-run toJSON. */
19
+ cell_meta_patch_json: string;
8
20
  };
9
21
 
10
22
  export type RuntimePreparedFailedRow = RuntimePreparedCompletedRow & {
@@ -50,6 +62,51 @@ export function completedRuntimeCellMetaPatch(input: {
50
62
  return patch;
51
63
  }
52
64
 
65
+ function serializeRuntimeRowPatch(value: Record<string, unknown>): string {
66
+ const serialized = stringifyPostgresJson(value);
67
+ if (serialized === undefined) {
68
+ throw new TypeError('JSON.stringify returned undefined.');
69
+ }
70
+ return serialized;
71
+ }
72
+
73
+ function unserializableRuntimeRowFailure(input: {
74
+ key: string;
75
+ inputIndex: number | null;
76
+ runId: string;
77
+ outputFields: readonly string[];
78
+ }): RuntimePreparedFailedRow {
79
+ let cellMetaPatch = Object.fromEntries(
80
+ input.outputFields.map((field) => [
81
+ field,
82
+ {
83
+ status: 'failed',
84
+ runId: input.runId,
85
+ error: RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE_MESSAGE,
86
+ },
87
+ ]),
88
+ );
89
+ let cellMetaPatchJson: string;
90
+ try {
91
+ cellMetaPatchJson = serializeRuntimeRowPatch(cellMetaPatch);
92
+ } catch {
93
+ // Authored output field names can themselves collide after PostgreSQL JSON
94
+ // normalization. Preserve the explicit row failure instead of allowing
95
+ // diagnostic metadata to turn it back into a batch-fatal exception.
96
+ cellMetaPatch = {};
97
+ cellMetaPatchJson = '{}';
98
+ }
99
+ return {
100
+ key: input.key,
101
+ input_index: input.inputIndex,
102
+ data_patch: {},
103
+ cell_meta_patch: cellMetaPatch,
104
+ data_patch_json: '{}',
105
+ cell_meta_patch_json: cellMetaPatchJson,
106
+ error: RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE_MESSAGE,
107
+ };
108
+ }
109
+
53
110
  export function prepareRuntimeSheetRowTransitions(input: {
54
111
  rows: Iterable<MapRowOutcome>;
55
112
  runId: string;
@@ -62,12 +119,40 @@ export function prepareRuntimeSheetRowTransitions(input: {
62
119
  const failedRows: RuntimePreparedFailedRow[] = [];
63
120
  for (const row of input.rows) {
64
121
  if (!row.key) continue;
122
+ const inputIndex = normalizeRuntimeMapInputIndex(row.inputIndex);
123
+ let cellMetaPatch: Record<string, unknown>;
124
+ let dataPatchJson: string;
125
+ let cellMetaPatchJson: string;
126
+ try {
127
+ cellMetaPatch =
128
+ row.status === 'failed'
129
+ ? (row.cellMetaPatch ?? {})
130
+ : completedRuntimeCellMetaPatch({
131
+ runId: input.runId,
132
+ outputFields: input.outputFields,
133
+ rowPatch: row.cellMetaPatch,
134
+ });
135
+ dataPatchJson = serializeRuntimeRowPatch(row.data);
136
+ cellMetaPatchJson = serializeRuntimeRowPatch(cellMetaPatch);
137
+ } catch {
138
+ failedRows.push(
139
+ unserializableRuntimeRowFailure({
140
+ key: row.key,
141
+ inputIndex,
142
+ runId: input.runId,
143
+ outputFields: input.outputFields,
144
+ }),
145
+ );
146
+ continue;
147
+ }
65
148
  if (row.status === 'failed') {
66
149
  failedRows.push({
67
150
  key: row.key,
68
- input_index: normalizeRuntimeMapInputIndex(row.inputIndex),
151
+ input_index: inputIndex,
69
152
  data_patch: row.data,
70
- cell_meta_patch: row.cellMetaPatch ?? {},
153
+ cell_meta_patch: cellMetaPatch,
154
+ data_patch_json: dataPatchJson,
155
+ cell_meta_patch_json: cellMetaPatchJson,
71
156
  error:
72
157
  typeof row.error === 'string' && row.error.trim()
73
158
  ? row.error
@@ -77,14 +162,47 @@ export function prepareRuntimeSheetRowTransitions(input: {
77
162
  }
78
163
  completedRows.push({
79
164
  key: row.key,
80
- input_index: normalizeRuntimeMapInputIndex(row.inputIndex),
165
+ input_index: inputIndex,
81
166
  data_patch: row.data,
82
- cell_meta_patch: completedRuntimeCellMetaPatch({
83
- runId: input.runId,
84
- outputFields: input.outputFields,
85
- rowPatch: row.cellMetaPatch,
86
- }),
167
+ cell_meta_patch: cellMetaPatch,
168
+ data_patch_json: dataPatchJson,
169
+ cell_meta_patch_json: cellMetaPatchJson,
87
170
  });
88
171
  }
89
172
  return { completedRows, failedRows };
90
173
  }
174
+
175
+ /**
176
+ * Apply the row serialization boundary before a gateway-only runtime request.
177
+ * This prevents JSON.stringify on the transport envelope from turning one
178
+ * malformed row into a batch-fatal error before the gateway can persist the
179
+ * healthy siblings.
180
+ */
181
+ export function prepareRuntimeSheetRowsForJsonTransport(input: {
182
+ rows: Iterable<MapRowOutcome>;
183
+ runId: string;
184
+ outputFields: readonly string[];
185
+ }): MapRowOutcome[] {
186
+ const rows: MapRowOutcome[] = [];
187
+ for (const row of input.rows) {
188
+ const { completedRows, failedRows } = prepareRuntimeSheetRowTransitions({
189
+ rows: [row],
190
+ runId: input.runId,
191
+ outputFields: input.outputFields,
192
+ });
193
+ const prepared = completedRows[0] ?? failedRows[0];
194
+ if (!prepared) continue;
195
+ const failed = failedRows[0];
196
+ rows.push({
197
+ key: prepared.key,
198
+ inputIndex: prepared.input_index,
199
+ data: JSON.parse(prepared.data_patch_json) as Record<string, unknown>,
200
+ cellMetaPatch: JSON.parse(prepared.cell_meta_patch_json) as Record<
201
+ string,
202
+ unknown
203
+ >,
204
+ ...(failed ? { status: 'failed' as const, error: failed.error } : {}),
205
+ });
206
+ }
207
+ return rows;
208
+ }
@@ -13,6 +13,7 @@ export type PlayStepLifecycleEvent = {
13
13
  type: string;
14
14
  transition: 'started' | 'completed' | 'failed';
15
15
  at: number;
16
+ error?: string;
16
17
  };
17
18
 
18
19
  function isAutoStartedSetupNode(type: string): boolean {
@@ -191,7 +192,7 @@ export class PlayStepLifecycleTracker {
191
192
  }
192
193
  }
193
194
 
194
- markStartedFailed(at = this.now()): void {
195
+ markStartedFailed(at = this.now(), error?: string): void {
195
196
  for (let index = this.nodes.length - 1; index >= 0; index -= 1) {
196
197
  const node = this.nodes[index]!;
197
198
  const existing = this.getProgress()[node.nodeId];
@@ -202,6 +203,7 @@ export class PlayStepLifecycleTracker {
202
203
  type: node.type,
203
204
  transition: 'failed',
204
205
  at,
206
+ ...(error?.trim() ? { error: error.trim() } : {}),
205
207
  });
206
208
  return;
207
209
  }
package/dist/cli/index.js CHANGED
@@ -625,10 +625,10 @@ var SDK_RELEASE = {
625
625
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
626
626
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
627
627
  // automatically without blocking their current command.
628
- version: "0.1.224",
628
+ version: "0.1.226",
629
629
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
630
630
  supportPolicy: {
631
- latest: "0.1.224",
631
+ latest: "0.1.226",
632
632
  minimumSupported: "0.1.53",
633
633
  deprecatedBelow: "0.1.219",
634
634
  commandMinimumSupported: [
@@ -1348,6 +1348,9 @@ function resolveTimingWindow(input2) {
1348
1348
  };
1349
1349
  }
1350
1350
 
1351
+ // ../shared_libs/play-runtime/run-failure.ts
1352
+ var PUBLIC_FAILURE_TEXT_BYTE_LIMIT = 64 * 1024;
1353
+
1351
1354
  // ../shared_libs/play-runtime/run-terminal-source.ts
1352
1355
  var PLAY_RUN_LEDGER_EVENT_SOURCES = [
1353
1356
  "worker",
@@ -1576,6 +1579,7 @@ function normalizePlayRunLedgerSnapshot(value, fallback) {
1576
1579
  label: optionalString(rawStep.label),
1577
1580
  kind: optionalString(rawStep.kind),
1578
1581
  status,
1582
+ error: optionalNullableString(rawStep.error),
1579
1583
  artifactTableNamespace: optionalNullableString(
1580
1584
  rawStep.artifactTableNamespace
1581
1585
  ),
@@ -1752,6 +1756,7 @@ function buildSnapshotFromLedger(snapshot) {
1752
1756
  const nodeStates = snapshot.orderedStepIds.map((stepId) => snapshot.stepsById[stepId]).filter((step) => Boolean(step)).map((step) => ({
1753
1757
  nodeId: step.stepId,
1754
1758
  status: step.status,
1759
+ error: step.error ?? null,
1755
1760
  artifactTableNamespace: step.artifactTableNamespace ?? null,
1756
1761
  progress: step.progress ? {
1757
1762
  completed: step.progress.completed,
@@ -13758,6 +13763,13 @@ function formatPackageValueOutputLines(packaged) {
13758
13763
  }
13759
13764
  return lines;
13760
13765
  }
13766
+ function authoredPackageStackFrames(packaged) {
13767
+ const stack = readRecordArray(packaged.errors).map((entry) => typeof entry.stack === "string" ? entry.stack : null).find((candidate) => Boolean(candidate?.trim()));
13768
+ if (!stack) return [];
13769
+ return stack.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("at ")).filter(
13770
+ (line) => !line.includes("/virtual/deepline-plays/") && !line.includes("/apps/play-runner/") && !line.includes("/shared_libs/play-runtime/") && !line.includes("node:internal")
13771
+ ).slice(0, 5);
13772
+ }
13761
13773
  function buildRunPackageTextLines(packaged) {
13762
13774
  const run = readRunPackageRun(packaged);
13763
13775
  const runId = typeof run.id === "string" ? run.id : "unknown";
@@ -13769,6 +13781,11 @@ function buildRunPackageTextLines(packaged) {
13769
13781
  const runError = typeof run.error === "string" && run.error.trim() ? run.error.trim() : null;
13770
13782
  if (runError && (status === "failed" || status === "cancelled")) {
13771
13783
  lines.push(` error: ${truncateErrorForDisplay(runError, runId)}`);
13784
+ const stackFrames = authoredPackageStackFrames(packaged);
13785
+ if (stackFrames.length > 0) {
13786
+ lines.push(" stack:");
13787
+ lines.push(...stackFrames.map((frame) => ` ${frame}`));
13788
+ }
13772
13789
  }
13773
13790
  const failedLogs = readRecord(packaged.failedLogs);
13774
13791
  const failedLogEntries = Array.isArray(failedLogs?.entries) ? failedLogs.entries.filter(
@@ -17820,12 +17837,12 @@ function normalizeAlias(value) {
17820
17837
  function renderExecuteStep(command, options = {
17821
17838
  force: false
17822
17839
  }) {
17823
- if (command.play) {
17824
- return renderPlayStep(command, options);
17825
- }
17826
17840
  if (command.tool === "run_javascript" && options.inlineRunJavascript && canInlineRunJavascript(command)) {
17827
17841
  return renderInlineJavascriptStep(command, options);
17828
17842
  }
17843
+ if (command.play) {
17844
+ return renderPlayStep(command, options);
17845
+ }
17829
17846
  if (options.idiomaticGetters) {
17830
17847
  return renderIdiomaticExecuteStep(command, options);
17831
17848
  }
@@ -610,10 +610,10 @@ var SDK_RELEASE = {
610
610
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
611
611
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
612
612
  // automatically without blocking their current command.
613
- version: "0.1.224",
613
+ version: "0.1.226",
614
614
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
615
615
  supportPolicy: {
616
- latest: "0.1.224",
616
+ latest: "0.1.226",
617
617
  minimumSupported: "0.1.53",
618
618
  deprecatedBelow: "0.1.219",
619
619
  commandMinimumSupported: [
@@ -1333,6 +1333,9 @@ function resolveTimingWindow(input2) {
1333
1333
  };
1334
1334
  }
1335
1335
 
1336
+ // ../shared_libs/play-runtime/run-failure.ts
1337
+ var PUBLIC_FAILURE_TEXT_BYTE_LIMIT = 64 * 1024;
1338
+
1336
1339
  // ../shared_libs/play-runtime/run-terminal-source.ts
1337
1340
  var PLAY_RUN_LEDGER_EVENT_SOURCES = [
1338
1341
  "worker",
@@ -1561,6 +1564,7 @@ function normalizePlayRunLedgerSnapshot(value, fallback) {
1561
1564
  label: optionalString(rawStep.label),
1562
1565
  kind: optionalString(rawStep.kind),
1563
1566
  status,
1567
+ error: optionalNullableString(rawStep.error),
1564
1568
  artifactTableNamespace: optionalNullableString(
1565
1569
  rawStep.artifactTableNamespace
1566
1570
  ),
@@ -1737,6 +1741,7 @@ function buildSnapshotFromLedger(snapshot) {
1737
1741
  const nodeStates = snapshot.orderedStepIds.map((stepId) => snapshot.stepsById[stepId]).filter((step) => Boolean(step)).map((step) => ({
1738
1742
  nodeId: step.stepId,
1739
1743
  status: step.status,
1744
+ error: step.error ?? null,
1740
1745
  artifactTableNamespace: step.artifactTableNamespace ?? null,
1741
1746
  progress: step.progress ? {
1742
1747
  completed: step.progress.completed,
@@ -13787,6 +13792,13 @@ function formatPackageValueOutputLines(packaged) {
13787
13792
  }
13788
13793
  return lines;
13789
13794
  }
13795
+ function authoredPackageStackFrames(packaged) {
13796
+ const stack = readRecordArray(packaged.errors).map((entry) => typeof entry.stack === "string" ? entry.stack : null).find((candidate) => Boolean(candidate?.trim()));
13797
+ if (!stack) return [];
13798
+ return stack.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("at ")).filter(
13799
+ (line) => !line.includes("/virtual/deepline-plays/") && !line.includes("/apps/play-runner/") && !line.includes("/shared_libs/play-runtime/") && !line.includes("node:internal")
13800
+ ).slice(0, 5);
13801
+ }
13790
13802
  function buildRunPackageTextLines(packaged) {
13791
13803
  const run = readRunPackageRun(packaged);
13792
13804
  const runId = typeof run.id === "string" ? run.id : "unknown";
@@ -13798,6 +13810,11 @@ function buildRunPackageTextLines(packaged) {
13798
13810
  const runError = typeof run.error === "string" && run.error.trim() ? run.error.trim() : null;
13799
13811
  if (runError && (status === "failed" || status === "cancelled")) {
13800
13812
  lines.push(` error: ${truncateErrorForDisplay(runError, runId)}`);
13813
+ const stackFrames = authoredPackageStackFrames(packaged);
13814
+ if (stackFrames.length > 0) {
13815
+ lines.push(" stack:");
13816
+ lines.push(...stackFrames.map((frame) => ` ${frame}`));
13817
+ }
13801
13818
  }
13802
13819
  const failedLogs = readRecord(packaged.failedLogs);
13803
13820
  const failedLogEntries = Array.isArray(failedLogs?.entries) ? failedLogs.entries.filter(
@@ -17849,12 +17866,12 @@ function normalizeAlias(value) {
17849
17866
  function renderExecuteStep(command, options = {
17850
17867
  force: false
17851
17868
  }) {
17852
- if (command.play) {
17853
- return renderPlayStep(command, options);
17854
- }
17855
17869
  if (command.tool === "run_javascript" && options.inlineRunJavascript && canInlineRunJavascript(command)) {
17856
17870
  return renderInlineJavascriptStep(command, options);
17857
17871
  }
17872
+ if (command.play) {
17873
+ return renderPlayStep(command, options);
17874
+ }
17858
17875
  if (options.idiomaticGetters) {
17859
17876
  return renderIdiomaticExecuteStep(command, options);
17860
17877
  }
package/dist/index.js CHANGED
@@ -424,10 +424,10 @@ var SDK_RELEASE = {
424
424
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
425
425
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
426
426
  // automatically without blocking their current command.
427
- version: "0.1.224",
427
+ version: "0.1.226",
428
428
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
429
429
  supportPolicy: {
430
- latest: "0.1.224",
430
+ latest: "0.1.226",
431
431
  minimumSupported: "0.1.53",
432
432
  deprecatedBelow: "0.1.219",
433
433
  commandMinimumSupported: [
@@ -1147,6 +1147,9 @@ function resolveTimingWindow(input) {
1147
1147
  };
1148
1148
  }
1149
1149
 
1150
+ // ../shared_libs/play-runtime/run-failure.ts
1151
+ var PUBLIC_FAILURE_TEXT_BYTE_LIMIT = 64 * 1024;
1152
+
1150
1153
  // ../shared_libs/play-runtime/run-terminal-source.ts
1151
1154
  var PLAY_RUN_LEDGER_EVENT_SOURCES = [
1152
1155
  "worker",
@@ -1375,6 +1378,7 @@ function normalizePlayRunLedgerSnapshot(value, fallback) {
1375
1378
  label: optionalString(rawStep.label),
1376
1379
  kind: optionalString(rawStep.kind),
1377
1380
  status,
1381
+ error: optionalNullableString(rawStep.error),
1378
1382
  artifactTableNamespace: optionalNullableString(
1379
1383
  rawStep.artifactTableNamespace
1380
1384
  ),
@@ -1551,6 +1555,7 @@ function buildSnapshotFromLedger(snapshot) {
1551
1555
  const nodeStates = snapshot.orderedStepIds.map((stepId) => snapshot.stepsById[stepId]).filter((step) => Boolean(step)).map((step) => ({
1552
1556
  nodeId: step.stepId,
1553
1557
  status: step.status,
1558
+ error: step.error ?? null,
1554
1559
  artifactTableNamespace: step.artifactTableNamespace ?? null,
1555
1560
  progress: step.progress ? {
1556
1561
  completed: step.progress.completed,
package/dist/index.mjs CHANGED
@@ -354,10 +354,10 @@ var SDK_RELEASE = {
354
354
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
355
355
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
356
356
  // automatically without blocking their current command.
357
- version: "0.1.224",
357
+ version: "0.1.226",
358
358
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
359
359
  supportPolicy: {
360
- latest: "0.1.224",
360
+ latest: "0.1.226",
361
361
  minimumSupported: "0.1.53",
362
362
  deprecatedBelow: "0.1.219",
363
363
  commandMinimumSupported: [
@@ -1077,6 +1077,9 @@ function resolveTimingWindow(input) {
1077
1077
  };
1078
1078
  }
1079
1079
 
1080
+ // ../shared_libs/play-runtime/run-failure.ts
1081
+ var PUBLIC_FAILURE_TEXT_BYTE_LIMIT = 64 * 1024;
1082
+
1080
1083
  // ../shared_libs/play-runtime/run-terminal-source.ts
1081
1084
  var PLAY_RUN_LEDGER_EVENT_SOURCES = [
1082
1085
  "worker",
@@ -1305,6 +1308,7 @@ function normalizePlayRunLedgerSnapshot(value, fallback) {
1305
1308
  label: optionalString(rawStep.label),
1306
1309
  kind: optionalString(rawStep.kind),
1307
1310
  status,
1311
+ error: optionalNullableString(rawStep.error),
1308
1312
  artifactTableNamespace: optionalNullableString(
1309
1313
  rawStep.artifactTableNamespace
1310
1314
  ),
@@ -1481,6 +1485,7 @@ function buildSnapshotFromLedger(snapshot) {
1481
1485
  const nodeStates = snapshot.orderedStepIds.map((stepId) => snapshot.stepsById[stepId]).filter((step) => Boolean(step)).map((step) => ({
1482
1486
  nodeId: step.stepId,
1483
1487
  status: step.status,
1488
+ error: step.error ?? null,
1484
1489
  artifactTableNamespace: step.artifactTableNamespace ?? null,
1485
1490
  progress: step.progress ? {
1486
1491
  completed: step.progress.completed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.224",
3
+ "version": "0.1.226",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {