deepline 0.1.226 → 0.1.228

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.
@@ -140,6 +140,11 @@ import type {
140
140
  PlayRunLedgerStepProgress,
141
141
  PlayRunLedgerStepStatus,
142
142
  } from '../../../shared_libs/play-runtime/run-ledger';
143
+ import {
144
+ MAX_LEDGER_LOG_LINE_LENGTH,
145
+ MAX_LEDGER_LOG_LINES_PER_EVENT,
146
+ } from '../../../shared_libs/play-runtime/ledger-safe-payload';
147
+ import { partitionWorkerLedgerEvents } from './runtime/ledger-event-batches';
143
148
  import type { PlayHarnessRpc } from '../../play-harness-worker/src/rpc-types';
144
149
  import {
145
150
  createCsvDatasetHandle,
@@ -2539,6 +2544,7 @@ type WorkerStepProgram = {
2539
2544
 
2540
2545
  type WorkerMapOptions = {
2541
2546
  description?: string;
2547
+ mode?: 'upsert' | 'net_new';
2542
2548
  concurrency?: number;
2543
2549
  key?:
2544
2550
  | string
@@ -4870,9 +4876,18 @@ function createMinimalWorkerCtx(
4870
4876
  runId: req.runId,
4871
4877
  inputOffset: 0,
4872
4878
  attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(req),
4879
+ ...(opts?.mode ? { mode: opts.mode } : {}),
4873
4880
  ...mapWriteAttempt,
4874
4881
  });
4875
4882
  const mapWriteVersion = mapReservation?.writeVersion ?? null;
4883
+ const netNewRowKeys =
4884
+ opts?.mode === 'net_new'
4885
+ ? new Set(
4886
+ (mapReservation?.pendingRows ?? [])
4887
+ .map((row) => resolveMapRowOutcomeKey(row))
4888
+ .filter((key): key is string => Boolean(key)),
4889
+ )
4890
+ : null;
4876
4891
 
4877
4892
  let totalRowsWritten = 0;
4878
4893
  let datasetLifecycleRegistered = false;
@@ -4896,7 +4911,7 @@ function createMinimalWorkerCtx(
4896
4911
  chunkIndex,
4897
4912
  });
4898
4913
  const keyStartedAt = nowMs();
4899
- const chunkEntries = chunkRows.map((row, localIndex) => {
4914
+ let chunkEntries = chunkRows.map((row, localIndex) => {
4900
4915
  const absoluteIndex = baseOffset + chunkStart + localIndex;
4901
4916
  const rowKey = resolveRowKey(
4902
4917
  row,
@@ -4906,6 +4921,11 @@ function createMinimalWorkerCtx(
4906
4921
  );
4907
4922
  return { row, absoluteIndex, rowKey };
4908
4923
  });
4924
+ if (netNewRowKeys) {
4925
+ chunkEntries = chunkEntries.filter(({ rowKey }) =>
4926
+ netNewRowKeys.has(rowKey),
4927
+ );
4928
+ }
4909
4929
  recordRunnerPerfTrace({
4910
4930
  req,
4911
4931
  phase: 'runner.map_chunk.keys',
@@ -4968,7 +4988,10 @@ function createMinimalWorkerCtx(
4968
4988
  },
4969
4989
  });
4970
4990
  mapLatencyProfile.recordPhase('prepare_rows', nowMs() - prepareStartedAt);
4971
- const progressTotalRows = rowCountHint ?? chunkRows.length;
4991
+ // net_new reserves the entire source before chunk execution, so its
4992
+ // progress describes the admitted subset rather than the source CSV.
4993
+ const progressTotalRows =
4994
+ netNewRowKeys?.size ?? rowCountHint ?? chunkRows.length;
4972
4995
  const preparedCompletedRows = Math.min(
4973
4996
  progressTotalRows,
4974
4997
  totalRowsWritten,
@@ -5023,15 +5046,8 @@ function createMinimalWorkerCtx(
5023
5046
  ({ rowKey }) => !preparedKeys.has(rowKey),
5024
5047
  );
5025
5048
  if (missingPreparedRows.length > 0) {
5026
- const rowKeySamples = missingPreparedRows
5027
- .map(({ rowKey }) => rowKey)
5028
- .slice(0, MAP_ROW_FAILURE_SAMPLE_LIMIT);
5029
5049
  throw new Error(
5030
- `ctx.dataset("${name}") runtime preparation omitted ${missingPreparedRows.length} row disposition(s). ` +
5031
- `Every input row must be claimed, completed, or blocked before execution starts.` +
5032
- (rowKeySamples.length > 0
5033
- ? ` Missing row keys: ${rowKeySamples.join(', ')}`
5034
- : ''),
5050
+ `ctx.dataset("${name}") preparation omitted ${missingPreparedRows.length} row disposition(s).`,
5035
5051
  );
5036
5052
  }
5037
5053
  const rowsToExecuteEntries = chunkEntries.filter(({ rowKey }) =>
@@ -5986,9 +6002,8 @@ function createMinimalWorkerCtx(
5986
6002
  // dataset (their recoverable state lives in the runtime sheet).
5987
6003
  if (key && executedRow) resultByKey.set(key, executedRow);
5988
6004
  }
5989
- const outEntries = chunkRows
5990
- .map((_row, index) => {
5991
- const entry = chunkEntries[index]!;
6005
+ const outEntries = chunkEntries
6006
+ .map((entry) => {
5992
6007
  const row = resultByKey.get(entry.rowKey);
5993
6008
  return row
5994
6009
  ? {
@@ -6531,11 +6546,11 @@ function createMinimalWorkerCtx(
6531
6546
  }
6532
6547
  enqueueMapProgressUpdate({
6533
6548
  completed: totalRowsWritten,
6534
- total: rowCountHint ?? undefined,
6549
+ total: netNewRowKeys?.size ?? rowCountHint ?? undefined,
6535
6550
  ...(totalRowsFailed > 0 ? { failed: totalRowsFailed } : {}),
6536
6551
  message: formatMapProgressMessage(
6537
6552
  totalRowsWritten,
6538
- rowCountHint ?? undefined,
6553
+ netNewRowKeys?.size ?? rowCountHint ?? undefined,
6539
6554
  ),
6540
6555
  });
6541
6556
  if (previewRows.length < WORKER_DATASET_PREVIEW_ROWS) {
@@ -7768,7 +7783,10 @@ async function executeRunRequest(
7768
7783
  let ledgerFlushQueueDepth = 0;
7769
7784
 
7770
7785
  const appendRunLogLine = (line: string) => {
7771
- const trimmed = redactSecretsFromLogString(line.trim());
7786
+ const trimmed = redactSecretsFromLogString(line.trim()).slice(
7787
+ 0,
7788
+ MAX_LEDGER_LOG_LINE_LENGTH,
7789
+ );
7772
7790
  if (!trimmed) return;
7773
7791
  workBudgetMeter.count('log');
7774
7792
  totalEmittedLogLines += 1;
@@ -7851,17 +7869,27 @@ async function executeRunRequest(
7851
7869
  pendingLedgerEvents = [];
7852
7870
 
7853
7871
  if (pendingRunLogLines.length > 0) {
7854
- events.push({
7855
- type: 'log.appended',
7856
- runId: req.runId,
7857
- source: 'worker',
7858
- occurredAt,
7859
- lines: pendingRunLogLines,
7860
- // Positional cursor: pendingRunLogLines always holds the LAST
7861
- // pending lines emitted on this channel, so the offset of its first
7862
- // line is total-emitted minus pending length.
7863
- channelOffset: totalEmittedLogLines - pendingRunLogLines.length,
7864
- });
7872
+ const channelOffset = totalEmittedLogLines - pendingRunLogLines.length;
7873
+ for (
7874
+ let start = 0;
7875
+ start < pendingRunLogLines.length;
7876
+ start += MAX_LEDGER_LOG_LINES_PER_EVENT
7877
+ ) {
7878
+ events.push({
7879
+ type: 'log.appended',
7880
+ runId: req.runId,
7881
+ source: 'worker',
7882
+ occurredAt,
7883
+ lines: pendingRunLogLines.slice(
7884
+ start,
7885
+ start + MAX_LEDGER_LOG_LINES_PER_EVENT,
7886
+ ),
7887
+ // Positional cursor: pendingRunLogLines always holds the LAST
7888
+ // pending lines emitted on this channel, so the offset of its first
7889
+ // line is total-emitted minus pending length.
7890
+ channelOffset: channelOffset + start,
7891
+ });
7892
+ }
7865
7893
  pendingRunLogLines = [];
7866
7894
  }
7867
7895
 
@@ -7946,19 +7974,26 @@ async function executeRunRequest(
7946
7974
  ledgerFlushInFlight = ledgerFlushInFlight
7947
7975
  .catch(() => undefined)
7948
7976
  .then(async () => {
7949
- try {
7950
- await postRuntimeApi(req.baseUrl, req.executorToken, {
7951
- action: 'append_run_events',
7952
- playId: req.runId,
7953
- events,
7954
- });
7955
- } catch {
7956
- pendingLedgerEvents = [...events, ...pendingLedgerEvents];
7957
- throw new Error('runtime run-ledger append failed');
7958
- } finally {
7959
- ledgerFlushQueueDepth = Math.max(0, ledgerFlushQueueDepth - 1);
7977
+ const batches = partitionWorkerLedgerEvents(events);
7978
+ for (let index = 0; index < batches.length; index += 1) {
7979
+ try {
7980
+ await postRuntimeApi(req.baseUrl, req.executorToken, {
7981
+ action: 'append_run_events',
7982
+ playId: req.runId,
7983
+ events: batches[index]!,
7984
+ });
7985
+ } catch {
7986
+ pendingLedgerEvents = [
7987
+ ...batches.slice(index).flat(),
7988
+ ...pendingLedgerEvents,
7989
+ ];
7990
+ throw new Error('runtime run-ledger append failed');
7991
+ }
7960
7992
  }
7961
7993
  })
7994
+ .finally(() => {
7995
+ ledgerFlushQueueDepth = Math.max(0, ledgerFlushQueueDepth - 1);
7996
+ })
7962
7997
  .catch(() => undefined);
7963
7998
  return force ? ledgerFlushInFlight : Promise.resolve();
7964
7999
  };
@@ -7978,15 +8013,21 @@ async function executeRunRequest(
7978
8013
  ]);
7979
8014
  const events = [...drainPendingLedgerEvents(now), terminalEvent];
7980
8015
  if (events.length === 0) return;
7981
- try {
7982
- await postRuntimeApi(req.baseUrl, req.executorToken, {
7983
- action: 'append_run_events',
7984
- playId: req.runId,
7985
- events,
7986
- });
7987
- } catch (error) {
7988
- pendingLedgerEvents = [...events, ...pendingLedgerEvents];
7989
- throw error;
8016
+ const batches = partitionWorkerLedgerEvents(events);
8017
+ for (let index = 0; index < batches.length; index += 1) {
8018
+ try {
8019
+ await postRuntimeApi(req.baseUrl, req.executorToken, {
8020
+ action: 'append_run_events',
8021
+ playId: req.runId,
8022
+ events: batches[index]!,
8023
+ });
8024
+ } catch (error) {
8025
+ pendingLedgerEvents = [
8026
+ ...batches.slice(index).flat(),
8027
+ ...pendingLedgerEvents,
8028
+ ];
8029
+ throw error;
8030
+ }
7990
8031
  }
7991
8032
  };
7992
8033
 
@@ -0,0 +1,81 @@
1
+ import {
2
+ MAX_LEDGER_LOG_LINE_LENGTH,
3
+ MAX_LEDGER_LOG_LINES_PER_EVENT,
4
+ } from '../../../../shared_libs/play-runtime/ledger-safe-payload';
5
+ import type { PlayRunLedgerEvent } from '../../../../shared_libs/play-runtime/run-ledger';
6
+
7
+ /** Keep a Worker-to-runtime append comfortably below the API and Convex limits. */
8
+ export const WORKER_LEDGER_EVENT_BATCH_MAX_BYTES = 128 * 1024;
9
+ export const WORKER_LEDGER_EVENTS_PER_APPEND_MAX = 100;
10
+
11
+ const encoder = new TextEncoder();
12
+
13
+ function encodedJsonBytes(value: unknown): number {
14
+ return encoder.encode(JSON.stringify(value)).length;
15
+ }
16
+
17
+ function splitLogEvent(
18
+ event: Extract<PlayRunLedgerEvent, { type: 'log.appended' }>,
19
+ ): PlayRunLedgerEvent[] {
20
+ const events: PlayRunLedgerEvent[] = [];
21
+ const lines = event.lines.map((line) =>
22
+ line.slice(0, MAX_LEDGER_LOG_LINE_LENGTH),
23
+ );
24
+ let start = 0;
25
+ while (start < lines.length) {
26
+ let end = start;
27
+ let bytes = encodedJsonBytes({ ...event, lines: [] });
28
+ while (end < lines.length && end - start < MAX_LEDGER_LOG_LINES_PER_EVENT) {
29
+ const lineBytes = encodedJsonBytes(lines[end]!);
30
+ if (
31
+ end > start &&
32
+ bytes + lineBytes + 1 > WORKER_LEDGER_EVENT_BATCH_MAX_BYTES
33
+ ) {
34
+ break;
35
+ }
36
+ bytes += lineBytes + 1;
37
+ end += 1;
38
+ }
39
+ events.push({
40
+ ...event,
41
+ lines: lines.slice(start, end),
42
+ ...(typeof event.channelOffset === 'number'
43
+ ? { channelOffset: event.channelOffset + start }
44
+ : {}),
45
+ });
46
+ start = end;
47
+ }
48
+ return events;
49
+ }
50
+
51
+ /**
52
+ * Partition ledger delivery without dropping positional log lines. The worker
53
+ * retries only unacknowledged batches, so each batch must be independently
54
+ * valid for the app-runtime and Convex boundaries.
55
+ */
56
+ export function partitionWorkerLedgerEvents(
57
+ events: readonly PlayRunLedgerEvent[],
58
+ ): PlayRunLedgerEvent[][] {
59
+ const batches: PlayRunLedgerEvent[][] = [];
60
+ let batch: PlayRunLedgerEvent[] = [];
61
+ let batchBytes = 2;
62
+
63
+ for (const event of events.flatMap((event) =>
64
+ event.type === 'log.appended' ? splitLogEvent(event) : [event],
65
+ )) {
66
+ const eventBytes = encodedJsonBytes(event);
67
+ if (
68
+ batch.length > 0 &&
69
+ (batch.length >= WORKER_LEDGER_EVENTS_PER_APPEND_MAX ||
70
+ batchBytes + eventBytes + 1 > WORKER_LEDGER_EVENT_BATCH_MAX_BYTES)
71
+ ) {
72
+ batches.push(batch);
73
+ batch = [];
74
+ batchBytes = 2;
75
+ }
76
+ batch.push(event);
77
+ batchBytes += eventBytes + 1;
78
+ }
79
+ if (batch.length > 0) batches.push(batch);
80
+ return batches;
81
+ }
@@ -243,8 +243,30 @@ type ExecuteToolRawOptions = {
243
243
  maxRetries?: number;
244
244
  };
245
245
 
246
- function resolveToolExecuteTimeoutMs(toolId: string): number | undefined {
246
+ /**
247
+ * The server launches the Apify run, polls it for `payload.timeoutMs`, then
248
+ * reads its dataset before replying. Keep the client request alive through
249
+ * that final hand-off so a pollable run id is never lost to the SDK default
250
+ * timeout.
251
+ */
252
+ const APIFY_SYNC_DEFAULT_TIMEOUT_MS = 300_000;
253
+ const APIFY_SYNC_RESPONSE_GRACE_MS = 90_000;
254
+
255
+ function resolveToolExecuteTimeoutMs(
256
+ toolId: string,
257
+ input: Record<string, unknown>,
258
+ ): number | undefined {
247
259
  const normalized = toolId.trim().toLowerCase();
260
+ if (normalized === 'apify_run_actor_sync') {
261
+ const requestedTimeoutMs = input.timeoutMs ?? APIFY_SYNC_DEFAULT_TIMEOUT_MS;
262
+ if (
263
+ typeof requestedTimeoutMs === 'number' &&
264
+ Number.isFinite(requestedTimeoutMs) &&
265
+ requestedTimeoutMs > 0
266
+ ) {
267
+ return Math.floor(requestedTimeoutMs) + APIFY_SYNC_RESPONSE_GRACE_MS;
268
+ }
269
+ }
248
270
  return normalized === 'deeplineagent' ||
249
271
  normalized === 'deeplineagent_deeplineagent' ||
250
272
  normalized === 'ai_inference' ||
@@ -1417,7 +1439,7 @@ export class DeeplineClient {
1417
1439
  headers,
1418
1440
  {
1419
1441
  forbiddenAsApiError: true,
1420
- timeout: options?.timeout ?? resolveToolExecuteTimeoutMs(toolId),
1442
+ timeout: options?.timeout ?? resolveToolExecuteTimeoutMs(toolId, input),
1421
1443
  maxRetries: options?.maxRetries ?? 0,
1422
1444
  exactUrlOnly: true,
1423
1445
  },
@@ -161,7 +161,7 @@ export interface PlayCallOptions {
161
161
  * sqlListeners: [
162
162
  * {
163
163
  * id: 'jobs',
164
- * tool: 'tamradar.company_radar',
164
+ * tool: 'deepline_native.company_radar',
165
165
  * stream: 'company_job_openings',
166
166
  * operations: ['INSERT'],
167
167
  * },
@@ -237,7 +237,7 @@ export type SqlListenerWhere = {
237
237
  export type SqlListenerDeclaration = {
238
238
  /** Short id unique inside this play. Deepline stores it as playName.id. */
239
239
  id: string;
240
- /** Modeled monitor tool id, for example "tamradar.company_radar". */
240
+ /** Modeled monitor tool id, for example "deepline_native.company_radar". */
241
241
  tool: string;
242
242
  /** Stream key exposed by the modeled monitor tool, for example "company_job_openings". */
243
243
  stream: string;
@@ -560,6 +560,11 @@ export type DatasetBuilder<
560
560
  */
561
561
  run(options?: {
562
562
  description?: string;
563
+ /**
564
+ * `upsert` (default) returns every input row, reusing persisted work for
565
+ * existing keys. `net_new` atomically inserts and returns only unseen keys.
566
+ */
567
+ mode?: 'upsert' | 'net_new';
563
568
  key?:
564
569
  | (keyof InputRow & string)
565
570
  | readonly (keyof InputRow & string)[]
@@ -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.226',
111
+ version: '0.1.228',
112
112
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
113
113
  supportPolicy: {
114
- latest: '0.1.226',
114
+ latest: '0.1.228',
115
115
  minimumSupported: '0.1.53',
116
116
  deprecatedBelow: '0.1.219',
117
117
  commandMinimumSupported: [