deepline 0.1.225 → 0.1.227

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.
@@ -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,
@@ -272,6 +273,8 @@ type RuntimeSheetPreparedRowDisposition = {
272
273
  key: string;
273
274
  disposition: RuntimeSheetPrepareDisposition;
274
275
  };
276
+
277
+ type RuntimeSheetDatasetMode = 'upsert' | 'net_new';
275
278
  const dbSessionCache = new Map<string, DbSessionCacheEntry>();
276
279
  const dbSessionInFlight = new Map<string, Promise<CreateDbSessionResponse>>();
277
280
  const postgresPools = new Map<string, RuntimePool>();
@@ -614,10 +617,22 @@ async function postRuntimeApi<TResponse>(
614
617
  typeof parsed?.error === 'string'
615
618
  ? parsed.error
616
619
  : `Runtime API request failed with status ${response.status}.`;
620
+ const errorCode = typeof parsed?.code === 'string' ? parsed.code : null;
621
+ const errorAction =
622
+ typeof parsed?.action === 'string' ? parsed.action : body.action;
623
+ const requestId = response.headers.get('x-deepline-request-id');
624
+ const diagnosticContext = [
625
+ `status ${response.status}`,
626
+ errorCode ? `code=${errorCode}` : null,
627
+ errorAction ? `action=${errorAction}` : null,
628
+ requestId ? `request_id=${requestId}` : null,
629
+ ]
630
+ .filter((value): value is string => Boolean(value))
631
+ .join(', ');
617
632
  throw new Error(
618
633
  details
619
- ? `${errorMessage} (status ${response.status}): ${details}`
620
- : `${errorMessage} (status ${response.status}).`,
634
+ ? `${errorMessage} (${diagnosticContext}): ${details}`
635
+ : `${errorMessage} (${diagnosticContext}).`,
621
636
  );
622
637
  }
623
638
 
@@ -2963,6 +2978,7 @@ async function prepareRuntimeSheetDatasetRows(
2963
2978
  physicalUpsertSetSql: string;
2964
2979
  outputPhysicalColumns: PhysicalSheetColumnProjection[];
2965
2980
  force?: boolean;
2981
+ mode?: RuntimeSheetDatasetMode;
2966
2982
  },
2967
2983
  ): Promise<{
2968
2984
  inserted: number;
@@ -3365,6 +3381,121 @@ async function prepareRuntimeSheetDatasetRows(
3365
3381
  return { inserted, rowDispositions };
3366
3382
  }
3367
3383
 
3384
+ /**
3385
+ * Atomically admit source-table keys that have not completed. A completed row
3386
+ * is consumed forever; failed rows and rows owned by this logical run remain
3387
+ * recoverable. This is deliberately a separate insert-only path: deriving
3388
+ * "new" from a read before the normal insert would race a concurrent sourcing
3389
+ * run.
3390
+ */
3391
+ async function prepareNetNewRuntimeSheetDatasetRows(
3392
+ client: RuntimeQueryClient,
3393
+ session: RuntimePostgresSession,
3394
+ input: {
3395
+ chunks: RuntimeDatasetRowEntry[][];
3396
+ runId: string;
3397
+ attemptId: string;
3398
+ attemptOwnerRunId: string;
3399
+ attemptExpiresAt: string;
3400
+ attemptSeq: number;
3401
+ writeVersion: number;
3402
+ physicalInsertColumnsSql: string;
3403
+ physicalInsertValuesSql: string;
3404
+ },
3405
+ ): Promise<{
3406
+ inserted: number;
3407
+ rowDispositions: RuntimeSheetPreparedRowDisposition[];
3408
+ }> {
3409
+ let inserted = 0;
3410
+ const rowDispositions: RuntimeSheetPreparedRowDisposition[] = [];
3411
+ for (const chunk of input.chunks) {
3412
+ const { rows } = await client.query<{
3413
+ inserted_count: number;
3414
+ row_dispositions: RuntimeSheetPreparedRowDisposition[];
3415
+ }>(
3416
+ `WITH input_rows AS (
3417
+ SELECT DISTINCT ON (key_values._key)
3418
+ key_values._key, payload_values.payload, index_values._input_index
3419
+ FROM unnest($1::text[]) WITH ORDINALITY AS key_values(_key, ord)
3420
+ JOIN unnest($2::jsonb[]) WITH ORDINALITY AS payload_values(payload, ord)
3421
+ ON payload_values.ord = key_values.ord
3422
+ JOIN unnest($3::bigint[]) WITH ORDINALITY AS index_values(_input_index, ord)
3423
+ ON index_values.ord = key_values.ord
3424
+ ORDER BY key_values._key, key_values.ord
3425
+ ),
3426
+ reclaimed_rows AS (
3427
+ UPDATE ${sheetTable(session)} AS target
3428
+ SET _status = 'pending',
3429
+ _run_id = $4::text,
3430
+ _input_index = input_rows._input_index,
3431
+ _attempt_id = $5::text,
3432
+ _attempt_owner_run_id = $6::text,
3433
+ _attempt_expires_at = $7::timestamptz,
3434
+ _attempt_seq = $8::integer,
3435
+ _write_version = $9::bigint,
3436
+ _writer_run_id = $4::text,
3437
+ _error = NULL,
3438
+ _updated_at = now(),
3439
+ _version = ${nextRuntimeSheetVersionExpression(session)}
3440
+ FROM input_rows
3441
+ WHERE target._key = input_rows._key
3442
+ AND (
3443
+ target._status = 'failed'
3444
+ OR (
3445
+ target._status IN ('pending', 'running')
3446
+ AND coalesce(target._attempt_owner_run_id, target._run_id) = $6::text
3447
+ )
3448
+ )
3449
+ RETURNING target._key
3450
+ ),
3451
+ inserted_rows AS (
3452
+ INSERT INTO ${sheetTable(session)}
3453
+ (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id,
3454
+ _attempt_expires_at, _attempt_seq, _write_version, _writer_run_id${input.physicalInsertColumnsSql})
3455
+ SELECT _key, 'pending', $4::text, _input_index, $5::text, $6::text,
3456
+ $7::timestamptz, $8::integer, $9::bigint, $4::text${input.physicalInsertValuesSql}
3457
+ FROM input_rows
3458
+ ON CONFLICT (_key) DO NOTHING
3459
+ RETURNING _key
3460
+ ),
3461
+ pending_rows AS (
3462
+ SELECT _key FROM inserted_rows
3463
+ UNION
3464
+ SELECT _key FROM reclaimed_rows
3465
+ )
3466
+ SELECT
3467
+ (SELECT count(*)::int FROM inserted_rows) AS inserted_count,
3468
+ coalesce(
3469
+ (SELECT jsonb_agg(jsonb_build_object('key', _key, 'disposition', 'pending')) FROM pending_rows),
3470
+ '[]'::jsonb
3471
+ ) AS row_dispositions`,
3472
+ [
3473
+ chunk.map((entry) => entry.key),
3474
+ chunk.map((entry) => stringifyPostgresJson(entry.row)),
3475
+ chunk.map((entry) => entry.inputIndex),
3476
+ input.runId,
3477
+ input.attemptId,
3478
+ input.attemptOwnerRunId,
3479
+ input.attemptExpiresAt,
3480
+ input.attemptSeq,
3481
+ input.writeVersion,
3482
+ ],
3483
+ );
3484
+ inserted += Number(rows[0]?.inserted_count ?? 0);
3485
+ if (Array.isArray(rows[0]?.row_dispositions)) {
3486
+ rowDispositions.push(
3487
+ ...rows[0].row_dispositions.filter(
3488
+ (row): row is RuntimeSheetPreparedRowDisposition =>
3489
+ row != null &&
3490
+ typeof row.key === 'string' &&
3491
+ row.disposition === 'pending',
3492
+ ),
3493
+ );
3494
+ }
3495
+ }
3496
+ return { inserted, rowDispositions };
3497
+ }
3498
+
3368
3499
  async function buildRuntimeSheetDatasetStartResult(
3369
3500
  client: RuntimeQueryClient,
3370
3501
  session: RuntimePostgresSession,
@@ -5548,6 +5679,7 @@ export async function startRuntimeSheetDataset(
5548
5679
  writeVersion?: number | null;
5549
5680
  inputOffset?: number;
5550
5681
  force?: boolean;
5682
+ mode?: RuntimeSheetDatasetMode;
5551
5683
  },
5552
5684
  ): Promise<PrepareRuntimeSheetResult> {
5553
5685
  if (context.dbSessionStrategy === 'gateway_only') {
@@ -5724,24 +5856,37 @@ export async function startRuntimeSheetDataset(
5724
5856
  rows: rowEntries.length,
5725
5857
  });
5726
5858
  const prepareStartedAt = Date.now();
5727
- const prepared = await prepareRuntimeSheetDatasetRows(client, session, {
5728
- chunks,
5729
- runId: input.runId,
5730
- normalizedPlayName,
5731
- normalizedTableNamespace,
5732
- attemptId,
5733
- attemptOwnerRunId,
5734
- attemptExpiresAt,
5735
- attemptSeq,
5736
- writeVersion,
5737
- physicalInsertColumnsSql,
5738
- physicalInsertValuesSql,
5739
- physicalRefreshSetSql:
5740
- input.force === true ? physicalRefreshSetSql : '',
5741
- physicalUpsertSetSql,
5742
- outputPhysicalColumns,
5743
- force: input.force === true,
5744
- });
5859
+ const prepared =
5860
+ input.mode === 'net_new'
5861
+ ? await prepareNetNewRuntimeSheetDatasetRows(client, session, {
5862
+ chunks,
5863
+ runId: input.runId,
5864
+ attemptId,
5865
+ attemptOwnerRunId,
5866
+ attemptExpiresAt,
5867
+ attemptSeq,
5868
+ writeVersion,
5869
+ physicalInsertColumnsSql,
5870
+ physicalInsertValuesSql,
5871
+ })
5872
+ : await prepareRuntimeSheetDatasetRows(client, session, {
5873
+ chunks,
5874
+ runId: input.runId,
5875
+ normalizedPlayName,
5876
+ normalizedTableNamespace,
5877
+ attemptId,
5878
+ attemptOwnerRunId,
5879
+ attemptExpiresAt,
5880
+ attemptSeq,
5881
+ writeVersion,
5882
+ physicalInsertColumnsSql,
5883
+ physicalInsertValuesSql,
5884
+ physicalRefreshSetSql:
5885
+ input.force === true ? physicalRefreshSetSql : '',
5886
+ physicalUpsertSetSql,
5887
+ outputPhysicalColumns,
5888
+ force: input.force === true,
5889
+ });
5745
5890
  timings.push({
5746
5891
  phase: 'prepare_rows_sql',
5747
5892
  ms: Date.now() - prepareStartedAt,
@@ -6151,12 +6296,8 @@ async function completeRuntimeMapRowChunks(
6151
6296
  for (const chunk of input.chunks) {
6152
6297
  const chunkKeys = chunk.map((row) => row.key);
6153
6298
  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
- );
6299
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6300
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6160
6301
  const targetChangedPatchedCellSql = changedPatchedCellSql(
6161
6302
  'target',
6162
6303
  'updates.data_patch',
@@ -6418,12 +6559,8 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
6418
6559
  for (const chunk of input.chunks) {
6419
6560
  const chunkKeys = chunk.map((row) => row.key);
6420
6561
  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
- );
6562
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6563
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6427
6564
  const targetChangedPatchedCellSql = changedPatchedCellSql(
6428
6565
  'target',
6429
6566
  'updates.data_patch',
@@ -6661,12 +6798,8 @@ async function insertMissingCompletedMapRowChunks(
6661
6798
  for (const chunk of input.chunks) {
6662
6799
  const chunkKeys = chunk.map((row) => row.key);
6663
6800
  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
- );
6801
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6802
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6670
6803
  const { rows } = await client.query<{ inserted: number }>(
6671
6804
  `WITH input_rows AS (
6672
6805
  SELECT key_values._key,
@@ -6833,12 +6966,8 @@ async function failRuntimeMapRowChunks(
6833
6966
  for (const chunk of input.chunks) {
6834
6967
  const chunkKeys = chunk.map((row) => row.key);
6835
6968
  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
- );
6969
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
6970
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
6842
6971
  const chunkErrors = chunk.map((row) => row.error);
6843
6972
  // Per-field failed-cell counts for the column summary, computed from the
6844
6973
  // cell meta patches (a failed row records exactly which cell failed).
@@ -7064,9 +7193,21 @@ export async function completeRuntimeMapRows(
7064
7193
  },
7065
7194
  ): Promise<RuntimeMapRowsWriteResult> {
7066
7195
  if (context.dbSessionStrategy === 'gateway_only') {
7196
+ const keyedRows = input.rows.map((row) => {
7197
+ if (row.key) return row;
7198
+ const key = resolveMapRowOutcomeKey(
7199
+ row as unknown as Record<string, unknown>,
7200
+ );
7201
+ return key ? { ...row, key } : row;
7202
+ });
7203
+ const rows = prepareRuntimeSheetRowsForJsonTransport({
7204
+ rows: keyedRows,
7205
+ runId: input.runId,
7206
+ outputFields: input.outputFields ?? [],
7207
+ });
7067
7208
  return await postRuntimeApi<RuntimeMapRowsWriteResult>(context, {
7068
7209
  action: 'runtime_sheet_complete_map_rows',
7069
- input,
7210
+ input: { ...input, rows },
7070
7211
  });
7071
7212
  }
7072
7213
  if (input.rows.length === 0) {