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.
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +291 -107
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +12 -2
- package/dist/bundling-sources/sdk/src/agent-runtime.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +80 -6
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +77 -8
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +41 -35
- package/dist/cli/index.js +61 -22
- package/dist/cli/index.mjs +61 -22
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
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.
|
|
1882
|
-
input.group,
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2029
|
-
|
|
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
|
-
(
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
}
|
|
7189
|
-
|
|
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(
|
|
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) &&
|
|
40
|
+
claudeRemote || sessionHome || ((pluginMode || pluginRoot) && projectDir)
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|