deepline 0.3.180 → 0.3.182

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.
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.180',
203
+ version: '0.3.182',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -763,7 +763,10 @@ export interface PlayRunPackage {
763
763
  sqlQualifiedTableName?: string;
764
764
  recovered?: true;
765
765
  exportUnavailable?: {
766
- reason: 'empty_dataset' | 'shared_table_namespace';
766
+ reason:
767
+ | 'empty_dataset'
768
+ | 'shared_table_namespace'
769
+ | 'returned_dataset_not_persisted';
767
770
  message: string;
768
771
  };
769
772
  preview?: Record<string, unknown>;
@@ -1073,6 +1073,7 @@ function getDbSessionCacheKey(input: {
1073
1073
  limits?: DbSessionLimits;
1074
1074
  sheetContract?: PlaySheetContract | null;
1075
1075
  userEmail?: string | null;
1076
+ preloadedSessionId?: string | null;
1076
1077
  }): string {
1077
1078
  // Worker processes are long-lived. Include a hash of the executor token plus
1078
1079
  // the full requested access shape so a pooled runtime cannot reuse a scoped
@@ -1092,12 +1093,15 @@ function getDbSessionCacheKey(input: {
1092
1093
  JSON.stringify(input.limits ?? {}),
1093
1094
  JSON.stringify(input.sheetContract ?? null),
1094
1095
  input.userEmail?.trim() ?? '',
1096
+ input.preloadedSessionId?.trim() ?? '',
1095
1097
  ].join('::');
1096
1098
  }
1097
1099
 
1098
1100
  function getRuntimeSheetEnsureCacheKey(input: {
1099
1101
  baseUrl?: string | null;
1100
1102
  orgId?: string | null;
1103
+ postgresUrl?: string | null;
1104
+ preloadedSessionId?: string | null;
1101
1105
  playName: string;
1102
1106
  runId?: string | null;
1103
1107
  tableNamespace: string;
@@ -1108,9 +1112,20 @@ function getRuntimeSheetEnsureCacheKey(input: {
1108
1112
  .update(JSON.stringify(input.sheetContract))
1109
1113
  .digest('hex')
1110
1114
  .slice(0, 24);
1115
+ // A preloaded session can be replaced while the logical play/run/table
1116
+ // identity stays the same. Do not reuse schema readiness from a different
1117
+ // Neon database or role session. Keep both the raw credential and session
1118
+ // identity out of the cache key and logs by hashing them together.
1119
+ const sessionIdentityHash = createHash('sha256')
1120
+ .update(input.postgresUrl?.trim() ?? '')
1121
+ .update('\0')
1122
+ .update(input.preloadedSessionId?.trim() ?? '')
1123
+ .digest('hex')
1124
+ .slice(0, 24);
1111
1125
  return [
1112
1126
  input.baseUrl?.trim() ?? '',
1113
1127
  input.orgId?.trim() ?? '',
1128
+ sessionIdentityHash,
1114
1129
  input.playName,
1115
1130
  input.runId?.trim() ?? '',
1116
1131
  normalizeTableNamespace(input.tableNamespace),
@@ -1337,6 +1352,8 @@ async function ensureRuntimeSheetForPreloadedSession(
1337
1352
  const cacheKey = getRuntimeSheetEnsureCacheKey({
1338
1353
  baseUrl: context.baseUrl,
1339
1354
  orgId: input.session.target.orgId,
1355
+ postgresUrl: input.session.postgresUrl,
1356
+ preloadedSessionId: input.session.sessionId,
1340
1357
  playName: context.playName,
1341
1358
  runId: context.runId,
1342
1359
  tableNamespace: input.tableNamespace,
@@ -1461,6 +1478,22 @@ async function ensureRuntimeSheetForPreloadedSession(
1461
1478
  }
1462
1479
  }
1463
1480
 
1481
+ /**
1482
+ * Test-only reset for the isolate-scoped runtime caches.
1483
+ *
1484
+ * Production workers intentionally retain these maps between requests. Tests
1485
+ * that replace the Postgres driver or session fixtures must clear them so a
1486
+ * prior case cannot satisfy a later case with a stale session or readiness
1487
+ * promise.
1488
+ */
1489
+ export function __resetRuntimeApiCachesForTests(): void {
1490
+ dbSessionCache.clear();
1491
+ dbSessionInFlight.clear();
1492
+ postgresPools.clear();
1493
+ runtimePostgresAdmissions.clear();
1494
+ runtimeSheetEnsureCache.clear();
1495
+ }
1496
+
1464
1497
  function operationsSatisfyRequest(
1465
1498
  candidate: readonly DbSessionOperation[],
1466
1499
  requested: readonly DbSessionOperation[],
@@ -1661,6 +1694,11 @@ async function getRuntimeDbSession(
1661
1694
  },
1662
1695
  ): Promise<CreateDbSessionResponse> {
1663
1696
  const userEmail = normalizeRuntimeUserEmail(context.userEmail);
1697
+ // Preloaded sessions are supplied by the caller and can change while the
1698
+ // logical request shape remains identical. Include the matched session
1699
+ // identity so a long-lived worker never returns a stale scoped database
1700
+ // session after a refresh or test/preview reset.
1701
+ const matchedPreloadedSession = findPreloadedRuntimeDbSession(context, input);
1664
1702
  const cacheKey = getDbSessionCacheKey({
1665
1703
  baseUrl: context.baseUrl,
1666
1704
  executorToken: context.executorToken,
@@ -1672,6 +1710,7 @@ async function getRuntimeDbSession(
1672
1710
  limits: input.limits,
1673
1711
  sheetContract: input.sheetContract,
1674
1712
  userEmail,
1713
+ preloadedSessionId: matchedPreloadedSession?.sessionId,
1675
1714
  });
1676
1715
  const cached = dbSessionCache.get(cacheKey)?.session;
1677
1716
  if (cached) {
@@ -1687,7 +1726,7 @@ async function getRuntimeDbSession(
1687
1726
  }
1688
1727
 
1689
1728
  const sessionPromise = (async (): Promise<CreateDbSessionResponse> => {
1690
- const preloaded = findPreloadedRuntimeDbSession(context, input);
1729
+ const preloaded = matchedPreloadedSession;
1691
1730
  if (preloaded) {
1692
1731
  const unwrappedPreloaded = await unwrapRuntimeDbSession(
1693
1732
  context,
@@ -13126,6 +13126,22 @@ function postgresSchedulerReceiptLeaseId(input: {
13126
13126
  : null;
13127
13127
  }
13128
13128
 
13129
+ function postgresSchedulerReceiptOwnerIsTerminalSql(
13130
+ receiptTableReference: string,
13131
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
13132
+ ): string {
13133
+ return `EXISTS (
13134
+ SELECT 1
13135
+ FROM ${tableName('runs', options)} AS receipt_owner_run
13136
+ WHERE receipt_owner_run.run_id = COALESCE(
13137
+ ${receiptTableReference}.lease_owner_run_id,
13138
+ ${receiptTableReference}.run_id
13139
+ )
13140
+ AND receipt_owner_run.org_id = ${receiptTableReference}.org_id
13141
+ AND receipt_owner_run.status IN ('completed', 'failed', 'cancelled')
13142
+ )`;
13143
+ }
13144
+
13129
13145
  function mapReceiptRow(row: ReceiptRow): RuntimeStepReceipt {
13130
13146
  const leaseExpiresAt = row.lease_expires_at;
13131
13147
  const outputRef = isRuntimeReceiptValueReference(row.output_ref)
@@ -13429,6 +13445,11 @@ export async function claimPostgresSchedulerReceipt(
13429
13445
  );
13430
13446
  const leaseId =
13431
13447
  input.leaseId?.trim() || postgresSchedulerReceiptLeaseId(input);
13448
+ const receiptTable = tableName('receipts', options);
13449
+ const ownerIsTerminal = postgresSchedulerReceiptOwnerIsTerminalSql(
13450
+ receiptTable,
13451
+ options,
13452
+ );
13432
13453
  const result = await client.query<ReceiptRow & { claim_state: string }>(
13433
13454
  `
13434
13455
  WITH terminal AS (
@@ -13436,14 +13457,14 @@ export async function claimPostgresSchedulerReceipt(
13436
13457
  -- explicitly forces refresh. Read them before the UPSERT so popular
13437
13458
  -- prebuilt keys do not take a conflicting-row lock or create WAL.
13438
13459
  SELECT k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
13439
- FROM ${tableName('receipts', options)}
13460
+ FROM ${receiptTable}
13440
13461
  WHERE org_id = $9
13441
13462
  AND k = $1
13442
13463
  AND status IN ('completed', 'skipped')
13443
13464
  AND ($7::boolean IS NOT TRUE OR run_id = $2)
13444
13465
  ),
13445
13466
  claimed AS (
13446
- INSERT INTO ${tableName('receipts', options)} (
13467
+ INSERT INTO ${receiptTable} (
13447
13468
  org_id,
13448
13469
  k,
13449
13470
  status,
@@ -13484,33 +13505,45 @@ export async function claimPostgresSchedulerReceipt(
13484
13505
  error_payload = NULL,
13485
13506
  failure_kind = 0,
13486
13507
  updated_at = now()
13487
- WHERE ${tableName('receipts', options)}.status = ANY($3::text[])
13488
- AND ${tableName('receipts', options)}.org_id = $9
13508
+ WHERE ${receiptTable}.status = ANY($3::text[])
13509
+ AND ${receiptTable}.org_id = $9
13489
13510
  AND (
13490
- ${tableName('receipts', options)}.status <> 'failed'
13511
+ ${receiptTable}.status <> 'failed'
13491
13512
  OR $7::boolean
13492
13513
  OR $8::boolean
13493
- OR ${tableName('receipts', options)}.run_id IS DISTINCT FROM $2
13514
+ OR ${receiptTable}.run_id IS DISTINCT FROM $2
13494
13515
  )
13495
13516
  AND (
13496
- ${tableName('receipts', options)}.status = 'failed'
13497
- OR ${tableName('receipts', options)}.lease_id IS NULL
13498
- AND ${tableName('receipts', options)}.lease_expires_at IS NULL
13499
- OR ${tableName('receipts', options)}.lease_expires_at <= now()
13517
+ (
13518
+ ${receiptTable}.status IN ('queued', 'running')
13519
+ AND ${ownerIsTerminal}
13520
+ )
13500
13521
  OR (
13501
- -- Billed-implies-durable: a higher scheduler invocation must never
13502
- -- steal a live work-receipt lease while the prior invocation may
13503
- -- still be inside a provider call. Same-run reclaims are allowed
13504
- -- only after the lease TTL proves the prior attempt is dead.
13505
- ${tableName('receipts', options)}.lease_id IS NOT NULL
13506
- AND ${tableName('receipts', options)}.lease_expires_at IS NOT NULL
13507
- AND ${tableName('receipts', options)}.lease_expires_at <= now()
13508
- AND
13509
- COALESCE(
13510
- ${tableName('receipts', options)}.lease_owner_run_id,
13511
- ${tableName('receipts', options)}.run_id
13512
- ) = $2
13513
- AND COALESCE(${tableName('receipts', options)}.lease_owner_attempt, 0) < $5::integer
13522
+ (
13523
+ ${receiptTable}.status NOT IN ('queued', 'running')
13524
+ OR $10::boolean
13525
+ )
13526
+ AND (
13527
+ ${receiptTable}.status = 'failed'
13528
+ OR ${receiptTable}.lease_id IS NULL
13529
+ AND ${receiptTable}.lease_expires_at IS NULL
13530
+ OR ${receiptTable}.lease_expires_at <= now()
13531
+ OR (
13532
+ -- Billed-implies-durable: a higher scheduler invocation must never
13533
+ -- steal a live work-receipt lease while the prior invocation may
13534
+ -- still be inside a provider call. Same-run reclaims are allowed
13535
+ -- only after the lease TTL proves the prior attempt is dead.
13536
+ ${receiptTable}.lease_id IS NOT NULL
13537
+ AND ${receiptTable}.lease_expires_at IS NOT NULL
13538
+ AND ${receiptTable}.lease_expires_at <= now()
13539
+ AND
13540
+ COALESCE(
13541
+ ${receiptTable}.lease_owner_run_id,
13542
+ ${receiptTable}.run_id
13543
+ ) = $2
13544
+ AND COALESCE(${receiptTable}.lease_owner_attempt, 0) < $5::integer
13545
+ )
13546
+ )
13514
13547
  )
13515
13548
  )
13516
13549
  RETURNING k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
@@ -13530,13 +13563,14 @@ export async function claimPostgresSchedulerReceipt(
13530
13563
  ? ['pending', 'failed', 'running', 'completed', 'skipped']
13531
13564
  : input.reclaimRunning
13532
13565
  ? ['pending', 'failed', 'running']
13533
- : ['pending', 'failed'],
13566
+ : ['pending', 'failed', 'queued', 'running'],
13534
13567
  leaseId,
13535
13568
  runAttempt,
13536
13569
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
13537
13570
  input.forceRefresh === true,
13538
13571
  input.forceFailedRefresh === true,
13539
13572
  receiptOrgId(options),
13573
+ input.forceRefresh === true || input.reclaimRunning === true,
13540
13574
  ],
13541
13575
  );
13542
13576
  if (result.rows[0]) {
@@ -13581,6 +13615,11 @@ export async function claimPostgresSchedulerReceipts(
13581
13615
  const providedLeaseId = input.leaseIds?.[index]?.trim();
13582
13616
  return providedLeaseId || postgresSchedulerReceiptLeaseId(input);
13583
13617
  });
13618
+ const receiptTable = tableName('receipts', options);
13619
+ const ownerIsTerminal = postgresSchedulerReceiptOwnerIsTerminalSql(
13620
+ receiptTable,
13621
+ options,
13622
+ );
13584
13623
  const result = await client.query<ReceiptRow & { claim_state?: string }>(
13585
13624
  `
13586
13625
  WITH inputs AS (
@@ -13608,7 +13647,7 @@ export async function claimPostgresSchedulerReceipts(
13608
13647
  receipt.lease_expires_at,
13609
13648
  inputs.ord
13610
13649
  FROM inputs
13611
- JOIN ${tableName('receipts', options)} AS receipt
13650
+ JOIN ${receiptTable} AS receipt
13612
13651
  ON receipt.org_id = $10
13613
13652
  AND receipt.k = inputs.k
13614
13653
  WHERE receipt.status IN ('completed', 'skipped')
@@ -13628,7 +13667,7 @@ export async function claimPostgresSchedulerReceipts(
13628
13667
  ORDER BY inputs.k, inputs.ord
13629
13668
  ),
13630
13669
  claimed AS (
13631
- INSERT INTO ${tableName('receipts', options)} (
13670
+ INSERT INTO ${receiptTable} (
13632
13671
  org_id,
13633
13672
  k,
13634
13673
  status,
@@ -13668,19 +13707,31 @@ export async function claimPostgresSchedulerReceipts(
13668
13707
  error_payload = NULL,
13669
13708
  failure_kind = 0,
13670
13709
  updated_at = now()
13671
- WHERE ${tableName('receipts', options)}.status = ANY($3::text[])
13672
- AND ${tableName('receipts', options)}.org_id = $10
13710
+ WHERE ${receiptTable}.status = ANY($3::text[])
13711
+ AND ${receiptTable}.org_id = $10
13673
13712
  AND (
13674
- ${tableName('receipts', options)}.status <> 'failed'
13713
+ ${receiptTable}.status <> 'failed'
13675
13714
  OR $7::boolean
13676
13715
  OR $8::boolean
13677
- OR ${tableName('receipts', options)}.run_id IS DISTINCT FROM $2
13716
+ OR ${receiptTable}.run_id IS DISTINCT FROM $2
13678
13717
  )
13679
13718
  AND (
13680
- ${tableName('receipts', options)}.status = 'failed'
13681
- OR ${tableName('receipts', options)}.lease_id IS NULL
13682
- AND ${tableName('receipts', options)}.lease_expires_at IS NULL
13683
- OR ${tableName('receipts', options)}.lease_expires_at <= now()
13719
+ (
13720
+ ${receiptTable}.status IN ('queued', 'running')
13721
+ AND ${ownerIsTerminal}
13722
+ )
13723
+ OR (
13724
+ (
13725
+ ${receiptTable}.status NOT IN ('queued', 'running')
13726
+ OR $11::boolean
13727
+ )
13728
+ AND (
13729
+ ${receiptTable}.status = 'failed'
13730
+ OR ${receiptTable}.lease_id IS NULL
13731
+ AND ${receiptTable}.lease_expires_at IS NULL
13732
+ OR ${receiptTable}.lease_expires_at <= now()
13733
+ )
13734
+ )
13684
13735
  )
13685
13736
  RETURNING k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
13686
13737
  ),
@@ -13703,7 +13754,7 @@ export async function claimPostgresSchedulerReceipts(
13703
13754
  AS replayed_claim,
13704
13755
  inputs.ord
13705
13756
  FROM inputs
13706
- JOIN ${tableName('receipts', options)} AS receipt
13757
+ JOIN ${receiptTable} AS receipt
13707
13758
  ON receipt.org_id = $10
13708
13759
  AND receipt.k = inputs.k
13709
13760
  WHERE (
@@ -13801,7 +13852,7 @@ export async function claimPostgresSchedulerReceipts(
13801
13852
  ? ['pending', 'failed', 'running', 'completed', 'skipped']
13802
13853
  : input.reclaimRunning
13803
13854
  ? ['pending', 'failed', 'running']
13804
- : ['pending', 'failed'],
13855
+ : ['pending', 'failed', 'queued', 'running'],
13805
13856
  leaseIds,
13806
13857
  runAttempt,
13807
13858
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
@@ -13809,6 +13860,7 @@ export async function claimPostgresSchedulerReceipts(
13809
13860
  input.forceFailedRefresh === true,
13810
13861
  input.forceRefresh === true,
13811
13862
  receiptOrgId(options),
13863
+ input.forceRefresh === true || input.reclaimRunning === true,
13812
13864
  ],
13813
13865
  );
13814
13866
  return result.rows.map((row) =>
@@ -14511,9 +14563,9 @@ export async function releasePostgresSchedulerReceipt(
14511
14563
 
14512
14564
  /**
14513
14565
  * The write that returns a claimed receipt to the free pool. Every release
14514
- * path shares it so a successor's non-forced claim — whose status set is
14515
- * `('pending','failed')` and whose lease guard requires no live lease — always
14516
- * matches what a dead owner left behind.
14566
+ * path shares it so a successor's non-forced claim always matches what a dead
14567
+ * owner left behind. Claimers also understand terminal ownership directly so
14568
+ * a later writer release can stop rewriting every receipt synchronously.
14517
14569
  */
14518
14570
  const RELEASE_RECEIPT_TO_PENDING_SET_CLAUSE = `status = 'pending',
14519
14571
  output_json = NULL,
@@ -1786,14 +1786,13 @@ function applySerializedListDatasets(
1786
1786
  });
1787
1787
  if (Object.prototype.hasOwnProperty.call(result.toolResponse, 'rawV2')) {
1788
1788
  const view = result.toolResponse.view ?? 'rawV2';
1789
- if (view === 'data' && isRecord(result.toolResponse.rawV2)) {
1790
- result.toolResponse.rawV2 = {
1791
- ...result.toolResponse.rawV2,
1792
- data: result.toolResponse.raw,
1793
- };
1794
- } else {
1795
- result.toolResponse.rawV2 = result.toolResponse.raw;
1796
- }
1789
+ result.toolResponse.rawV2 = rawV2WithSerializedListRows({
1790
+ rawV2: result.toolResponse.rawV2,
1791
+ view,
1792
+ lists: result._metadata.lists,
1793
+ listDatasets,
1794
+ listRows,
1795
+ });
1797
1796
  }
1798
1797
  Object.defineProperty(result, SERIALIZED_TOOL_LIST_ROWS, {
1799
1798
  value: listRows,
@@ -1886,15 +1885,32 @@ function responseMetaOutsideRawV2(input: {
1886
1885
  return Object.keys(responseMeta).length > 0 ? responseMeta : undefined;
1887
1886
  }
1888
1887
 
1889
- function rawV2WithSerializedRawPreview(input: {
1888
+ function rawV2WithSerializedListRows(input: {
1890
1889
  rawV2: unknown;
1891
1890
  view: 'data' | 'rawV2';
1892
- raw: unknown;
1891
+ lists: Record<string, ToolResultListMetadata>;
1892
+ listDatasets:
1893
+ | Record<string, SerializedPlayDataset<Record<string, unknown>>>
1894
+ | undefined;
1895
+ listRows: Record<string, Array<Record<string, unknown>>> | undefined;
1893
1896
  }): unknown {
1894
1897
  if (input.view === 'data' && isRecord(input.rawV2)) {
1895
- return { ...input.rawV2, data: input.raw };
1898
+ return {
1899
+ ...input.rawV2,
1900
+ data: rawWithSerializedListRows({
1901
+ raw: input.rawV2.data,
1902
+ lists: input.lists,
1903
+ listDatasets: input.listDatasets,
1904
+ listRows: input.listRows,
1905
+ }),
1906
+ };
1896
1907
  }
1897
- return input.raw;
1908
+ return rawWithSerializedListRows({
1909
+ raw: input.rawV2,
1910
+ lists: input.lists,
1911
+ listDatasets: input.listDatasets,
1912
+ listRows: input.listRows,
1913
+ });
1898
1914
  }
1899
1915
 
1900
1916
  export function serializeToolExecuteResult(
@@ -1915,18 +1931,6 @@ export function serializeToolExecuteResult(
1915
1931
  });
1916
1932
  if (Object.prototype.hasOwnProperty.call(value.toolResponse, 'rawV2')) {
1917
1933
  const view = value.toolResponse.view ?? 'rawV2';
1918
- const rawV2 = rawV2WithSerializedRawPreview({
1919
- rawV2: value.toolResponse.rawV2,
1920
- view,
1921
- raw:
1922
- view === 'data'
1923
- ? serializedRaw
1924
- : serializedRawWithListPreviews({
1925
- raw: value.toolResponse.rawV2,
1926
- metadata,
1927
- listDatasets,
1928
- }),
1929
- });
1930
1934
  const responseMeta = responseMetaOutsideRawV2({
1931
1935
  rawV2: value.toolResponse.rawV2,
1932
1936
  view,
@@ -1939,8 +1943,20 @@ export function serializeToolExecuteResult(
1939
1943
  ...(isRecord(value.meta) ? { meta: value.meta } : {}),
1940
1944
  ...(isRecord(value.billing) ? { billing: value.billing } : {}),
1941
1945
  toolResponse: {
1942
- rawV2,
1946
+ // rawV2 is the canonical complete provider response. Only the legacy
1947
+ // compatibility projection is allowed to use bounded list previews.
1948
+ rawV2: value.toolResponse.rawV2,
1943
1949
  view,
1950
+ ...(sameJsonValue(
1951
+ serializedRaw,
1952
+ legacyRawFromToolResponseRawV2(
1953
+ value.toolResponse.rawV2,
1954
+ view,
1955
+ responseMeta,
1956
+ ),
1957
+ )
1958
+ ? {}
1959
+ : { legacyRaw: serializedRaw }),
1944
1960
  ...(responseMeta ? { responseMeta } : {}),
1945
1961
  },
1946
1962
  ...(listDatasets ? { listDatasets } : {}),
@@ -2001,13 +2017,19 @@ export function deserializeToolExecuteResult(
2001
2017
  view,
2002
2018
  value.toolResponse.responseMeta,
2003
2019
  );
2020
+ const legacyRaw = Object.prototype.hasOwnProperty.call(
2021
+ value.toolResponse,
2022
+ 'legacyRaw',
2023
+ )
2024
+ ? value.toolResponse.legacyRaw
2025
+ : raw;
2004
2026
  return applySerializedListDatasets(
2005
2027
  applySerializedTargetValues(
2006
2028
  createToolExecuteResult({
2007
2029
  status: value.status,
2008
2030
  jobId: value.job_id,
2009
2031
  result: {
2010
- data: raw,
2032
+ data: legacyRaw,
2011
2033
  ...(Object.keys(meta).length > 0 ? { meta } : {}),
2012
2034
  },
2013
2035
  response: {
@@ -139,6 +139,8 @@ export type SerializedToolExecuteResultV2 = Omit<
139
139
  toolResponse: {
140
140
  rawV2: unknown;
141
141
  view: 'data' | 'rawV2';
142
+ /** Bounded legacy compatibility projection; canonical data stays in rawV2. */
143
+ legacyRaw?: unknown;
142
144
  /** Deepline-owned additions not already represented in `rawV2.meta`. */
143
145
  responseMeta?: Record<string, unknown>;
144
146
  };
package/dist/cli/index.js CHANGED
@@ -3051,7 +3051,7 @@ var SDK_RELEASE = {
3051
3051
  // getters keep their established compatibility behavior.
3052
3052
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3053
3053
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3054
- version: "0.3.180",
3054
+ version: "0.3.182",
3055
3055
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3056
3056
  packageCapabilities: {
3057
3057
  updatePreferences: 1
@@ -14321,7 +14321,7 @@ function canonicalRowsInfoFromCandidate(input2) {
14321
14321
  candidate.value.recoveredReturnPaths
14322
14322
  )
14323
14323
  } : {},
14324
- ...isRecord8(candidate.value.exportUnavailable) && (candidate.value.exportUnavailable.reason === "empty_dataset" || candidate.value.exportUnavailable.reason === "shared_table_namespace") ? {
14324
+ ...isRecord8(candidate.value.exportUnavailable) && (candidate.value.exportUnavailable.reason === "empty_dataset" || candidate.value.exportUnavailable.reason === "shared_table_namespace" || candidate.value.exportUnavailable.reason === "returned_dataset_not_persisted") ? {
14325
14325
  exportUnavailableReason: candidate.value.exportUnavailable.reason,
14326
14326
  ...typeof candidate.value.exportUnavailable.message === "string" ? {
14327
14327
  exportUnavailableMessage: candidate.value.exportUnavailable.message
@@ -26362,15 +26362,37 @@ function assertDatasetHasExportableRows(input2) {
26362
26362
  const datasetLabel = input2.datasetPath ?? input2.rowsInfo.tableNamespace ?? input2.rowsInfo.source ?? "result.rows";
26363
26363
  const available = input2.availableRows.map((info) => info.tableNamespace ?? info.source).filter((name) => typeof name === "string");
26364
26364
  const runStatus = String(input2.status.status ?? "").toLowerCase();
26365
+ const advertisedRows = Math.max(
26366
+ input2.rowsInfo.totalRows,
26367
+ input2.rowsInfo.rows.length
26368
+ );
26369
+ const terminalRunWithMissingAdvertisedRows = advertisedRows > 0 && (runStatus === "completed" || runStatus === "failed" || runStatus === "cancelled");
26370
+ const inspectCommand = `deepline runs get ${input2.status.runId} --full --json`;
26371
+ const retryCommand = runExportRetryCommand(
26372
+ input2.status.runId,
26373
+ "output.csv",
26374
+ datasetLabel
26375
+ );
26376
+ const explanation = terminalRunWithMissingAdvertisedRows ? ` The terminal result advertised ${advertisedRows} row(s), but none are currently attributed to this run's durable sheet. Those rows may have been reused from an earlier run or the projection may still be lagging; export refuses to guess another run's rows. Retry the same export after projection catches up (do not rerun the Play): ${retryCommand}. If it remains empty, inspect the full package with ${inspectCommand} and export the run that owns the durable dataset; this command will not use a database-wide table query that could mix runs.` : ` The run status is '${runStatus || "unknown"}', so it likely failed before persisting rows to this dataset. That means provider calls may have been billed without a durable row; inspect ${inspectCommand}, fix the failure cause, then rerun the Play so completed receipts can be reused.`;
26365
26377
  throw new DeeplineError(
26366
- `Run ${input2.status.runId} has zero rows to export from dataset '${datasetLabel}'. The run status is '${runStatus || "unknown"}', so it likely failed before persisting rows to this dataset \u2014 provider calls may have been billed without a durable row. Fix the run's failure cause and re-run to resume.` + (available.length > 0 ? ` Available datasets: ${available.join(", ")}.` : " No datasets were materialized for this run."),
26378
+ `Run ${input2.status.runId} has zero rows to export from dataset '${datasetLabel}'.` + explanation + (available.length > 0 ? ` Available datasets: ${available.join(", ")}.` : " No datasets were materialized for this run."),
26367
26379
  void 0,
26368
26380
  "RUN_EXPORT_EMPTY",
26369
26381
  {
26370
26382
  runId: input2.status.runId,
26371
26383
  dataset: datasetLabel,
26372
26384
  available,
26373
- runStatus: runStatus || null
26385
+ runStatus: runStatus || null,
26386
+ guidance: terminalRunWithMissingAdvertisedRows ? {
26387
+ inspect_command: inspectCommand,
26388
+ retry_command: retryCommand,
26389
+ do_not_rerun: true,
26390
+ reason: "Run output is terminal but its rows are not attributed to this run; export refuses to guess another writer."
26391
+ } : {
26392
+ inspect_command: inspectCommand,
26393
+ do_not_rerun: false,
26394
+ reason: "The run did not prove that any rows were durably persisted for this dataset."
26395
+ }
26374
26396
  }
26375
26397
  );
26376
26398
  }
@@ -26452,6 +26474,24 @@ ${commands.join("\n")}`,
26452
26474
  `Run ${status.runId} did not expose a row-shaped final output to export.`
26453
26475
  );
26454
26476
  }
26477
+ if (rowsInfo.exportUnavailableReason === "returned_dataset_not_persisted") {
26478
+ const datasetLabel = options.datasetPath ?? rowsInfo.source ?? rowsInfo.datasetId ?? "result.rows";
26479
+ throw new DeeplineError(
26480
+ rowsInfo.exportUnavailableMessage ?? `Run ${status.runId} returned rows for '${datasetLabel}' without a run-scoped durable dataset. Persist the rows with ctx.dataset("rows", rows).run() before returning them.`,
26481
+ void 0,
26482
+ "RUN_EXPORT_NOT_AVAILABLE",
26483
+ {
26484
+ runId: status.runId,
26485
+ dataset: datasetLabel,
26486
+ reason: rowsInfo.exportUnavailableReason,
26487
+ guidance: {
26488
+ do_not_rerun: false,
26489
+ reason: "The returned rows exist only in the in-memory tool result.",
26490
+ persist_rows: 'ctx.dataset("rows", rows).run()'
26491
+ }
26492
+ }
26493
+ );
26494
+ }
26455
26495
  const attempts = Math.max(1, Math.trunc(options.attempts ?? 1));
26456
26496
  const retryDelayMs = Math.max(0, Math.trunc(options.retryDelayMs ?? 0));
26457
26497
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -3046,7 +3046,7 @@ var SDK_RELEASE = {
3046
3046
  // getters keep their established compatibility behavior.
3047
3047
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3048
3048
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3049
- version: "0.3.180",
3049
+ version: "0.3.182",
3050
3050
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3051
3051
  packageCapabilities: {
3052
3052
  updatePreferences: 1
@@ -14336,7 +14336,7 @@ function canonicalRowsInfoFromCandidate(input2) {
14336
14336
  candidate.value.recoveredReturnPaths
14337
14337
  )
14338
14338
  } : {},
14339
- ...isRecord8(candidate.value.exportUnavailable) && (candidate.value.exportUnavailable.reason === "empty_dataset" || candidate.value.exportUnavailable.reason === "shared_table_namespace") ? {
14339
+ ...isRecord8(candidate.value.exportUnavailable) && (candidate.value.exportUnavailable.reason === "empty_dataset" || candidate.value.exportUnavailable.reason === "shared_table_namespace" || candidate.value.exportUnavailable.reason === "returned_dataset_not_persisted") ? {
14340
14340
  exportUnavailableReason: candidate.value.exportUnavailable.reason,
14341
14341
  ...typeof candidate.value.exportUnavailable.message === "string" ? {
14342
14342
  exportUnavailableMessage: candidate.value.exportUnavailable.message
@@ -26434,15 +26434,37 @@ function assertDatasetHasExportableRows(input2) {
26434
26434
  const datasetLabel = input2.datasetPath ?? input2.rowsInfo.tableNamespace ?? input2.rowsInfo.source ?? "result.rows";
26435
26435
  const available = input2.availableRows.map((info) => info.tableNamespace ?? info.source).filter((name) => typeof name === "string");
26436
26436
  const runStatus = String(input2.status.status ?? "").toLowerCase();
26437
+ const advertisedRows = Math.max(
26438
+ input2.rowsInfo.totalRows,
26439
+ input2.rowsInfo.rows.length
26440
+ );
26441
+ const terminalRunWithMissingAdvertisedRows = advertisedRows > 0 && (runStatus === "completed" || runStatus === "failed" || runStatus === "cancelled");
26442
+ const inspectCommand = `deepline runs get ${input2.status.runId} --full --json`;
26443
+ const retryCommand = runExportRetryCommand(
26444
+ input2.status.runId,
26445
+ "output.csv",
26446
+ datasetLabel
26447
+ );
26448
+ const explanation = terminalRunWithMissingAdvertisedRows ? ` The terminal result advertised ${advertisedRows} row(s), but none are currently attributed to this run's durable sheet. Those rows may have been reused from an earlier run or the projection may still be lagging; export refuses to guess another run's rows. Retry the same export after projection catches up (do not rerun the Play): ${retryCommand}. If it remains empty, inspect the full package with ${inspectCommand} and export the run that owns the durable dataset; this command will not use a database-wide table query that could mix runs.` : ` The run status is '${runStatus || "unknown"}', so it likely failed before persisting rows to this dataset. That means provider calls may have been billed without a durable row; inspect ${inspectCommand}, fix the failure cause, then rerun the Play so completed receipts can be reused.`;
26437
26449
  throw new DeeplineError(
26438
- `Run ${input2.status.runId} has zero rows to export from dataset '${datasetLabel}'. The run status is '${runStatus || "unknown"}', so it likely failed before persisting rows to this dataset \u2014 provider calls may have been billed without a durable row. Fix the run's failure cause and re-run to resume.` + (available.length > 0 ? ` Available datasets: ${available.join(", ")}.` : " No datasets were materialized for this run."),
26450
+ `Run ${input2.status.runId} has zero rows to export from dataset '${datasetLabel}'.` + explanation + (available.length > 0 ? ` Available datasets: ${available.join(", ")}.` : " No datasets were materialized for this run."),
26439
26451
  void 0,
26440
26452
  "RUN_EXPORT_EMPTY",
26441
26453
  {
26442
26454
  runId: input2.status.runId,
26443
26455
  dataset: datasetLabel,
26444
26456
  available,
26445
- runStatus: runStatus || null
26457
+ runStatus: runStatus || null,
26458
+ guidance: terminalRunWithMissingAdvertisedRows ? {
26459
+ inspect_command: inspectCommand,
26460
+ retry_command: retryCommand,
26461
+ do_not_rerun: true,
26462
+ reason: "Run output is terminal but its rows are not attributed to this run; export refuses to guess another writer."
26463
+ } : {
26464
+ inspect_command: inspectCommand,
26465
+ do_not_rerun: false,
26466
+ reason: "The run did not prove that any rows were durably persisted for this dataset."
26467
+ }
26446
26468
  }
26447
26469
  );
26448
26470
  }
@@ -26524,6 +26546,24 @@ ${commands.join("\n")}`,
26524
26546
  `Run ${status.runId} did not expose a row-shaped final output to export.`
26525
26547
  );
26526
26548
  }
26549
+ if (rowsInfo.exportUnavailableReason === "returned_dataset_not_persisted") {
26550
+ const datasetLabel = options.datasetPath ?? rowsInfo.source ?? rowsInfo.datasetId ?? "result.rows";
26551
+ throw new DeeplineError(
26552
+ rowsInfo.exportUnavailableMessage ?? `Run ${status.runId} returned rows for '${datasetLabel}' without a run-scoped durable dataset. Persist the rows with ctx.dataset("rows", rows).run() before returning them.`,
26553
+ void 0,
26554
+ "RUN_EXPORT_NOT_AVAILABLE",
26555
+ {
26556
+ runId: status.runId,
26557
+ dataset: datasetLabel,
26558
+ reason: rowsInfo.exportUnavailableReason,
26559
+ guidance: {
26560
+ do_not_rerun: false,
26561
+ reason: "The returned rows exist only in the in-memory tool result.",
26562
+ persist_rows: 'ctx.dataset("rows", rows).run()'
26563
+ }
26564
+ }
26565
+ );
26566
+ }
26527
26567
  const attempts = Math.max(1, Math.trunc(options.attempts ?? 1));
26528
26568
  const retryDelayMs = Math.max(0, Math.trunc(options.retryDelayMs ?? 0));
26529
26569
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
package/dist/index.d.mts CHANGED
@@ -1680,7 +1680,7 @@ interface PlayRunPackage {
1680
1680
  sqlQualifiedTableName?: string;
1681
1681
  recovered?: true;
1682
1682
  exportUnavailable?: {
1683
- reason: 'empty_dataset' | 'shared_table_namespace';
1683
+ reason: 'empty_dataset' | 'shared_table_namespace' | 'returned_dataset_not_persisted';
1684
1684
  message: string;
1685
1685
  };
1686
1686
  preview?: Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -1680,7 +1680,7 @@ interface PlayRunPackage {
1680
1680
  sqlQualifiedTableName?: string;
1681
1681
  recovered?: true;
1682
1682
  exportUnavailable?: {
1683
- reason: 'empty_dataset' | 'shared_table_namespace';
1683
+ reason: 'empty_dataset' | 'shared_table_namespace' | 'returned_dataset_not_persisted';
1684
1684
  message: string;
1685
1685
  };
1686
1686
  preview?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -864,7 +864,7 @@ var SDK_RELEASE = {
864
864
  // getters keep their established compatibility behavior.
865
865
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
866
866
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
867
- version: "0.3.180",
867
+ version: "0.3.182",
868
868
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
869
869
  packageCapabilities: {
870
870
  updatePreferences: 1
package/dist/index.mjs CHANGED
@@ -768,7 +768,7 @@ var SDK_RELEASE = {
768
768
  // getters keep their established compatibility behavior.
769
769
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
770
770
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
771
- version: "0.3.180",
771
+ version: "0.3.182",
772
772
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
773
773
  packageCapabilities: {
774
774
  updatePreferences: 1
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.180";
152
+ readonly version: "0.3.182";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.d.ts CHANGED
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.180";
152
+ readonly version: "0.3.182";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.js CHANGED
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
74
74
  // getters keep their established compatibility behavior.
75
75
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
76
76
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
77
- version: "0.3.180",
77
+ version: "0.3.182",
78
78
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
79
79
  packageCapabilities: {
80
80
  updatePreferences: 1
package/dist/release.mjs CHANGED
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
48
48
  // getters keep their established compatibility behavior.
49
49
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
50
50
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
51
- version: "0.3.180",
51
+ version: "0.3.182",
52
52
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
53
53
  packageCapabilities: {
54
54
  updatePreferences: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.180",
3
+ "version": "0.3.182",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",