deepline 0.1.165 → 0.1.167

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,16 +1918,65 @@ 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
+ );
1959
+ return [];
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
+ });
1842
1973
  return [];
1974
+ } catch (error) {
1975
+ if (!(error instanceof RuntimeReceiptWaitTimeoutError)) {
1976
+ this.rejectRawRequests(input.group, error);
1977
+ return [];
1978
+ }
1979
+ waitError = error;
1843
1980
  }
1844
1981
  if (
1845
1982
  !canReclaimTimedOutWorkerToolReceipt({
@@ -1847,7 +1984,7 @@ class WorkerToolBatchScheduler {
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);
1887
2036
  return [];
1888
2037
  }
1889
- this.rejectRawRequests(input.group, input.waitError);
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
+ }
2084
+ return [];
2085
+ }
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());
@@ -4137,6 +4322,7 @@ function createMinimalWorkerCtx(
4137
4322
  workflowStep?: WorkflowStep,
4138
4323
  abortSignal?: AbortSignal,
4139
4324
  callbacks?: WorkerCtxCallbacks,
4325
+ runtimeDeadlineMs?: number,
4140
4326
  ): unknown {
4141
4327
  const { governor, resolvePacing: resolveToolPacing } =
4142
4328
  createGovernorForRun(req);
@@ -4258,6 +4444,7 @@ function createMinimalWorkerCtx(
4258
4444
  callbacks,
4259
4445
  receiptStore,
4260
4446
  true,
4447
+ runtimeDeadlineMs,
4261
4448
  );
4262
4449
  // Local ancestry chain that always ENDS with the currently-executing play
4263
4450
  // (req.playName). The /api/v2/plays/run lineage validator requires the
@@ -4397,9 +4584,7 @@ function createMinimalWorkerCtx(
4397
4584
  row: Record<string, unknown>,
4398
4585
  ): number | null => {
4399
4586
  const value = row[MAP_ROW_OUTCOME_RUNTIME_FIELDS.inputIndex];
4400
- return typeof value === 'number' && Number.isFinite(value)
4401
- ? value
4402
- : null;
4587
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
4403
4588
  };
4404
4589
  const deriveDefaultMapRowIdentity = (
4405
4590
  row: Record<string, unknown>,
@@ -4461,12 +4646,7 @@ function createMinimalWorkerCtx(
4461
4646
  ): string => {
4462
4647
  const explicitKeyValue = resolveExplicitKeyValue(row, index);
4463
4648
  return explicitKeyValue == null
4464
- ? deriveDefaultMapRowIdentity(
4465
- row,
4466
- index,
4467
- chunkIndex,
4468
- chunkLocalIndex,
4469
- )
4649
+ ? deriveDefaultMapRowIdentity(row, index, chunkIndex, chunkLocalIndex)
4470
4650
  : derivePlayRowIdentityFromKey(explicitKeyValue, name);
4471
4651
  };
4472
4652
  // Cross-chunk dedupe accumulators for the single end-of-map log line.
@@ -4743,6 +4923,7 @@ function createMinimalWorkerCtx(
4743
4923
  callbacks,
4744
4924
  receiptStore,
4745
4925
  false,
4926
+ runtimeDeadlineMs,
4746
4927
  );
4747
4928
  let stepCellsCompleted = 0;
4748
4929
  let stepCellsSkipped = 0;
@@ -7165,6 +7346,7 @@ async function executeRunRequest(
7165
7346
 
7166
7347
  stepLifecycle?.markPreDatasetStepsStarted(startedAt);
7167
7348
  flushLedgerEvents(false);
7349
+ const runtimeDeadlineMs = nowMs() + STANDARD_PLAY_RUNTIME_LIMIT_MS;
7168
7350
  const ctx = createMinimalWorkerCtx(
7169
7351
  req,
7170
7352
  wrappedEmit,
@@ -7172,6 +7354,7 @@ async function executeRunRequest(
7172
7354
  workflowStep,
7173
7355
  abortSignal,
7174
7356
  workerCallbacks,
7357
+ runtimeDeadlineMs,
7175
7358
  );
7176
7359
  // Hard wall-clock cap on active user-code runtime. CF Workflows does not
7177
7360
  // impose a play-level execution ceiling on this substrate, so without this a
@@ -7179,14 +7362,15 @@ async function executeRunRequest(
7179
7362
  // token expires. Aborting the controller surfaces cooperatively through the
7180
7363
  // same assertNotAborted checks used for harness cancellation.
7181
7364
  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);
7365
+ const runtimeDeadlineTimer = setTimeout(
7366
+ () => {
7367
+ runtimeLimitExceeded = true;
7368
+ if (!abortSignal.aborted) {
7369
+ abortController.abort(STANDARD_PLAY_RUNTIME_LIMIT_MESSAGE);
7370
+ }
7371
+ },
7372
+ Math.max(1, runtimeDeadlineMs - nowMs()),
7373
+ );
7190
7374
  try {
7191
7375
  const playStartedAt = nowMs();
7192
7376
  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: {
@@ -35,9 +35,9 @@ export function isCoworkLikeSandbox(): boolean {
35
35
  const projectDir = Boolean(process.env.CLAUDE_PROJECT_DIR?.trim());
36
36
  const pluginRoot = Boolean(process.env.DEEPLINE_PLUGIN_ROOT?.trim());
37
37
  const home = process.env.HOME?.trim() || homedir();
38
- const sessionHome = home.startsWith('/sessions/');
38
+ const sessionHome = home === '/sessions' || home.startsWith('/sessions/');
39
39
  return (
40
- (pluginMode || pluginRoot) && (claudeRemote || projectDir || sessionHome)
40
+ claudeRemote || sessionHome || ((pluginMode || pluginRoot) && projectDir)
41
41
  );
42
42
  }
43
43