deepline 0.1.166 → 0.1.168

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.
@@ -148,10 +148,13 @@ import {
148
148
  } from './runtime/receipts';
149
149
  import {
150
150
  RuntimeReceiptWaitTimeoutError,
151
+ resolveRuntimeToolReceiptWaitMaxAttempts,
152
+ resolveRuntimeToolReceiptWaitTimeoutMs,
151
153
  waitForCompletedRuntimeReceipt,
152
154
  } from '../../../shared_libs/play-runtime/durable-receipt-execution';
153
155
  import type { RuntimeStepReceipt } from '../../../shared_libs/play-runtime/ctx-types';
154
156
  import {
157
+ canReclaimFailedWorkerToolReceipt,
155
158
  canReclaimTimedOutWorkerToolReceipt,
156
159
  markWorkerToolReceiptResultCached,
157
160
  markWorkerToolReceiptResultExecution,
@@ -531,20 +534,74 @@ const RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS = 180_000;
531
534
  const RUNTIME_API_RETRY_DELAYS_MS = [
532
535
  250, 750, 1500, 3000, 5000, 10000,
533
536
  ] as const;
537
+ const STANDARD_PLAY_RUNTIME_LIMIT_MS =
538
+ STANDARD_PLAY_RUNTIME_LIMIT_SECONDS * 1000;
539
+ const STANDARD_PLAY_RUNTIME_LIMIT_MESSAGE = `Based on this plan, max runtime is ${STANDARD_PLAY_RUNTIME_LIMIT_LABEL}. Use smaller batches; ask for runtime.`;
540
+
541
+ function resolveRuntimeDeadlineRemainingMs(runtimeDeadlineMs?: number): number {
542
+ if (
543
+ typeof runtimeDeadlineMs !== 'number' ||
544
+ !Number.isFinite(runtimeDeadlineMs)
545
+ ) {
546
+ return Number.POSITIVE_INFINITY;
547
+ }
548
+ const remainingMs = Math.floor(runtimeDeadlineMs - nowMs());
549
+ if (remainingMs <= 0) {
550
+ throw new WorkflowAbortError(STANDARD_PLAY_RUNTIME_LIMIT_MESSAGE);
551
+ }
552
+ return remainingMs;
553
+ }
554
+
555
+ function resolveToolRuntimeApiTimeout(input: {
556
+ requestInput: Record<string, unknown>;
557
+ runtimeDeadlineMs?: number;
558
+ }): { timeoutMs: number; timeoutErrorMessage?: string } {
559
+ const toolTimeoutMs = resolveRuntimeToolReceiptWaitTimeoutMs(
560
+ input.requestInput,
561
+ );
562
+ const remainingMs = resolveRuntimeDeadlineRemainingMs(
563
+ input.runtimeDeadlineMs,
564
+ );
565
+ if (remainingMs < toolTimeoutMs) {
566
+ return {
567
+ timeoutMs: Math.max(1, remainingMs),
568
+ timeoutErrorMessage: STANDARD_PLAY_RUNTIME_LIMIT_MESSAGE,
569
+ };
570
+ }
571
+ return { timeoutMs: toolTimeoutMs };
572
+ }
534
573
 
535
574
  async function fetchRuntimeApi(
536
575
  baseUrl: string,
537
576
  path: string,
538
577
  init: RequestInit,
578
+ options?: {
579
+ timeoutMsOverride?: number;
580
+ timeoutErrorMessage?: string;
581
+ abortSignal?: AbortSignal;
582
+ },
539
583
  ): Promise<Response> {
540
584
  const timeoutMs =
541
- path === '/api/v2/plays/run'
542
- ? RUNTIME_API_PLAY_RUN_TIMEOUT_MS
543
- : path === '/api/v2/plays/internal/egress-fetch'
544
- ? RUNTIME_API_EGRESS_FETCH_TIMEOUT_MS
545
- : /^\/api\/v2\/integrations\/[^/]+\/execute$/.test(path)
546
- ? RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS
547
- : RUNTIME_API_TIMEOUT_MS;
585
+ typeof options?.timeoutMsOverride === 'number' &&
586
+ Number.isFinite(options.timeoutMsOverride)
587
+ ? Math.max(1, Math.ceil(options.timeoutMsOverride))
588
+ : path === '/api/v2/plays/run'
589
+ ? RUNTIME_API_PLAY_RUN_TIMEOUT_MS
590
+ : path === '/api/v2/plays/internal/egress-fetch'
591
+ ? RUNTIME_API_EGRESS_FETCH_TIMEOUT_MS
592
+ : /^\/api\/v2\/integrations\/[^/]+\/execute$/.test(path)
593
+ ? RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS
594
+ : RUNTIME_API_TIMEOUT_MS;
595
+ const abortSignal = options?.abortSignal;
596
+ const abortError = () =>
597
+ abortSignal?.reason instanceof Error
598
+ ? abortSignal.reason
599
+ : new Error(
600
+ `[play-harness] runtime API call aborted. path=${path} baseUrl=${baseUrl}`,
601
+ );
602
+ if (abortSignal?.aborted) {
603
+ throw abortError();
604
+ }
548
605
  const controller = new AbortController();
549
606
  let timeout: ReturnType<typeof setTimeout> | null = null;
550
607
  const timeoutPromise = new Promise<never>((_, reject) => {
@@ -552,7 +609,8 @@ async function fetchRuntimeApi(
552
609
  controller.abort();
553
610
  reject(
554
611
  new Error(
555
- `[play-harness] runtime API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
612
+ options?.timeoutErrorMessage ??
613
+ `[play-harness] runtime API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
556
614
  ),
557
615
  );
558
616
  }, timeoutMs);
@@ -566,6 +624,9 @@ async function fetchRuntimeApi(
566
624
  if (!cachedRuntimeApiBinding) {
567
625
  throw new Error('[play-harness] RUNTIME_API service binding is required');
568
626
  }
627
+ if (abortSignal?.aborted) {
628
+ throw abortError();
629
+ }
569
630
  const responsePromise = callRuntimeApiRpcBinding(
570
631
  cachedRuntimeApiBinding,
571
632
  mergedInit,
@@ -575,6 +636,10 @@ async function fetchRuntimeApi(
575
636
  timeoutMs,
576
637
  },
577
638
  );
639
+ // RUNTIME_API service bindings do not consume RequestInit.signal once the RPC
640
+ // is in flight. After dispatch, wait for the owner call to settle so the
641
+ // durable receipt records the real provider result instead of failing early
642
+ // and inviting duplicate provider work on retry.
578
643
  const response = await Promise.race([responsePromise, timeoutPromise]);
579
644
  if (await isRuntimeApiBindingNotFoundResponse(response)) {
580
645
  throw new Error(
@@ -585,7 +650,8 @@ async function fetchRuntimeApi(
585
650
  } catch (err) {
586
651
  if (err instanceof Error && err.name === 'AbortError') {
587
652
  throw new Error(
588
- `[play-harness] runtime API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
653
+ options?.timeoutErrorMessage ??
654
+ `[play-harness] runtime API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
589
655
  );
590
656
  }
591
657
  throw err;
@@ -1188,6 +1254,8 @@ async function executeTool(
1188
1254
  onProviderBackpressure?: (retryAfterMs: number) => void,
1189
1255
  onRetryAttempt?: () => void,
1190
1256
  transientHttpRetrySafe = false,
1257
+ abortSignal?: AbortSignal,
1258
+ runtimeDeadlineMs?: number,
1191
1259
  ): Promise<ToolExecuteResult> {
1192
1260
  if (args.toolId === 'test_wait_for_event' && workflowStep) {
1193
1261
  const result = await waitForSyntheticIntegrationEvent(
@@ -1208,6 +1276,8 @@ async function executeTool(
1208
1276
  onProviderBackpressure,
1209
1277
  onRetryAttempt,
1210
1278
  transientHttpRetrySafe,
1279
+ abortSignal,
1280
+ runtimeDeadlineMs,
1211
1281
  );
1212
1282
  }
1213
1283
 
@@ -1219,6 +1289,8 @@ async function executeToolWithLifecycle(
1219
1289
  onProviderBackpressure?: (retryAfterMs: number) => void,
1220
1290
  onRetryAttempt?: () => void,
1221
1291
  transientHttpRetrySafe = false,
1292
+ abortSignal?: AbortSignal,
1293
+ runtimeDeadlineMs?: number,
1222
1294
  ): Promise<ToolExecuteResult> {
1223
1295
  callbacks?.onToolCalled?.(args.toolId, nowMs());
1224
1296
  try {
@@ -1229,6 +1301,8 @@ async function executeToolWithLifecycle(
1229
1301
  onProviderBackpressure,
1230
1302
  onRetryAttempt,
1231
1303
  transientHttpRetrySafe,
1304
+ abortSignal,
1305
+ runtimeDeadlineMs,
1232
1306
  );
1233
1307
  } catch (error) {
1234
1308
  callbacks?.onToolFailed?.(args.toolId, nowMs());
@@ -1365,6 +1439,8 @@ async function callToolDirect(
1365
1439
  // policy.budgets.maxRetryCount effectively unenforced.
1366
1440
  onRetryAttempt?: () => void,
1367
1441
  transientHttpRetrySafe = false,
1442
+ abortSignal?: AbortSignal,
1443
+ runtimeDeadlineMs?: number,
1368
1444
  ): Promise<ToolExecuteResult> {
1369
1445
  const { id, toolId, input } = args;
1370
1446
  const path = `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`;
@@ -1377,20 +1453,33 @@ async function callToolDirect(
1377
1453
  requestAttempt += 1;
1378
1454
  let res: Response;
1379
1455
  try {
1380
- res = await fetchRuntimeApi(req.baseUrl, path, {
1381
- method: 'POST',
1382
- headers: {
1383
- 'content-type': 'application/json',
1384
- authorization: `Bearer ${req.executorToken}`,
1385
- 'x-deepline-request-id': `${req.runId}:${toolId}:${id}:attempt:${requestAttempt}`,
1386
- [EXECUTE_RESPONSE_CONTRACT_HEADER]: V2_EXECUTE_RESPONSE_CONTRACT,
1387
- [EXECUTE_TOOL_METADATA_HEADER]: 'true',
1388
- },
1389
- body: JSON.stringify({
1390
- payload: input,
1391
- metadata: { parent_run_id: req.runId },
1392
- }),
1456
+ const runtimeApiTimeout = resolveToolRuntimeApiTimeout({
1457
+ requestInput: input,
1458
+ runtimeDeadlineMs,
1393
1459
  });
1460
+ res = await fetchRuntimeApi(
1461
+ req.baseUrl,
1462
+ path,
1463
+ {
1464
+ method: 'POST',
1465
+ headers: {
1466
+ 'content-type': 'application/json',
1467
+ authorization: `Bearer ${req.executorToken}`,
1468
+ 'x-deepline-request-id': `${req.runId}:${toolId}:${id}:attempt:${requestAttempt}`,
1469
+ [EXECUTE_RESPONSE_CONTRACT_HEADER]: V2_EXECUTE_RESPONSE_CONTRACT,
1470
+ [EXECUTE_TOOL_METADATA_HEADER]: 'true',
1471
+ },
1472
+ body: JSON.stringify({
1473
+ payload: input,
1474
+ metadata: { parent_run_id: req.runId },
1475
+ }),
1476
+ },
1477
+ {
1478
+ timeoutMsOverride: runtimeApiTimeout.timeoutMs,
1479
+ timeoutErrorMessage: runtimeApiTimeout.timeoutErrorMessage,
1480
+ abortSignal,
1481
+ },
1482
+ );
1394
1483
  } catch (error) {
1395
1484
  transportAttempt += 1;
1396
1485
  const message = error instanceof Error ? error.message : String(error);
@@ -1538,6 +1627,7 @@ type WorkerToolBatchRequest = {
1538
1627
  cacheKey: string;
1539
1628
  receiptKey: string | null;
1540
1629
  force: boolean;
1630
+ receiptWaitMaxAttempts: number;
1541
1631
  toolId: string;
1542
1632
  input: Record<string, unknown>;
1543
1633
  workflowStep?: WorkflowStep;
@@ -1641,6 +1731,7 @@ class WorkerToolBatchScheduler {
1641
1731
  private readonly callbacks?: WorkerCtxCallbacks,
1642
1732
  private readonly receiptStore?: WorkerRuntimeReceiptStore,
1643
1733
  private readonly allowLocalRetryReceipts = false,
1734
+ private readonly runtimeDeadlineMs?: number,
1644
1735
  ) {}
1645
1736
 
1646
1737
  /**
@@ -1685,6 +1776,7 @@ class WorkerToolBatchScheduler {
1685
1776
  cacheKey: receiptKey,
1686
1777
  receiptKey,
1687
1778
  force: options?.force === true,
1779
+ receiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(input),
1688
1780
  toolId,
1689
1781
  input,
1690
1782
  workflowStep,
@@ -1747,19 +1839,19 @@ class WorkerToolBatchScheduler {
1747
1839
  }
1748
1840
  }
1749
1841
 
1750
- private async waitForDurableToolReceipt(
1751
- receiptKey: string,
1752
- ): Promise<unknown> {
1753
- if (!this.receiptStore) {
1754
- throw new Error('Worker durable tool receipt store is not configured.');
1755
- }
1756
- if (!this.receiptStore.getReceipt) {
1842
+ private async waitForDurableToolReceipt(input: {
1843
+ receiptKey: string;
1844
+ maxAttempts: number;
1845
+ }): Promise<WorkerRuntimeReceipt> {
1846
+ if (!this.receiptStore?.getReceipt) {
1757
1847
  throw new Error(
1758
- 'Worker durable tool receipt wait requires read-only receipt lookup.',
1848
+ 'Worker durable tool receipt wait requires receipt lookup.',
1759
1849
  );
1760
1850
  }
1761
1851
  const receipt = await waitForCompletedRuntimeReceipt({
1762
- receiptKey,
1852
+ receiptKey: input.receiptKey,
1853
+ maxAttempts: input.maxAttempts,
1854
+ abortSignal: this.abortSignal,
1763
1855
  store: {
1764
1856
  getMany: async (receiptKeys) => {
1765
1857
  const receipts = new Map<string, RuntimeStepReceipt>();
@@ -1783,11 +1875,7 @@ class WorkerToolBatchScheduler {
1783
1875
  },
1784
1876
  },
1785
1877
  });
1786
- return markWorkerToolReceiptResultCached(
1787
- deserializeDurableStepValue(receipt.output),
1788
- receiptKey,
1789
- receiptKey,
1790
- );
1878
+ return receipt;
1791
1879
  }
1792
1880
 
1793
1881
  private settleRequests(
@@ -1830,24 +1918,73 @@ class WorkerToolBatchScheduler {
1830
1918
  this.onRequestsSettled?.(requests.length);
1831
1919
  }
1832
1920
 
1833
- private async reclaimTimedOutDurableToolReceiptGroup(input: {
1921
+ private async resolveCompletedDurableToolReceiptGroup(input: {
1922
+ group: WorkerToolBatchRequest[];
1923
+ receiptKey: string;
1924
+ receipt: WorkerRuntimeReceipt;
1925
+ source: 'cache' | 'in_flight';
1926
+ }): Promise<void> {
1927
+ const [first] = input.group;
1928
+ if (!first) return;
1929
+ const value = deserializeDurableStepValue(input.receipt.output);
1930
+ const result =
1931
+ input.source === 'cache'
1932
+ ? markWorkerToolReceiptResultCached(
1933
+ value,
1934
+ first.cacheKey,
1935
+ input.receiptKey,
1936
+ )
1937
+ : markWorkerToolReceiptResultExecution(value, {
1938
+ kind: 'in_flight',
1939
+ receiptKey: input.receiptKey,
1940
+ attachedToReceiptKey: input.receiptKey,
1941
+ });
1942
+ for (const request of input.group) {
1943
+ request.resolve(result);
1944
+ }
1945
+ this.onRequestsSettled?.(input.group.length);
1946
+ }
1947
+
1948
+ private async reclaimRunningDurableToolReceiptGroupAfterWait(input: {
1834
1949
  group: WorkerToolBatchRequest[];
1835
1950
  receiptKey: string;
1836
1951
  runningReceipt: WorkerRuntimeReceipt;
1837
- waitError: unknown;
1838
1952
  }): Promise<ClaimedWorkerToolBatchRequest[]> {
1839
1953
  const [request, ...followers] = input.group;
1840
1954
  if (!request || !this.receiptStore) {
1841
- this.rejectRawRequests(input.group, input.waitError);
1955
+ this.rejectRawRequests(
1956
+ input.group,
1957
+ new RuntimeReceiptWaitTimeoutError(input.receiptKey),
1958
+ );
1842
1959
  return [];
1843
1960
  }
1961
+ let waitError: unknown = null;
1962
+ try {
1963
+ const receipt = await this.waitForDurableToolReceipt({
1964
+ receiptKey: input.receiptKey,
1965
+ maxAttempts: request.receiptWaitMaxAttempts,
1966
+ });
1967
+ await this.resolveCompletedDurableToolReceiptGroup({
1968
+ group: input.group,
1969
+ receiptKey: input.receiptKey,
1970
+ receipt,
1971
+ source: 'in_flight',
1972
+ });
1973
+ return [];
1974
+ } catch (error) {
1975
+ if (!(error instanceof RuntimeReceiptWaitTimeoutError)) {
1976
+ this.rejectRawRequests(input.group, error);
1977
+ return [];
1978
+ }
1979
+ waitError = error;
1980
+ }
1844
1981
  if (
1845
1982
  !canReclaimTimedOutWorkerToolReceipt({
1846
1983
  ownerRunId: input.runningReceipt.runId,
1847
1984
  currentRunId: this.req.runId,
1848
1985
  })
1849
1986
  ) {
1850
- this.rejectRawRequests(input.group, input.waitError);
1987
+ this.rejectRawRequests(input.group, waitError);
1851
1988
  return [];
1852
1989
  }
1853
1990
  let claim: WorkerRuntimeReceiptClaim;
@@ -1866,27 +2003,92 @@ class WorkerToolBatchScheduler {
1866
2003
  return [{ request, receiptKey: input.receiptKey, followers }];
1867
2004
  }
1868
2005
  if (claim.disposition === 'reused') {
1869
- const result = markWorkerToolReceiptResultCached(
1870
- deserializeDurableStepValue(claim.receipt.output),
1871
- request.cacheKey,
1872
- input.receiptKey,
1873
- );
1874
- for (const pending of input.group) {
1875
- pending.resolve(result);
1876
- }
1877
- this.onRequestsSettled?.(input.group.length);
2006
+ await this.resolveCompletedDurableToolReceiptGroup({
2007
+ group: input.group,
2008
+ receiptKey: input.receiptKey,
2009
+ receipt: claim.receipt,
2010
+ source: 'cache',
2011
+ });
1878
2012
  return [];
1879
2013
  }
1880
2014
  if (claim.disposition === 'failed') {
1881
- this.rejectRawRequests(
1882
- input.group,
1883
- new Error(
1884
- `Durable tool call ${input.receiptKey} failed: ${claim.receipt.error ?? 'unknown error'}`,
1885
- ),
1886
- );
2015
+ return await this.reclaimFailedDurableToolReceiptGroup({
2016
+ group: input.group,
2017
+ receiptKey: input.receiptKey,
2018
+ failedReceipt: claim.receipt,
2019
+ });
2020
+ }
2021
+ this.rejectRawRequests(input.group, waitError);
2022
+ return [];
2023
+ }
2024
+
2025
+ private async reclaimFailedDurableToolReceiptGroup(input: {
2026
+ group: WorkerToolBatchRequest[];
2027
+ receiptKey: string;
2028
+ failedReceipt: WorkerRuntimeReceipt;
2029
+ }): Promise<ClaimedWorkerToolBatchRequest[]> {
2030
+ const failedError = new Error(
2031
+ `Durable tool call ${input.receiptKey} failed: ${input.failedReceipt.error ?? 'unknown error'}`,
2032
+ );
2033
+ const [request, ...followers] = input.group;
2034
+ if (!request || !this.receiptStore) {
2035
+ this.rejectRawRequests(input.group, failedError);
2036
+ return [];
2037
+ }
2038
+ if (
2039
+ !canReclaimFailedWorkerToolReceipt({
2040
+ error: input.failedReceipt.error,
2041
+ })
2042
+ ) {
2043
+ this.rejectRawRequests(input.group, failedError);
2044
+ return [];
2045
+ }
2046
+ let claim: WorkerRuntimeReceiptClaim;
2047
+ try {
2048
+ claim = await this.receiptStore.claimReceipt({
2049
+ playName: this.req.playName,
2050
+ runId: this.req.runId,
2051
+ key: input.receiptKey,
2052
+ });
2053
+ } catch (error) {
2054
+ this.rejectRawRequests(input.group, error);
2055
+ return [];
2056
+ }
2057
+ if (claim.disposition === 'claimed') {
2058
+ return [{ request, receiptKey: input.receiptKey, followers }];
2059
+ }
2060
+ if (claim.disposition === 'reused') {
2061
+ await this.resolveCompletedDurableToolReceiptGroup({
2062
+ group: input.group,
2063
+ receiptKey: input.receiptKey,
2064
+ receipt: claim.receipt,
2065
+ source: 'cache',
2066
+ });
2067
+ return [];
2068
+ }
2069
+ if (claim.disposition === 'running') {
2070
+ try {
2071
+ const receipt = await this.waitForDurableToolReceipt({
2072
+ receiptKey: input.receiptKey,
2073
+ maxAttempts: request.receiptWaitMaxAttempts,
2074
+ });
2075
+ await this.resolveCompletedDurableToolReceiptGroup({
2076
+ group: input.group,
2077
+ receiptKey: input.receiptKey,
2078
+ receipt,
2079
+ source: 'in_flight',
2080
+ });
2081
+ } catch (error) {
2082
+ this.rejectRawRequests(input.group, error);
2083
+ }
1887
2084
  return [];
1888
2085
  }
1889
- this.rejectRawRequests(input.group, input.waitError);
2086
+ this.rejectRawRequests(
2087
+ input.group,
2088
+ new Error(
2089
+ `Durable tool call ${input.receiptKey} failed: ${claim.receipt.error ?? 'unknown error'}`,
2090
+ ),
2091
+ );
1890
2092
  return [];
1891
2093
  }
1892
2094
 
@@ -2025,45 +2227,22 @@ class WorkerToolBatchScheduler {
2025
2227
  continue;
2026
2228
  }
2027
2229
  if (claim.disposition === 'failed') {
2028
- const error = new Error(
2029
- `Durable tool call ${receiptKey} failed: ${claim.receipt.error ?? 'unknown error'}`,
2230
+ claimedRequests.push(
2231
+ ...(await this.reclaimFailedDurableToolReceiptGroup({
2232
+ group,
2233
+ receiptKey,
2234
+ failedReceipt: claim.receipt,
2235
+ })),
2030
2236
  );
2031
- this.rejectRawRequests(group, error);
2032
2237
  continue;
2033
2238
  }
2034
2239
  if (claim.disposition === 'running') {
2035
2240
  deferredClaimedRequests.push(
2036
- (async (): Promise<ClaimedWorkerToolBatchRequest[]> => {
2037
- let waitError: unknown = new RuntimeReceiptWaitTimeoutError(
2038
- receiptKey,
2039
- );
2040
- try {
2041
- const result = await this.waitForDurableToolReceipt(receiptKey);
2042
- for (const request of group) {
2043
- request.resolve(
2044
- markWorkerToolReceiptResultExecution(result, {
2045
- kind: 'in_flight',
2046
- receiptKey,
2047
- attachedToReceiptKey: receiptKey,
2048
- }),
2049
- );
2050
- }
2051
- this.onRequestsSettled?.(group.length);
2052
- return [];
2053
- } catch (error) {
2054
- waitError = error;
2055
- if (!(error instanceof RuntimeReceiptWaitTimeoutError)) {
2056
- this.rejectRawRequests(group, error);
2057
- return [];
2058
- }
2059
- }
2060
- return await this.reclaimTimedOutDurableToolReceiptGroup({
2061
- group,
2062
- receiptKey,
2063
- runningReceipt: claim.receipt,
2064
- waitError,
2065
- });
2066
- })(),
2241
+ this.reclaimRunningDurableToolReceiptGroupAfterWait({
2242
+ group,
2243
+ receiptKey,
2244
+ runningReceipt: claim.receipt,
2245
+ }),
2067
2246
  );
2068
2247
  continue;
2069
2248
  }
@@ -2288,6 +2467,8 @@ class WorkerToolBatchScheduler {
2288
2467
  (retryAfterMs) => this.reportBackpressure(toolId, retryAfterMs),
2289
2468
  () => this.governor.chargeBudget('retry'),
2290
2469
  toolContract?.retrySafeTransientHttp === true,
2470
+ this.abortSignal,
2471
+ this.runtimeDeadlineMs,
2291
2472
  );
2292
2473
  this.settleRequests(
2293
2474
  claimed,
@@ -2328,6 +2509,7 @@ class WorkerToolBatchScheduler {
2328
2509
  WORKER_TOOL_BATCH_DEFAULT_PARALLELISM,
2329
2510
  ),
2330
2511
  abortSignal: this.abortSignal,
2512
+ runtimeDeadlineMs: this.runtimeDeadlineMs,
2331
2513
  reportBackpressure: (retryAfterMs) =>
2332
2514
  this.reportBackpressure(toolId, retryAfterMs),
2333
2515
  resolveToolContract: this.resolvePacing,
@@ -2376,6 +2558,7 @@ async function executeBatchedWorkerToolGroup(input: {
2376
2558
  governor: PlayExecutionGovernor;
2377
2559
  suggestedParallelism: number;
2378
2560
  abortSignal?: AbortSignal;
2561
+ runtimeDeadlineMs?: number;
2379
2562
  reportBackpressure: (retryAfterMs: number) => void;
2380
2563
  resolveToolContract: WorkerPacingResolver;
2381
2564
  onRequestsSettled?: (count: number) => void;
@@ -2444,6 +2627,8 @@ async function executeBatchedWorkerToolGroup(input: {
2444
2627
  input.reportBackpressure,
2445
2628
  () => input.governor.chargeBudget('retry'),
2446
2629
  toolContract?.retrySafeTransientHttp === true,
2630
+ input.abortSignal,
2631
+ input.runtimeDeadlineMs,
2447
2632
  );
2448
2633
  } catch (error) {
2449
2634
  input.callbacks?.onToolFailed?.(batch.batchOperation, nowMs());
@@ -3684,21 +3869,13 @@ async function persistCompletedMapRows(input: {
3684
3869
  const retry = await harnessPersistCompletedSheetRows(persistRequest);
3685
3870
  retryWritten = retry.rowsWritten;
3686
3871
  retryVisible = await readVisibleRowCount();
3687
- if (retryVisible >= rows.length) {
3688
- return {
3689
- rows: rows.length,
3690
- written: result.rowsWritten,
3691
- visible: visibleRows,
3692
- retryWritten,
3693
- retryVisible,
3694
- };
3695
- }
3696
- throw new Error(
3697
- `Runtime sheet persistence mismatch for ${tableNamespace}: wrote ${result.rowsWritten}/${rows.length}; visible ${visibleRows}; retry wrote ${retryWritten}/${rows.length}; retry visible ${retryVisible}; run ${req.runId}.`,
3698
- );
3872
+ if (retryVisible < rows.length)
3873
+ throw new Error(
3874
+ `Runtime sheet persistence mismatch for ${tableNamespace}: wrote ${result.rowsWritten}/${rows.length}; visible ${visibleRows}; retry wrote ${retryWritten}/${rows.length}; retry visible ${retryVisible}; run ${req.runId}.`,
3875
+ );
3699
3876
  }
3700
3877
  }
3701
- if (result.rowsWritten !== rows.length) {
3878
+ if (result.rowsWritten !== rows.length && visibleRows < rows.length) {
3702
3879
  throw new Error(
3703
3880
  `Runtime sheet persistence mismatch for ${tableNamespace}: wrote ${result.rowsWritten}/${rows.length}; run ${req.runId}.`,
3704
3881
  );
@@ -4137,6 +4314,7 @@ function createMinimalWorkerCtx(
4137
4314
  workflowStep?: WorkflowStep,
4138
4315
  abortSignal?: AbortSignal,
4139
4316
  callbacks?: WorkerCtxCallbacks,
4317
+ runtimeDeadlineMs?: number,
4140
4318
  ): unknown {
4141
4319
  const { governor, resolvePacing: resolveToolPacing } =
4142
4320
  createGovernorForRun(req);
@@ -4258,6 +4436,7 @@ function createMinimalWorkerCtx(
4258
4436
  callbacks,
4259
4437
  receiptStore,
4260
4438
  true,
4439
+ runtimeDeadlineMs,
4261
4440
  );
4262
4441
  // Local ancestry chain that always ENDS with the currently-executing play
4263
4442
  // (req.playName). The /api/v2/plays/run lineage validator requires the
@@ -4397,9 +4576,7 @@ function createMinimalWorkerCtx(
4397
4576
  row: Record<string, unknown>,
4398
4577
  ): number | null => {
4399
4578
  const value = row[MAP_ROW_OUTCOME_RUNTIME_FIELDS.inputIndex];
4400
- return typeof value === 'number' && Number.isFinite(value)
4401
- ? value
4402
- : null;
4579
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
4403
4580
  };
4404
4581
  const deriveDefaultMapRowIdentity = (
4405
4582
  row: Record<string, unknown>,
@@ -4461,12 +4638,7 @@ function createMinimalWorkerCtx(
4461
4638
  ): string => {
4462
4639
  const explicitKeyValue = resolveExplicitKeyValue(row, index);
4463
4640
  return explicitKeyValue == null
4464
- ? deriveDefaultMapRowIdentity(
4465
- row,
4466
- index,
4467
- chunkIndex,
4468
- chunkLocalIndex,
4469
- )
4641
+ ? deriveDefaultMapRowIdentity(row, index, chunkIndex, chunkLocalIndex)
4470
4642
  : derivePlayRowIdentityFromKey(explicitKeyValue, name);
4471
4643
  };
4472
4644
  // Cross-chunk dedupe accumulators for the single end-of-map log line.
@@ -4743,6 +4915,7 @@ function createMinimalWorkerCtx(
4743
4915
  callbacks,
4744
4916
  receiptStore,
4745
4917
  false,
4918
+ runtimeDeadlineMs,
4746
4919
  );
4747
4920
  let stepCellsCompleted = 0;
4748
4921
  let stepCellsSkipped = 0;
@@ -7165,6 +7338,7 @@ async function executeRunRequest(
7165
7338
 
7166
7339
  stepLifecycle?.markPreDatasetStepsStarted(startedAt);
7167
7340
  flushLedgerEvents(false);
7341
+ const runtimeDeadlineMs = nowMs() + STANDARD_PLAY_RUNTIME_LIMIT_MS;
7168
7342
  const ctx = createMinimalWorkerCtx(
7169
7343
  req,
7170
7344
  wrappedEmit,
@@ -7172,6 +7346,7 @@ async function executeRunRequest(
7172
7346
  workflowStep,
7173
7347
  abortSignal,
7174
7348
  workerCallbacks,
7349
+ runtimeDeadlineMs,
7175
7350
  );
7176
7351
  // Hard wall-clock cap on active user-code runtime. CF Workflows does not
7177
7352
  // impose a play-level execution ceiling on this substrate, so without this a
@@ -7179,14 +7354,15 @@ async function executeRunRequest(
7179
7354
  // token expires. Aborting the controller surfaces cooperatively through the
7180
7355
  // same assertNotAborted checks used for harness cancellation.
7181
7356
  let runtimeLimitExceeded = false;
7182
- const runtimeDeadlineTimer = setTimeout(() => {
7183
- runtimeLimitExceeded = true;
7184
- if (!abortSignal.aborted) {
7185
- abortController.abort(
7186
- `Based on this plan, max runtime is ${STANDARD_PLAY_RUNTIME_LIMIT_LABEL}. Use smaller batches; ask for runtime.`,
7187
- );
7188
- }
7189
- }, STANDARD_PLAY_RUNTIME_LIMIT_SECONDS * 1000);
7357
+ const runtimeDeadlineTimer = setTimeout(
7358
+ () => {
7359
+ runtimeLimitExceeded = true;
7360
+ if (!abortSignal.aborted) {
7361
+ abortController.abort(STANDARD_PLAY_RUNTIME_LIMIT_MESSAGE);
7362
+ }
7363
+ },
7364
+ Math.max(1, runtimeDeadlineMs - nowMs()),
7365
+ );
7190
7366
  try {
7191
7367
  const playStartedAt = nowMs();
7192
7368
  const result = await (
@@ -23,9 +23,19 @@ export function canReclaimTimedOutWorkerToolReceipt(input: {
23
23
  ownerRunId?: string | null;
24
24
  currentRunId: string;
25
25
  }): boolean {
26
- const ownerRunId = input.ownerRunId?.trim();
27
26
  const currentRunId = input.currentRunId.trim();
28
- return Boolean(ownerRunId && currentRunId && ownerRunId === currentRunId);
27
+ return Boolean(currentRunId);
28
+ }
29
+
30
+ export function canReclaimFailedWorkerToolReceipt(input: {
31
+ error?: string | null;
32
+ }): boolean {
33
+ const error = input.error?.trim();
34
+ return Boolean(
35
+ error &&
36
+ (/\b(cancell?ed|aborted|terminate[d]?)\b/i.test(error) ||
37
+ /\bmax runtime\b/i.test(error)),
38
+ );
29
39
  }
30
40
 
31
41
  export function selectWorkerToolReceipt(input: {
@@ -104,10 +104,10 @@ export const SDK_RELEASE = {
104
104
  // 0.1.111 ships dataset-native tool list getters and result row datasets.
105
105
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
106
106
  // fields shipped in 0.1.153.
107
- version: '0.1.166',
107
+ version: '0.1.168',
108
108
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
109
109
  supportPolicy: {
110
- latest: '0.1.166',
110
+ latest: '0.1.168',
111
111
  minimumSupported: '0.1.53',
112
112
  deprecatedBelow: '0.1.53',
113
113
  commandMinimumSupported: [
@@ -4335,7 +4335,7 @@ export class PlayContextImpl {
4335
4335
  }
4336
4336
  };
4337
4337
 
4338
- const durableWaiters: Promise<void>[] = [];
4338
+ const durableExistingRunningHandlers: Promise<void>[] = [];
4339
4339
  if (
4340
4340
  pendingRequests.length > 0 &&
4341
4341
  (this.#options.claimRuntimeStepReceipts ||
@@ -4448,7 +4448,7 @@ export class PlayContextImpl {
4448
4448
  liveFollowersByOwnerCallId.set(owner.callId, followers);
4449
4449
  }
4450
4450
  };
4451
- const waitForReceiptOrTimeout = async (
4451
+ const reclaimExistingRunningReceiptAfterWait = async (
4452
4452
  receiptKey: string,
4453
4453
  requestsForKey: ToolCallRequest[],
4454
4454
  ): Promise<void> => {
@@ -4469,6 +4469,65 @@ export class PlayContextImpl {
4469
4469
  return;
4470
4470
  }
4471
4471
  }
4472
+ const reclaimed = (
4473
+ await this.claimRuntimeStepReceipts(
4474
+ [receiptKey],
4475
+ this.currentRunId,
4476
+ true,
4477
+ )
4478
+ ).get(receiptKey);
4479
+ if (
4480
+ reclaimed?.status === 'completed' ||
4481
+ reclaimed?.status === 'skipped'
4482
+ ) {
4483
+ await resolveRequestsFromReceipt(
4484
+ requestsForKey,
4485
+ reclaimed,
4486
+ 'cache',
4487
+ );
4488
+ durableRecoveredRequests.push(...requestsForKey);
4489
+ return;
4490
+ }
4491
+ if (reclaimed?.status === 'failed') {
4492
+ for (const request of requestsForKey) {
4493
+ await this.rejectToolCall(
4494
+ toolId,
4495
+ request,
4496
+ new Error(reclaimed.error ?? 'Durable tool call failed.'),
4497
+ );
4498
+ }
4499
+ return;
4500
+ }
4501
+ if (
4502
+ reclaimed?.status === 'running' &&
4503
+ reclaimed.claimState !== 'existing'
4504
+ ) {
4505
+ const [owner, ...waiters] = requestsForKey;
4506
+ if (!owner) return;
4507
+ if (waiters.length > 0) {
4508
+ liveFollowersByOwnerCallId.set(owner.callId, waiters);
4509
+ }
4510
+ try {
4511
+ const execution = await this.callToolExecutionAPI(
4512
+ toolId,
4513
+ owner.input,
4514
+ );
4515
+ const result = await this.resolveToolCall(
4516
+ toolId,
4517
+ owner,
4518
+ execution?.result ?? null,
4519
+ execution?.metadata ?? null,
4520
+ execution?.jobId,
4521
+ execution?.meta,
4522
+ );
4523
+ resolveLiveFollowers(owner, result);
4524
+ recordToolStep([owner]);
4525
+ this.#options.onBatchComplete?.(this.checkpoint);
4526
+ } catch (error) {
4527
+ await rejectWithLiveFollowers(owner, error);
4528
+ }
4529
+ return;
4530
+ }
4472
4531
  for (const request of requestsForKey) {
4473
4532
  await this.rejectToolCall(
4474
4533
  toolId,
@@ -4510,8 +4569,23 @@ export class PlayContextImpl {
4510
4569
  claimOwnerWithLiveFollowers(owner, waiters);
4511
4570
  continue;
4512
4571
  }
4513
- durableWaiters.push(
4514
- waitForReceiptOrTimeout(receiptKey, requestsForKey),
4572
+ if (
4573
+ claim?.status === 'running' &&
4574
+ claim.claimState === 'existing'
4575
+ ) {
4576
+ durableExistingRunningHandlers.push(
4577
+ reclaimExistingRunningReceiptAfterWait(
4578
+ receiptKey,
4579
+ requestsForKey,
4580
+ ),
4581
+ );
4582
+ continue;
4583
+ }
4584
+ durableExistingRunningHandlers.push(
4585
+ reclaimExistingRunningReceiptAfterWait(
4586
+ receiptKey,
4587
+ requestsForKey,
4588
+ ),
4515
4589
  );
4516
4590
  }
4517
4591
 
@@ -4621,8 +4695,8 @@ export class PlayContextImpl {
4621
4695
  }
4622
4696
  }
4623
4697
  }
4624
- if (durableWaiters.length > 0) {
4625
- await Promise.allSettled(durableWaiters);
4698
+ if (durableExistingRunningHandlers.length > 0) {
4699
+ await Promise.allSettled(durableExistingRunningHandlers);
4626
4700
  }
4627
4701
  }),
4628
4702
  );
@@ -10,6 +10,9 @@ import type { DurableReceiptRecoverySource } from './tool-execution-outcome';
10
10
 
11
11
  const DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS = 240;
12
12
  const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
13
+ const TOOL_RECEIPT_DEFAULT_WAIT_MS = 300_000;
14
+ const TOOL_RECEIPT_COMPLETION_BUFFER_MS = 30_000;
15
+ const TOOL_RECEIPT_MAX_WAIT_MS = 30 * 60_000;
13
16
 
14
17
  export class RuntimeReceiptWaitTimeoutError extends Error {
15
18
  constructor(key: string) {
@@ -18,6 +21,76 @@ export class RuntimeReceiptWaitTimeoutError extends Error {
18
21
  }
19
22
  }
20
23
 
24
+ export function resolveRuntimeToolReceiptWaitTimeoutMs(
25
+ requestInput: Record<string, unknown>,
26
+ ): number {
27
+ const explicitTimeoutCandidate =
28
+ requestInput.timeoutMs ??
29
+ requestInput.timeout_ms ??
30
+ requestInput.max_wait_ms;
31
+ const explicitTimeoutMs =
32
+ typeof explicitTimeoutCandidate === 'number' &&
33
+ isFinite(explicitTimeoutCandidate) &&
34
+ explicitTimeoutCandidate > 0
35
+ ? explicitTimeoutCandidate
36
+ : undefined;
37
+ const toolTimeoutMs = explicitTimeoutMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS;
38
+ return Math.min(
39
+ TOOL_RECEIPT_MAX_WAIT_MS,
40
+ Math.max(60_000, toolTimeoutMs + TOOL_RECEIPT_COMPLETION_BUFFER_MS),
41
+ );
42
+ }
43
+
44
+ export function resolveRuntimeToolReceiptWaitMaxAttempts(
45
+ requestInput: Record<string, unknown>,
46
+ ): number {
47
+ return Math.ceil(
48
+ resolveRuntimeToolReceiptWaitTimeoutMs(requestInput) /
49
+ DURABLE_RECEIPT_WAIT_DELAY_MS,
50
+ );
51
+ }
52
+
53
+ function throwIfReceiptWaitAborted(signal?: AbortSignal): void {
54
+ if (!signal?.aborted) return;
55
+ throw signal.reason instanceof Error
56
+ ? signal.reason
57
+ : new Error(
58
+ typeof signal.reason === 'string' && signal.reason.trim()
59
+ ? signal.reason
60
+ : 'Durable receipt wait aborted.',
61
+ );
62
+ }
63
+
64
+ async function sleepReceiptWait(
65
+ delayMs: number,
66
+ signal?: AbortSignal,
67
+ ): Promise<void> {
68
+ throwIfReceiptWaitAborted(signal);
69
+ if (delayMs <= 0) return;
70
+ await new Promise<void>((resolve, reject) => {
71
+ const timeout = setTimeout(finish, delayMs);
72
+ const abort = () => {
73
+ clearTimeout(timeout);
74
+ reject(
75
+ signal?.reason instanceof Error
76
+ ? signal.reason
77
+ : new Error(
78
+ typeof signal?.reason === 'string' && signal.reason.trim()
79
+ ? signal.reason
80
+ : 'Durable receipt wait aborted.',
81
+ ),
82
+ );
83
+ };
84
+ function finish() {
85
+ signal?.removeEventListener('abort', abort);
86
+ resolve();
87
+ }
88
+ signal?.addEventListener('abort', abort, { once: true });
89
+ if (signal?.aborted) abort();
90
+ });
91
+ throwIfReceiptWaitAborted(signal);
92
+ }
93
+
21
94
  export type DurableReceiptOperation = 'step' | 'tool' | 'fetch' | 'runPlay';
22
95
 
23
96
  export type DurableReceiptExecutionStore = {
@@ -55,12 +128,14 @@ export async function waitForCompletedRuntimeReceipt(input: {
55
128
  store: Pick<DurableReceiptExecutionStore, 'getMany'>;
56
129
  maxAttempts?: number;
57
130
  delayMs?: number;
131
+ abortSignal?: AbortSignal;
58
132
  }): Promise<RuntimeStepReceipt> {
59
133
  const maxAttempts = input.maxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
60
134
  const delayMs = input.delayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
61
135
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
136
+ throwIfReceiptWaitAborted(input.abortSignal);
62
137
  if (attempt > 0) {
63
- await new Promise((resolve) => setTimeout(resolve, delayMs));
138
+ await sleepReceiptWait(delayMs, input.abortSignal);
64
139
  }
65
140
  const receipt = (await input.store.getMany([input.receiptKey])).get(
66
141
  input.receiptKey,
@@ -189,13 +264,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
189
264
  try {
190
265
  return await waitForRunningReceiptOrTimeout();
191
266
  } catch (error) {
192
- if (
193
- input.repairRunningReceiptForSameRunAfterWaitTimeout === true &&
194
- error instanceof RuntimeReceiptWaitTimeoutError &&
195
- receipt.status === 'running' &&
196
- typeof receipt.runId === 'string' &&
197
- receipt.runId.trim() === input.runId
198
- ) {
267
+ if (error instanceof RuntimeReceiptWaitTimeoutError) {
199
268
  const recovered = await reclaimRunningReceipt();
200
269
  if (recovered.kind === 'recovered') return recovered;
201
270
  return { kind: 'claimed' };
package/dist/cli/index.js CHANGED
@@ -622,10 +622,10 @@ var SDK_RELEASE = {
622
622
  // 0.1.111 ships dataset-native tool list getters and result row datasets.
623
623
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
624
624
  // fields shipped in 0.1.153.
625
- version: "0.1.166",
625
+ version: "0.1.168",
626
626
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
627
627
  supportPolicy: {
628
- latest: "0.1.166",
628
+ latest: "0.1.168",
629
629
  minimumSupported: "0.1.53",
630
630
  deprecatedBelow: "0.1.53",
631
631
  commandMinimumSupported: [
@@ -607,10 +607,10 @@ var SDK_RELEASE = {
607
607
  // 0.1.111 ships dataset-native tool list getters and result row datasets.
608
608
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
609
609
  // fields shipped in 0.1.153.
610
- version: "0.1.166",
610
+ version: "0.1.168",
611
611
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
612
612
  supportPolicy: {
613
- latest: "0.1.166",
613
+ latest: "0.1.168",
614
614
  minimumSupported: "0.1.53",
615
615
  deprecatedBelow: "0.1.53",
616
616
  commandMinimumSupported: [
package/dist/index.js CHANGED
@@ -421,10 +421,10 @@ var SDK_RELEASE = {
421
421
  // 0.1.111 ships dataset-native tool list getters and result row datasets.
422
422
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
423
423
  // fields shipped in 0.1.153.
424
- version: "0.1.166",
424
+ version: "0.1.168",
425
425
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
426
426
  supportPolicy: {
427
- latest: "0.1.166",
427
+ latest: "0.1.168",
428
428
  minimumSupported: "0.1.53",
429
429
  deprecatedBelow: "0.1.53",
430
430
  commandMinimumSupported: [
package/dist/index.mjs CHANGED
@@ -351,10 +351,10 @@ var SDK_RELEASE = {
351
351
  // 0.1.111 ships dataset-native tool list getters and result row datasets.
352
352
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
353
353
  // fields shipped in 0.1.153.
354
- version: "0.1.166",
354
+ version: "0.1.168",
355
355
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
356
356
  supportPolicy: {
357
- latest: "0.1.166",
357
+ latest: "0.1.168",
358
358
  minimumSupported: "0.1.53",
359
359
  deprecatedBelow: "0.1.53",
360
360
  commandMinimumSupported: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.166",
3
+ "version": "0.1.168",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {