deepline 0.1.131 → 0.1.132

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.
@@ -1466,6 +1466,9 @@ type WorkerToolBatchRequest = {
1466
1466
 
1467
1467
  const WORKER_TOOL_BATCH_GRACE_MS = 250;
1468
1468
  const MAP_EXECUTION_HEARTBEAT_INTERVAL_MS = 5_000;
1469
+ const MAP_INCREMENTAL_PERSIST_CHUNK_ROWS = 100;
1470
+ const MAP_INCREMENTAL_PERSIST_CHUNK_BYTES = 1 * 1024 * 1024;
1471
+ const MAP_INCREMENTAL_PERSIST_INTERVAL_MS = 100;
1469
1472
  /**
1470
1473
  * Bounded number of per-row failure samples carried in chunk summaries and the
1471
1474
  * map's terminal partial-failure log. Every failed row is persisted with its
@@ -3850,6 +3853,153 @@ function createMinimalWorkerCtx(
3850
3853
  reportSettledToolRequests,
3851
3854
  );
3852
3855
  const generatedOutputFields = new Set<string>();
3856
+ const persistedExecutedIndexes = new Set<number>();
3857
+ const persistedFailedIndexes = new Set<number>();
3858
+ let pendingPersistRows = 0;
3859
+ let pendingPersistBytes = 0;
3860
+ let scheduledPersistTimer: ReturnType<typeof setTimeout> | null = null;
3861
+ let persistFlushChain: Promise<void> = Promise.resolve();
3862
+ let persistFailure: unknown = null;
3863
+
3864
+ const clearScheduledPersistTimer = () => {
3865
+ if (scheduledPersistTimer) {
3866
+ clearTimeout(scheduledPersistTimer);
3867
+ scheduledPersistTimer = null;
3868
+ }
3869
+ };
3870
+
3871
+ const persistExecutedRows = async () => {
3872
+ const rowsToPersist = executedRows
3873
+ .map((row, executedIndex) =>
3874
+ row && !persistedExecutedIndexes.has(executedIndex)
3875
+ ? {
3876
+ row,
3877
+ executedIndex,
3878
+ }
3879
+ : null,
3880
+ )
3881
+ .filter(
3882
+ (
3883
+ entry,
3884
+ ): entry is {
3885
+ row: T & Record<string, unknown>;
3886
+ executedIndex: number;
3887
+ } => entry !== null,
3888
+ );
3889
+ const allFailedRowsToPersist = failedRowEntries
3890
+ .map((failure, executedIndex) =>
3891
+ failure && !persistedFailedIndexes.has(executedIndex)
3892
+ ? {
3893
+ failure,
3894
+ executedIndex,
3895
+ }
3896
+ : null,
3897
+ )
3898
+ .filter(
3899
+ (
3900
+ entry,
3901
+ ): entry is {
3902
+ failure: { row: T & Record<string, unknown>; error: string };
3903
+ executedIndex: number;
3904
+ } => entry !== null,
3905
+ );
3906
+ // Under the default isolation, every failed row persists as a
3907
+ // recoverable `_status='failed'` row (it re-executes free next run).
3908
+ // Under `onRowError: 'fail'` the run dies, so a failed row's partial
3909
+ // data is persisted ONLY as a last-resort recovery: when this chunk has
3910
+ // no other recoverable rows (no successful executed rows and no
3911
+ // already-completed rows). That keeps a partial fail-fast run's export
3912
+ // to the rows that fully committed before the failure, while an
3913
+ // all-rows-failed fail-fast run still exposes the persisted partial
3914
+ // cells instead of advertising an empty, unrecoverable dataset.
3915
+ const failedRowsToPersist =
3916
+ failFastRowErrors &&
3917
+ (rowsToPersist.length > 0 ||
3918
+ persistedExecutedIndexes.size > 0 ||
3919
+ prepared.completedRows.length > 0)
3920
+ ? []
3921
+ : allFailedRowsToPersist;
3922
+ if (rowsToPersist.length === 0 && failedRowsToPersist.length === 0) {
3923
+ return;
3924
+ }
3925
+ await persistCompletedMapRows({
3926
+ req,
3927
+ tableNamespace: name,
3928
+ outputFields,
3929
+ extraOutputFields: Array.from(generatedOutputFields),
3930
+ rows: [
3931
+ ...rowsToPersist.map(({ row, executedIndex }) => ({
3932
+ ...row,
3933
+ ...(executedCellMetaPatches[executedIndex]
3934
+ ? {
3935
+ __deeplineCellMetaPatch:
3936
+ executedCellMetaPatches[executedIndex],
3937
+ }
3938
+ : {}),
3939
+ __deeplineRowKey:
3940
+ uniqueRowsToExecuteEntries[executedIndex]!.rowKey,
3941
+ })),
3942
+ // Failed rows persist as recoverable `_status='failed'` sheet
3943
+ // rows: partial data + per-cell failure meta + the row error.
3944
+ ...failedRowsToPersist.map(({ failure, executedIndex }) => ({
3945
+ ...failure.row,
3946
+ ...(executedCellMetaPatches[executedIndex]
3947
+ ? {
3948
+ __deeplineCellMetaPatch:
3949
+ executedCellMetaPatches[executedIndex],
3950
+ }
3951
+ : {}),
3952
+ __deeplineRowKey:
3953
+ uniqueRowsToExecuteEntries[executedIndex]!.rowKey,
3954
+ __deeplineRowStatus: 'failed',
3955
+ __deeplineRowError: failure.error,
3956
+ })),
3957
+ ],
3958
+ });
3959
+ for (const { executedIndex } of rowsToPersist) {
3960
+ persistedExecutedIndexes.add(executedIndex);
3961
+ }
3962
+ for (const { executedIndex } of failedRowsToPersist) {
3963
+ persistedFailedIndexes.add(executedIndex);
3964
+ }
3965
+ };
3966
+
3967
+ const enqueuePersistExecutedRows = (): Promise<void> => {
3968
+ clearScheduledPersistTimer();
3969
+ pendingPersistRows = 0;
3970
+ pendingPersistBytes = 0;
3971
+ const task = persistFlushChain.then(async () => {
3972
+ if (persistFailure) throw persistFailure;
3973
+ await persistExecutedRows();
3974
+ });
3975
+ persistFlushChain = task.catch((error) => {
3976
+ persistFailure ??= error;
3977
+ });
3978
+ return task;
3979
+ };
3980
+
3981
+ const schedulePersistExecutedRows = () => {
3982
+ if (persistFailure) return;
3983
+ if (
3984
+ pendingPersistRows >= MAP_INCREMENTAL_PERSIST_CHUNK_ROWS ||
3985
+ pendingPersistBytes >= MAP_INCREMENTAL_PERSIST_CHUNK_BYTES
3986
+ ) {
3987
+ void enqueuePersistExecutedRows().catch(() => undefined);
3988
+ return;
3989
+ }
3990
+ if (scheduledPersistTimer) return;
3991
+ scheduledPersistTimer = setTimeout(() => {
3992
+ scheduledPersistTimer = null;
3993
+ void enqueuePersistExecutedRows().catch(() => undefined);
3994
+ }, MAP_INCREMENTAL_PERSIST_INTERVAL_MS);
3995
+ };
3996
+
3997
+ const notePersistableRow = (row: Record<string, unknown>) => {
3998
+ pendingPersistRows += 1;
3999
+ pendingPersistBytes += JSON.stringify(row).length;
4000
+ schedulePersistExecutedRows();
4001
+ };
4002
+
3853
4003
  let idx = 0;
3854
4004
  const workers: Array<Promise<void>> = [];
3855
4005
  for (let w = 0; w < concurrency; w += 1) {
@@ -4025,6 +4175,7 @@ function createMinimalWorkerCtx(
4025
4175
  executedRows[myIndex] = enriched as T &
4026
4176
  Record<string, unknown>;
4027
4177
  completedExecutedRows += 1;
4178
+ notePersistableRow(enriched);
4028
4179
  reportChunkProgress(false);
4029
4180
  } catch (rowError) {
4030
4181
  // Abort/budget errors stay run-fatal and leave no partial
@@ -4045,19 +4196,19 @@ function createMinimalWorkerCtx(
4045
4196
  Object.keys(cellMetaPatch).length > 0
4046
4197
  ? cellMetaPatch
4047
4198
  : undefined;
4048
- // Keep the partially-enriched row so its already-succeeded
4049
- // sibling cells (e.g. a contact column that ran before the
4050
- // failing column) persist as a recoverable `_status='failed'`
4051
- // sheet row. This holds for BOTH the default isolation path
4052
- // (row re-executes free on the next run) AND `onRowError:
4053
- // 'fail'`: the chunk still persists every recorded row, so the
4054
- // failed run advertises a working recovered export even when
4055
- // every row fails (see the runMap-level fail-fast throw).
4199
+ // Keep the partially-enriched row. Default isolation persists
4200
+ // it as `_status='failed'` so the row can re-execute free on
4201
+ // the next run. Fail-fast persists failed rows only after the
4202
+ // chunk settles and only when every row failed; otherwise only
4203
+ // fully committed successful rows are recoverable.
4056
4204
  failedRowEntries[myIndex] = {
4057
4205
  row: enriched as T & Record<string, unknown>,
4058
4206
  error: message,
4059
4207
  };
4060
4208
  failedExecutedRows += 1;
4209
+ if (!failFastRowErrors) {
4210
+ notePersistableRow(enriched);
4211
+ }
4061
4212
  // Bounded per-chunk samples: every failure is persisted on
4062
4213
  // its row, but only the first few get a log line so a wide
4063
4214
  // outage cannot flood the Run Log Stream.
@@ -4069,7 +4220,7 @@ function createMinimalWorkerCtx(
4069
4220
  `Row ${absoluteIndex} of ctx.dataset("${name}") failed` +
4070
4221
  `${activeField ? ` at column "${activeField}"` : ''}: ${message} ` +
4071
4222
  (failFastRowErrors
4072
- ? '(row persisted as failed; onRowError:"fail" fails the run after committing it)'
4223
+ ? '(row recorded as failed; onRowError:"fail" persists it only if every row fails)'
4073
4224
  : '(row recorded as failed; sibling rows continue and the row re-executes on the next run)'),
4074
4225
  ts: nowMs(),
4075
4226
  });
@@ -4098,93 +4249,6 @@ function createMinimalWorkerCtx(
4098
4249
  })(),
4099
4250
  );
4100
4251
  }
4101
- const persistExecutedRows = async () => {
4102
- const rowsToPersist = executedRows
4103
- .map((row, executedIndex) =>
4104
- row
4105
- ? {
4106
- row,
4107
- executedIndex,
4108
- }
4109
- : null,
4110
- )
4111
- .filter(
4112
- (
4113
- entry,
4114
- ): entry is {
4115
- row: T & Record<string, unknown>;
4116
- executedIndex: number;
4117
- } => entry !== null,
4118
- );
4119
- const allFailedRowsToPersist = failedRowEntries
4120
- .map((failure, executedIndex) =>
4121
- failure
4122
- ? {
4123
- failure,
4124
- executedIndex,
4125
- }
4126
- : null,
4127
- )
4128
- .filter(
4129
- (
4130
- entry,
4131
- ): entry is {
4132
- failure: { row: T & Record<string, unknown>; error: string };
4133
- executedIndex: number;
4134
- } => entry !== null,
4135
- );
4136
- // Under the default isolation, every failed row persists as a
4137
- // recoverable `_status='failed'` row (it re-executes free next run).
4138
- // Under `onRowError: 'fail'` the run dies, so a failed row's partial
4139
- // data is persisted ONLY as a last-resort recovery: when this chunk has
4140
- // no other recoverable rows (no successful executed rows and no
4141
- // already-completed rows). That keeps a partial fail-fast run's export
4142
- // to the rows that fully committed before the failure, while an
4143
- // all-rows-failed fail-fast run still exposes the persisted partial
4144
- // cells instead of advertising an empty, unrecoverable dataset.
4145
- const failedRowsToPersist =
4146
- failFastRowErrors &&
4147
- (rowsToPersist.length > 0 || prepared.completedRows.length > 0)
4148
- ? []
4149
- : allFailedRowsToPersist;
4150
- if (rowsToPersist.length === 0 && failedRowsToPersist.length === 0) {
4151
- return;
4152
- }
4153
- await persistCompletedMapRows({
4154
- req,
4155
- tableNamespace: name,
4156
- outputFields,
4157
- extraOutputFields: Array.from(generatedOutputFields),
4158
- rows: [
4159
- ...rowsToPersist.map(({ row, executedIndex }) => ({
4160
- ...row,
4161
- ...(executedCellMetaPatches[executedIndex]
4162
- ? {
4163
- __deeplineCellMetaPatch:
4164
- executedCellMetaPatches[executedIndex],
4165
- }
4166
- : {}),
4167
- __deeplineRowKey:
4168
- uniqueRowsToExecuteEntries[executedIndex]!.rowKey,
4169
- })),
4170
- // Failed rows persist as recoverable `_status='failed'` sheet
4171
- // rows: partial data + per-cell failure meta + the row error.
4172
- ...failedRowsToPersist.map(({ failure, executedIndex }) => ({
4173
- ...failure.row,
4174
- ...(executedCellMetaPatches[executedIndex]
4175
- ? {
4176
- __deeplineCellMetaPatch:
4177
- executedCellMetaPatches[executedIndex],
4178
- }
4179
- : {}),
4180
- __deeplineRowKey:
4181
- uniqueRowsToExecuteEntries[executedIndex]!.rowKey,
4182
- __deeplineRowStatus: 'failed',
4183
- __deeplineRowError: failure.error,
4184
- })),
4185
- ],
4186
- });
4187
- };
4188
4252
  const workersStartedAt = nowMs();
4189
4253
  // Track completion with a boolean flag rather than narrowing a
4190
4254
  // closure-assigned `| null` variable: TypeScript's control-flow analysis
@@ -4230,7 +4294,9 @@ function createMinimalWorkerCtx(
4230
4294
  },
4231
4295
  });
4232
4296
  try {
4233
- await persistExecutedRows();
4297
+ await enqueuePersistExecutedRows();
4298
+ await persistFlushChain;
4299
+ if (persistFailure) throw persistFailure;
4234
4300
  recordRunnerPerfTrace({
4235
4301
  req,
4236
4302
  phase: 'runner.map_chunk.persist_rows',
@@ -30,19 +30,29 @@ export async function executeChunkedRequests<TRequest, TResult>(input: {
30
30
  const results: Array<ChunkExecutionResult<TRequest, TResult>> = [];
31
31
  for (let start = 0; start < input.requests.length; start += input.batchSize) {
32
32
  const chunk = input.requests.slice(start, start + input.batchSize);
33
- const settled = await Promise.allSettled(
34
- chunk.map((request) => input.execute(request)),
33
+ let notifyChain: Promise<void> = Promise.resolve();
34
+ const notify = async (
35
+ entry: ChunkExecutionResult<TRequest, TResult>,
36
+ ): Promise<void> => {
37
+ results.push(entry);
38
+ notifyChain = notifyChain.then(
39
+ async () => await input.onChunkComplete?.([entry]),
40
+ );
41
+ await notifyChain;
42
+ };
43
+
44
+ await Promise.all(
45
+ chunk.map(async (request) => {
46
+ let entry: ChunkExecutionResult<TRequest, TResult>;
47
+ try {
48
+ entry = { request, result: await input.execute(request) };
49
+ } catch (error) {
50
+ entry = { request, result: null, error };
51
+ }
52
+ await notify(entry);
53
+ }),
35
54
  );
36
- for (let index = 0; index < chunk.length; index += 1) {
37
- const request = chunk[index]!;
38
- const outcome = settled[index]!;
39
- if (outcome.status === 'rejected') {
40
- results.push({ request, result: null, error: outcome.reason });
41
- continue;
42
- }
43
- results.push({ request, result: outcome.value });
44
- }
45
- await input.onChunkComplete?.(results.slice(results.length - chunk.length));
55
+ await notifyChain;
46
56
  }
47
57
  return results;
48
58
  }
@@ -101,10 +101,10 @@ export const SDK_RELEASE = {
101
101
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
102
102
  // the SDK enrich generator's one-second stale policy.
103
103
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
104
- version: '0.1.131',
104
+ version: '0.1.132',
105
105
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
106
106
  supportPolicy: {
107
- latest: '0.1.131',
107
+ latest: '0.1.132',
108
108
  minimumSupported: '0.1.53',
109
109
  deprecatedBelow: '0.1.53',
110
110
  commandMinimumSupported: [
@@ -45,30 +45,37 @@ export async function executeChunkedRequests<TRequest, TResult>(input: {
45
45
 
46
46
  for (let start = 0; start < input.requests.length; start += input.batchSize) {
47
47
  const chunk = input.requests.slice(start, start + input.batchSize);
48
- const settled = await Promise.allSettled(
49
- chunk.map((request) => input.execute(request)),
50
- );
51
-
52
- for (let index = 0; index < chunk.length; index += 1) {
53
- const request = chunk[index]!;
54
- const outcome = settled[index]!;
55
- if (outcome.status === 'rejected') {
56
- input.onRequestError?.(request, outcome.reason);
57
- results.push({
58
- request,
59
- result: null,
60
- error: formatChunkExecutionError(outcome.reason),
61
- });
62
- continue;
63
- }
64
- results.push({
65
- request,
66
- result: outcome.value,
67
- });
68
- }
48
+ let notifyChain: Promise<void> = Promise.resolve();
49
+ const notify = async (
50
+ entry: ChunkExecutionResult<TRequest, TResult>,
51
+ ): Promise<void> => {
52
+ results.push(entry);
53
+ notifyChain = notifyChain.then(
54
+ async () => await input.onChunkComplete?.([entry]),
55
+ );
56
+ await notifyChain;
57
+ };
69
58
 
70
- const completedChunk = results.slice(results.length - chunk.length);
71
- await input.onChunkComplete?.(completedChunk);
59
+ await Promise.all(
60
+ chunk.map(async (request) => {
61
+ let entry: ChunkExecutionResult<TRequest, TResult>;
62
+ try {
63
+ entry = {
64
+ request,
65
+ result: await input.execute(request),
66
+ };
67
+ } catch (error) {
68
+ input.onRequestError?.(request, error);
69
+ entry = {
70
+ request,
71
+ result: null,
72
+ error: formatChunkExecutionError(error),
73
+ };
74
+ }
75
+ await notify(entry);
76
+ }),
77
+ );
78
+ await notifyChain;
72
79
  }
73
80
 
74
81
  return results;
@@ -168,6 +168,9 @@ const MAP_FRAME_FLUSH_ROW_INTERVAL = 100;
168
168
  /** Executed-row sheet persistence chunking: rows AND bytes, whichever first. */
169
169
  const MAP_PERSIST_CHUNK_ROWS = 2_000;
170
170
  const MAP_PERSIST_CHUNK_BYTES = 8 * 1024 * 1024;
171
+ const MAP_INCREMENTAL_PERSIST_CHUNK_ROWS = 100;
172
+ const MAP_INCREMENTAL_PERSIST_CHUNK_BYTES = 1 * 1024 * 1024;
173
+ const MAP_INCREMENTAL_PERSIST_INTERVAL_MS = 100;
171
174
  const MAP_FRAME_FLUSH_INTERVAL_MS = 250;
172
175
  const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
173
176
  const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
@@ -219,11 +222,143 @@ function comparePersistableMapRowsByInputIndex(
219
222
  return leftIndex - rightIndex;
220
223
  }
221
224
 
225
+ function persistableMapRowIdentity(row: PersistableMapRow): string | null {
226
+ if (row.key) return `key:${row.key}`;
227
+ return typeof row.inputIndex === 'number' && Number.isFinite(row.inputIndex)
228
+ ? `index:${Math.floor(row.inputIndex)}`
229
+ : null;
230
+ }
231
+
232
+ function persistableMapRowBytes(row: PersistableMapRow): number {
233
+ return JSON.stringify(row).length;
234
+ }
235
+
222
236
  type FieldMapRunResult = {
223
237
  completedRows: PersistableMapRow[];
224
238
  failedRows: PersistableMapRow[];
225
239
  };
226
240
 
241
+ type IncrementalMapRowPersistence = {
242
+ persistRows: (rows: PersistableMapRow[]) => Promise<void>;
243
+ isPersisted: (row: PersistableMapRow) => boolean;
244
+ flush: () => Promise<void>;
245
+ };
246
+
247
+ function createIncrementalMapRowPersistence(
248
+ persistRows: (rows: PersistableMapRow[]) => Promise<void>,
249
+ ): IncrementalMapRowPersistence {
250
+ const persisted = new Set<string>();
251
+ const queued = new Set<string>();
252
+ let pendingRows: PersistableMapRow[] = [];
253
+ let pendingBytes = 0;
254
+ let pendingResolvers: Array<{
255
+ resolve: () => void;
256
+ reject: (error: unknown) => void;
257
+ }> = [];
258
+ let scheduledTimer: ReturnType<typeof setTimeout> | null = null;
259
+ let flushChain: Promise<void> = Promise.resolve();
260
+
261
+ const clearScheduledTimer = (): void => {
262
+ if (scheduledTimer) {
263
+ clearTimeout(scheduledTimer);
264
+ scheduledTimer = null;
265
+ }
266
+ };
267
+
268
+ const flushPendingRows = (): Promise<void> => {
269
+ clearScheduledTimer();
270
+ const rows = pendingRows;
271
+ const resolvers = pendingResolvers;
272
+ pendingRows = [];
273
+ pendingBytes = 0;
274
+ pendingResolvers = [];
275
+ if (rows.length === 0) {
276
+ resolvers.forEach(({ resolve }) => resolve());
277
+ return flushChain;
278
+ }
279
+ flushChain = flushChain
280
+ .then(async () => {
281
+ await persistRows(rows);
282
+ for (const row of rows) {
283
+ const identity = persistableMapRowIdentity(row);
284
+ if (identity) {
285
+ persisted.add(identity);
286
+ queued.delete(identity);
287
+ }
288
+ }
289
+ })
290
+ .then(
291
+ () => {
292
+ resolvers.forEach(({ resolve }) => resolve());
293
+ },
294
+ (error) => {
295
+ for (const row of rows) {
296
+ const identity = persistableMapRowIdentity(row);
297
+ if (identity) queued.delete(identity);
298
+ }
299
+ resolvers.forEach(({ reject }) => reject(error));
300
+ throw error;
301
+ },
302
+ );
303
+ return flushChain;
304
+ };
305
+
306
+ const scheduleFlush = (): void => {
307
+ if (scheduledTimer) return;
308
+ scheduledTimer = setTimeout(() => {
309
+ scheduledTimer = null;
310
+ void flushPendingRows().catch(() => {
311
+ // Each queued caller is rejected by flushPendingRows; this scheduled
312
+ // fire-and-forget path must not create a second unhandled rejection.
313
+ });
314
+ }, MAP_INCREMENTAL_PERSIST_INTERVAL_MS);
315
+ };
316
+
317
+ return {
318
+ persistRows: async (rows) => {
319
+ const freshRows: PersistableMapRow[] = [];
320
+ for (const row of rows) {
321
+ const identity = persistableMapRowIdentity(row);
322
+ if (identity && (persisted.has(identity) || queued.has(identity))) {
323
+ continue;
324
+ }
325
+ if (identity) queued.add(identity);
326
+ freshRows.push(row);
327
+ }
328
+ if (freshRows.length === 0) return;
329
+ await new Promise<void>((resolve, reject) => {
330
+ pendingRows.push(...freshRows);
331
+ pendingBytes += freshRows.reduce(
332
+ (total, row) => total + persistableMapRowBytes(row),
333
+ 0,
334
+ );
335
+ pendingResolvers.push({ resolve, reject });
336
+ if (
337
+ pendingRows.length >= MAP_INCREMENTAL_PERSIST_CHUNK_ROWS ||
338
+ pendingBytes >= MAP_INCREMENTAL_PERSIST_CHUNK_BYTES
339
+ ) {
340
+ void flushPendingRows().catch(() => {
341
+ // Each queued caller is rejected by flushPendingRows.
342
+ });
343
+ return;
344
+ }
345
+ scheduleFlush();
346
+ });
347
+ },
348
+ isPersisted: (row) => {
349
+ const identity = persistableMapRowIdentity(row);
350
+ return identity ? persisted.has(identity) : false;
351
+ },
352
+ flush: async () => {
353
+ if (pendingRows.length > 0) {
354
+ clearScheduledTimer();
355
+ await flushPendingRows();
356
+ }
357
+ await flushChain;
358
+ },
359
+ };
360
+ }
361
+
227
362
  class FailFastMapRowsError extends Error {
228
363
  readonly completedRows: PersistableMapRow[];
229
364
  readonly failedRows: PersistableMapRow[];
@@ -1819,6 +1954,7 @@ export class PlayContextImpl {
1819
1954
  let totalInputCount = 0;
1820
1955
  let rawItems: Record<string, unknown>[] = [];
1821
1956
  let itemsToProcess: Array<Record<string, unknown>> = [];
1957
+ let itemOriginalIndexes: number[] = [];
1822
1958
  let completedItemsByKey: Map<string, Record<string, unknown>> | null = null;
1823
1959
  const datasetColumnNames = Object.keys(input);
1824
1960
  const stripFieldOutputs = (row: Record<string, unknown>) => {
@@ -1923,6 +2059,7 @@ export class PlayContextImpl {
1923
2059
  itemsToProcess = materializedItems.map((item) =>
1924
2060
  this.toOutputRow(item as Record<string, unknown>),
1925
2061
  );
2062
+ itemOriginalIndexes = materializedItems.map((_item, index) => index);
1926
2063
  if (this.#options.onMapStart) {
1927
2064
  const shouldPassRowKey = explicitKeyResolver != null;
1928
2065
  // toSerializableCsvAliasedRow (not a plain spread): projected CSV
@@ -1968,7 +2105,7 @@ export class PlayContextImpl {
1968
2105
  const rowKey = persistedRowIdentity(row, index);
1969
2106
  if (rowKey) pendingRowsByKey.set(rowKey, row);
1970
2107
  }
1971
- itemsToProcess = materializedItems
2108
+ const pendingItems = materializedItems
1972
2109
  .map((item, index) => ({
1973
2110
  row: this.toOutputRow(item as Record<string, unknown>),
1974
2111
  index,
@@ -1981,8 +2118,13 @@ export class PlayContextImpl {
1981
2118
  // the sheet round-trip. Merge the persisted data/meta over the
1982
2119
  // in-memory row so key functions and column resolvers keep seeing
1983
2120
  // projected aliases.
1984
- return persisted ? cloneCsvAliasedRow(row, persisted) : row;
2121
+ return {
2122
+ row: persisted ? cloneCsvAliasedRow(row, persisted) : row,
2123
+ index,
2124
+ };
1985
2125
  });
2126
+ itemsToProcess = pendingItems.map((item) => item.row);
2127
+ itemOriginalIndexes = pendingItems.map((item) => item.index);
1986
2128
  if (mapStartResult.completedRows.length > 0) {
1987
2129
  completedItemsByKey = new Map();
1988
2130
  for (
@@ -2008,7 +2150,7 @@ export class PlayContextImpl {
2008
2150
  const completedRowKeys =
2009
2151
  completedItemsByKey != null ? [...completedItemsByKey.keys()] : [];
2010
2152
  const pendingRowKeys = itemsToProcess.map((item, index) =>
2011
- rowIdentity(this.toOutputRow(item), index),
2153
+ rowIdentity(this.toOutputRow(item), itemOriginalIndexes[index] ?? index),
2012
2154
  );
2013
2155
  this.setMapFrame({
2014
2156
  mapInvocationId: mapScope.mapInvocationId,
@@ -2040,9 +2182,10 @@ export class PlayContextImpl {
2040
2182
  >();
2041
2183
  for (let index = 0; index < itemsToProcess.length; index += 1) {
2042
2184
  const row = itemsToProcess[index]!;
2043
- const rowKey = rowIdentity(row, index);
2185
+ const originalIndex = itemOriginalIndexes[index] ?? index;
2186
+ const rowKey = rowIdentity(row, originalIndex);
2044
2187
  if (!rowsToExecuteByKey.has(rowKey)) {
2045
- rowsToExecuteByKey.set(rowKey, { row, originalIndex: index });
2188
+ rowsToExecuteByKey.set(rowKey, { row, originalIndex });
2046
2189
  }
2047
2190
  }
2048
2191
  const rowsToExecuteEntries = [...rowsToExecuteByKey.entries()].map(
@@ -2090,6 +2233,9 @@ export class PlayContextImpl {
2090
2233
  }
2091
2234
  await flushChunk();
2092
2235
  };
2236
+ const incrementalPersistence = this.#options.onMapRowsCompleted
2237
+ ? createIncrementalMapRowPersistence(persistMapRows)
2238
+ : null;
2093
2239
 
2094
2240
  let mapResult: FieldMapRunResult;
2095
2241
  try {
@@ -2108,6 +2254,7 @@ export class PlayContextImpl {
2108
2254
  executionRowIndexes: rowsToExecuteEntries.map(
2109
2255
  (entry) => entry.originalIndex,
2110
2256
  ),
2257
+ incrementalPersistence,
2111
2258
  },
2112
2259
  );
2113
2260
  } catch (error) {
@@ -2116,11 +2263,17 @@ export class PlayContextImpl {
2116
2263
  error.completedRows.length > 0
2117
2264
  ? error.completedRows
2118
2265
  : error.failedRows;
2266
+ await incrementalPersistence?.flush();
2267
+ const unpersistedRows = incrementalPersistence
2268
+ ? rowsToPersist.filter(
2269
+ (row) => !incrementalPersistence.isPersisted(row),
2270
+ )
2271
+ : rowsToPersist;
2119
2272
  const persistStartedAt = Date.now();
2120
- await persistMapRows(rowsToPersist);
2121
- if (rowsToPersist.length > 0) {
2273
+ await persistMapRows(unpersistedRows);
2274
+ if (unpersistedRows.length > 0) {
2122
2275
  this.log(
2123
- `Persisted ${rowsToPersist.length} fail-fast rows to sheet ${resolvedTableNamespace} in ${Date.now() - persistStartedAt}ms`,
2276
+ `Persisted ${unpersistedRows.length} fail-fast rows to sheet ${resolvedTableNamespace} in ${Date.now() - persistStartedAt}ms`,
2124
2277
  );
2125
2278
  }
2126
2279
  this.activeMapCellMeta = null;
@@ -2164,9 +2317,11 @@ export class PlayContextImpl {
2164
2317
  // source of truth, not this in-memory results array. Chunked by rows AND
2165
2318
  // bytes so large cells (scraped pages) never produce oversized writes.
2166
2319
  if (resultsByKey.size > 0 || mapResult.failedRows.length > 0) {
2320
+ await incrementalPersistence?.flush();
2167
2321
  const mapCellMeta = this.activeMapCellMeta;
2168
2322
  const persistRows: PersistableMapRow[] = [];
2169
2323
  for (const row of mapResult.completedRows) {
2324
+ if (incrementalPersistence?.isPersisted(row)) continue;
2170
2325
  const rowKey = row.key;
2171
2326
  const meta = mapCellMeta?.get(rowKey);
2172
2327
  persistRows.push({
@@ -2178,12 +2333,18 @@ export class PlayContextImpl {
2178
2333
  },
2179
2334
  });
2180
2335
  }
2181
- persistRows.push(...mapResult.failedRows);
2336
+ persistRows.push(
2337
+ ...mapResult.failedRows.filter(
2338
+ (row) => !incrementalPersistence?.isPersisted(row),
2339
+ ),
2340
+ );
2182
2341
  const persistStartedAt = Date.now();
2183
2342
  await persistMapRows(persistRows);
2184
- this.log(
2185
- `Persisted ${persistRows.length} executed rows to sheet ${resolvedTableNamespace} in ${Date.now() - persistStartedAt}ms`,
2186
- );
2343
+ if (persistRows.length > 0) {
2344
+ this.log(
2345
+ `Persisted ${persistRows.length} executed rows to sheet ${resolvedTableNamespace} in ${Date.now() - persistStartedAt}ms`,
2346
+ );
2347
+ }
2187
2348
  }
2188
2349
  this.activeMapCellMeta = null;
2189
2350
 
@@ -2255,6 +2416,7 @@ export class PlayContextImpl {
2255
2416
  * want to throttle a wide map.
2256
2417
  */
2257
2418
  concurrency?: number;
2419
+ incrementalPersistence?: IncrementalMapRowPersistence | null;
2258
2420
  },
2259
2421
  ): Promise<FieldMapRunResult> {
2260
2422
  const fieldEntries = Object.entries(definition);
@@ -2299,6 +2461,14 @@ export class PlayContextImpl {
2299
2461
  const failFastOnRowError = runtimeOptions?.onRowError === 'fail';
2300
2462
  const completedRowsToPersist: PersistableMapRow[] = [];
2301
2463
  const failedRowsToPersist: PersistableMapRow[] = [];
2464
+ const incrementalPersistence =
2465
+ runtimeOptions?.incrementalPersistence ?? null;
2466
+ const enqueueIncrementalPersist = (row: PersistableMapRow): void => {
2467
+ void incrementalPersistence?.persistRows([row]).catch(() => {
2468
+ // The final map-level flush awaits the same chain and surfaces the
2469
+ // persistence failure loudly without holding a completed row slot open.
2470
+ });
2471
+ };
2302
2472
 
2303
2473
  if (completedRows > 0 || pendingRows !== totalRows) {
2304
2474
  this.log(
@@ -2605,7 +2775,7 @@ export class PlayContextImpl {
2605
2775
  : {},
2606
2776
  });
2607
2777
  if (failFastOnRowError) {
2608
- failedRowsToPersist.push({
2778
+ const failedRow: PersistableMapRow = {
2609
2779
  key: rowKey,
2610
2780
  inputIndex: rowIndex,
2611
2781
  data: this.toPersistedOutputRow(
@@ -2616,21 +2786,23 @@ export class PlayContextImpl {
2616
2786
  : {}),
2617
2787
  status: 'failed',
2618
2788
  error: formattedError,
2619
- });
2789
+ };
2790
+ failedRowsToPersist.push(failedRow);
2620
2791
  throw error;
2621
2792
  }
2622
2793
  const failedData = this.toPersistedOutputRow(
2623
2794
  cloneCsvAliasedRow(baseRow, computedFields),
2624
2795
  );
2625
2796
  const cellMetaPatch = this.activeMapCellMeta?.get(rowKey);
2626
- failedRowsToPersist.push({
2797
+ const failedRow: PersistableMapRow = {
2627
2798
  key: rowKey,
2628
2799
  inputIndex: rowIndex,
2629
2800
  data: failedData,
2630
2801
  ...(cellMetaPatch ? { cellMetaPatch } : {}),
2631
2802
  status: 'failed',
2632
2803
  error: formattedError,
2633
- });
2804
+ };
2805
+ failedRowsToPersist.push(failedRow);
2634
2806
  updateMapFrameProgress({
2635
2807
  failedRowKey: rowKey,
2636
2808
  });
@@ -2642,6 +2814,7 @@ export class PlayContextImpl {
2642
2814
  error: formattedError,
2643
2815
  dataPatch: {},
2644
2816
  });
2817
+ enqueueIncrementalPersist(failedRow);
2645
2818
  return FAILED_ROW;
2646
2819
  }
2647
2820
  const cellValue = this.serializeCellValue(value);
@@ -2682,14 +2855,16 @@ export class PlayContextImpl {
2682
2855
  dataPatch: {},
2683
2856
  });
2684
2857
  const publicRow = this.toPublicOutputRow(merged);
2685
- completedRowsToPersist.push({
2858
+ const completedRow: PersistableMapRow = {
2686
2859
  key: rowKey,
2687
2860
  inputIndex: rowIndex,
2688
2861
  data: this.toPersistedOutputRow(merged),
2689
2862
  ...(this.activeMapCellMeta?.get(rowKey)
2690
2863
  ? { cellMetaPatch: this.activeMapCellMeta.get(rowKey) }
2691
2864
  : {}),
2692
- });
2865
+ };
2866
+ completedRowsToPersist.push(completedRow);
2867
+ enqueueIncrementalPersist(completedRow);
2693
2868
  return publicRow;
2694
2869
  } catch (error) {
2695
2870
  if (isPlayRowExecutionSuspendedError(error)) {
@@ -2807,6 +2982,7 @@ export class PlayContextImpl {
2807
2982
  ).values(),
2808
2983
  ];
2809
2984
  this.pendingRowEventBoundaries = [];
2985
+ await incrementalPersistence?.flush();
2810
2986
  this.#options.onBatchComplete?.(this.checkpoint);
2811
2987
  throw new PlayExecutionSuspendedError({
2812
2988
  kind: 'integration_event_batch',
@@ -4960,7 +5136,42 @@ export class PlayContextImpl {
4960
5136
  [...byTool.entries()].map(async ([toolId, requests]) => {
4961
5137
  this.log(`Executing tool batch ${toolId}: ${requests.length} calls`);
4962
5138
 
5139
+ const recordToolStep = (stepRequests: ToolCallRequest[]): void => {
5140
+ if (stepRequests.length === 0) return;
5141
+ const stepResults: PlayStepRowResult[] = stepRequests.map((req) => {
5142
+ const cachedResult = this.getCachedToolResult(
5143
+ toolId,
5144
+ this.buildToolResultCacheKey({
5145
+ rowId: req.rowId,
5146
+ tableNamespace: req.tableNamespace,
5147
+ rowKey: req.rowKey ?? undefined,
5148
+ callId: req.callId,
5149
+ }),
5150
+ );
5151
+ return {
5152
+ rowId: req.rowId,
5153
+ status: cachedResult?.result != null ? 'completed' : 'failed',
5154
+ success: cachedResult?.result != null,
5155
+ value: cachedResult?.result,
5156
+ error: cachedResult?.result != null ? null : 'Tool call failed',
5157
+ };
5158
+ });
5159
+ const toolStep = {
5160
+ type: 'tool' as const,
5161
+ toolId,
5162
+ // Keep the step trace preview-sized for large map pages.
5163
+ results: compactRowResultsPreview(stepResults),
5164
+ description: normalizeStepDescription(stepRequests[0]?.description),
5165
+ };
5166
+ if (this.activeDatasetStep) {
5167
+ this.activeDatasetStep.substeps.push(toolStep);
5168
+ } else {
5169
+ this.steps.push(toolStep);
5170
+ }
5171
+ };
5172
+
4963
5173
  const pendingRequests: ToolCallRequest[] = [];
5174
+ const recoveredRequests: ToolCallRequest[] = [];
4964
5175
  for (const req of requests) {
4965
5176
  const cached = this.getCachedToolResult(
4966
5177
  toolId,
@@ -4978,10 +5189,12 @@ export class PlayContextImpl {
4978
5189
  resolver.resolve(cached.result);
4979
5190
  this.toolCallResolvers.delete(req.callId);
4980
5191
  }
5192
+ recoveredRequests.push(req);
4981
5193
  } else {
4982
5194
  pendingRequests.push(req);
4983
5195
  }
4984
5196
  }
5197
+ recordToolStep(recoveredRequests);
4985
5198
 
4986
5199
  if (pendingRequests.length > 0) {
4987
5200
  const strategy =
@@ -5001,49 +5214,47 @@ export class PlayContextImpl {
5001
5214
  4,
5002
5215
  )
5003
5216
  : 4;
5004
- for (
5005
- let start = 0;
5006
- start < compiledBatches.length;
5007
- start += batchSize
5008
- ) {
5009
- const chunk = compiledBatches.slice(start, start + batchSize);
5010
- const settled = await Promise.allSettled(
5011
- chunk.map((batch) =>
5012
- this.callToolAPI(batch.batchOperation, batch.batchPayload),
5217
+ await executeChunkedRequests({
5218
+ requests: compiledBatches,
5219
+ batchSize,
5220
+ execute: async (batch) =>
5221
+ await this.callToolAPI(
5222
+ batch.batchOperation,
5223
+ batch.batchPayload,
5013
5224
  ),
5014
- );
5015
-
5016
- for (let index = 0; index < chunk.length; index += 1) {
5017
- const requestBatch = chunk[index]!;
5018
- const outcome = settled[index]!;
5019
- if (outcome.status === 'fulfilled') {
5225
+ onChunkComplete: async (chunkResults) => {
5226
+ for (const entry of chunkResults) {
5227
+ if (entry.error !== undefined) {
5228
+ for (const request of entry.request.memberRequests) {
5229
+ this.rejectToolCall(toolId, request, entry.error);
5230
+ }
5231
+ continue;
5232
+ }
5020
5233
  const splitResults =
5021
- outcome.value != null
5022
- ? requestBatch.splitResults(outcome.value)
5023
- : requestBatch.memberRequests.map(() => null);
5234
+ entry.result != null
5235
+ ? entry.request.splitResults(entry.result)
5236
+ : entry.request.memberRequests.map(() => null);
5024
5237
 
5025
5238
  for (
5026
5239
  let index = 0;
5027
- index < requestBatch.memberRequests.length;
5240
+ index < entry.request.memberRequests.length;
5028
5241
  index += 1
5029
5242
  ) {
5030
- const request = requestBatch.memberRequests[index]!;
5243
+ const request = entry.request.memberRequests[index]!;
5031
5244
  await this.resolveToolCall(
5032
5245
  toolId,
5033
5246
  request,
5034
5247
  splitResults[index] ?? null,
5035
5248
  );
5036
5249
  }
5037
- continue;
5038
5250
  }
5039
5251
 
5040
- for (const request of requestBatch.memberRequests) {
5041
- this.rejectToolCall(toolId, request, outcome.reason);
5042
- }
5043
- }
5044
-
5045
- this.#options.onBatchComplete?.(this.checkpoint);
5046
- }
5252
+ recordToolStep(
5253
+ chunkResults.flatMap((entry) => entry.request.memberRequests),
5254
+ );
5255
+ this.#options.onBatchComplete?.(this.checkpoint);
5256
+ },
5257
+ });
5047
5258
  } else {
5048
5259
  const batchSize = await this.governor.suggestedParallelism(
5049
5260
  toolId,
@@ -5055,66 +5266,32 @@ export class PlayContextImpl {
5055
5266
  start += batchSize
5056
5267
  ) {
5057
5268
  const chunk = pendingRequests.slice(start, start + batchSize);
5058
- const settled = await Promise.allSettled(
5059
- chunk.map((request) =>
5060
- this.callToolExecutionAPI(toolId, request.input),
5061
- ),
5269
+ await Promise.allSettled(
5270
+ chunk.map(async (request) => {
5271
+ try {
5272
+ const execution = await this.callToolExecutionAPI(
5273
+ toolId,
5274
+ request.input,
5275
+ );
5276
+ await this.resolveToolCall(
5277
+ toolId,
5278
+ request,
5279
+ execution?.result ?? null,
5280
+ execution?.metadata ?? null,
5281
+ execution?.jobId,
5282
+ execution?.meta,
5283
+ );
5284
+ } catch (error) {
5285
+ this.rejectToolCall(toolId, request, error);
5286
+ }
5287
+ }),
5062
5288
  );
5063
5289
 
5064
- for (let index = 0; index < chunk.length; index += 1) {
5065
- const request = chunk[index]!;
5066
- const outcome = settled[index]!;
5067
- if (outcome.status === 'fulfilled') {
5068
- await this.resolveToolCall(
5069
- toolId,
5070
- request,
5071
- outcome.value?.result ?? null,
5072
- outcome.value?.metadata ?? null,
5073
- outcome.value?.jobId,
5074
- outcome.value?.meta,
5075
- );
5076
- continue;
5077
- }
5078
-
5079
- this.rejectToolCall(toolId, request, outcome.reason);
5080
- }
5081
-
5290
+ recordToolStep(chunk);
5082
5291
  this.#options.onBatchComplete?.(this.checkpoint);
5083
5292
  }
5084
5293
  }
5085
5294
  }
5086
-
5087
- // Record step — nest under map if inside one
5088
- const stepResults: PlayStepRowResult[] = requests.map((req) => {
5089
- const cachedResult = this.getCachedToolResult(
5090
- toolId,
5091
- this.buildToolResultCacheKey({
5092
- rowId: req.rowId,
5093
- tableNamespace: req.tableNamespace,
5094
- rowKey: req.rowKey ?? undefined,
5095
- callId: req.callId,
5096
- }),
5097
- );
5098
- return {
5099
- rowId: req.rowId,
5100
- status: cachedResult?.result != null ? 'completed' : 'failed',
5101
- success: cachedResult?.result != null,
5102
- value: cachedResult?.result,
5103
- error: cachedResult?.result != null ? null : 'Tool call failed',
5104
- };
5105
- });
5106
- const toolStep = {
5107
- type: 'tool' as const,
5108
- toolId,
5109
- // Keep the step trace preview-sized for large map pages.
5110
- results: compactRowResultsPreview(stepResults),
5111
- description: normalizeStepDescription(requests[0]?.description),
5112
- };
5113
- if (this.activeDatasetStep) {
5114
- this.activeDatasetStep.substeps.push(toolStep);
5115
- } else {
5116
- this.steps.push(toolStep);
5117
- }
5118
5295
  }),
5119
5296
  );
5120
5297
  }
package/dist/cli/index.js CHANGED
@@ -413,10 +413,10 @@ var SDK_RELEASE = {
413
413
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
414
414
  // the SDK enrich generator's one-second stale policy.
415
415
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
416
- version: "0.1.131",
416
+ version: "0.1.132",
417
417
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
418
418
  supportPolicy: {
419
- latest: "0.1.131",
419
+ latest: "0.1.132",
420
420
  minimumSupported: "0.1.53",
421
421
  deprecatedBelow: "0.1.53",
422
422
  commandMinimumSupported: [
@@ -5027,8 +5027,14 @@ function recentUsageLines(entries) {
5027
5027
  const op = `${humanize(entry.provider)} ${humanize(entry.operation)}`.trim();
5028
5028
  const charge = entry.billing_mode === "no_bill" ? "free" : `${entry.credits ?? 0} cr`;
5029
5029
  const state = entry.charge_state === "temporary_hold" ? "temporary hold, awaiting actual usage" : entry.status || "completed";
5030
+ const auditBits = [
5031
+ entry.outcome ? `outcome=${entry.outcome}` : null,
5032
+ entry.pricing_basis ? `priced_by=${entry.pricing_basis}` : null,
5033
+ entry.run_id ? `run=${entry.run_id}` : null,
5034
+ entry.request_id ? `request=${entry.request_id}` : null
5035
+ ].filter(Boolean);
5030
5036
  lines.push(
5031
- `${op} | ${charge} | ${state} | ${entry.created_at || "unknown"}`
5037
+ `${op} | ${charge} | ${state}${auditBits.length > 0 ? ` | ${auditBits.join(" | ")}` : ""} | ${entry.created_at || "unknown"}`
5032
5038
  );
5033
5039
  }
5034
5040
  return lines;
@@ -5045,14 +5051,24 @@ function summarizeLedgerRows(summary, rows) {
5045
5051
  }
5046
5052
  function ledgerApiEntryToRow(entry) {
5047
5053
  const metadata = typeof entry.metadata === "object" && entry.metadata !== null ? entry.metadata : {};
5054
+ const audit = typeof entry.billing_audit === "object" && entry.billing_audit !== null ? entry.billing_audit : {};
5048
5055
  return {
5049
5056
  created_at: entry.created_at ?? "",
5050
5057
  delta_credits: entry.delta ?? "",
5051
5058
  reason: entry.reason ?? "",
5052
- provider: metadata.provider ?? "",
5053
- operation: metadata.operation ?? "",
5054
- request_id: metadata.requestId ?? metadata.request_id ?? "",
5055
- run_id: metadata.runId ?? metadata.run_id ?? "",
5059
+ provider: entry.provider ?? audit.provider ?? metadata.provider ?? "",
5060
+ operation: entry.operation ?? audit.operation ?? metadata.operation ?? "",
5061
+ request_id: entry.request_id ?? audit.request_id ?? metadata.requestId ?? metadata.request_id ?? "",
5062
+ run_id: entry.run_id ?? audit.run_id ?? metadata.runId ?? metadata.run_id ?? "",
5063
+ billing_stage: entry.billing_stage ?? audit.billing_stage ?? metadata.billingStage ?? "",
5064
+ charge_state: entry.charge_state ?? audit.charge_state ?? "",
5065
+ billing_mode: entry.billing_mode ?? audit.billing_mode ?? metadata.billingMode ?? metadata.chargeMode ?? "",
5066
+ pricing_model: entry.pricing_model ?? audit.pricing_model ?? metadata.pricingModel ?? "",
5067
+ pricing_basis: entry.pricing_basis ?? audit.pricing_basis ?? "",
5068
+ charge_credits: entry.charge_credits ?? audit.charge_credits ?? "",
5069
+ outcome: entry.outcome ?? audit.outcome ?? "",
5070
+ result_count: entry.result_count ?? audit.result_count ?? "",
5071
+ deepline_rough_usd: entry.deepline_rough_usd ?? "",
5056
5072
  api_key_id: entry.api_key_id ?? "",
5057
5073
  stripe_session_id: entry.stripe_session_id ?? ""
5058
5074
  };
@@ -5068,6 +5084,15 @@ function ledgerRowsToCsv(rows, header) {
5068
5084
  "operation",
5069
5085
  "request_id",
5070
5086
  "run_id",
5087
+ "billing_stage",
5088
+ "charge_state",
5089
+ "billing_mode",
5090
+ "pricing_model",
5091
+ "pricing_basis",
5092
+ "charge_credits",
5093
+ "outcome",
5094
+ "result_count",
5095
+ "deepline_rough_usd",
5071
5096
  "api_key_id",
5072
5097
  "stripe_session_id"
5073
5098
  ]
@@ -5246,6 +5271,7 @@ async function handleLedgerExportAll(options) {
5246
5271
  for (; ; ) {
5247
5272
  const params = new URLSearchParams({ limit: "5000" });
5248
5273
  if (cursor !== null) params.set("cursor", cursor);
5274
+ if (options.runId) params.set("run_id", options.runId);
5249
5275
  const payload = await http.get(
5250
5276
  `/api/v2/billing/ledger?${params.toString()}`
5251
5277
  );
@@ -5273,13 +5299,14 @@ async function handleLedgerExportAll(options) {
5273
5299
  row_count: summary.row_count,
5274
5300
  net_delta_credits: summary.net_delta_credits,
5275
5301
  scope: "current_auth_context",
5302
+ ...options.runId ? { run_id: options.runId } : {},
5276
5303
  render: {
5277
5304
  sections: [
5278
5305
  {
5279
5306
  title: "billing ledger",
5280
5307
  lines: [
5281
5308
  `Billing ledger written to ${outputPath}`,
5282
- `${summary.row_count} row(s) exported for the current auth context.`,
5309
+ `${summary.row_count} row(s) exported for the current auth context${options.runId ? ` and run ${options.runId}` : ""}.`,
5283
5310
  `Net ledger delta: ${summary.net_delta_credits} Deepline Credits`
5284
5311
  ]
5285
5312
  }
@@ -5703,6 +5730,7 @@ Notes:
5703
5730
  Examples:
5704
5731
  deepline billing ledger export all
5705
5732
  deepline billing ledger export all --output ./ledger.csv
5733
+ deepline billing ledger export all --run-id play/demo/run/abc123
5706
5734
  deepline billing ledger export all --json
5707
5735
  `
5708
5736
  ).addCommand(
@@ -5724,7 +5752,7 @@ Examples:
5724
5752
  deepline billing ledger export all
5725
5753
  deepline billing ledger export all --json
5726
5754
  `
5727
- ).option("--output <path>", "Write CSV to an explicit path").option(
5755
+ ).option("--output <path>", "Write CSV to an explicit path").option("--run-id <run_id>", "Export ledger rows for one play run.").option(
5728
5756
  "--json",
5729
5757
  "Emit JSON output. Also automatic when stdout is piped"
5730
5758
  ).action(handleLedgerExportAll)
@@ -390,10 +390,10 @@ var SDK_RELEASE = {
390
390
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
391
391
  // the SDK enrich generator's one-second stale policy.
392
392
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
393
- version: "0.1.131",
393
+ version: "0.1.132",
394
394
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
395
395
  supportPolicy: {
396
- latest: "0.1.131",
396
+ latest: "0.1.132",
397
397
  minimumSupported: "0.1.53",
398
398
  deprecatedBelow: "0.1.53",
399
399
  commandMinimumSupported: [
@@ -5016,8 +5016,14 @@ function recentUsageLines(entries) {
5016
5016
  const op = `${humanize(entry.provider)} ${humanize(entry.operation)}`.trim();
5017
5017
  const charge = entry.billing_mode === "no_bill" ? "free" : `${entry.credits ?? 0} cr`;
5018
5018
  const state = entry.charge_state === "temporary_hold" ? "temporary hold, awaiting actual usage" : entry.status || "completed";
5019
+ const auditBits = [
5020
+ entry.outcome ? `outcome=${entry.outcome}` : null,
5021
+ entry.pricing_basis ? `priced_by=${entry.pricing_basis}` : null,
5022
+ entry.run_id ? `run=${entry.run_id}` : null,
5023
+ entry.request_id ? `request=${entry.request_id}` : null
5024
+ ].filter(Boolean);
5019
5025
  lines.push(
5020
- `${op} | ${charge} | ${state} | ${entry.created_at || "unknown"}`
5026
+ `${op} | ${charge} | ${state}${auditBits.length > 0 ? ` | ${auditBits.join(" | ")}` : ""} | ${entry.created_at || "unknown"}`
5021
5027
  );
5022
5028
  }
5023
5029
  return lines;
@@ -5034,14 +5040,24 @@ function summarizeLedgerRows(summary, rows) {
5034
5040
  }
5035
5041
  function ledgerApiEntryToRow(entry) {
5036
5042
  const metadata = typeof entry.metadata === "object" && entry.metadata !== null ? entry.metadata : {};
5043
+ const audit = typeof entry.billing_audit === "object" && entry.billing_audit !== null ? entry.billing_audit : {};
5037
5044
  return {
5038
5045
  created_at: entry.created_at ?? "",
5039
5046
  delta_credits: entry.delta ?? "",
5040
5047
  reason: entry.reason ?? "",
5041
- provider: metadata.provider ?? "",
5042
- operation: metadata.operation ?? "",
5043
- request_id: metadata.requestId ?? metadata.request_id ?? "",
5044
- run_id: metadata.runId ?? metadata.run_id ?? "",
5048
+ provider: entry.provider ?? audit.provider ?? metadata.provider ?? "",
5049
+ operation: entry.operation ?? audit.operation ?? metadata.operation ?? "",
5050
+ request_id: entry.request_id ?? audit.request_id ?? metadata.requestId ?? metadata.request_id ?? "",
5051
+ run_id: entry.run_id ?? audit.run_id ?? metadata.runId ?? metadata.run_id ?? "",
5052
+ billing_stage: entry.billing_stage ?? audit.billing_stage ?? metadata.billingStage ?? "",
5053
+ charge_state: entry.charge_state ?? audit.charge_state ?? "",
5054
+ billing_mode: entry.billing_mode ?? audit.billing_mode ?? metadata.billingMode ?? metadata.chargeMode ?? "",
5055
+ pricing_model: entry.pricing_model ?? audit.pricing_model ?? metadata.pricingModel ?? "",
5056
+ pricing_basis: entry.pricing_basis ?? audit.pricing_basis ?? "",
5057
+ charge_credits: entry.charge_credits ?? audit.charge_credits ?? "",
5058
+ outcome: entry.outcome ?? audit.outcome ?? "",
5059
+ result_count: entry.result_count ?? audit.result_count ?? "",
5060
+ deepline_rough_usd: entry.deepline_rough_usd ?? "",
5045
5061
  api_key_id: entry.api_key_id ?? "",
5046
5062
  stripe_session_id: entry.stripe_session_id ?? ""
5047
5063
  };
@@ -5057,6 +5073,15 @@ function ledgerRowsToCsv(rows, header) {
5057
5073
  "operation",
5058
5074
  "request_id",
5059
5075
  "run_id",
5076
+ "billing_stage",
5077
+ "charge_state",
5078
+ "billing_mode",
5079
+ "pricing_model",
5080
+ "pricing_basis",
5081
+ "charge_credits",
5082
+ "outcome",
5083
+ "result_count",
5084
+ "deepline_rough_usd",
5060
5085
  "api_key_id",
5061
5086
  "stripe_session_id"
5062
5087
  ]
@@ -5235,6 +5260,7 @@ async function handleLedgerExportAll(options) {
5235
5260
  for (; ; ) {
5236
5261
  const params = new URLSearchParams({ limit: "5000" });
5237
5262
  if (cursor !== null) params.set("cursor", cursor);
5263
+ if (options.runId) params.set("run_id", options.runId);
5238
5264
  const payload = await http.get(
5239
5265
  `/api/v2/billing/ledger?${params.toString()}`
5240
5266
  );
@@ -5262,13 +5288,14 @@ async function handleLedgerExportAll(options) {
5262
5288
  row_count: summary.row_count,
5263
5289
  net_delta_credits: summary.net_delta_credits,
5264
5290
  scope: "current_auth_context",
5291
+ ...options.runId ? { run_id: options.runId } : {},
5265
5292
  render: {
5266
5293
  sections: [
5267
5294
  {
5268
5295
  title: "billing ledger",
5269
5296
  lines: [
5270
5297
  `Billing ledger written to ${outputPath}`,
5271
- `${summary.row_count} row(s) exported for the current auth context.`,
5298
+ `${summary.row_count} row(s) exported for the current auth context${options.runId ? ` and run ${options.runId}` : ""}.`,
5272
5299
  `Net ledger delta: ${summary.net_delta_credits} Deepline Credits`
5273
5300
  ]
5274
5301
  }
@@ -5692,6 +5719,7 @@ Notes:
5692
5719
  Examples:
5693
5720
  deepline billing ledger export all
5694
5721
  deepline billing ledger export all --output ./ledger.csv
5722
+ deepline billing ledger export all --run-id play/demo/run/abc123
5695
5723
  deepline billing ledger export all --json
5696
5724
  `
5697
5725
  ).addCommand(
@@ -5713,7 +5741,7 @@ Examples:
5713
5741
  deepline billing ledger export all
5714
5742
  deepline billing ledger export all --json
5715
5743
  `
5716
- ).option("--output <path>", "Write CSV to an explicit path").option(
5744
+ ).option("--output <path>", "Write CSV to an explicit path").option("--run-id <run_id>", "Export ledger rows for one play run.").option(
5717
5745
  "--json",
5718
5746
  "Emit JSON output. Also automatic when stdout is piped"
5719
5747
  ).action(handleLedgerExportAll)
package/dist/index.js CHANGED
@@ -284,10 +284,10 @@ var SDK_RELEASE = {
284
284
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
285
285
  // the SDK enrich generator's one-second stale policy.
286
286
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
287
- version: "0.1.131",
287
+ version: "0.1.132",
288
288
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
289
289
  supportPolicy: {
290
- latest: "0.1.131",
290
+ latest: "0.1.132",
291
291
  minimumSupported: "0.1.53",
292
292
  deprecatedBelow: "0.1.53",
293
293
  commandMinimumSupported: [
package/dist/index.mjs CHANGED
@@ -206,10 +206,10 @@ var SDK_RELEASE = {
206
206
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
207
207
  // the SDK enrich generator's one-second stale policy.
208
208
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
209
- version: "0.1.131",
209
+ version: "0.1.132",
210
210
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
211
211
  supportPolicy: {
212
- latest: "0.1.131",
212
+ latest: "0.1.132",
213
213
  minimumSupported: "0.1.53",
214
214
  deprecatedBelow: "0.1.53",
215
215
  commandMinimumSupported: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.131",
3
+ "version": "0.1.132",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {