deepline 0.1.199 → 0.1.200
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/coordinator-entry.ts +5 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +51 -17
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
- package/dist/bundling-sources/sdk/src/http.ts +5 -2
- package/dist/bundling-sources/sdk/src/play.ts +1 -1
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
- package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
- package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
- package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +2 -3
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +459 -103
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
- package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
- package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
- package/dist/cli/index.js +167 -54
- package/dist/cli/index.mjs +167 -54
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -8
- package/dist/index.mjs +8 -8
- package/dist/plays/bundle-play-file.mjs +15 -20
- package/package.json +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
- package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
- package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
- package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
* PlayContextImpl — the cloud execution engine.
|
|
3
3
|
*
|
|
4
4
|
* Batching model:
|
|
5
|
-
* 1. ctx.dataset("table_key", rows).withColumn("field", resolver).run()
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 1. ctx.dataset("table_key", rows).withColumn("field", resolver).run()
|
|
6
|
+
* admits a bounded row worker pool.
|
|
7
|
+
* 2. ctx.tools.execute() calls inside admitted field resolvers QUEUE requests.
|
|
8
|
+
* 3. The drain loop runs queued provider batches while row workers wait.
|
|
9
|
+
* 4. Provider results resolve rows, and the next pending rows are admitted.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
* - checkpoint: recovered
|
|
12
|
-
* - onBatchComplete: called after each provider batch
|
|
11
|
+
* Runtime integration:
|
|
12
|
+
* - checkpoint: recovered on retry (skip completed batches)
|
|
13
|
+
* - onBatchComplete: called after each provider batch for scheduler checkpointing
|
|
13
14
|
*/
|
|
14
15
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
15
16
|
import {
|
|
16
17
|
createDeferredPlayDataset,
|
|
18
|
+
isPlayDataset,
|
|
19
|
+
iteratePlayDatasetInputPages,
|
|
17
20
|
materializePlayDatasetInput,
|
|
18
21
|
} from '@shared_libs/plays/dataset';
|
|
19
22
|
import type { PlayDataset, PlayDatasetInput } from '@shared_libs/plays/dataset';
|
|
@@ -21,6 +24,12 @@ import {
|
|
|
21
24
|
compileRequestsWithStrategy,
|
|
22
25
|
executeChunkedRequests,
|
|
23
26
|
} from './batch-runtime';
|
|
27
|
+
import { AdaptiveToolDispatcher } from './adaptive-tool-dispatcher';
|
|
28
|
+
import {
|
|
29
|
+
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
30
|
+
runtimeLeaseHeartbeatIntervalFromExpiry,
|
|
31
|
+
} from './lease-policy';
|
|
32
|
+
import { createRuntimeReceiptHeartbeatSupervisor } from './receipt-heartbeat-supervisor';
|
|
24
33
|
import type { PlayQueueHint } from './governor/rate-state-backend';
|
|
25
34
|
import type { MapRowOutcome } from './durability-store';
|
|
26
35
|
import type { WorkReceiptFailureKind } from './work-receipts';
|
|
@@ -41,6 +50,7 @@ import {
|
|
|
41
50
|
PLAY_RUNTIME_API_COMPAT_PATH,
|
|
42
51
|
PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH,
|
|
43
52
|
} from './runtime-api-paths';
|
|
53
|
+
import { vercelProtectionBypassHeader } from './vercel-protection';
|
|
44
54
|
export {
|
|
45
55
|
RuntimeSheetRowsBlockedError,
|
|
46
56
|
type RuntimeSheetBlockedRowDetail,
|
|
@@ -52,6 +62,10 @@ import {
|
|
|
52
62
|
type PacingResolver,
|
|
53
63
|
type PlayExecutionGovernor,
|
|
54
64
|
} from './governor/governor';
|
|
65
|
+
import {
|
|
66
|
+
createRuntimeResourceGovernor,
|
|
67
|
+
type RuntimeResourceGovernor,
|
|
68
|
+
} from './resource-governor';
|
|
55
69
|
import { CTX_FETCH_EGRESS_TOOL_ID } from './builtin-pacing';
|
|
56
70
|
import { InMemoryRateStateBackend } from './governor/in-memory-rate-state-backend';
|
|
57
71
|
import { pacingPolicyForTool } from './pacing';
|
|
@@ -101,6 +115,7 @@ import {
|
|
|
101
115
|
runtimeReceiptFailureKindForError,
|
|
102
116
|
runtimeReceiptOutput as durableRuntimeReceiptOutput,
|
|
103
117
|
waitForCompletedRuntimeReceipt,
|
|
118
|
+
waitForCompletedRuntimeReceipts,
|
|
104
119
|
type DurableReceiptExecutionStore,
|
|
105
120
|
} from './durable-receipt-execution';
|
|
106
121
|
import {
|
|
@@ -116,6 +131,10 @@ import {
|
|
|
116
131
|
tripRuntimePersistenceLatch,
|
|
117
132
|
type RuntimePersistenceLatch,
|
|
118
133
|
} from './persistence-latch';
|
|
134
|
+
import {
|
|
135
|
+
assertCustomerOutputObjectWithinLimit,
|
|
136
|
+
assertRuntimeReceiptOutputWithinLimit,
|
|
137
|
+
} from './output-size-limits';
|
|
119
138
|
import { createRuntimeDatasetId } from './dataset-id';
|
|
120
139
|
import { dedupeExplicitMapKeyRows } from './map-row-identity';
|
|
121
140
|
import {
|
|
@@ -127,6 +146,7 @@ import {
|
|
|
127
146
|
sha256Hex,
|
|
128
147
|
stableStringify,
|
|
129
148
|
} from '@shared_libs/plays/row-identity';
|
|
149
|
+
import { sqlSafePlayColumnName } from '@shared_libs/plays/static-pipeline';
|
|
130
150
|
import {
|
|
131
151
|
DEEPLINE_CELL_META_FIELD,
|
|
132
152
|
previousCellFromValue,
|
|
@@ -138,6 +158,15 @@ import {
|
|
|
138
158
|
stripCsvProjectionMetadata,
|
|
139
159
|
toSerializableCsvAliasedRow,
|
|
140
160
|
} from './csv-rename';
|
|
161
|
+
import {
|
|
162
|
+
createRuntimeMapMaterializationTracker,
|
|
163
|
+
createRuntimeMapRetainedRowsTracker,
|
|
164
|
+
estimateRuntimeMapRowsMemory,
|
|
165
|
+
resolveRuntimeMapRowAdmission,
|
|
166
|
+
runtimeMapJsonByteLength,
|
|
167
|
+
resolveRuntimeMapMemoryLimits,
|
|
168
|
+
RuntimeMapMemoryLimitError,
|
|
169
|
+
} from './map-memory-limits';
|
|
141
170
|
import { setSpanAttributes, withActiveSpan } from './tracing';
|
|
142
171
|
import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from './runtime-constraints';
|
|
143
172
|
import {
|
|
@@ -234,11 +263,83 @@ const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
|
234
263
|
const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
235
264
|
const FETCH_TRANSPORT_MAX_ATTEMPTS = 3;
|
|
236
265
|
const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
|
|
266
|
+
const CHILD_PLAY_LAUNCH_WRITE_CONFLICT_RETRY_DELAYS_MS = [
|
|
267
|
+
50, 100, 200, 400, 800,
|
|
268
|
+
] as const;
|
|
269
|
+
const CHILD_PLAY_LAUNCH_ADMISSION_RETRY_DELAYS_MS = [
|
|
270
|
+
250, 500, 1_000, 2_000, 4_000, 8_000, 8_000, 8_000, 8_000, 8_000, 8_000,
|
|
271
|
+
8_000, 8_000, 8_000, 8_000,
|
|
272
|
+
] as const;
|
|
237
273
|
const NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS = 100;
|
|
238
274
|
const NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS = 25;
|
|
275
|
+
// A single batched runtime-step-receipt request (get/claim/complete/fail/seed)
|
|
276
|
+
// carries at most this many keys. At map scale a run can hold 5k-10k receipt
|
|
277
|
+
// keys; sending them all in one request made the server claim/complete them
|
|
278
|
+
// one-per-key serially in a single HTTP call, which crossed the ~100s origin
|
|
279
|
+
// cap and 524'd (ADR-0012 receipt fence at 5k/10k rows). Chunking bounds each
|
|
280
|
+
// request's server-side work while keeping per-key results faithful — chunks
|
|
281
|
+
// are dispatched in order and their aligned result arrays concatenated, so the
|
|
282
|
+
// key→receipt mapping is identical to the unchunked path.
|
|
283
|
+
export const RUNTIME_STEP_RECEIPT_REQUEST_CHUNK_SIZE = 1_000;
|
|
284
|
+
|
|
285
|
+
export function chunkArrayForReceiptRequest<T>(items: readonly T[]): T[][] {
|
|
286
|
+
if (items.length <= RUNTIME_STEP_RECEIPT_REQUEST_CHUNK_SIZE) {
|
|
287
|
+
return items.length === 0 ? [] : [[...items]];
|
|
288
|
+
}
|
|
289
|
+
const chunks: T[][] = [];
|
|
290
|
+
for (
|
|
291
|
+
let start = 0;
|
|
292
|
+
start < items.length;
|
|
293
|
+
start += RUNTIME_STEP_RECEIPT_REQUEST_CHUNK_SIZE
|
|
294
|
+
) {
|
|
295
|
+
chunks.push(
|
|
296
|
+
items.slice(start, start + RUNTIME_STEP_RECEIPT_REQUEST_CHUNK_SIZE),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return chunks;
|
|
300
|
+
}
|
|
301
|
+
|
|
239
302
|
type SafeFetchModule = typeof import('@shared_libs/security/safe-fetch');
|
|
240
303
|
let safeFetchModule: Promise<SafeFetchModule> | null = null;
|
|
241
304
|
|
|
305
|
+
function sleepTransientRuntimeMs(ms: number): Promise<void> {
|
|
306
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isConvexWriteConflictMessage(message: string): boolean {
|
|
310
|
+
return (
|
|
311
|
+
message.includes('OptimisticConcurrencyControlFailure') ||
|
|
312
|
+
/changed while this mutation was being run/i.test(message) ||
|
|
313
|
+
/write conflict/i.test(message)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isChildPlayLaunchAdmissionMessage(message: string): boolean {
|
|
318
|
+
return /too many play runs (?:are )?(?:starting|active)/i.test(message);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function childPlayLaunchRetryDelayMs(input: {
|
|
322
|
+
status: number;
|
|
323
|
+
message: string;
|
|
324
|
+
attempt: number;
|
|
325
|
+
}): number | null {
|
|
326
|
+
if (
|
|
327
|
+
input.status === 429 &&
|
|
328
|
+
isChildPlayLaunchAdmissionMessage(input.message)
|
|
329
|
+
) {
|
|
330
|
+
return CHILD_PLAY_LAUNCH_ADMISSION_RETRY_DELAYS_MS[input.attempt] ?? null;
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
![409, 429, 500, 502, 503, 504].includes(input.status) ||
|
|
334
|
+
!isConvexWriteConflictMessage(input.message)
|
|
335
|
+
) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return (
|
|
339
|
+
CHILD_PLAY_LAUNCH_WRITE_CONFLICT_RETRY_DELAYS_MS[input.attempt] ?? null
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
242
343
|
export async function waitForNodeRuntimeMapRowsVisible(input: {
|
|
243
344
|
mapName: string;
|
|
244
345
|
tableNamespace: string;
|
|
@@ -291,6 +392,27 @@ export async function waitForNodeRuntimeMapRowsVisible(input: {
|
|
|
291
392
|
);
|
|
292
393
|
}
|
|
293
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Persisted sheet fields to layer onto a fresh map input row during seeding,
|
|
397
|
+
* excluding any column the current run's input already provides. Input columns
|
|
398
|
+
* stay authoritative (a persisted row is matched only by the map key and may
|
|
399
|
+
* belong to a different run that merely shares that key); runtime fields like
|
|
400
|
+
* __deeplineRowKey and re-derivable output columns — never present on the raw
|
|
401
|
+
* input row — are restored.
|
|
402
|
+
*/
|
|
403
|
+
export function persistedFieldsAbsentFromInputRow(
|
|
404
|
+
inputRow: Record<string, unknown>,
|
|
405
|
+
persisted: Record<string, unknown>,
|
|
406
|
+
): Record<string, unknown> {
|
|
407
|
+
const extra: Record<string, unknown> = {};
|
|
408
|
+
for (const key of Object.keys(persisted)) {
|
|
409
|
+
if (!Object.prototype.hasOwnProperty.call(inputRow, key)) {
|
|
410
|
+
extra[key] = persisted[key];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return extra;
|
|
414
|
+
}
|
|
415
|
+
|
|
294
416
|
export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
|
|
295
417
|
mapName: string;
|
|
296
418
|
tableNamespace: string;
|
|
@@ -443,6 +565,10 @@ type ToolExecutionApiOptions = {
|
|
|
443
565
|
durableCallReceiptKey?: string | null;
|
|
444
566
|
executionAuthScopeDigest?: string | null;
|
|
445
567
|
providerIdempotencyKey?: string | null;
|
|
568
|
+
receiptLeaseExpiresAt?: string | null;
|
|
569
|
+
beforeProviderCall?: () => Promise<void> | void;
|
|
570
|
+
heartbeatReceipt?: () => Promise<void> | void;
|
|
571
|
+
parkProviderCall?: () => Promise<void> | void;
|
|
446
572
|
customerDbDataset?: {
|
|
447
573
|
limit: number;
|
|
448
574
|
offset: number;
|
|
@@ -458,6 +584,10 @@ function shouldPersistMapCellField(fieldName: string): boolean {
|
|
|
458
584
|
return !fieldName.startsWith('_') || fieldName === '_metadata';
|
|
459
585
|
}
|
|
460
586
|
|
|
587
|
+
function runtimeSheetPatchFieldName(fieldName: string): string {
|
|
588
|
+
return fieldName.includes('.') ? sqlSafePlayColumnName(fieldName) : fieldName;
|
|
589
|
+
}
|
|
590
|
+
|
|
461
591
|
type PersistableMapRow = MapRowOutcome;
|
|
462
592
|
|
|
463
593
|
function comparePersistableMapRowsByInputIndex(
|
|
@@ -486,6 +616,22 @@ function persistableMapRowBytes(row: PersistableMapRow): number {
|
|
|
486
616
|
return JSON.stringify(row).length;
|
|
487
617
|
}
|
|
488
618
|
|
|
619
|
+
function assertPersistableMapRowWithinCustomerOutputLimits(input: {
|
|
620
|
+
mapName: string;
|
|
621
|
+
row: PersistableMapRow;
|
|
622
|
+
}): void {
|
|
623
|
+
const rowLabel =
|
|
624
|
+
input.row.key?.trim() ||
|
|
625
|
+
(typeof input.row.inputIndex === 'number' &&
|
|
626
|
+
Number.isFinite(input.row.inputIndex)
|
|
627
|
+
? `input:${Math.floor(input.row.inputIndex)}`
|
|
628
|
+
: 'unknown');
|
|
629
|
+
assertCustomerOutputObjectWithinLimit({
|
|
630
|
+
value: input.row.data,
|
|
631
|
+
path: `ctx.dataset("${input.mapName}").row("${rowLabel}")`,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
489
635
|
function failedMapRowLogLabel(row: PersistableMapRow): string {
|
|
490
636
|
const rowId = row.data?.row_id;
|
|
491
637
|
if (typeof rowId === 'string' && rowId.trim()) return rowId.trim();
|
|
@@ -786,6 +932,10 @@ class RuntimeDatasetBuilder<T extends Record<string, unknown>> {
|
|
|
786
932
|
|
|
787
933
|
const WAITING_ROW = Symbol('deepline.waiting_row');
|
|
788
934
|
const FAILED_ROW = Symbol('deepline.failed_row');
|
|
935
|
+
type MapRowExecutionResult =
|
|
936
|
+
| Record<string, unknown>
|
|
937
|
+
| typeof WAITING_ROW
|
|
938
|
+
| typeof FAILED_ROW;
|
|
789
939
|
|
|
790
940
|
function normalizeFetchHeaders(
|
|
791
941
|
headers: RequestInit['headers'],
|
|
@@ -933,6 +1083,7 @@ export class PlayContextImpl {
|
|
|
933
1083
|
* via `chargeBudget`; child plays fork lineage-global counters via `forkChild`.
|
|
934
1084
|
*/
|
|
935
1085
|
private readonly governor: PlayExecutionGovernor;
|
|
1086
|
+
private readonly resourceGovernor: RuntimeResourceGovernor;
|
|
936
1087
|
/**
|
|
937
1088
|
* Lineage identity for this context (run/play ids + ancestry). Seeded from the
|
|
938
1089
|
* Governor snapshot; used only for receipt keys, durable-boundary scoping, and
|
|
@@ -1071,6 +1222,9 @@ export class PlayContextImpl {
|
|
|
1071
1222
|
ancestryPlayIds: [rootPlayId],
|
|
1072
1223
|
},
|
|
1073
1224
|
});
|
|
1225
|
+
this.resourceGovernor = createRuntimeResourceGovernor({
|
|
1226
|
+
executionGovernor: this.governor,
|
|
1227
|
+
});
|
|
1074
1228
|
this.governance = this.governor.snapshot();
|
|
1075
1229
|
}
|
|
1076
1230
|
|
|
@@ -1191,7 +1345,13 @@ export class PlayContextImpl {
|
|
|
1191
1345
|
}
|
|
1192
1346
|
}
|
|
1193
1347
|
|
|
1194
|
-
private
|
|
1348
|
+
private async vercelProtectionHeaders(): Promise<Record<string, string>> {
|
|
1349
|
+
return vercelProtectionBypassHeader(
|
|
1350
|
+
this.#options.vercelProtectionBypassToken,
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private async runtimeApiHeaders(): Promise<Record<string, string>> {
|
|
1195
1355
|
if (!this.#options.executorToken?.trim()) {
|
|
1196
1356
|
throw new Error(
|
|
1197
1357
|
'ctx.runPlay scheduled child launch requires an executor token.',
|
|
@@ -1207,12 +1367,7 @@ export class PlayContextImpl {
|
|
|
1207
1367
|
[SYNTHETIC_RUN_HEADER]: '1',
|
|
1208
1368
|
}
|
|
1209
1369
|
: {}),
|
|
1210
|
-
...(this
|
|
1211
|
-
? {
|
|
1212
|
-
'x-vercel-protection-bypass':
|
|
1213
|
-
this.#options.vercelProtectionBypassToken.trim(),
|
|
1214
|
-
}
|
|
1215
|
-
: {}),
|
|
1370
|
+
...(await this.vercelProtectionHeaders()),
|
|
1216
1371
|
};
|
|
1217
1372
|
}
|
|
1218
1373
|
|
|
@@ -1250,25 +1405,36 @@ export class PlayContextImpl {
|
|
|
1250
1405
|
descendantCount: input.childGovernance.descendantCount,
|
|
1251
1406
|
waterfallStepExecutions: input.childGovernance.waterfallStepExecutions,
|
|
1252
1407
|
};
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
internalRunPlay: {
|
|
1265
|
-
...governance,
|
|
1266
|
-
description: input.description ?? null,
|
|
1267
|
-
},
|
|
1268
|
-
}),
|
|
1408
|
+
const requestBody = JSON.stringify({
|
|
1409
|
+
name: input.childPlayName,
|
|
1410
|
+
workflowId: input.childRunId,
|
|
1411
|
+
input: input.childInput,
|
|
1412
|
+
// Inherit the parent's scheduler profile so an `absurd` parent launches
|
|
1413
|
+
// its child on the same scheduler. Absent threading defaults to absurd.
|
|
1414
|
+
profile: this.#options.childRunProfile?.trim() || 'absurd',
|
|
1415
|
+
waitForCompletionMs: 0,
|
|
1416
|
+
internalRunPlay: {
|
|
1417
|
+
...governance,
|
|
1418
|
+
description: input.description ?? null,
|
|
1269
1419
|
},
|
|
1270
|
-
);
|
|
1271
|
-
|
|
1420
|
+
});
|
|
1421
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1422
|
+
const response = await fetch(
|
|
1423
|
+
`${this.runtimeApiBaseUrl()}/api/v2/plays/run`,
|
|
1424
|
+
{
|
|
1425
|
+
method: 'POST',
|
|
1426
|
+
headers: await this.runtimeApiHeaders(),
|
|
1427
|
+
body: requestBody,
|
|
1428
|
+
},
|
|
1429
|
+
);
|
|
1430
|
+
if (response.ok) {
|
|
1431
|
+
if (attempt > 0) {
|
|
1432
|
+
this.log(
|
|
1433
|
+
`ctx.runPlay(${input.key}): child launch recovered after ${attempt} retry(s)`,
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1272
1438
|
const body = (await response.json().catch(() => null)) as {
|
|
1273
1439
|
error?: unknown;
|
|
1274
1440
|
} | null;
|
|
@@ -1276,9 +1442,24 @@ export class PlayContextImpl {
|
|
|
1276
1442
|
typeof body?.error === 'string' && body.error.trim()
|
|
1277
1443
|
? body.error.trim()
|
|
1278
1444
|
: response.statusText;
|
|
1279
|
-
|
|
1280
|
-
|
|
1445
|
+
const retryDelayMs = childPlayLaunchRetryDelayMs({
|
|
1446
|
+
status: response.status,
|
|
1447
|
+
message,
|
|
1448
|
+
attempt,
|
|
1449
|
+
});
|
|
1450
|
+
if (retryDelayMs === null) {
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
`ctx.runPlay(${input.key}) failed to launch child play "${input.childPlayName}": ${message} (status ${response.status}).`,
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
const retryReason =
|
|
1456
|
+
response.status === 429 && isChildPlayLaunchAdmissionMessage(message)
|
|
1457
|
+
? 'start admission backpressure'
|
|
1458
|
+
: 'transient write conflict';
|
|
1459
|
+
this.log(
|
|
1460
|
+
`ctx.runPlay(${input.key}): retrying child launch after ${retryReason} (attempt ${attempt + 1})`,
|
|
1281
1461
|
);
|
|
1462
|
+
await sleepTransientRuntimeMs(retryDelayMs);
|
|
1282
1463
|
}
|
|
1283
1464
|
}
|
|
1284
1465
|
|
|
@@ -1294,7 +1475,7 @@ export class PlayContextImpl {
|
|
|
1294
1475
|
`${this.runtimeApiBaseUrl()}${PLAY_RUNTIME_API_COMPAT_PATH}`,
|
|
1295
1476
|
{
|
|
1296
1477
|
method: 'POST',
|
|
1297
|
-
headers: this.runtimeApiHeaders(),
|
|
1478
|
+
headers: await this.runtimeApiHeaders(),
|
|
1298
1479
|
body: JSON.stringify({
|
|
1299
1480
|
action: 'read_child_run_terminal_snapshot',
|
|
1300
1481
|
parentRunId: this.governance.currentRunId,
|
|
@@ -1412,12 +1593,7 @@ export class PlayContextImpl {
|
|
|
1412
1593
|
headers: {
|
|
1413
1594
|
Authorization: `Bearer ${this.#options.executorToken}`,
|
|
1414
1595
|
'Content-Type': 'application/json',
|
|
1415
|
-
...(this
|
|
1416
|
-
? {
|
|
1417
|
-
'x-vercel-protection-bypass':
|
|
1418
|
-
this.#options.vercelProtectionBypassToken.trim(),
|
|
1419
|
-
}
|
|
1420
|
-
: {}),
|
|
1596
|
+
...(await this.vercelProtectionHeaders()),
|
|
1421
1597
|
},
|
|
1422
1598
|
body: JSON.stringify({
|
|
1423
1599
|
action: 'resolve_secret',
|
|
@@ -1459,7 +1635,8 @@ export class PlayContextImpl {
|
|
|
1459
1635
|
artifactTableNamespace: string;
|
|
1460
1636
|
mapNodeId?: string | null;
|
|
1461
1637
|
explicitKey?:
|
|
1462
|
-
((row: Record<string, unknown>, index: number) => string)
|
|
1638
|
+
| ((row: Record<string, unknown>, index: number) => string)
|
|
1639
|
+
| null;
|
|
1463
1640
|
}): MapExecutionScope {
|
|
1464
1641
|
const mapInvocationId = `${input.logicalNamespace}:${this.mapInvocationIndex}`;
|
|
1465
1642
|
this.mapInvocationIndex += 1;
|
|
@@ -1507,7 +1684,7 @@ export class PlayContextImpl {
|
|
|
1507
1684
|
return receipt
|
|
1508
1685
|
? {
|
|
1509
1686
|
key: key.trim(),
|
|
1510
|
-
status: '
|
|
1687
|
+
status: 'queued',
|
|
1511
1688
|
}
|
|
1512
1689
|
: null;
|
|
1513
1690
|
}
|
|
@@ -1548,6 +1725,10 @@ export class PlayContextImpl {
|
|
|
1548
1725
|
leaseId?: string | null,
|
|
1549
1726
|
): Promise<RuntimeStepReceipt | null> {
|
|
1550
1727
|
assertNoSecretTaint(output, 'ctx.step receipt output');
|
|
1728
|
+
assertRuntimeReceiptOutputWithinLimit({
|
|
1729
|
+
output,
|
|
1730
|
+
path: `ctx.step receipt "${key}"`,
|
|
1731
|
+
});
|
|
1551
1732
|
if (!this.#options.completeRuntimeStepReceipt) {
|
|
1552
1733
|
return null;
|
|
1553
1734
|
}
|
|
@@ -1639,6 +1820,30 @@ export class PlayContextImpl {
|
|
|
1639
1820
|
byLeaseId.set(target.leaseId, keys);
|
|
1640
1821
|
}
|
|
1641
1822
|
|
|
1823
|
+
if (
|
|
1824
|
+
this.#options.markRuntimeStepReceiptsRunning ||
|
|
1825
|
+
this.#options.markRuntimeStepReceiptRunning
|
|
1826
|
+
) {
|
|
1827
|
+
const receipts = await this.markRuntimeStepReceiptsRunning(
|
|
1828
|
+
targets.map((target) => ({
|
|
1829
|
+
key: target.receiptKey,
|
|
1830
|
+
runId: this.currentRunId,
|
|
1831
|
+
leaseId: target.leaseId,
|
|
1832
|
+
})),
|
|
1833
|
+
);
|
|
1834
|
+
for (const target of targets) {
|
|
1835
|
+
const receipt = receipts.get(target.receiptKey);
|
|
1836
|
+
if (!this.runtimeToolReceiptStillOwned(receipt, target.leaseId)) {
|
|
1837
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
1838
|
+
receiptKey: target.receiptKey,
|
|
1839
|
+
runId: this.currentRunId,
|
|
1840
|
+
leaseId: target.leaseId,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1642
1847
|
if (this.#options.heartbeatRuntimeStepReceipts) {
|
|
1643
1848
|
for (const [leaseId, keys] of byLeaseId) {
|
|
1644
1849
|
const receipts = await this.#options.heartbeatRuntimeStepReceipts({
|
|
@@ -1702,7 +1907,11 @@ export class PlayContextImpl {
|
|
|
1702
1907
|
receipt: RuntimeStepReceipt | null | undefined,
|
|
1703
1908
|
leaseId: string,
|
|
1704
1909
|
): boolean {
|
|
1705
|
-
if (!receipt
|
|
1910
|
+
if (!receipt) return false;
|
|
1911
|
+
if (receipt.status === 'completed' || receipt.status === 'skipped') {
|
|
1912
|
+
return true;
|
|
1913
|
+
}
|
|
1914
|
+
if (receipt.status !== 'running') return false;
|
|
1706
1915
|
if (receipt.leaseId !== leaseId) return false;
|
|
1707
1916
|
const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
|
|
1708
1917
|
return !ownerRunId || ownerRunId === this.currentRunId;
|
|
@@ -1727,6 +1936,18 @@ export class PlayContextImpl {
|
|
|
1727
1936
|
return ownerAttempt === this.currentRunAttempt;
|
|
1728
1937
|
}
|
|
1729
1938
|
|
|
1939
|
+
private isOwnedClaimedRuntimeReceipt(
|
|
1940
|
+
receipt: RuntimeStepReceipt | null | undefined,
|
|
1941
|
+
): receipt is RuntimeStepReceipt {
|
|
1942
|
+
return (
|
|
1943
|
+
Boolean(receipt) &&
|
|
1944
|
+
receipt?.claimState !== 'existing' &&
|
|
1945
|
+
(receipt?.status === 'queued' ||
|
|
1946
|
+
receipt?.status === 'pending' ||
|
|
1947
|
+
receipt?.status === 'running')
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1730
1951
|
private async failRuntimeStepReceipt(
|
|
1731
1952
|
key: string,
|
|
1732
1953
|
runId: string,
|
|
@@ -1774,6 +1995,38 @@ export class PlayContextImpl {
|
|
|
1774
1995
|
};
|
|
1775
1996
|
}
|
|
1776
1997
|
|
|
1998
|
+
/**
|
|
1999
|
+
* Dispatch a batched runtime-step-receipt request in bounded chunks and
|
|
2000
|
+
* concatenate the aligned result arrays back into one array positioned
|
|
2001
|
+
* identically to `items`. Every batch adapter returns receipts index-aligned
|
|
2002
|
+
* to the keys/receipts it was given, and chunks are dispatched sequentially in
|
|
2003
|
+
* input order, so the concatenation preserves the exact per-key mapping the
|
|
2004
|
+
* callers rely on. Sequential dispatch also keeps at most one receipt request
|
|
2005
|
+
* in flight, bounding tenant-DB and origin load at map scale. See
|
|
2006
|
+
* RUNTIME_STEP_RECEIPT_REQUEST_CHUNK_SIZE.
|
|
2007
|
+
*/
|
|
2008
|
+
private async dispatchChunkedRuntimeReceiptRequest<TItem>(
|
|
2009
|
+
items: readonly TItem[],
|
|
2010
|
+
dispatchChunk: (
|
|
2011
|
+
chunk: TItem[],
|
|
2012
|
+
) =>
|
|
2013
|
+
| Promise<Array<RuntimeStepReceipt | null>>
|
|
2014
|
+
| Array<RuntimeStepReceipt | null>,
|
|
2015
|
+
): Promise<Array<RuntimeStepReceipt | null>> {
|
|
2016
|
+
const chunks = chunkArrayForReceiptRequest(items);
|
|
2017
|
+
if (chunks.length <= 1) {
|
|
2018
|
+
return chunks.length === 0 ? [] : dispatchChunk(chunks[0]!);
|
|
2019
|
+
}
|
|
2020
|
+
const results: Array<RuntimeStepReceipt | null> = [];
|
|
2021
|
+
for (const chunk of chunks) {
|
|
2022
|
+
const chunkResults = await dispatchChunk(chunk);
|
|
2023
|
+
for (const result of chunkResults) {
|
|
2024
|
+
results.push(result);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return results;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1777
2030
|
private async getRuntimeStepReceipts(
|
|
1778
2031
|
keys: string[],
|
|
1779
2032
|
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
@@ -1781,8 +2034,11 @@ export class PlayContextImpl {
|
|
|
1781
2034
|
...new Set(keys.map((key) => key.trim()).filter(Boolean)),
|
|
1782
2035
|
];
|
|
1783
2036
|
if (uniqueKeys.length === 0) return new Map();
|
|
1784
|
-
const
|
|
1785
|
-
|
|
2037
|
+
const getReceipts = this.#options.getRuntimeStepReceipts;
|
|
2038
|
+
const receipts = getReceipts
|
|
2039
|
+
? await this.dispatchChunkedRuntimeReceiptRequest(uniqueKeys, (chunk) =>
|
|
2040
|
+
getReceipts({ keys: chunk }),
|
|
2041
|
+
)
|
|
1786
2042
|
: await Promise.all(
|
|
1787
2043
|
uniqueKeys.map((key) => this.getRuntimeStepReceipt(key)),
|
|
1788
2044
|
);
|
|
@@ -1808,16 +2064,19 @@ export class PlayContextImpl {
|
|
|
1808
2064
|
...new Set(keys.map((key) => key.trim()).filter(Boolean)),
|
|
1809
2065
|
];
|
|
1810
2066
|
if (uniqueKeys.length === 0) return new Map();
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2067
|
+
const claimReceipts = this.#options.claimRuntimeStepReceipts;
|
|
2068
|
+
const receipts = claimReceipts
|
|
2069
|
+
? await this.dispatchChunkedRuntimeReceiptRequest(uniqueKeys, (chunk) =>
|
|
2070
|
+
claimReceipts({
|
|
2071
|
+
keys: chunk,
|
|
2072
|
+
runId,
|
|
2073
|
+
runAttempt: this.currentRunAttempt,
|
|
2074
|
+
leaseAware: true,
|
|
2075
|
+
...(reclaimRunning ? { reclaimRunning: true } : {}),
|
|
2076
|
+
...(forceRefresh ? { forceRefresh: true } : {}),
|
|
2077
|
+
...(forceFailedRefresh ? { forceFailedRefresh: true } : {}),
|
|
2078
|
+
}),
|
|
2079
|
+
)
|
|
1821
2080
|
: await Promise.all(
|
|
1822
2081
|
uniqueKeys.map((key) =>
|
|
1823
2082
|
this.claimRuntimeStepReceipt(
|
|
@@ -1840,6 +2099,111 @@ export class PlayContextImpl {
|
|
|
1840
2099
|
return byKey;
|
|
1841
2100
|
}
|
|
1842
2101
|
|
|
2102
|
+
private async markRuntimeStepReceiptRunning(input: {
|
|
2103
|
+
key: string;
|
|
2104
|
+
runId: string;
|
|
2105
|
+
leaseId?: string | null;
|
|
2106
|
+
}): Promise<RuntimeStepReceipt | null> {
|
|
2107
|
+
if (!this.#options.markRuntimeStepReceiptRunning) {
|
|
2108
|
+
if (input.leaseId && this.#options.heartbeatRuntimeStepReceipts) {
|
|
2109
|
+
return await this.heartbeatRuntimeStepReceipt(
|
|
2110
|
+
input.key,
|
|
2111
|
+
input.runId,
|
|
2112
|
+
input.leaseId,
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
return await this.getRuntimeStepReceipt(input.key);
|
|
2116
|
+
}
|
|
2117
|
+
const receipt = await this.#options.markRuntimeStepReceiptRunning({
|
|
2118
|
+
key: input.key,
|
|
2119
|
+
runId: input.runId,
|
|
2120
|
+
runAttempt: this.currentRunAttempt,
|
|
2121
|
+
...(input.leaseId ? { leaseId: input.leaseId } : {}),
|
|
2122
|
+
});
|
|
2123
|
+
return this.normalizeRuntimeStepReceipt(input.key, receipt);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
private async markRuntimeStepReceiptsRunning(
|
|
2127
|
+
receipts: Array<{
|
|
2128
|
+
key: string;
|
|
2129
|
+
runId: string;
|
|
2130
|
+
leaseId?: string | null;
|
|
2131
|
+
}>,
|
|
2132
|
+
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
2133
|
+
const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
|
|
2134
|
+
if (normalizedInputs.length === 0) return new Map();
|
|
2135
|
+
let marked: Array<RuntimeStepReceipt | null>;
|
|
2136
|
+
const markReceipts = this.#options.markRuntimeStepReceiptsRunning;
|
|
2137
|
+
if (markReceipts) {
|
|
2138
|
+
marked = await this.dispatchChunkedRuntimeReceiptRequest(
|
|
2139
|
+
normalizedInputs.map((receipt) => ({
|
|
2140
|
+
...receipt,
|
|
2141
|
+
runAttempt: this.currentRunAttempt,
|
|
2142
|
+
})),
|
|
2143
|
+
(chunk) => markReceipts({ receipts: chunk }),
|
|
2144
|
+
);
|
|
2145
|
+
} else {
|
|
2146
|
+
marked = await Promise.all(
|
|
2147
|
+
normalizedInputs.map((receipt) =>
|
|
2148
|
+
this.markRuntimeStepReceiptRunning(receipt),
|
|
2149
|
+
),
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
const byKey = new Map<string, RuntimeStepReceipt>();
|
|
2153
|
+
for (let index = 0; index < marked.length; index += 1) {
|
|
2154
|
+
const normalized = this.normalizeRuntimeStepReceipt(
|
|
2155
|
+
normalizedInputs[index]?.key ?? '',
|
|
2156
|
+
marked[index],
|
|
2157
|
+
);
|
|
2158
|
+
if (normalized) byKey.set(normalized.key, normalized);
|
|
2159
|
+
}
|
|
2160
|
+
return byKey;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
private async markRuntimeStepReceiptsQueued(
|
|
2164
|
+
receipts: Array<{
|
|
2165
|
+
key: string;
|
|
2166
|
+
runId: string;
|
|
2167
|
+
leaseId?: string | null;
|
|
2168
|
+
}>,
|
|
2169
|
+
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
2170
|
+
const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
|
|
2171
|
+
if (normalizedInputs.length === 0) return new Map();
|
|
2172
|
+
if (!this.#options.markRuntimeStepReceiptsQueued) {
|
|
2173
|
+
return new Map();
|
|
2174
|
+
}
|
|
2175
|
+
const queued = await this.dispatchChunkedRuntimeReceiptRequest(
|
|
2176
|
+
normalizedInputs.map((receipt) => ({
|
|
2177
|
+
...receipt,
|
|
2178
|
+
runAttempt: this.currentRunAttempt,
|
|
2179
|
+
})),
|
|
2180
|
+
(chunk) =>
|
|
2181
|
+
this.#options.markRuntimeStepReceiptsQueued!({ receipts: chunk }),
|
|
2182
|
+
);
|
|
2183
|
+
const byKey = new Map<string, RuntimeStepReceipt>();
|
|
2184
|
+
for (let index = 0; index < queued.length; index += 1) {
|
|
2185
|
+
const normalized = this.normalizeRuntimeStepReceipt(
|
|
2186
|
+
normalizedInputs[index]?.key ?? '',
|
|
2187
|
+
queued[index],
|
|
2188
|
+
);
|
|
2189
|
+
if (normalized) byKey.set(normalized.key, normalized);
|
|
2190
|
+
}
|
|
2191
|
+
return byKey;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
private async parkRuntimeToolReceiptsQueued(
|
|
2195
|
+
requests: ToolCallRequest[],
|
|
2196
|
+
): Promise<void> {
|
|
2197
|
+
const targets = requests.flatMap((request) => {
|
|
2198
|
+
const receiptKey = request.receiptKey?.trim() || null;
|
|
2199
|
+
const leaseId = request.receiptLeaseId?.trim() || null;
|
|
2200
|
+
if (!receiptKey || !leaseId) return [];
|
|
2201
|
+
return [{ key: receiptKey, runId: this.currentRunId, leaseId }];
|
|
2202
|
+
});
|
|
2203
|
+
if (targets.length === 0) return;
|
|
2204
|
+
await this.markRuntimeStepReceiptsQueued(targets);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1843
2207
|
private async completeRuntimeStepReceipts(
|
|
1844
2208
|
receipts: Array<{
|
|
1845
2209
|
key: string;
|
|
@@ -1853,16 +2217,22 @@ export class PlayContextImpl {
|
|
|
1853
2217
|
if (normalizedInputs.length === 0) return new Map();
|
|
1854
2218
|
for (const receipt of normalizedInputs) {
|
|
1855
2219
|
assertNoSecretTaint(receipt.output, 'ctx.tool receipt output');
|
|
2220
|
+
assertRuntimeReceiptOutputWithinLimit({
|
|
2221
|
+
output: receipt.output,
|
|
2222
|
+
path: `ctx.tool receipt "${receipt.key}"`,
|
|
2223
|
+
});
|
|
1856
2224
|
}
|
|
1857
2225
|
let completed: Array<RuntimeStepReceipt | null>;
|
|
1858
2226
|
try {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2227
|
+
const completeReceipts = this.#options.completeRuntimeStepReceipts;
|
|
2228
|
+
completed = completeReceipts
|
|
2229
|
+
? await this.dispatchChunkedRuntimeReceiptRequest(
|
|
2230
|
+
normalizedInputs.map((receipt) => ({
|
|
1862
2231
|
...receipt,
|
|
1863
2232
|
runAttempt: receipt.runAttempt ?? this.currentRunAttempt,
|
|
1864
2233
|
})),
|
|
1865
|
-
|
|
2234
|
+
(chunk) => completeReceipts({ receipts: chunk }),
|
|
2235
|
+
)
|
|
1866
2236
|
: await Promise.all(
|
|
1867
2237
|
normalizedInputs.map((receipt) =>
|
|
1868
2238
|
this.completeRuntimeStepReceipt(
|
|
@@ -1998,13 +2368,15 @@ export class PlayContextImpl {
|
|
|
1998
2368
|
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
1999
2369
|
const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
|
|
2000
2370
|
if (normalizedInputs.length === 0) return new Map();
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2371
|
+
const failReceipts = this.#options.failRuntimeStepReceipts;
|
|
2372
|
+
const failed = failReceipts
|
|
2373
|
+
? await this.dispatchChunkedRuntimeReceiptRequest(
|
|
2374
|
+
normalizedInputs.map((receipt) => ({
|
|
2004
2375
|
...receipt,
|
|
2005
2376
|
runAttempt: receipt.runAttempt ?? this.currentRunAttempt,
|
|
2006
2377
|
})),
|
|
2007
|
-
|
|
2378
|
+
(chunk) => failReceipts({ receipts: chunk }),
|
|
2379
|
+
)
|
|
2008
2380
|
: await Promise.all(
|
|
2009
2381
|
normalizedInputs.map((receipt) =>
|
|
2010
2382
|
this.failRuntimeStepReceipt(
|
|
@@ -2114,6 +2486,17 @@ export class PlayContextImpl {
|
|
|
2114
2486
|
forceRefresh,
|
|
2115
2487
|
forceFailedRefresh,
|
|
2116
2488
|
),
|
|
2489
|
+
...(this.#options.markRuntimeStepReceiptRunning ||
|
|
2490
|
+
this.#options.markRuntimeStepReceiptsRunning
|
|
2491
|
+
? {
|
|
2492
|
+
markRunning: (receiptKey, runId, leaseId) =>
|
|
2493
|
+
this.markRuntimeStepReceiptRunning({
|
|
2494
|
+
key: receiptKey,
|
|
2495
|
+
runId,
|
|
2496
|
+
leaseId,
|
|
2497
|
+
}),
|
|
2498
|
+
}
|
|
2499
|
+
: {}),
|
|
2117
2500
|
complete: (receiptKey, runId, output, leaseId) =>
|
|
2118
2501
|
this.completeRuntimeStepReceipt(receiptKey, runId, output, leaseId),
|
|
2119
2502
|
release: (receiptKey, runId, leaseId) =>
|
|
@@ -2133,7 +2516,10 @@ export class PlayContextImpl {
|
|
|
2133
2516
|
failureKind,
|
|
2134
2517
|
),
|
|
2135
2518
|
canPersistFailure: Boolean(this.#options.failRuntimeStepReceipt),
|
|
2136
|
-
canPersistCompletion: Boolean(
|
|
2519
|
+
canPersistCompletion: Boolean(
|
|
2520
|
+
this.#options.completeRuntimeStepReceipt ||
|
|
2521
|
+
this.#options.completeRuntimeStepReceipts,
|
|
2522
|
+
),
|
|
2137
2523
|
};
|
|
2138
2524
|
}
|
|
2139
2525
|
|
|
@@ -2152,6 +2538,21 @@ export class PlayContextImpl {
|
|
|
2152
2538
|
});
|
|
2153
2539
|
}
|
|
2154
2540
|
|
|
2541
|
+
private async waitForCompletedRuntimeToolReceipts(
|
|
2542
|
+
keys: string[],
|
|
2543
|
+
maxAttempts?: number,
|
|
2544
|
+
): Promise<{
|
|
2545
|
+
completed: Map<string, RuntimeStepReceipt>;
|
|
2546
|
+
failed: Map<string, Error>;
|
|
2547
|
+
timedOut: Set<string>;
|
|
2548
|
+
}> {
|
|
2549
|
+
return await waitForCompletedRuntimeReceipts({
|
|
2550
|
+
receiptKeys: keys,
|
|
2551
|
+
store: this.durableReceiptExecutionStore(),
|
|
2552
|
+
maxAttempts,
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2155
2556
|
private async executeWithRuntimeReceipt<T>(
|
|
2156
2557
|
operation: DurableCtxOperation,
|
|
2157
2558
|
id: string,
|
|
@@ -2174,6 +2575,7 @@ export class PlayContextImpl {
|
|
|
2174
2575
|
) => T;
|
|
2175
2576
|
onClaimedResult?: (output: T, receiptKey: string) => T;
|
|
2176
2577
|
shouldPersistFailure?: (error: unknown) => boolean;
|
|
2578
|
+
markRunningBeforeExecute?: boolean;
|
|
2177
2579
|
execute: (context: { leaseId: string | null }) => Promise<T>;
|
|
2178
2580
|
},
|
|
2179
2581
|
): Promise<T> {
|
|
@@ -2204,6 +2606,7 @@ export class PlayContextImpl {
|
|
|
2204
2606
|
onRecovered: opts.onRecovered,
|
|
2205
2607
|
onClaimedResult: opts.onClaimedResult,
|
|
2206
2608
|
shouldPersistFailure: opts.shouldPersistFailure,
|
|
2609
|
+
markRunningBeforeExecute: opts.markRunningBeforeExecute,
|
|
2207
2610
|
formatError: (error) => this.formatRuntimeError(error),
|
|
2208
2611
|
log: (message) => this.log(message),
|
|
2209
2612
|
execute: opts.execute,
|
|
@@ -2820,8 +3223,8 @@ export class PlayContextImpl {
|
|
|
2820
3223
|
completed?.status !== 'completed' &&
|
|
2821
3224
|
completed?.status !== 'skipped'
|
|
2822
3225
|
) {
|
|
2823
|
-
|
|
2824
|
-
`Durable tool call ${receiptKey}
|
|
3226
|
+
this.log(
|
|
3227
|
+
`Durable tool call ${receiptKey} completed live after its receipt lease moved; using live result without overwriting the receipt.`,
|
|
2825
3228
|
);
|
|
2826
3229
|
}
|
|
2827
3230
|
const finalWrapped =
|
|
@@ -2885,6 +3288,7 @@ export class PlayContextImpl {
|
|
|
2885
3288
|
let rejectionError = error;
|
|
2886
3289
|
const receiptKey = request.receiptKey?.trim() || null;
|
|
2887
3290
|
if (receiptKey && options?.persistReceiptFailure !== false) {
|
|
3291
|
+
const persistenceAlreadyFailed = this.persistenceLatch.tripped;
|
|
2888
3292
|
try {
|
|
2889
3293
|
await this.failRuntimeStepReceipts([
|
|
2890
3294
|
{
|
|
@@ -2897,10 +3301,13 @@ export class PlayContextImpl {
|
|
|
2897
3301
|
]);
|
|
2898
3302
|
} catch (receiptError) {
|
|
2899
3303
|
tripRuntimePersistenceLatch(this.persistenceLatch, receiptError);
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3304
|
+
if (!persistenceAlreadyFailed) {
|
|
3305
|
+
const receiptFailureMessage = this.formatRuntimeError(receiptError);
|
|
3306
|
+
rejectionError = new AggregateError(
|
|
3307
|
+
[error, receiptError],
|
|
3308
|
+
`Tool call failed and durable receipt could not be marked failed: ${message}; receipt failure: ${receiptFailureMessage}`,
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
2904
3311
|
}
|
|
2905
3312
|
}
|
|
2906
3313
|
const resolver = this.toolCallResolvers.get(request.callId);
|
|
@@ -2951,7 +3358,7 @@ export class PlayContextImpl {
|
|
|
2951
3358
|
// The activity loads the actual data before creating the ctx
|
|
2952
3359
|
throw new Error(
|
|
2953
3360
|
`ctx.csv("${path}") is handled by the workflow. ` +
|
|
2954
|
-
`CSV data is loaded
|
|
3361
|
+
`CSV data is loaded by the runtime and passed to your play via input.`,
|
|
2955
3362
|
);
|
|
2956
3363
|
}
|
|
2957
3364
|
|
|
@@ -3006,7 +3413,8 @@ export class PlayContextImpl {
|
|
|
3006
3413
|
_key: string,
|
|
3007
3414
|
_items: PlayDatasetInput<T>,
|
|
3008
3415
|
_input?:
|
|
3009
|
-
MapFieldDefinition<T, Record<string, unknown>>
|
|
3416
|
+
| MapFieldDefinition<T, Record<string, unknown>>
|
|
3417
|
+
| RuntimeStepProgram,
|
|
3010
3418
|
_options?: DatasetOptions<T>,
|
|
3011
3419
|
): never {
|
|
3012
3420
|
void _key;
|
|
@@ -3092,7 +3500,8 @@ export class PlayContextImpl {
|
|
|
3092
3500
|
|
|
3093
3501
|
const userKeyOption = options?.key;
|
|
3094
3502
|
let explicitKeyResolver:
|
|
3095
|
-
((row: Record<string, unknown>, index: number) => string)
|
|
3503
|
+
| ((row: Record<string, unknown>, index: number) => string)
|
|
3504
|
+
| null = null;
|
|
3096
3505
|
if (userKeyOption !== undefined) {
|
|
3097
3506
|
explicitKeyResolver = (row, index) => {
|
|
3098
3507
|
const inputRow = stripFieldOutputs(row);
|
|
@@ -3143,7 +3552,516 @@ export class PlayContextImpl {
|
|
|
3143
3552
|
: derivePlayRowIdentity(stableInputRow(row), resolvedTableNamespace);
|
|
3144
3553
|
};
|
|
3145
3554
|
|
|
3146
|
-
|
|
3555
|
+
// Identity for SEED rows (pre-execution raw inputs), matching the server's
|
|
3556
|
+
// own derivation in startRuntimeSheetDataset: hash the FULL cleaned input
|
|
3557
|
+
// row, output-named columns INCLUDED. Two distinct input rows that differ
|
|
3558
|
+
// only in a column sharing an output's name (e.g. a CSV `domain` column +
|
|
3559
|
+
// withColumn('domain', ...)) must stay two rows — the stripped-row
|
|
3560
|
+
// `rowIdentity` fallback would collapse them to one key, silently dropping
|
|
3561
|
+
// a row from execution and the sheet. Output-insensitivity across the
|
|
3562
|
+
// execution round-trip is provided by CARRYING this key on the row
|
|
3563
|
+
// (__deeplineRowKey, which rowIdentity prefers), not by stripping.
|
|
3564
|
+
const seedRowIdentity = (row: Record<string, unknown>, index = 0) => {
|
|
3565
|
+
const runtimeKey = resolveMapRowOutcomeKey(row);
|
|
3566
|
+
if (runtimeKey) return runtimeKey;
|
|
3567
|
+
if (explicitKeyResolver) {
|
|
3568
|
+
return derivePlayRowIdentityFromKey(
|
|
3569
|
+
explicitKeyResolver(row, index),
|
|
3570
|
+
resolvedTableNamespace,
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3573
|
+
return derivePlayRowIdentity(
|
|
3574
|
+
toSerializableCsvAliasedRow(row),
|
|
3575
|
+
resolvedTableNamespace,
|
|
3576
|
+
);
|
|
3577
|
+
};
|
|
3578
|
+
|
|
3579
|
+
const mapMemoryLimits = resolveRuntimeMapMemoryLimits(
|
|
3580
|
+
this.#options.runtimeMapMemoryLimits,
|
|
3581
|
+
);
|
|
3582
|
+
const materializationMemoryTracker = createRuntimeMapMaterializationTracker(
|
|
3583
|
+
{
|
|
3584
|
+
mapName: normalizedMapNamespace,
|
|
3585
|
+
budgetBytes: mapMemoryLimits.materializedBudgetBytes,
|
|
3586
|
+
},
|
|
3587
|
+
);
|
|
3588
|
+
const shouldStreamRuntimeBackedDataset =
|
|
3589
|
+
isPlayDataset<T>(items) &&
|
|
3590
|
+
this.#options.runtimeSheetBackedMapDatasets === true &&
|
|
3591
|
+
Boolean(
|
|
3592
|
+
this.#options.onMapStart &&
|
|
3593
|
+
this.#options.onMapRowsCompleted &&
|
|
3594
|
+
this.#options.baseUrl &&
|
|
3595
|
+
this.#options.executorToken &&
|
|
3596
|
+
this.#options.playName &&
|
|
3597
|
+
this.#options.runId,
|
|
3598
|
+
);
|
|
3599
|
+
if (shouldStreamRuntimeBackedDataset) {
|
|
3600
|
+
totalInputCount = await items.count();
|
|
3601
|
+
const mapScope = this.createMapExecutionScope({
|
|
3602
|
+
logicalNamespace: normalizedMapNamespace,
|
|
3603
|
+
artifactTableNamespace: resolvedTableNamespace,
|
|
3604
|
+
explicitKey: explicitKeyResolver,
|
|
3605
|
+
});
|
|
3606
|
+
this.setMapFrame({
|
|
3607
|
+
mapInvocationId: mapScope.mapInvocationId,
|
|
3608
|
+
mapNodeId: mapScope.mapNodeId ?? null,
|
|
3609
|
+
logicalNamespace: mapScope.logicalNamespace,
|
|
3610
|
+
artifactTableNamespace: mapScope.artifactTableNamespace,
|
|
3611
|
+
status: 'running',
|
|
3612
|
+
totalRows: totalInputCount,
|
|
3613
|
+
completedRowKeys: [],
|
|
3614
|
+
pendingRowKeys: [],
|
|
3615
|
+
completedRowsCount: 0,
|
|
3616
|
+
pendingRowsCount: totalInputCount,
|
|
3617
|
+
failedRowsCount: 0,
|
|
3618
|
+
startedAt: Date.now(),
|
|
3619
|
+
updatedAt: Date.now(),
|
|
3620
|
+
});
|
|
3621
|
+
this.emitExecutionEvent({
|
|
3622
|
+
type: 'map.started',
|
|
3623
|
+
mapInvocationId: mapScope.mapInvocationId,
|
|
3624
|
+
mapNodeId: mapScope.mapNodeId ?? null,
|
|
3625
|
+
logicalNamespace: mapScope.logicalNamespace,
|
|
3626
|
+
artifactTableNamespace: mapScope.artifactTableNamespace,
|
|
3627
|
+
totalRows: totalInputCount,
|
|
3628
|
+
completedRows: 0,
|
|
3629
|
+
pendingRows: totalInputCount,
|
|
3630
|
+
at: Date.now(),
|
|
3631
|
+
});
|
|
3632
|
+
|
|
3633
|
+
const seenExplicitKeys = new Set<string>();
|
|
3634
|
+
let processedCount = 0;
|
|
3635
|
+
let successfulCount = 0;
|
|
3636
|
+
let failedCount = 0;
|
|
3637
|
+
let duplicateReuseCount = 0;
|
|
3638
|
+
let executedCount = 0;
|
|
3639
|
+
const previewRows: Record<string, unknown>[] = [];
|
|
3640
|
+
|
|
3641
|
+
const persistMapRows = async (rows: PersistableMapRow[]) => {
|
|
3642
|
+
if (!this.#options.onMapRowsCompleted || rows.length === 0) {
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
for (const row of rows) {
|
|
3646
|
+
assertPersistableMapRowWithinCustomerOutputLimits({
|
|
3647
|
+
mapName: normalizedMapNamespace,
|
|
3648
|
+
row,
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
let chunk: PersistableMapRow[] = [];
|
|
3652
|
+
let chunkBytes = 0;
|
|
3653
|
+
const flushChunk = async () => {
|
|
3654
|
+
if (chunk.length === 0) return;
|
|
3655
|
+
try {
|
|
3656
|
+
const sheetFlushLease =
|
|
3657
|
+
await this.resourceGovernor.acquireSheetFlush({
|
|
3658
|
+
estimatedBytes: chunkBytes,
|
|
3659
|
+
rowCount: chunk.length,
|
|
3660
|
+
});
|
|
3661
|
+
const flushStartedAt = Date.now();
|
|
3662
|
+
try {
|
|
3663
|
+
await this.#options.onMapRowsCompleted!({
|
|
3664
|
+
playName: this.#options.playName,
|
|
3665
|
+
playId: this.#options.playId,
|
|
3666
|
+
runId: this.#options.runId,
|
|
3667
|
+
executorToken: this.#options.executorToken,
|
|
3668
|
+
tableNamespace: resolvedTableNamespace,
|
|
3669
|
+
rows: chunk,
|
|
3670
|
+
outputFields: datasetColumnNames.filter((field) =>
|
|
3671
|
+
shouldPersistMapCellField(field),
|
|
3672
|
+
),
|
|
3673
|
+
staticPipeline: this.#options.staticPipeline ?? null,
|
|
3674
|
+
});
|
|
3675
|
+
this.resourceGovernor.observe({
|
|
3676
|
+
sheetFlushBytes: chunkBytes,
|
|
3677
|
+
sheetFlushLatencyMs: Date.now() - flushStartedAt,
|
|
3678
|
+
});
|
|
3679
|
+
} finally {
|
|
3680
|
+
sheetFlushLease.release();
|
|
3681
|
+
}
|
|
3682
|
+
} catch (error) {
|
|
3683
|
+
tripRuntimePersistenceLatch(this.persistenceLatch, error);
|
|
3684
|
+
throw error;
|
|
3685
|
+
}
|
|
3686
|
+
chunk = [];
|
|
3687
|
+
chunkBytes = 0;
|
|
3688
|
+
};
|
|
3689
|
+
for (const row of rows) {
|
|
3690
|
+
const rowBytes = persistableMapRowBytes(row);
|
|
3691
|
+
if (
|
|
3692
|
+
chunk.length >= MAP_PERSIST_CHUNK_ROWS ||
|
|
3693
|
+
(chunkBytes + rowBytes > MAP_PERSIST_CHUNK_BYTES &&
|
|
3694
|
+
chunk.length > 0)
|
|
3695
|
+
) {
|
|
3696
|
+
await flushChunk();
|
|
3697
|
+
}
|
|
3698
|
+
chunk.push(row);
|
|
3699
|
+
chunkBytes += rowBytes;
|
|
3700
|
+
}
|
|
3701
|
+
await flushChunk();
|
|
3702
|
+
};
|
|
3703
|
+
|
|
3704
|
+
for await (const page of iteratePlayDatasetInputPages(items)) {
|
|
3705
|
+
const pageRawItems = page.rows.map((item) =>
|
|
3706
|
+
this.toOutputRow(item as Record<string, unknown>),
|
|
3707
|
+
);
|
|
3708
|
+
const pageMemoryEstimate = estimateRuntimeMapRowsMemory(pageRawItems);
|
|
3709
|
+
if (
|
|
3710
|
+
pageMemoryEstimate.estimatedResidentBytes >
|
|
3711
|
+
mapMemoryLimits.materializedBudgetBytes
|
|
3712
|
+
) {
|
|
3713
|
+
throw new RuntimeMapMemoryLimitError({
|
|
3714
|
+
mapName: normalizedMapNamespace,
|
|
3715
|
+
phase: 'materialize',
|
|
3716
|
+
estimate: pageMemoryEstimate,
|
|
3717
|
+
budgetBytes: mapMemoryLimits.materializedBudgetBytes,
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
const pageStartRows = pageRawItems.map((row, index) => {
|
|
3721
|
+
const originalIndex = page.offset + index;
|
|
3722
|
+
return {
|
|
3723
|
+
...toSerializableCsvAliasedRow(row),
|
|
3724
|
+
...mapRowOutcomeRuntimeFields({
|
|
3725
|
+
key: seedRowIdentity(row, originalIndex),
|
|
3726
|
+
inputIndex: originalIndex,
|
|
3727
|
+
}),
|
|
3728
|
+
};
|
|
3729
|
+
});
|
|
3730
|
+
const mapStartResult = await this.#options.onMapStart!(
|
|
3731
|
+
pageStartRows,
|
|
3732
|
+
resolvedTableNamespace,
|
|
3733
|
+
{
|
|
3734
|
+
playName: this.#options.playName,
|
|
3735
|
+
playId: this.#options.playId,
|
|
3736
|
+
runId: this.#options.runId,
|
|
3737
|
+
executorToken: this.#options.executorToken,
|
|
3738
|
+
staticPipeline: this.#options.staticPipeline,
|
|
3739
|
+
forceRefresh: this.#options.cachePolicy?.forceToolRefresh === true,
|
|
3740
|
+
inputOffset: page.offset,
|
|
3741
|
+
},
|
|
3742
|
+
);
|
|
3743
|
+
resolvedTableNamespace = normalizeTableNamespace(
|
|
3744
|
+
mapStartResult.tableNamespace,
|
|
3745
|
+
);
|
|
3746
|
+
if ((mapStartResult.blockedRows?.length ?? 0) > 0) {
|
|
3747
|
+
throw new RuntimeSheetRowsBlockedError({
|
|
3748
|
+
tableNamespace: resolvedTableNamespace,
|
|
3749
|
+
blockedRows: mapStartResult.blockedRows ?? [],
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
const persistedRowIdentity = (
|
|
3754
|
+
row: Record<string, unknown>,
|
|
3755
|
+
index = 0,
|
|
3756
|
+
) => resolveMapRowOutcomeKey(row) ?? rowIdentity(row, index);
|
|
3757
|
+
const pendingRowsByKey = new Map<string, Record<string, unknown>>();
|
|
3758
|
+
for (
|
|
3759
|
+
let index = 0;
|
|
3760
|
+
index < mapStartResult.pendingRows.length;
|
|
3761
|
+
index += 1
|
|
3762
|
+
) {
|
|
3763
|
+
const row = mapStartResult.pendingRows[index]!;
|
|
3764
|
+
const rowKey = persistedRowIdentity(row, page.offset + index);
|
|
3765
|
+
if (rowKey) pendingRowsByKey.set(rowKey, row);
|
|
3766
|
+
}
|
|
3767
|
+
const completedRowsByKey = new Map<string, Record<string, unknown>>();
|
|
3768
|
+
for (const row of mapStartResult.completedRows ?? []) {
|
|
3769
|
+
const rowKey = persistedRowIdentity(row);
|
|
3770
|
+
if (!rowKey) continue;
|
|
3771
|
+
completedRowsByKey.set(rowKey, row);
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
const seededItems = page.rows.map((item, index) => {
|
|
3775
|
+
const originalIndex = page.offset + index;
|
|
3776
|
+
const row = this.toOutputRow(item as Record<string, unknown>);
|
|
3777
|
+
const rowKey = seedRowIdentity(row, originalIndex);
|
|
3778
|
+
const persisted =
|
|
3779
|
+
pendingRowsByKey.get(rowKey) ?? completedRowsByKey.get(rowKey);
|
|
3780
|
+
return {
|
|
3781
|
+
row: persisted
|
|
3782
|
+
? cloneCsvAliasedRow(
|
|
3783
|
+
row,
|
|
3784
|
+
persistedFieldsAbsentFromInputRow(row, persisted),
|
|
3785
|
+
)
|
|
3786
|
+
: row,
|
|
3787
|
+
index: originalIndex,
|
|
3788
|
+
};
|
|
3789
|
+
});
|
|
3790
|
+
|
|
3791
|
+
const rowsToExecuteByKey = new Map<
|
|
3792
|
+
string,
|
|
3793
|
+
{ row: Record<string, unknown>; originalIndex: number }
|
|
3794
|
+
>();
|
|
3795
|
+
for (const { row, index } of seededItems) {
|
|
3796
|
+
if (explicitKeyResolver) {
|
|
3797
|
+
const explicitKey = explicitKeyResolver(row, index);
|
|
3798
|
+
if (seenExplicitKeys.has(explicitKey)) {
|
|
3799
|
+
duplicateReuseCount += 1;
|
|
3800
|
+
processedCount += 1;
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
seenExplicitKeys.add(explicitKey);
|
|
3804
|
+
}
|
|
3805
|
+
const rowKey = rowIdentity(row, index);
|
|
3806
|
+
if (!rowsToExecuteByKey.has(rowKey)) {
|
|
3807
|
+
rowsToExecuteByKey.set(rowKey, { row, originalIndex: index });
|
|
3808
|
+
} else {
|
|
3809
|
+
duplicateReuseCount += 1;
|
|
3810
|
+
processedCount += 1;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
const rowsToExecuteEntries = [...rowsToExecuteByKey.entries()].map(
|
|
3814
|
+
([rowKey, entry]) => ({ rowKey, ...entry }),
|
|
3815
|
+
);
|
|
3816
|
+
const rowsToExecute = rowsToExecuteEntries.map((entry) => entry.row);
|
|
3817
|
+
const rowsToExecuteMemoryEstimate =
|
|
3818
|
+
estimateRuntimeMapRowsMemory(rowsToExecute);
|
|
3819
|
+
const incrementalPersistence =
|
|
3820
|
+
createIncrementalMapRowPersistence(persistMapRows);
|
|
3821
|
+
|
|
3822
|
+
this.activeMapCellMeta = new Map();
|
|
3823
|
+
this.setMapFrame({
|
|
3824
|
+
mapInvocationId: mapScope.mapInvocationId,
|
|
3825
|
+
mapNodeId: mapScope.mapNodeId ?? null,
|
|
3826
|
+
logicalNamespace: mapScope.logicalNamespace,
|
|
3827
|
+
artifactTableNamespace: resolvedTableNamespace,
|
|
3828
|
+
status: 'running',
|
|
3829
|
+
totalRows: totalInputCount,
|
|
3830
|
+
completedRowKeys: [],
|
|
3831
|
+
pendingRowKeys: rowsToExecuteEntries.map((entry) => entry.rowKey),
|
|
3832
|
+
completedRowsCount: Math.max(0, processedCount - failedCount),
|
|
3833
|
+
pendingRowsCount: Math.max(0, totalInputCount - processedCount),
|
|
3834
|
+
failedRowsCount: failedCount,
|
|
3835
|
+
startedAt:
|
|
3836
|
+
this.checkpoint.mapFrames?.[mapScope.mapInvocationId]?.startedAt ??
|
|
3837
|
+
Date.now(),
|
|
3838
|
+
updatedAt: Date.now(),
|
|
3839
|
+
});
|
|
3840
|
+
|
|
3841
|
+
let mapResult: FieldMapRunResult;
|
|
3842
|
+
try {
|
|
3843
|
+
mapResult = await this.runFieldMap(
|
|
3844
|
+
rowsToExecute,
|
|
3845
|
+
mapScope,
|
|
3846
|
+
input as MapFieldDefinition<Record<string, unknown>>,
|
|
3847
|
+
options?.description,
|
|
3848
|
+
{
|
|
3849
|
+
totalRows: totalInputCount,
|
|
3850
|
+
completedRows: duplicateReuseCount,
|
|
3851
|
+
},
|
|
3852
|
+
{
|
|
3853
|
+
emitTerminalEvent: false,
|
|
3854
|
+
onRowError: options?.onRowError,
|
|
3855
|
+
executionRowKeys: rowsToExecuteEntries.map(
|
|
3856
|
+
(entry) => entry.rowKey,
|
|
3857
|
+
),
|
|
3858
|
+
executionRowIndexes: rowsToExecuteEntries.map(
|
|
3859
|
+
(entry) => entry.originalIndex,
|
|
3860
|
+
),
|
|
3861
|
+
progressCompletedOffset: Math.max(
|
|
3862
|
+
0,
|
|
3863
|
+
processedCount - failedCount,
|
|
3864
|
+
),
|
|
3865
|
+
progressFailedOffset: failedCount,
|
|
3866
|
+
progressTotalRows: totalInputCount,
|
|
3867
|
+
largestRowBytes: rowsToExecuteMemoryEstimate.largestRowBytes,
|
|
3868
|
+
retainedRowsMemoryBudgetBytes:
|
|
3869
|
+
mapMemoryLimits.materializedBudgetBytes,
|
|
3870
|
+
activeRowsMemoryBudgetBytes:
|
|
3871
|
+
mapMemoryLimits.activeRowsBudgetBytes,
|
|
3872
|
+
incrementalPersistence,
|
|
3873
|
+
},
|
|
3874
|
+
);
|
|
3875
|
+
} catch (error) {
|
|
3876
|
+
if (error instanceof FailFastMapRowsError) {
|
|
3877
|
+
const rowsToPersist = [...error.completedRows, ...error.failedRows];
|
|
3878
|
+
await incrementalPersistence.flush();
|
|
3879
|
+
const unpersistedRows = rowsToPersist.filter(
|
|
3880
|
+
(row) => !incrementalPersistence.isPersisted(row),
|
|
3881
|
+
);
|
|
3882
|
+
await persistMapRows(unpersistedRows);
|
|
3883
|
+
this.activeMapCellMeta = null;
|
|
3884
|
+
throw error.cause;
|
|
3885
|
+
}
|
|
3886
|
+
this.activeMapCellMeta = null;
|
|
3887
|
+
throw error;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
await incrementalPersistence.flush();
|
|
3891
|
+
const mapCellMeta = this.activeMapCellMeta;
|
|
3892
|
+
const persistRows: PersistableMapRow[] = [];
|
|
3893
|
+
for (const row of mapResult.completedRows) {
|
|
3894
|
+
if (incrementalPersistence.isPersisted(row)) continue;
|
|
3895
|
+
const meta = mapCellMeta?.get(row.key);
|
|
3896
|
+
persistRows.push(
|
|
3897
|
+
completedMapRowOutcome({
|
|
3898
|
+
key: row.key,
|
|
3899
|
+
data: row.data,
|
|
3900
|
+
inputIndex: row.inputIndex,
|
|
3901
|
+
cellMetaPatch: {
|
|
3902
|
+
...(row.cellMetaPatch ?? {}),
|
|
3903
|
+
...(meta ?? {}),
|
|
3904
|
+
},
|
|
3905
|
+
}),
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
persistRows.push(
|
|
3909
|
+
...mapResult.failedRows.filter(
|
|
3910
|
+
(row) => !incrementalPersistence.isPersisted(row),
|
|
3911
|
+
),
|
|
3912
|
+
);
|
|
3913
|
+
await persistMapRows(persistRows);
|
|
3914
|
+
this.activeMapCellMeta = null;
|
|
3915
|
+
|
|
3916
|
+
for (const row of mapResult.completedRows) {
|
|
3917
|
+
if (previewRows.length >= 5) break;
|
|
3918
|
+
previewRows.push(this.toMaterializedOutputRow(row.data));
|
|
3919
|
+
}
|
|
3920
|
+
executedCount += rowsToExecute.length;
|
|
3921
|
+
successfulCount += mapResult.completedRows.length;
|
|
3922
|
+
failedCount += mapResult.failedRows.length;
|
|
3923
|
+
processedCount += rowsToExecute.length;
|
|
3924
|
+
page.rows.length = 0;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
this.setMapFrame({
|
|
3928
|
+
mapInvocationId: mapScope.mapInvocationId,
|
|
3929
|
+
mapNodeId: mapScope.mapNodeId ?? null,
|
|
3930
|
+
logicalNamespace: mapScope.logicalNamespace,
|
|
3931
|
+
artifactTableNamespace: resolvedTableNamespace,
|
|
3932
|
+
status: 'completed',
|
|
3933
|
+
totalRows: totalInputCount,
|
|
3934
|
+
completedRowKeys: [],
|
|
3935
|
+
pendingRowKeys: [],
|
|
3936
|
+
completedRowsCount: Math.max(0, processedCount - failedCount),
|
|
3937
|
+
pendingRowsCount: 0,
|
|
3938
|
+
failedRowsCount: failedCount,
|
|
3939
|
+
startedAt:
|
|
3940
|
+
this.checkpoint.mapFrames?.[mapScope.mapInvocationId]?.startedAt ??
|
|
3941
|
+
Date.now(),
|
|
3942
|
+
updatedAt: Date.now(),
|
|
3943
|
+
});
|
|
3944
|
+
this.emitExecutionEvent({
|
|
3945
|
+
type: 'map.completed',
|
|
3946
|
+
mapInvocationId: mapScope.mapInvocationId,
|
|
3947
|
+
mapNodeId: mapScope.mapNodeId ?? null,
|
|
3948
|
+
logicalNamespace: mapScope.logicalNamespace,
|
|
3949
|
+
artifactTableNamespace: resolvedTableNamespace,
|
|
3950
|
+
completedRows: Math.max(0, processedCount - failedCount),
|
|
3951
|
+
failedRows: failedCount,
|
|
3952
|
+
totalRows: totalInputCount,
|
|
3953
|
+
at: Date.now(),
|
|
3954
|
+
});
|
|
3955
|
+
|
|
3956
|
+
const readRuntimeBackedMapRows = async (input: {
|
|
3957
|
+
limit: number;
|
|
3958
|
+
offset: number;
|
|
3959
|
+
}) => {
|
|
3960
|
+
const result = await readRuntimeSheetDatasetRows(
|
|
3961
|
+
{
|
|
3962
|
+
baseUrl: this.#options.baseUrl!,
|
|
3963
|
+
executorToken: this.#options.executorToken!,
|
|
3964
|
+
playName: this.#options.playName!,
|
|
3965
|
+
userEmail: this.#options.userEmail,
|
|
3966
|
+
runId: this.#options.runId!,
|
|
3967
|
+
},
|
|
3968
|
+
{
|
|
3969
|
+
tableNamespace: resolvedTableNamespace,
|
|
3970
|
+
runId: this.#options.runId!,
|
|
3971
|
+
limit: input.limit,
|
|
3972
|
+
offset: input.offset,
|
|
3973
|
+
},
|
|
3974
|
+
);
|
|
3975
|
+
return result.rows.map((row) => this.toMaterializedOutputRow(row));
|
|
3976
|
+
};
|
|
3977
|
+
const materializeRuntimeBackedMapRows = async (limit?: number) => {
|
|
3978
|
+
if (limit !== undefined && limit <= 0) return [];
|
|
3979
|
+
const pageSize = 1000;
|
|
3980
|
+
const materialized: Record<string, unknown>[] = [];
|
|
3981
|
+
let offset = 0;
|
|
3982
|
+
while (true) {
|
|
3983
|
+
const remaining =
|
|
3984
|
+
limit === undefined
|
|
3985
|
+
? pageSize
|
|
3986
|
+
: Math.max(0, limit - materialized.length);
|
|
3987
|
+
if (remaining === 0) break;
|
|
3988
|
+
const rows = await readRuntimeBackedMapRows({
|
|
3989
|
+
limit: Math.min(pageSize, remaining),
|
|
3990
|
+
offset,
|
|
3991
|
+
});
|
|
3992
|
+
if (rows.length === 0) break;
|
|
3993
|
+
materialized.push(...rows);
|
|
3994
|
+
if (limit !== undefined && materialized.length >= limit) {
|
|
3995
|
+
return materialized.slice(0, limit);
|
|
3996
|
+
}
|
|
3997
|
+
offset += rows.length;
|
|
3998
|
+
}
|
|
3999
|
+
return materialized;
|
|
4000
|
+
};
|
|
4001
|
+
|
|
4002
|
+
return createDeferredPlayDataset({
|
|
4003
|
+
datasetKind: 'map',
|
|
4004
|
+
datasetId: createRuntimeDatasetId(
|
|
4005
|
+
this.#options.playName ?? this.#options.playId ?? 'play',
|
|
4006
|
+
resolvedTableNamespace,
|
|
4007
|
+
),
|
|
4008
|
+
count: successfulCount,
|
|
4009
|
+
backing: {
|
|
4010
|
+
storage: 'neon_sheet',
|
|
4011
|
+
sheet: {
|
|
4012
|
+
playName: this.#options.playName!,
|
|
4013
|
+
tableNamespace: resolvedTableNamespace,
|
|
4014
|
+
},
|
|
4015
|
+
},
|
|
4016
|
+
previewRows,
|
|
4017
|
+
tableNamespace: resolvedTableNamespace,
|
|
4018
|
+
workProgress: {
|
|
4019
|
+
total: totalInputCount,
|
|
4020
|
+
executed: executedCount,
|
|
4021
|
+
reused: duplicateReuseCount,
|
|
4022
|
+
skipped: duplicateReuseCount,
|
|
4023
|
+
pending: 0,
|
|
4024
|
+
failed: failedCount,
|
|
4025
|
+
...(duplicateReuseCount > 0
|
|
4026
|
+
? { duplicates: { exact: duplicateReuseCount } }
|
|
4027
|
+
: {}),
|
|
4028
|
+
},
|
|
4029
|
+
resolvers: {
|
|
4030
|
+
count: async () => successfulCount,
|
|
4031
|
+
peek: async (limit) =>
|
|
4032
|
+
limit <= 0
|
|
4033
|
+
? []
|
|
4034
|
+
: await readRuntimeBackedMapRows({
|
|
4035
|
+
limit,
|
|
4036
|
+
offset: 0,
|
|
4037
|
+
}),
|
|
4038
|
+
materialize: materializeRuntimeBackedMapRows,
|
|
4039
|
+
iterate: () =>
|
|
4040
|
+
({
|
|
4041
|
+
async *[Symbol.asyncIterator]() {
|
|
4042
|
+
const pageSize = 1000;
|
|
4043
|
+
let offset = 0;
|
|
4044
|
+
while (true) {
|
|
4045
|
+
const rows = await readRuntimeBackedMapRows({
|
|
4046
|
+
limit: pageSize,
|
|
4047
|
+
offset,
|
|
4048
|
+
});
|
|
4049
|
+
if (rows.length === 0) return;
|
|
4050
|
+
for (const row of rows) {
|
|
4051
|
+
yield row;
|
|
4052
|
+
}
|
|
4053
|
+
offset += rows.length;
|
|
4054
|
+
}
|
|
4055
|
+
},
|
|
4056
|
+
}) as AsyncIterable<Record<string, unknown>>,
|
|
4057
|
+
},
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
4060
|
+
const rawMaterializedItems = await materializePlayDatasetInput(items, {
|
|
4061
|
+
onRow: (row) => {
|
|
4062
|
+
materializationMemoryTracker.track(row);
|
|
4063
|
+
},
|
|
4064
|
+
});
|
|
3147
4065
|
// Silent dedupe of duplicate explicit map keys: keep the first row per
|
|
3148
4066
|
// canonical key and drop subsequent duplicates. Deduping the materialized
|
|
3149
4067
|
// input here keeps every downstream derivation (rawItems, itemsToProcess,
|
|
@@ -3176,19 +4094,32 @@ export class PlayContextImpl {
|
|
|
3176
4094
|
itemOriginalIndexes = materializedItems.map((_item, index) => index);
|
|
3177
4095
|
const mapStartSeedRowsByKey = new Map<string, Record<string, unknown>>();
|
|
3178
4096
|
if (this.#options.onMapStart) {
|
|
3179
|
-
|
|
4097
|
+
// ALWAYS stamp the canonical seed row key — including no-explicit-key
|
|
4098
|
+
// datasets — using seedRowIdentity (full-input-row hash, matching the
|
|
4099
|
+
// server's own derivation). Without the stamp, seed and completion keys
|
|
4100
|
+
// diverge whenever an input column shares a name with an output column
|
|
4101
|
+
// (e.g. a CSV `domain` column + withColumn('domain', ...)): the seed
|
|
4102
|
+
// row persists under the full-row key while completion/visibility used
|
|
4103
|
+
// a stripped-row key, so the input-index repair flipped the seed row
|
|
4104
|
+
// terminal under the OLD key and the visibility barrier polled a key
|
|
4105
|
+
// that never existed — "Runtime sheet visibility mismatch ... saw 0;
|
|
4106
|
+
// write reported 1" (28-packaged-literal-csv on the Daytona runner).
|
|
4107
|
+
// Stamping a STRIPPED-row key instead would collapse distinct input
|
|
4108
|
+
// rows differing only in output-named columns into one sheet row
|
|
4109
|
+
// (resultView expected 2, got 1). Full-input identity keeps them
|
|
4110
|
+
// distinct; the carried __deeplineRowKey keeps identity stable across
|
|
4111
|
+
// the execution round-trip.
|
|
4112
|
+
//
|
|
3180
4113
|
// toSerializableCsvAliasedRow (not a plain spread): projected CSV
|
|
3181
4114
|
// aliases ride on non-enumerable props after stripCsvProjectionMetadata,
|
|
3182
4115
|
// so a spread would silently drop them before the sheet write and the
|
|
3183
4116
|
// aliases would never persist as visible input cells.
|
|
3184
|
-
const mapStartRows =
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
}))
|
|
3191
|
-
: rawItems.map((row) => toSerializableCsvAliasedRow(row));
|
|
4117
|
+
const mapStartRows = rawItems.map((row, index) => ({
|
|
4118
|
+
...toSerializableCsvAliasedRow(row),
|
|
4119
|
+
...mapRowOutcomeRuntimeFields({
|
|
4120
|
+
key: seedRowIdentity(row, index),
|
|
4121
|
+
}),
|
|
4122
|
+
}));
|
|
3192
4123
|
const mapStartResult = await this.#options.onMapStart(
|
|
3193
4124
|
mapStartRows,
|
|
3194
4125
|
resolvedTableNamespace,
|
|
@@ -3233,15 +4164,31 @@ export class PlayContextImpl {
|
|
|
3233
4164
|
index,
|
|
3234
4165
|
}))
|
|
3235
4166
|
.map(({ row, index }) => {
|
|
3236
|
-
|
|
4167
|
+
// Look up by the SAME seed identity the stamp above used — the
|
|
4168
|
+
// merged persisted row then carries __deeplineRowKey, which every
|
|
4169
|
+
// downstream rowIdentity call prefers over re-derivation.
|
|
4170
|
+
const rowKey = seedRowIdentity(row, index);
|
|
3237
4171
|
const persisted =
|
|
3238
4172
|
pendingRowsByKey.get(rowKey) ?? mapStartSeedRowsByKey.get(rowKey);
|
|
3239
|
-
//
|
|
3240
|
-
//
|
|
3241
|
-
//
|
|
3242
|
-
//
|
|
4173
|
+
// Restore persisted OUTPUT columns, cell meta, and the carried
|
|
4174
|
+
// __deeplineRowKey so key functions and column resolvers keep seeing
|
|
4175
|
+
// projected values/aliases across the sheet round-trip. But the
|
|
4176
|
+
// CURRENT run's INPUT columns are authoritative and must win: a
|
|
4177
|
+
// persisted row is matched only by the map key, so for a play whose
|
|
4178
|
+
// key does not capture every input column it can be a DIFFERENT run's
|
|
4179
|
+
// row that merely shares that key. Letting its stale input columns
|
|
4180
|
+
// overwrite this run's inputs corrupts input-derived tool arguments
|
|
4181
|
+
// (e.g. scenario 87 vs 78 collide on key `id`='target' but pass
|
|
4182
|
+
// distinct `key` columns to the probe tool, flipping a terminal
|
|
4183
|
+
// failure into a spurious success). Merge only persisted fields
|
|
4184
|
+
// absent from the fresh input row.
|
|
3243
4185
|
return {
|
|
3244
|
-
row: persisted
|
|
4186
|
+
row: persisted
|
|
4187
|
+
? cloneCsvAliasedRow(
|
|
4188
|
+
row,
|
|
4189
|
+
persistedFieldsAbsentFromInputRow(row, persisted),
|
|
4190
|
+
)
|
|
4191
|
+
: row,
|
|
3245
4192
|
index,
|
|
3246
4193
|
};
|
|
3247
4194
|
});
|
|
@@ -3298,6 +4245,8 @@ export class PlayContextImpl {
|
|
|
3298
4245
|
([rowKey, entry]) => ({ rowKey, ...entry }),
|
|
3299
4246
|
);
|
|
3300
4247
|
const rowsToExecute = rowsToExecuteEntries.map((entry) => entry.row);
|
|
4248
|
+
const rowsToExecuteMemoryEstimate =
|
|
4249
|
+
estimateRuntimeMapRowsMemory(rowsToExecute);
|
|
3301
4250
|
const duplicateReuseCount = Math.max(
|
|
3302
4251
|
0,
|
|
3303
4252
|
itemsToProcess.length - rowsToExecute.length,
|
|
@@ -3308,23 +4257,44 @@ export class PlayContextImpl {
|
|
|
3308
4257
|
if (!this.#options.onMapRowsCompleted || rows.length === 0) {
|
|
3309
4258
|
return;
|
|
3310
4259
|
}
|
|
4260
|
+
for (const row of rows) {
|
|
4261
|
+
assertPersistableMapRowWithinCustomerOutputLimits({
|
|
4262
|
+
mapName: normalizedMapNamespace,
|
|
4263
|
+
row,
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
3311
4266
|
let chunk: PersistableMapRow[] = [];
|
|
3312
4267
|
let chunkBytes = 0;
|
|
3313
4268
|
const flushChunk = async () => {
|
|
3314
4269
|
if (chunk.length === 0) return;
|
|
3315
4270
|
try {
|
|
3316
|
-
await this
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
4271
|
+
const sheetFlushLease = await this.resourceGovernor.acquireSheetFlush(
|
|
4272
|
+
{
|
|
4273
|
+
estimatedBytes: chunkBytes,
|
|
4274
|
+
rowCount: chunk.length,
|
|
4275
|
+
},
|
|
4276
|
+
);
|
|
4277
|
+
const flushStartedAt = Date.now();
|
|
4278
|
+
try {
|
|
4279
|
+
await this.#options.onMapRowsCompleted!({
|
|
4280
|
+
playName: this.#options.playName,
|
|
4281
|
+
playId: this.#options.playId,
|
|
4282
|
+
runId: this.#options.runId,
|
|
4283
|
+
executorToken: this.#options.executorToken,
|
|
4284
|
+
tableNamespace: resolvedTableNamespace,
|
|
4285
|
+
rows: chunk,
|
|
4286
|
+
outputFields: datasetColumnNames.filter((field) =>
|
|
4287
|
+
shouldPersistMapCellField(field),
|
|
4288
|
+
),
|
|
4289
|
+
staticPipeline: this.#options.staticPipeline ?? null,
|
|
4290
|
+
});
|
|
4291
|
+
this.resourceGovernor.observe({
|
|
4292
|
+
sheetFlushBytes: chunkBytes,
|
|
4293
|
+
sheetFlushLatencyMs: Date.now() - flushStartedAt,
|
|
4294
|
+
});
|
|
4295
|
+
} finally {
|
|
4296
|
+
sheetFlushLease.release();
|
|
4297
|
+
}
|
|
3328
4298
|
} catch (error) {
|
|
3329
4299
|
// Output-sheet flush failed: trip the breaker so the dispatch loops
|
|
3330
4300
|
// stop dispatching new provider calls for the rest of this map.
|
|
@@ -3368,6 +4338,10 @@ export class PlayContextImpl {
|
|
|
3368
4338
|
executionRowIndexes: rowsToExecuteEntries.map(
|
|
3369
4339
|
(entry) => entry.originalIndex,
|
|
3370
4340
|
),
|
|
4341
|
+
largestRowBytes: rowsToExecuteMemoryEstimate.largestRowBytes,
|
|
4342
|
+
retainedRowsMemoryBudgetBytes:
|
|
4343
|
+
mapMemoryLimits.materializedBudgetBytes,
|
|
4344
|
+
activeRowsMemoryBudgetBytes: mapMemoryLimits.activeRowsBudgetBytes,
|
|
3371
4345
|
incrementalPersistence,
|
|
3372
4346
|
},
|
|
3373
4347
|
);
|
|
@@ -3499,6 +4473,63 @@ export class PlayContextImpl {
|
|
|
3499
4473
|
});
|
|
3500
4474
|
}
|
|
3501
4475
|
|
|
4476
|
+
const readRuntimeBackedMapRows =
|
|
4477
|
+
this.#options.runtimeSheetBackedMapDatasets === true &&
|
|
4478
|
+
this.#options.baseUrl &&
|
|
4479
|
+
this.#options.executorToken &&
|
|
4480
|
+
this.#options.playName &&
|
|
4481
|
+
this.#options.runId
|
|
4482
|
+
? async (input: { limit: number; offset: number }) => {
|
|
4483
|
+
const result = await readRuntimeSheetDatasetRows(
|
|
4484
|
+
{
|
|
4485
|
+
baseUrl: this.#options.baseUrl!,
|
|
4486
|
+
executorToken: this.#options.executorToken!,
|
|
4487
|
+
playName: this.#options.playName!,
|
|
4488
|
+
userEmail: this.#options.userEmail,
|
|
4489
|
+
runId: this.#options.runId!,
|
|
4490
|
+
},
|
|
4491
|
+
{
|
|
4492
|
+
tableNamespace: resolvedTableNamespace,
|
|
4493
|
+
runId: this.#options.runId!,
|
|
4494
|
+
limit: input.limit,
|
|
4495
|
+
offset: input.offset,
|
|
4496
|
+
},
|
|
4497
|
+
);
|
|
4498
|
+
return result.rows.map((row) => this.toMaterializedOutputRow(row));
|
|
4499
|
+
}
|
|
4500
|
+
: null;
|
|
4501
|
+
const materializeRuntimeBackedMapRows = async (limit?: number) => {
|
|
4502
|
+
if (!readRuntimeBackedMapRows) {
|
|
4503
|
+
return limit === undefined
|
|
4504
|
+
? results.map((row) => this.toMaterializedOutputRow(row))
|
|
4505
|
+
: results
|
|
4506
|
+
.slice(0, Math.max(0, limit))
|
|
4507
|
+
.map((row) => this.toMaterializedOutputRow(row));
|
|
4508
|
+
}
|
|
4509
|
+
if (limit !== undefined && limit <= 0) return [];
|
|
4510
|
+
const pageSize = 1000;
|
|
4511
|
+
const materialized: Record<string, unknown>[] = [];
|
|
4512
|
+
let offset = 0;
|
|
4513
|
+
while (true) {
|
|
4514
|
+
const remaining =
|
|
4515
|
+
limit === undefined
|
|
4516
|
+
? pageSize
|
|
4517
|
+
: Math.max(0, limit - materialized.length);
|
|
4518
|
+
if (remaining === 0) break;
|
|
4519
|
+
const rows = await readRuntimeBackedMapRows({
|
|
4520
|
+
limit: Math.min(pageSize, remaining),
|
|
4521
|
+
offset,
|
|
4522
|
+
});
|
|
4523
|
+
if (rows.length === 0) break;
|
|
4524
|
+
materialized.push(...rows);
|
|
4525
|
+
if (limit !== undefined && materialized.length >= limit) {
|
|
4526
|
+
return materialized.slice(0, limit);
|
|
4527
|
+
}
|
|
4528
|
+
offset += rows.length;
|
|
4529
|
+
}
|
|
4530
|
+
return materialized;
|
|
4531
|
+
};
|
|
4532
|
+
|
|
3502
4533
|
return createDeferredPlayDataset({
|
|
3503
4534
|
datasetKind: 'map',
|
|
3504
4535
|
datasetId: createRuntimeDatasetId(
|
|
@@ -3506,6 +4537,15 @@ export class PlayContextImpl {
|
|
|
3506
4537
|
resolvedTableNamespace,
|
|
3507
4538
|
),
|
|
3508
4539
|
count: results.length,
|
|
4540
|
+
backing: readRuntimeBackedMapRows
|
|
4541
|
+
? {
|
|
4542
|
+
storage: 'neon_sheet',
|
|
4543
|
+
sheet: {
|
|
4544
|
+
playName: this.#options.playName!,
|
|
4545
|
+
tableNamespace: resolvedTableNamespace,
|
|
4546
|
+
},
|
|
4547
|
+
}
|
|
4548
|
+
: undefined,
|
|
3509
4549
|
previewRows: results
|
|
3510
4550
|
.slice(0, 5)
|
|
3511
4551
|
.map((row) => this.toMaterializedOutputRow(row)),
|
|
@@ -3524,18 +4564,35 @@ export class PlayContextImpl {
|
|
|
3524
4564
|
resolvers: {
|
|
3525
4565
|
count: async () => results.length,
|
|
3526
4566
|
peek: async (limit) =>
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
4567
|
+
readRuntimeBackedMapRows
|
|
4568
|
+
? limit <= 0
|
|
4569
|
+
? []
|
|
4570
|
+
: await readRuntimeBackedMapRows({
|
|
4571
|
+
limit,
|
|
4572
|
+
offset: 0,
|
|
4573
|
+
})
|
|
3533
4574
|
: results
|
|
3534
4575
|
.slice(0, Math.max(0, limit))
|
|
3535
4576
|
.map((row) => this.toMaterializedOutputRow(row)),
|
|
4577
|
+
materialize: materializeRuntimeBackedMapRows,
|
|
3536
4578
|
iterate: () =>
|
|
3537
4579
|
({
|
|
3538
4580
|
async *[Symbol.asyncIterator]() {
|
|
4581
|
+
if (readRuntimeBackedMapRows) {
|
|
4582
|
+
const pageSize = 1000;
|
|
4583
|
+
let offset = 0;
|
|
4584
|
+
while (true) {
|
|
4585
|
+
const rows = await readRuntimeBackedMapRows({
|
|
4586
|
+
limit: pageSize,
|
|
4587
|
+
offset,
|
|
4588
|
+
});
|
|
4589
|
+
if (rows.length === 0) return;
|
|
4590
|
+
for (const row of rows) {
|
|
4591
|
+
yield row;
|
|
4592
|
+
}
|
|
4593
|
+
offset += rows.length;
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
3539
4596
|
for (const row of results) {
|
|
3540
4597
|
yield row;
|
|
3541
4598
|
}
|
|
@@ -3561,12 +4618,16 @@ export class PlayContextImpl {
|
|
|
3561
4618
|
executionRowIndexes?: number[];
|
|
3562
4619
|
/**
|
|
3563
4620
|
* Opt-in cap on how many row resolvers run their body concurrently. When
|
|
3564
|
-
* unset
|
|
3565
|
-
*
|
|
3566
|
-
* rows wait for a slot, bounding in-flight per-row work for callers who
|
|
3567
|
-
* want to throttle a wide map.
|
|
4621
|
+
* unset, the Governor's rowDefault is used. Excess rows stay pending as
|
|
4622
|
+
* indexes until a worker is free.
|
|
3568
4623
|
*/
|
|
3569
4624
|
concurrency?: number;
|
|
4625
|
+
largestRowBytes?: number;
|
|
4626
|
+
retainedRowsMemoryBudgetBytes?: number;
|
|
4627
|
+
activeRowsMemoryBudgetBytes?: number;
|
|
4628
|
+
progressCompletedOffset?: number;
|
|
4629
|
+
progressFailedOffset?: number;
|
|
4630
|
+
progressTotalRows?: number;
|
|
3570
4631
|
incrementalPersistence?: IncrementalMapRowPersistence | null;
|
|
3571
4632
|
},
|
|
3572
4633
|
): Promise<FieldMapRunResult> {
|
|
@@ -3596,7 +4657,9 @@ export class PlayContextImpl {
|
|
|
3596
4657
|
runtimeOptions?.executionRowIndexes?.[index] ?? index;
|
|
3597
4658
|
|
|
3598
4659
|
const totalRows = Math.max(
|
|
3599
|
-
|
|
4660
|
+
runtimeOptions?.progressTotalRows ??
|
|
4661
|
+
executionSummary?.totalRows ??
|
|
4662
|
+
items.length,
|
|
3600
4663
|
items.length,
|
|
3601
4664
|
);
|
|
3602
4665
|
const completedRows = Math.min(
|
|
@@ -3610,6 +4673,10 @@ export class PlayContextImpl {
|
|
|
3610
4673
|
const failedRowsToPersist: PersistableMapRow[] = [];
|
|
3611
4674
|
const incrementalPersistence =
|
|
3612
4675
|
runtimeOptions?.incrementalPersistence ?? null;
|
|
4676
|
+
const retainedRowsMemoryTracker = createRuntimeMapRetainedRowsTracker({
|
|
4677
|
+
mapName: normalizedTableNamespace,
|
|
4678
|
+
budgetBytes: runtimeOptions?.retainedRowsMemoryBudgetBytes,
|
|
4679
|
+
});
|
|
3613
4680
|
const enqueueIncrementalPersist = (row: PersistableMapRow): void => {
|
|
3614
4681
|
void incrementalPersistence?.persistRows([row]).catch(() => {
|
|
3615
4682
|
// The final map-level flush awaits the same chain and surfaces the
|
|
@@ -3728,17 +4795,21 @@ export class PlayContextImpl {
|
|
|
3728
4795
|
? 'map.progress'
|
|
3729
4796
|
: null);
|
|
3730
4797
|
if (progressEventType) {
|
|
4798
|
+
const failedRows = Math.max(
|
|
4799
|
+
0,
|
|
4800
|
+
(runtimeOptions?.progressFailedOffset ?? 0) +
|
|
4801
|
+
(items.length - completedRowKeys.size - pendingRowKeys.size),
|
|
4802
|
+
);
|
|
3731
4803
|
this.emitExecutionEvent({
|
|
3732
4804
|
type: progressEventType,
|
|
3733
4805
|
mapInvocationId: mapScope.mapInvocationId,
|
|
3734
4806
|
mapNodeId: mapScope.mapNodeId ?? null,
|
|
3735
4807
|
logicalNamespace: mapScope.logicalNamespace,
|
|
3736
4808
|
artifactTableNamespace: mapScope.artifactTableNamespace,
|
|
3737
|
-
completedRows:
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
),
|
|
4809
|
+
completedRows:
|
|
4810
|
+
(runtimeOptions?.progressCompletedOffset ?? 0) +
|
|
4811
|
+
completedRowKeys.size,
|
|
4812
|
+
failedRows,
|
|
3742
4813
|
totalRows,
|
|
3743
4814
|
at: Date.now(),
|
|
3744
4815
|
} as PlayExecutionEvent);
|
|
@@ -3753,6 +4824,22 @@ export class PlayContextImpl {
|
|
|
3753
4824
|
normalizedTableNamespace,
|
|
3754
4825
|
executionRowKey,
|
|
3755
4826
|
);
|
|
4827
|
+
const pureResultsMemoryEstimate = estimateRuntimeMapRowsMemory(results);
|
|
4828
|
+
const retainedRowsBudgetBytes =
|
|
4829
|
+
runtimeOptions?.retainedRowsMemoryBudgetBytes ??
|
|
4830
|
+
resolveRuntimeMapMemoryLimits(this.#options.runtimeMapMemoryLimits)
|
|
4831
|
+
.materializedBudgetBytes;
|
|
4832
|
+
if (
|
|
4833
|
+
pureResultsMemoryEstimate.estimatedResidentBytes >
|
|
4834
|
+
retainedRowsBudgetBytes
|
|
4835
|
+
) {
|
|
4836
|
+
throw new RuntimeMapMemoryLimitError({
|
|
4837
|
+
mapName: normalizedTableNamespace,
|
|
4838
|
+
phase: 'retained_rows',
|
|
4839
|
+
estimate: pureResultsMemoryEstimate,
|
|
4840
|
+
budgetBytes: retainedRowsBudgetBytes,
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
3756
4843
|
// One batched frame update for the whole pure map. The per-row loop
|
|
3757
4844
|
// this replaces ran AFTER every row had already computed, emitting 150k
|
|
3758
4845
|
// post-hoc map.progress events and re-copying the completed-keys array
|
|
@@ -3794,46 +4881,64 @@ export class PlayContextImpl {
|
|
|
3794
4881
|
|
|
3795
4882
|
this.initializeRowStates(items);
|
|
3796
4883
|
|
|
3797
|
-
// Bound on how many row resolver bodies
|
|
4884
|
+
// Bound on how many row resolver bodies exist at once. An explicit
|
|
3798
4885
|
// `concurrency` is clamped to [1, policy.concurrency.rowMax]; unset uses
|
|
3799
|
-
// policy.concurrency.rowDefault (resolved by the Governor).
|
|
3800
|
-
//
|
|
3801
|
-
//
|
|
3802
|
-
//
|
|
3803
|
-
const
|
|
4886
|
+
// policy.concurrency.rowDefault (resolved by the Governor). Pending rows are
|
|
4887
|
+
// indexes, not promises, so they do not pre-claim receipts, queue tools, or
|
|
4888
|
+
// retain row closures before admission. Pure-JS maps use a separate fast
|
|
4889
|
+
// path and never reach here.
|
|
4890
|
+
const requestedRowConcurrency = this.resourceGovernor.resolveRowConcurrency(
|
|
3804
4891
|
runtimeOptions?.concurrency,
|
|
3805
4892
|
);
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
4893
|
+
const largestRowBytes =
|
|
4894
|
+
runtimeOptions?.largestRowBytes ??
|
|
4895
|
+
estimateRuntimeMapRowsMemory(items).largestRowBytes;
|
|
4896
|
+
const rowAdmission = resolveRuntimeMapRowAdmission({
|
|
4897
|
+
rowCount: items.length,
|
|
4898
|
+
requestedConcurrency: requestedRowConcurrency,
|
|
4899
|
+
largestRowBytes,
|
|
4900
|
+
activeRowsBudgetBytes: runtimeOptions?.activeRowsMemoryBudgetBytes,
|
|
4901
|
+
});
|
|
4902
|
+
const rowConcurrencyLimit = rowAdmission.concurrency;
|
|
4903
|
+
if (items.length > 0 && rowConcurrencyLimit === 0) {
|
|
4904
|
+
throw new RuntimeMapMemoryLimitError({
|
|
4905
|
+
mapName: normalizedTableNamespace,
|
|
4906
|
+
phase: 'active_rows',
|
|
4907
|
+
estimate: {
|
|
4908
|
+
rowCount: items.length,
|
|
4909
|
+
serializedBytes: largestRowBytes,
|
|
4910
|
+
largestRowBytes,
|
|
4911
|
+
estimatedResidentBytes: rowAdmission.estimatedBytesPerActiveRow,
|
|
4912
|
+
},
|
|
4913
|
+
budgetBytes:
|
|
4914
|
+
runtimeOptions?.activeRowsMemoryBudgetBytes ??
|
|
4915
|
+
resolveRuntimeMapMemoryLimits().activeRowsBudgetBytes,
|
|
4916
|
+
activeRows: 1,
|
|
4917
|
+
rowConcurrency: requestedRowConcurrency,
|
|
4918
|
+
});
|
|
4919
|
+
}
|
|
4920
|
+
this.resourceGovernor.observe({
|
|
4921
|
+
memoryBytes: rowAdmission.estimatedResidentBytes,
|
|
4922
|
+
});
|
|
4923
|
+
const settledResults = new Array<MapRowExecutionResult | undefined>(
|
|
4924
|
+
items.length,
|
|
4925
|
+
);
|
|
4926
|
+
let nextRowIndex = 0;
|
|
4927
|
+
let firstRowError: unknown = null;
|
|
3825
4928
|
|
|
3826
|
-
const
|
|
4929
|
+
const runRow = async (idx: number): Promise<MapRowExecutionResult> => {
|
|
4930
|
+
const item = items[idx];
|
|
3827
4931
|
const baseRow = this.toOutputRow(item);
|
|
3828
4932
|
const rowKey = executionRowKey(baseRow, idx);
|
|
3829
4933
|
const rowIndex = executionRowIndex(idx);
|
|
3830
4934
|
const computedFields: Record<string, unknown> = {};
|
|
3831
4935
|
let activeFieldName: string | null = null;
|
|
3832
4936
|
|
|
3833
|
-
// Global row slot
|
|
3834
|
-
//
|
|
3835
|
-
const globalRowSlot = await this.
|
|
3836
|
-
|
|
4937
|
+
// Global row slot keeps concurrent maps in the same run under rowMax. The
|
|
4938
|
+
// worker pool above enforces this map's requested/default concurrency.
|
|
4939
|
+
const globalRowSlot = await this.resourceGovernor.acquireRow({
|
|
4940
|
+
estimatedBytes: runtimeMapJsonByteLength(baseRow),
|
|
4941
|
+
});
|
|
3837
4942
|
try {
|
|
3838
4943
|
for (const [fieldName, resolver] of fieldEntries) {
|
|
3839
4944
|
activeFieldName = fieldName;
|
|
@@ -3910,6 +5015,7 @@ export class PlayContextImpl {
|
|
|
3910
5015
|
: {}),
|
|
3911
5016
|
error: formattedError,
|
|
3912
5017
|
});
|
|
5018
|
+
retainedRowsMemoryTracker.track(failedRow);
|
|
3913
5019
|
failedRowsToPersist.push(failedRow);
|
|
3914
5020
|
throw error;
|
|
3915
5021
|
}
|
|
@@ -3924,6 +5030,7 @@ export class PlayContextImpl {
|
|
|
3924
5030
|
...(cellMetaPatch ? { cellMetaPatch } : {}),
|
|
3925
5031
|
error: formattedError,
|
|
3926
5032
|
});
|
|
5033
|
+
retainedRowsMemoryTracker.track(failedRow);
|
|
3927
5034
|
failedRowsToPersist.push(failedRow);
|
|
3928
5035
|
updateMapFrameProgress({
|
|
3929
5036
|
failedRowKey: rowKey,
|
|
@@ -3942,6 +5049,31 @@ export class PlayContextImpl {
|
|
|
3942
5049
|
const cellValue = this.serializeCellValue(value);
|
|
3943
5050
|
computedFields[fieldName] = cellValue;
|
|
3944
5051
|
this.rowStates.get(idx)?.results.set(fieldName, cellValue);
|
|
5052
|
+
const currentCellMeta =
|
|
5053
|
+
this.activeMapCellMeta?.get(rowKey)?.[fieldName];
|
|
5054
|
+
const currentCellStatus =
|
|
5055
|
+
currentCellMeta &&
|
|
5056
|
+
typeof currentCellMeta === 'object' &&
|
|
5057
|
+
!Array.isArray(currentCellMeta) &&
|
|
5058
|
+
'status' in currentCellMeta
|
|
5059
|
+
? (currentCellMeta as { status?: unknown }).status
|
|
5060
|
+
: null;
|
|
5061
|
+
if (currentCellStatus === 'skipped') {
|
|
5062
|
+
if (
|
|
5063
|
+
shouldPersistMapCellField(fieldName) &&
|
|
5064
|
+
Object.prototype.hasOwnProperty.call(computedFields, fieldName)
|
|
5065
|
+
) {
|
|
5066
|
+
this.emitScopedRowUpdate(rowKey, normalizedTableNamespace, {
|
|
5067
|
+
rowId: idx,
|
|
5068
|
+
status: 'running',
|
|
5069
|
+
stage: 'skipped',
|
|
5070
|
+
provider: null,
|
|
5071
|
+
error: null,
|
|
5072
|
+
dataPatch: { [fieldName]: cellValue },
|
|
5073
|
+
});
|
|
5074
|
+
}
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
3945
5077
|
this.emitScopedFieldMetaUpdate({
|
|
3946
5078
|
rowId: idx,
|
|
3947
5079
|
key: rowKey,
|
|
@@ -3978,6 +5110,7 @@ export class PlayContextImpl {
|
|
|
3978
5110
|
? { cellMetaPatch: this.activeMapCellMeta.get(rowKey) }
|
|
3979
5111
|
: {}),
|
|
3980
5112
|
});
|
|
5113
|
+
retainedRowsMemoryTracker.track(completedRow);
|
|
3981
5114
|
completedRowsToPersist.push(completedRow);
|
|
3982
5115
|
enqueueIncrementalPersist(completedRow);
|
|
3983
5116
|
return publicRow;
|
|
@@ -4023,16 +5156,30 @@ export class PlayContextImpl {
|
|
|
4023
5156
|
});
|
|
4024
5157
|
throw error;
|
|
4025
5158
|
} finally {
|
|
4026
|
-
releaseLocalRowSlot();
|
|
4027
5159
|
globalRowSlot.release();
|
|
4028
5160
|
}
|
|
5161
|
+
};
|
|
5162
|
+
|
|
5163
|
+
const workerCount = Math.min(rowConcurrencyLimit, items.length);
|
|
5164
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
5165
|
+
while (firstRowError == null) {
|
|
5166
|
+
const idx = nextRowIndex;
|
|
5167
|
+
nextRowIndex += 1;
|
|
5168
|
+
if (idx >= items.length) return;
|
|
5169
|
+
try {
|
|
5170
|
+
settledResults[idx] = await runRow(idx);
|
|
5171
|
+
} catch (error) {
|
|
5172
|
+
firstRowError = error;
|
|
5173
|
+
throw error;
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
4029
5176
|
});
|
|
4030
5177
|
|
|
4031
|
-
await this.drainQueuedWork(
|
|
5178
|
+
await this.drainQueuedWork(workers);
|
|
4032
5179
|
this.lastDatasetStep = datasetStep;
|
|
4033
5180
|
this.activeDatasetStep = null;
|
|
4034
5181
|
|
|
4035
|
-
const settled = await Promise.allSettled(
|
|
5182
|
+
const settled = await Promise.allSettled(workers);
|
|
4036
5183
|
const rejected = settled.find(
|
|
4037
5184
|
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
|
4038
5185
|
);
|
|
@@ -4043,12 +5190,15 @@ export class PlayContextImpl {
|
|
|
4043
5190
|
failedRows: failedRowsToPersist,
|
|
4044
5191
|
});
|
|
4045
5192
|
}
|
|
4046
|
-
const
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
5193
|
+
const missingResultIndex = settledResults.findIndex(
|
|
5194
|
+
(result) => result === undefined,
|
|
5195
|
+
);
|
|
5196
|
+
if (missingResultIndex !== -1) {
|
|
5197
|
+
throw new Error(
|
|
5198
|
+
`Runtime map execution worker pool ended before row ${missingResultIndex} settled for ctx.dataset("${normalizedTableNamespace}").`,
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
const rowResults = settledResults as MapRowExecutionResult[];
|
|
4052
5202
|
const accountedRowIndexes = new Set<number>();
|
|
4053
5203
|
for (const row of completedRowsToPersist) {
|
|
4054
5204
|
if (typeof row.inputIndex === 'number') {
|
|
@@ -4062,8 +5212,8 @@ export class PlayContextImpl {
|
|
|
4062
5212
|
}
|
|
4063
5213
|
if (this.pendingRowEventBoundaries.length > 0) {
|
|
4064
5214
|
const completedRowKeys = new Set<string>();
|
|
4065
|
-
for (let index = 0; index <
|
|
4066
|
-
const result =
|
|
5215
|
+
for (let index = 0; index < rowResults.length; index += 1) {
|
|
5216
|
+
const result = rowResults[index];
|
|
4067
5217
|
const rawItem = this.toOutputRow(
|
|
4068
5218
|
items[index] as Record<string, unknown>,
|
|
4069
5219
|
);
|
|
@@ -4129,7 +5279,7 @@ export class PlayContextImpl {
|
|
|
4129
5279
|
`missing input index(es): ${missingRowIndexes.slice(0, 10).join(', ') || 'unknown'}.`,
|
|
4130
5280
|
);
|
|
4131
5281
|
}
|
|
4132
|
-
const results =
|
|
5282
|
+
const results = rowResults.filter(
|
|
4133
5283
|
(result): result is Record<string, unknown> =>
|
|
4134
5284
|
result !== WAITING_ROW && result !== FAILED_ROW,
|
|
4135
5285
|
);
|
|
@@ -4232,6 +5382,25 @@ export class PlayContextImpl {
|
|
|
4232
5382
|
undefined,
|
|
4233
5383
|
);
|
|
4234
5384
|
produced[step.name] = value;
|
|
5385
|
+
const rowStore = rowContext.getStore();
|
|
5386
|
+
const fieldName = stepPath.join('.');
|
|
5387
|
+
const patchFieldName = runtimeSheetPatchFieldName(fieldName);
|
|
5388
|
+
if (rowStore && shouldPersistMapCellField(patchFieldName)) {
|
|
5389
|
+
const cellValue = this.serializeCellValue(value);
|
|
5390
|
+
this.emitScopedFieldMetaUpdate({
|
|
5391
|
+
rowId: rowStore.rowId,
|
|
5392
|
+
key: rowStore.rowKey ?? null,
|
|
5393
|
+
tableNamespace: rowStore.tableNamespace ?? null,
|
|
5394
|
+
fieldName,
|
|
5395
|
+
status: 'completed',
|
|
5396
|
+
rowStatus: 'running',
|
|
5397
|
+
stage: 'completed',
|
|
5398
|
+
provider: null,
|
|
5399
|
+
error: null,
|
|
5400
|
+
completedAt: Date.now(),
|
|
5401
|
+
dataPatch: { [patchFieldName]: cellValue },
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
4235
5404
|
currentRow = cloneCsvAliasedRow(currentRow, { [step.name]: value });
|
|
4236
5405
|
}
|
|
4237
5406
|
if (typeof program.returnResolver === 'function') {
|
|
@@ -4626,6 +5795,7 @@ export class PlayContextImpl {
|
|
|
4626
5795
|
? `Calling tool: ${toolId} (force)`
|
|
4627
5796
|
: `Calling tool: ${toolId}`,
|
|
4628
5797
|
);
|
|
5798
|
+
const directReceiptLeaseId = context?.leaseId?.trim() || null;
|
|
4629
5799
|
const execution = await this.callToolExecutionAPI(toolId, input, {
|
|
4630
5800
|
...(cacheableToolResult
|
|
4631
5801
|
? {
|
|
@@ -4634,11 +5804,43 @@ export class PlayContextImpl {
|
|
|
4634
5804
|
providerIdempotencyKey: this.providerIdempotencyKeyForToolCall({
|
|
4635
5805
|
cacheKey: directCacheKey,
|
|
4636
5806
|
force: toolCachePolicy.force,
|
|
4637
|
-
leaseId:
|
|
5807
|
+
leaseId: directReceiptLeaseId,
|
|
4638
5808
|
}),
|
|
4639
5809
|
}
|
|
4640
5810
|
: {}),
|
|
4641
5811
|
timeoutMs: resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs),
|
|
5812
|
+
...(directReceiptLeaseId &&
|
|
5813
|
+
(this.#options.markRuntimeStepReceiptRunning ||
|
|
5814
|
+
this.#options.markRuntimeStepReceiptsRunning)
|
|
5815
|
+
? {
|
|
5816
|
+
beforeProviderCall: async () => {
|
|
5817
|
+
await this.assertRuntimeToolReceiptOwnership([
|
|
5818
|
+
{
|
|
5819
|
+
callId: 'direct',
|
|
5820
|
+
cacheKey: directCacheKey,
|
|
5821
|
+
receiptKey: directCacheKey,
|
|
5822
|
+
receiptLeaseId: directReceiptLeaseId,
|
|
5823
|
+
rowId: -1,
|
|
5824
|
+
toolId,
|
|
5825
|
+
input,
|
|
5826
|
+
},
|
|
5827
|
+
]);
|
|
5828
|
+
},
|
|
5829
|
+
...(this.#options.markRuntimeStepReceiptsQueued
|
|
5830
|
+
? {
|
|
5831
|
+
parkProviderCall: async () => {
|
|
5832
|
+
await this.markRuntimeStepReceiptsQueued([
|
|
5833
|
+
{
|
|
5834
|
+
key: directCacheKey,
|
|
5835
|
+
runId: this.currentRunId,
|
|
5836
|
+
leaseId: directReceiptLeaseId,
|
|
5837
|
+
},
|
|
5838
|
+
]);
|
|
5839
|
+
},
|
|
5840
|
+
}
|
|
5841
|
+
: {}),
|
|
5842
|
+
}
|
|
5843
|
+
: {}),
|
|
4642
5844
|
});
|
|
4643
5845
|
const wrapped = await this.wrapToolExecutionResult({
|
|
4644
5846
|
toolId,
|
|
@@ -4788,6 +5990,7 @@ export class PlayContextImpl {
|
|
|
4788
5990
|
),
|
|
4789
5991
|
shouldPersistFailure: (error) =>
|
|
4790
5992
|
!(error instanceof ToolExecuteAuthScopeChangedError),
|
|
5993
|
+
markRunningBeforeExecute: false,
|
|
4791
5994
|
execute: ({ leaseId }) => executeTool({ leaseId }),
|
|
4792
5995
|
runningReceiptWaitMaxAttempts:
|
|
4793
5996
|
resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
@@ -5262,12 +6465,7 @@ export class PlayContextImpl {
|
|
|
5262
6465
|
headers: {
|
|
5263
6466
|
Authorization: `Bearer ${this.#options.executorToken}`,
|
|
5264
6467
|
'Content-Type': 'application/json',
|
|
5265
|
-
...(this
|
|
5266
|
-
? {
|
|
5267
|
-
'x-vercel-protection-bypass':
|
|
5268
|
-
this.#options.vercelProtectionBypassToken.trim(),
|
|
5269
|
-
}
|
|
5270
|
-
: {}),
|
|
6468
|
+
...(await this.vercelProtectionHeaders()),
|
|
5271
6469
|
},
|
|
5272
6470
|
body: JSON.stringify({
|
|
5273
6471
|
parentRunId: input.parentRunId,
|
|
@@ -5469,9 +6667,11 @@ export class PlayContextImpl {
|
|
|
5469
6667
|
// ctx.fetch is arbitrary customer-directed egress. Pace it through
|
|
5470
6668
|
// the same generic_http_request lane as the explicit Generic HTTP
|
|
5471
6669
|
// integration tool so switching syntax cannot bypass per-org limits.
|
|
5472
|
-
const egressSlot = await this.
|
|
5473
|
-
|
|
5474
|
-
|
|
6670
|
+
const egressSlot = await this.resourceGovernor.acquireTool({
|
|
6671
|
+
orgId: this.#options.orgId ?? null,
|
|
6672
|
+
providerResourceKey: CTX_FETCH_EGRESS_TOOL_ID,
|
|
6673
|
+
toolId: CTX_FETCH_EGRESS_TOOL_ID,
|
|
6674
|
+
});
|
|
5475
6675
|
let response: Response | null = null;
|
|
5476
6676
|
try {
|
|
5477
6677
|
const canRetryTransport = ['GET', 'HEAD', 'OPTIONS'].includes(
|
|
@@ -5781,11 +6981,12 @@ export class PlayContextImpl {
|
|
|
5781
6981
|
): Promise<void> => {
|
|
5782
6982
|
const followers = liveFollowersByOwnerCallId.get(owner.callId) ?? [];
|
|
5783
6983
|
liveFollowersByOwnerCallId.delete(owner.callId);
|
|
6984
|
+
const shouldPersistReceiptFailure =
|
|
6985
|
+
!(error instanceof ToolExecuteAuthScopeChangedError) &&
|
|
6986
|
+
!this.persistenceLatch.tripped;
|
|
5784
6987
|
for (const request of [owner, ...followers]) {
|
|
5785
6988
|
await this.rejectToolCall(toolId, request, error, {
|
|
5786
|
-
persistReceiptFailure:
|
|
5787
|
-
error instanceof ToolExecuteAuthScopeChangedError
|
|
5788
|
-
),
|
|
6989
|
+
persistReceiptFailure: shouldPersistReceiptFailure,
|
|
5789
6990
|
});
|
|
5790
6991
|
}
|
|
5791
6992
|
};
|
|
@@ -5833,7 +7034,13 @@ export class PlayContextImpl {
|
|
|
5833
7034
|
return timeoutMs > 0 ? timeoutMs : undefined;
|
|
5834
7035
|
};
|
|
5835
7036
|
|
|
5836
|
-
const
|
|
7037
|
+
const durableExistingRunningWaits: Array<{
|
|
7038
|
+
receiptKey: string;
|
|
7039
|
+
requestsForKey: ToolCallRequest[];
|
|
7040
|
+
forceAfterWait: boolean;
|
|
7041
|
+
waitMaxAttempts?: number;
|
|
7042
|
+
}> = [];
|
|
7043
|
+
let durableExistingRunningHandler: Promise<void> | null = null;
|
|
5837
7044
|
if (
|
|
5838
7045
|
pendingRequests.length > 0 &&
|
|
5839
7046
|
(this.#options.claimRuntimeStepReceipts ||
|
|
@@ -5882,7 +7089,12 @@ export class PlayContextImpl {
|
|
|
5882
7089
|
: new Map<string, RuntimeStepReceipt>();
|
|
5883
7090
|
const forcedRunningReceiptKeys = new Set(
|
|
5884
7091
|
[...forcedExisting]
|
|
5885
|
-
.filter(
|
|
7092
|
+
.filter(
|
|
7093
|
+
([, receipt]) =>
|
|
7094
|
+
receipt.status === 'queued' ||
|
|
7095
|
+
receipt.status === 'pending' ||
|
|
7096
|
+
receipt.status === 'running',
|
|
7097
|
+
)
|
|
5886
7098
|
.map(([receiptKey]) => receiptKey),
|
|
5887
7099
|
);
|
|
5888
7100
|
const claimableForcedReceiptKeys = forcedReceiptKeys.filter(
|
|
@@ -5982,44 +7194,75 @@ export class PlayContextImpl {
|
|
|
5982
7194
|
liveFollowersByOwnerCallId.set(owner.callId, followers);
|
|
5983
7195
|
}
|
|
5984
7196
|
};
|
|
7197
|
+
const resolveReceiptWaitMaxAttempts = (
|
|
7198
|
+
requestsForKey: ToolCallRequest[],
|
|
7199
|
+
): number | undefined =>
|
|
7200
|
+
requestsForKey.length > 0
|
|
7201
|
+
? Math.max(
|
|
7202
|
+
...requestsForKey.map((request) =>
|
|
7203
|
+
resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
7204
|
+
typeof request.receiptWaitMs === 'number'
|
|
7205
|
+
? { max_wait_ms: request.receiptWaitMs }
|
|
7206
|
+
: request.input,
|
|
7207
|
+
),
|
|
7208
|
+
),
|
|
7209
|
+
)
|
|
7210
|
+
: undefined;
|
|
7211
|
+
const queueExistingRunningReceiptWait = (
|
|
7212
|
+
receiptKey: string,
|
|
7213
|
+
requestsForKey: ToolCallRequest[],
|
|
7214
|
+
forceAfterWait = false,
|
|
7215
|
+
): void => {
|
|
7216
|
+
durableExistingRunningWaits.push({
|
|
7217
|
+
receiptKey,
|
|
7218
|
+
requestsForKey,
|
|
7219
|
+
forceAfterWait,
|
|
7220
|
+
waitMaxAttempts: resolveReceiptWaitMaxAttempts(requestsForKey),
|
|
7221
|
+
});
|
|
7222
|
+
};
|
|
5985
7223
|
const reclaimExistingRunningReceiptAfterWait = async (
|
|
5986
7224
|
receiptKey: string,
|
|
5987
7225
|
requestsForKey: ToolCallRequest[],
|
|
5988
7226
|
forceAfterWait = false,
|
|
7227
|
+
waitResult?: {
|
|
7228
|
+
completedReceipt?: RuntimeStepReceipt;
|
|
7229
|
+
skipWait?: boolean;
|
|
7230
|
+
},
|
|
5989
7231
|
): Promise<void> => {
|
|
5990
|
-
|
|
5991
|
-
requestsForKey.length > 0
|
|
5992
|
-
? Math.max(
|
|
5993
|
-
...requestsForKey.map((request) =>
|
|
5994
|
-
resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
5995
|
-
typeof request.receiptWaitMs === 'number'
|
|
5996
|
-
? { max_wait_ms: request.receiptWaitMs }
|
|
5997
|
-
: request.input,
|
|
5998
|
-
),
|
|
5999
|
-
),
|
|
6000
|
-
)
|
|
6001
|
-
: undefined;
|
|
6002
|
-
try {
|
|
6003
|
-
const completed = await this.waitForCompletedRuntimeToolReceipt(
|
|
6004
|
-
receiptKey,
|
|
6005
|
-
waitMaxAttempts,
|
|
6006
|
-
);
|
|
7232
|
+
if (waitResult?.completedReceipt) {
|
|
6007
7233
|
if (!forceAfterWait) {
|
|
6008
7234
|
await resolveRequestsFromReceipt(
|
|
6009
7235
|
requestsForKey,
|
|
6010
|
-
|
|
7236
|
+
waitResult.completedReceipt,
|
|
6011
7237
|
'in_flight',
|
|
6012
7238
|
);
|
|
6013
7239
|
return;
|
|
6014
7240
|
}
|
|
6015
|
-
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
7241
|
+
} else if (waitResult?.skipWait !== true) {
|
|
7242
|
+
const waitMaxAttempts =
|
|
7243
|
+
resolveReceiptWaitMaxAttempts(requestsForKey);
|
|
7244
|
+
try {
|
|
7245
|
+
const completed = await this.waitForCompletedRuntimeToolReceipt(
|
|
7246
|
+
receiptKey,
|
|
7247
|
+
waitMaxAttempts,
|
|
7248
|
+
);
|
|
7249
|
+
if (!forceAfterWait) {
|
|
7250
|
+
await resolveRequestsFromReceipt(
|
|
7251
|
+
requestsForKey,
|
|
7252
|
+
completed,
|
|
7253
|
+
'in_flight',
|
|
7254
|
+
);
|
|
7255
|
+
return;
|
|
7256
|
+
}
|
|
7257
|
+
} catch (error) {
|
|
7258
|
+
if (!(error instanceof RuntimeReceiptWaitTimeoutError)) {
|
|
7259
|
+
for (const request of requestsForKey) {
|
|
7260
|
+
await this.rejectToolCall(toolId, request, error, {
|
|
7261
|
+
persistReceiptFailure: false,
|
|
7262
|
+
});
|
|
7263
|
+
}
|
|
7264
|
+
return;
|
|
6021
7265
|
}
|
|
6022
|
-
return;
|
|
6023
7266
|
}
|
|
6024
7267
|
}
|
|
6025
7268
|
const reclaimed = (
|
|
@@ -6052,10 +7295,7 @@ export class PlayContextImpl {
|
|
|
6052
7295
|
}
|
|
6053
7296
|
return;
|
|
6054
7297
|
}
|
|
6055
|
-
if (
|
|
6056
|
-
reclaimed?.status === 'running' &&
|
|
6057
|
-
reclaimed.claimState !== 'existing'
|
|
6058
|
-
) {
|
|
7298
|
+
if (this.isOwnedClaimedRuntimeReceipt(reclaimed)) {
|
|
6059
7299
|
const [owner, ...waiters] = requestsForKey;
|
|
6060
7300
|
if (!owner) return;
|
|
6061
7301
|
if (!reclaimed.leaseId) {
|
|
@@ -6066,17 +7306,24 @@ export class PlayContextImpl {
|
|
|
6066
7306
|
});
|
|
6067
7307
|
}
|
|
6068
7308
|
owner.receiptLeaseId = reclaimed.leaseId ?? null;
|
|
7309
|
+
owner.receiptLeaseExpiresAt = reclaimed.leaseExpiresAt ?? null;
|
|
6069
7310
|
if (waiters.length > 0) {
|
|
6070
7311
|
liveFollowersByOwnerCallId.set(owner.callId, waiters);
|
|
6071
7312
|
}
|
|
6072
7313
|
try {
|
|
6073
|
-
await this.assertRuntimeToolReceiptOwnership([owner]);
|
|
6074
7314
|
const execution = await this.callToolExecutionAPI(
|
|
6075
7315
|
toolId,
|
|
6076
7316
|
owner.input,
|
|
6077
7317
|
{
|
|
7318
|
+
beforeProviderCall: () =>
|
|
7319
|
+
this.assertRuntimeToolReceiptOwnership([owner]),
|
|
7320
|
+
parkProviderCall: () =>
|
|
7321
|
+
this.parkRuntimeToolReceiptsQueued([owner]),
|
|
6078
7322
|
durableCallReceiptKey: receiptKey,
|
|
6079
7323
|
executionAuthScopeDigest: owner.executionAuthScopeDigest,
|
|
7324
|
+
receiptLeaseExpiresAt: owner.receiptLeaseExpiresAt,
|
|
7325
|
+
heartbeatReceipt: () =>
|
|
7326
|
+
this.assertRuntimeToolReceiptOwnership([owner]),
|
|
6080
7327
|
providerIdempotencyKey:
|
|
6081
7328
|
this.providerIdempotencyKeyForToolCall({
|
|
6082
7329
|
cacheKey: owner.cacheKey,
|
|
@@ -6112,17 +7359,50 @@ export class PlayContextImpl {
|
|
|
6112
7359
|
);
|
|
6113
7360
|
}
|
|
6114
7361
|
};
|
|
7362
|
+
const processExistingRunningReceiptWaits =
|
|
7363
|
+
async (): Promise<void> => {
|
|
7364
|
+
if (durableExistingRunningWaits.length === 0) return;
|
|
7365
|
+
const waitMaxAttempts = Math.max(
|
|
7366
|
+
...durableExistingRunningWaits.map(
|
|
7367
|
+
(wait) => wait.waitMaxAttempts ?? 0,
|
|
7368
|
+
),
|
|
7369
|
+
);
|
|
7370
|
+
const waited = await this.waitForCompletedRuntimeToolReceipts(
|
|
7371
|
+
durableExistingRunningWaits.map((wait) => wait.receiptKey),
|
|
7372
|
+
waitMaxAttempts > 0 ? waitMaxAttempts : undefined,
|
|
7373
|
+
);
|
|
7374
|
+
await Promise.allSettled(
|
|
7375
|
+
durableExistingRunningWaits.map(async (wait) => {
|
|
7376
|
+
const failed = waited.failed.get(wait.receiptKey);
|
|
7377
|
+
if (failed) {
|
|
7378
|
+
for (const request of wait.requestsForKey) {
|
|
7379
|
+
await this.rejectToolCall(toolId, request, failed, {
|
|
7380
|
+
persistReceiptFailure: false,
|
|
7381
|
+
});
|
|
7382
|
+
}
|
|
7383
|
+
return;
|
|
7384
|
+
}
|
|
7385
|
+
await reclaimExistingRunningReceiptAfterWait(
|
|
7386
|
+
wait.receiptKey,
|
|
7387
|
+
wait.requestsForKey,
|
|
7388
|
+
wait.forceAfterWait,
|
|
7389
|
+
{
|
|
7390
|
+
completedReceipt: waited.completed.get(wait.receiptKey),
|
|
7391
|
+
skipWait: true,
|
|
7392
|
+
},
|
|
7393
|
+
);
|
|
7394
|
+
}),
|
|
7395
|
+
);
|
|
7396
|
+
};
|
|
6115
7397
|
|
|
6116
7398
|
for (const [receiptKey, group] of requestsByReceiptKey) {
|
|
6117
7399
|
const requestsForKey = group.requests;
|
|
6118
7400
|
const claim = claims.get(receiptKey);
|
|
6119
7401
|
if (!claim) {
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
group.forceRefresh,
|
|
6125
|
-
),
|
|
7402
|
+
queueExistingRunningReceiptWait(
|
|
7403
|
+
receiptKey,
|
|
7404
|
+
requestsForKey,
|
|
7405
|
+
group.forceRefresh,
|
|
6126
7406
|
);
|
|
6127
7407
|
continue;
|
|
6128
7408
|
}
|
|
@@ -6142,9 +7422,19 @@ export class PlayContextImpl {
|
|
|
6142
7422
|
continue;
|
|
6143
7423
|
}
|
|
6144
7424
|
if (
|
|
6145
|
-
claim?.status === '
|
|
6146
|
-
|
|
7425
|
+
(claim?.status === 'queued' ||
|
|
7426
|
+
claim?.status === 'pending' ||
|
|
7427
|
+
claim?.status === 'running') &&
|
|
7428
|
+
claim.claimState === 'existing'
|
|
6147
7429
|
) {
|
|
7430
|
+
queueExistingRunningReceiptWait(
|
|
7431
|
+
receiptKey,
|
|
7432
|
+
requestsForKey,
|
|
7433
|
+
group.forceRefresh,
|
|
7434
|
+
);
|
|
7435
|
+
continue;
|
|
7436
|
+
}
|
|
7437
|
+
if (this.isOwnedClaimedRuntimeReceipt(claim)) {
|
|
6148
7438
|
const [owner, ...waiters] = requestsForKey;
|
|
6149
7439
|
if (!claim.leaseId) {
|
|
6150
7440
|
throw new RuntimeReceiptLeaseLostError({
|
|
@@ -6155,34 +7445,24 @@ export class PlayContextImpl {
|
|
|
6155
7445
|
}
|
|
6156
7446
|
if (owner) {
|
|
6157
7447
|
owner.receiptLeaseId = claim.leaseId ?? null;
|
|
7448
|
+
owner.receiptLeaseExpiresAt = claim.leaseExpiresAt ?? null;
|
|
6158
7449
|
}
|
|
6159
7450
|
claimOwnerWithLiveFollowers(owner, waiters);
|
|
6160
7451
|
continue;
|
|
6161
7452
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
durableExistingRunningHandlers.push(
|
|
6167
|
-
reclaimExistingRunningReceiptAfterWait(
|
|
6168
|
-
receiptKey,
|
|
6169
|
-
requestsForKey,
|
|
6170
|
-
group.forceRefresh,
|
|
6171
|
-
),
|
|
6172
|
-
);
|
|
6173
|
-
continue;
|
|
6174
|
-
}
|
|
6175
|
-
durableExistingRunningHandlers.push(
|
|
6176
|
-
reclaimExistingRunningReceiptAfterWait(
|
|
6177
|
-
receiptKey,
|
|
6178
|
-
requestsForKey,
|
|
6179
|
-
group.forceRefresh,
|
|
6180
|
-
),
|
|
7453
|
+
queueExistingRunningReceiptWait(
|
|
7454
|
+
receiptKey,
|
|
7455
|
+
requestsForKey,
|
|
7456
|
+
group.forceRefresh,
|
|
6181
7457
|
);
|
|
6182
7458
|
}
|
|
6183
7459
|
|
|
6184
7460
|
pendingRequests.length = 0;
|
|
6185
7461
|
pendingRequests.push(...claimedRequests);
|
|
7462
|
+
durableExistingRunningHandler =
|
|
7463
|
+
durableExistingRunningWaits.length > 0
|
|
7464
|
+
? processExistingRunningReceiptWaits()
|
|
7465
|
+
: null;
|
|
6186
7466
|
recordToolStep(durableRecoveredRequests);
|
|
6187
7467
|
}
|
|
6188
7468
|
|
|
@@ -6199,7 +7479,7 @@ export class PlayContextImpl {
|
|
|
6199
7479
|
|
|
6200
7480
|
const batchSize =
|
|
6201
7481
|
compiledBatches.length > 0
|
|
6202
|
-
? await this.
|
|
7482
|
+
? await this.resourceGovernor.suggestedToolParallelism(
|
|
6203
7483
|
compiledBatches[0]!.batchOperation,
|
|
6204
7484
|
this.governor.policy.pacing
|
|
6205
7485
|
.workerToolBatchDefaultParallelism,
|
|
@@ -6218,9 +7498,6 @@ export class PlayContextImpl {
|
|
|
6218
7498
|
this.persistenceLatch,
|
|
6219
7499
|
);
|
|
6220
7500
|
}
|
|
6221
|
-
await this.assertRuntimeToolReceiptOwnership(
|
|
6222
|
-
batch.memberRequests,
|
|
6223
|
-
);
|
|
6224
7501
|
const receiptKeys = batch.memberRequests.map(
|
|
6225
7502
|
(request) => request.cacheKey,
|
|
6226
7503
|
);
|
|
@@ -6254,9 +7531,29 @@ export class PlayContextImpl {
|
|
|
6254
7531
|
}),
|
|
6255
7532
|
),
|
|
6256
7533
|
}),
|
|
7534
|
+
receiptLeaseExpiresAt: batch.memberRequests.reduce<
|
|
7535
|
+
string | null
|
|
7536
|
+
>((earliest, request) => {
|
|
7537
|
+
const expiresAt = request.receiptLeaseExpiresAt ?? null;
|
|
7538
|
+
if (!expiresAt) return earliest;
|
|
7539
|
+
if (!earliest) return expiresAt;
|
|
7540
|
+
return Date.parse(expiresAt) < Date.parse(earliest)
|
|
7541
|
+
? expiresAt
|
|
7542
|
+
: earliest;
|
|
7543
|
+
}, null),
|
|
6257
7544
|
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
|
|
6258
7545
|
batch.memberRequests,
|
|
6259
7546
|
),
|
|
7547
|
+
beforeProviderCall: () =>
|
|
7548
|
+
this.assertRuntimeToolReceiptOwnership(
|
|
7549
|
+
batch.memberRequests,
|
|
7550
|
+
),
|
|
7551
|
+
heartbeatReceipt: () =>
|
|
7552
|
+
this.assertRuntimeToolReceiptOwnership(
|
|
7553
|
+
batch.memberRequests,
|
|
7554
|
+
),
|
|
7555
|
+
parkProviderCall: () =>
|
|
7556
|
+
this.parkRuntimeToolReceiptsQueued(batch.memberRequests),
|
|
6260
7557
|
},
|
|
6261
7558
|
);
|
|
6262
7559
|
},
|
|
@@ -6299,77 +7596,144 @@ export class PlayContextImpl {
|
|
|
6299
7596
|
},
|
|
6300
7597
|
});
|
|
6301
7598
|
} else {
|
|
6302
|
-
const
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
for (
|
|
6307
|
-
let start = 0;
|
|
6308
|
-
start < pendingRequests.length;
|
|
6309
|
-
start += batchSize
|
|
6310
|
-
) {
|
|
6311
|
-
const chunk = pendingRequests.slice(start, start + batchSize);
|
|
6312
|
-
await Promise.allSettled(
|
|
6313
|
-
chunk.map(async (request) => {
|
|
6314
|
-
// Circuit breaker: do not dispatch this provider call once a
|
|
6315
|
-
// persistence failure has occurred in this run.
|
|
6316
|
-
if (this.persistenceLatch.tripped) {
|
|
6317
|
-
this.persistenceLatch.preventedCallCount += 1;
|
|
6318
|
-
await rejectWithLiveFollowers(
|
|
6319
|
-
request,
|
|
6320
|
-
new RuntimePersistenceCircuitOpenError(
|
|
6321
|
-
this.persistenceLatch,
|
|
6322
|
-
),
|
|
6323
|
-
);
|
|
6324
|
-
return;
|
|
6325
|
-
}
|
|
6326
|
-
try {
|
|
6327
|
-
await this.assertRuntimeToolReceiptOwnership([request]);
|
|
6328
|
-
const execution = await this.callToolExecutionAPI(
|
|
6329
|
-
toolId,
|
|
6330
|
-
request.input,
|
|
6331
|
-
{
|
|
6332
|
-
...(request.receiptKey
|
|
6333
|
-
? {
|
|
6334
|
-
durableCallReceiptKey: request.receiptKey,
|
|
6335
|
-
executionAuthScopeDigest:
|
|
6336
|
-
request.executionAuthScopeDigest,
|
|
6337
|
-
providerIdempotencyKey:
|
|
6338
|
-
this.providerIdempotencyKeyForToolCall({
|
|
6339
|
-
cacheKey: request.receiptKey,
|
|
6340
|
-
force: request.force === true,
|
|
6341
|
-
leaseId: request.receiptLeaseId,
|
|
6342
|
-
}),
|
|
6343
|
-
}
|
|
6344
|
-
: {}),
|
|
6345
|
-
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
|
|
6346
|
-
request,
|
|
6347
|
-
]),
|
|
6348
|
-
},
|
|
6349
|
-
);
|
|
6350
|
-
const result = await this.resolveToolCall(
|
|
6351
|
-
toolId,
|
|
6352
|
-
request,
|
|
6353
|
-
execution?.result ?? null,
|
|
6354
|
-
execution?.metadata ?? null,
|
|
6355
|
-
execution?.jobId,
|
|
6356
|
-
execution?.meta,
|
|
6357
|
-
);
|
|
6358
|
-
liveStepResults.set(request.callId, result);
|
|
6359
|
-
resolveLiveFollowers(request, result);
|
|
6360
|
-
} catch (error) {
|
|
6361
|
-
await rejectWithLiveFollowers(request, error);
|
|
6362
|
-
}
|
|
6363
|
-
}),
|
|
7599
|
+
const initialRequestsPerSecond =
|
|
7600
|
+
await this.resourceGovernor.suggestedToolParallelism(
|
|
7601
|
+
toolId,
|
|
7602
|
+
this.governor.policy.pacing.suggestedMaxParallelism,
|
|
6364
7603
|
);
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
7604
|
+
const dispatcher = new AdaptiveToolDispatcher<
|
|
7605
|
+
ToolCallRequest,
|
|
7606
|
+
unknown
|
|
7607
|
+
>({
|
|
7608
|
+
initialRequestsPerSecond,
|
|
7609
|
+
maxRequestsPerSecond:
|
|
7610
|
+
this.governor.policy.pacing.suggestedMaxParallelism,
|
|
7611
|
+
maxConcurrency: this.governor.policy.concurrency.toolCalls,
|
|
7612
|
+
defaultLatencyMs: 1_000,
|
|
7613
|
+
safetyFactor: 1.5,
|
|
7614
|
+
});
|
|
7615
|
+
const completionBuffer: Array<{
|
|
7616
|
+
request: ToolCallRequest;
|
|
7617
|
+
result: unknown | null;
|
|
7618
|
+
metadata?: ToolResultMetadataInput | null;
|
|
7619
|
+
jobId?: string;
|
|
7620
|
+
meta?: Record<string, unknown>;
|
|
7621
|
+
resolve: (value: unknown) => void;
|
|
7622
|
+
reject: (error: unknown) => void;
|
|
7623
|
+
}> = [];
|
|
7624
|
+
const completionFailedCallIds = new Set<string>();
|
|
7625
|
+
let completionFlushScheduled = false;
|
|
7626
|
+
const flushCompletionBuffer = async (): Promise<void> => {
|
|
7627
|
+
const entries = completionBuffer.splice(0);
|
|
7628
|
+
if (entries.length === 0) return;
|
|
7629
|
+
try {
|
|
7630
|
+
const resolvedResults = await this.resolveToolCallBatchResults(
|
|
7631
|
+
toolId,
|
|
7632
|
+
entries.map((entry) => ({
|
|
7633
|
+
request: entry.request,
|
|
7634
|
+
result: entry.result,
|
|
7635
|
+
metadata: entry.metadata,
|
|
7636
|
+
jobId: entry.jobId,
|
|
7637
|
+
meta: entry.meta,
|
|
7638
|
+
})),
|
|
7639
|
+
);
|
|
7640
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
7641
|
+
const entry = entries[index]!;
|
|
7642
|
+
const result = resolvedResults[index];
|
|
7643
|
+
liveStepResults.set(entry.request.callId, result);
|
|
7644
|
+
resolveLiveFollowers(entry.request, result);
|
|
7645
|
+
entry.resolve(result);
|
|
7646
|
+
}
|
|
7647
|
+
} catch (error) {
|
|
7648
|
+
for (const entry of entries) {
|
|
7649
|
+
completionFailedCallIds.add(entry.request.callId);
|
|
7650
|
+
await this.rejectToolCall(toolId, entry.request, error, {
|
|
7651
|
+
persistReceiptFailure: false,
|
|
7652
|
+
});
|
|
7653
|
+
entry.reject(error);
|
|
7654
|
+
}
|
|
7655
|
+
}
|
|
7656
|
+
};
|
|
7657
|
+
const enqueueCompletion = (
|
|
7658
|
+
request: ToolCallRequest,
|
|
7659
|
+
execution: ParsedToolExecuteResponse,
|
|
7660
|
+
): Promise<unknown> =>
|
|
7661
|
+
new Promise((resolve, reject) => {
|
|
7662
|
+
completionBuffer.push({
|
|
7663
|
+
request,
|
|
7664
|
+
result: execution.result ?? null,
|
|
7665
|
+
metadata: execution.metadata ?? null,
|
|
7666
|
+
jobId: execution.jobId,
|
|
7667
|
+
meta: execution.meta,
|
|
7668
|
+
resolve,
|
|
7669
|
+
reject,
|
|
7670
|
+
});
|
|
7671
|
+
if (completionFlushScheduled) return;
|
|
7672
|
+
completionFlushScheduled = true;
|
|
7673
|
+
setTimeout(() => {
|
|
7674
|
+
completionFlushScheduled = false;
|
|
7675
|
+
void flushCompletionBuffer();
|
|
7676
|
+
}, 0);
|
|
7677
|
+
});
|
|
7678
|
+
const dispatchResults = await dispatcher.dispatch(
|
|
7679
|
+
pendingRequests,
|
|
7680
|
+
async (request) => {
|
|
7681
|
+
// Circuit breaker: do not dispatch this provider call once a
|
|
7682
|
+
// persistence failure has occurred in this run.
|
|
7683
|
+
if (this.persistenceLatch.tripped) {
|
|
7684
|
+
this.persistenceLatch.preventedCallCount += 1;
|
|
7685
|
+
throw new RuntimePersistenceCircuitOpenError(
|
|
7686
|
+
this.persistenceLatch,
|
|
7687
|
+
);
|
|
7688
|
+
}
|
|
7689
|
+
const execution = await this.callToolExecutionAPI(
|
|
7690
|
+
toolId,
|
|
7691
|
+
request.input,
|
|
7692
|
+
{
|
|
7693
|
+
beforeProviderCall: () =>
|
|
7694
|
+
this.assertRuntimeToolReceiptOwnership([request]),
|
|
7695
|
+
parkProviderCall: () =>
|
|
7696
|
+
this.parkRuntimeToolReceiptsQueued([request]),
|
|
7697
|
+
...(request.receiptKey
|
|
7698
|
+
? {
|
|
7699
|
+
durableCallReceiptKey: request.receiptKey,
|
|
7700
|
+
executionAuthScopeDigest:
|
|
7701
|
+
request.executionAuthScopeDigest,
|
|
7702
|
+
providerIdempotencyKey:
|
|
7703
|
+
this.providerIdempotencyKeyForToolCall({
|
|
7704
|
+
cacheKey: request.receiptKey,
|
|
7705
|
+
force: request.force === true,
|
|
7706
|
+
leaseId: request.receiptLeaseId,
|
|
7707
|
+
}),
|
|
7708
|
+
receiptLeaseExpiresAt: request.receiptLeaseExpiresAt,
|
|
7709
|
+
heartbeatReceipt: () =>
|
|
7710
|
+
this.assertRuntimeToolReceiptOwnership([request]),
|
|
7711
|
+
}
|
|
7712
|
+
: {}),
|
|
7713
|
+
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
|
|
7714
|
+
request,
|
|
7715
|
+
]),
|
|
7716
|
+
},
|
|
7717
|
+
);
|
|
7718
|
+
return await enqueueCompletion(request, execution);
|
|
7719
|
+
},
|
|
7720
|
+
);
|
|
7721
|
+
const failedEntries = dispatchResults.filter(
|
|
7722
|
+
(entry) =>
|
|
7723
|
+
entry.error !== undefined &&
|
|
7724
|
+
!completionFailedCallIds.has(entry.job.callId),
|
|
7725
|
+
);
|
|
7726
|
+
await Promise.allSettled(
|
|
7727
|
+
failedEntries.map((entry) =>
|
|
7728
|
+
rejectWithLiveFollowers(entry.job, entry.error),
|
|
7729
|
+
),
|
|
7730
|
+
);
|
|
7731
|
+
recordToolStep(pendingRequests);
|
|
7732
|
+
this.#options.onBatchComplete?.(this.checkpoint);
|
|
6369
7733
|
}
|
|
6370
7734
|
}
|
|
6371
|
-
if (
|
|
6372
|
-
await
|
|
7735
|
+
if (durableExistingRunningHandler) {
|
|
7736
|
+
await durableExistingRunningHandler;
|
|
6373
7737
|
}
|
|
6374
7738
|
}),
|
|
6375
7739
|
);
|
|
@@ -6498,7 +7862,11 @@ export class PlayContextImpl {
|
|
|
6498
7862
|
// The Governor's tool slot is the single seam for tool-call budget + the
|
|
6499
7863
|
// global tool-concurrency backstop + per-(org, provider) pacing. It blocks
|
|
6500
7864
|
// until all three are satisfied and is held (across retries) until release.
|
|
6501
|
-
const toolSlot = await this.
|
|
7865
|
+
const toolSlot = await this.resourceGovernor.acquireTool({
|
|
7866
|
+
orgId: this.#options.orgId ?? null,
|
|
7867
|
+
providerResourceKey: `tool:${toolId}`,
|
|
7868
|
+
toolId,
|
|
7869
|
+
});
|
|
6502
7870
|
try {
|
|
6503
7871
|
return await withActiveSpan(
|
|
6504
7872
|
'plays.tool.execute',
|
|
@@ -6538,7 +7906,15 @@ export class PlayContextImpl {
|
|
|
6538
7906
|
const deeplineRequestId = providerIdempotencyKey
|
|
6539
7907
|
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
6540
7908
|
: null;
|
|
6541
|
-
|
|
7909
|
+
// Receipt ownership is a liveness contract, not a one-time check.
|
|
7910
|
+
// Keep it alive for the whole provider HTTP request. The cadence
|
|
7911
|
+
// comes from the store-issued expiry because a remote runner may
|
|
7912
|
+
// not share the coordinator's TTL environment.
|
|
7913
|
+
const hasReceiptHeartbeat = Boolean(options?.heartbeatReceipt);
|
|
7914
|
+
const abortController =
|
|
7915
|
+
timeoutMs || hasReceiptHeartbeat ? new AbortController() : null;
|
|
7916
|
+
const receiptHeartbeat = options?.heartbeatReceipt;
|
|
7917
|
+
let heartbeatFailure: unknown = null;
|
|
6542
7918
|
const timeoutHandle = timeoutMs
|
|
6543
7919
|
? setTimeout(() => {
|
|
6544
7920
|
abortController?.abort(
|
|
@@ -6549,75 +7925,114 @@ export class PlayContextImpl {
|
|
|
6549
7925
|
}, timeoutMs)
|
|
6550
7926
|
: null;
|
|
6551
7927
|
try {
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
7928
|
+
const providerCallStartedAt = Date.now();
|
|
7929
|
+
await options?.beforeProviderCall?.();
|
|
7930
|
+
const heartbeatIntervalMs = hasReceiptHeartbeat
|
|
7931
|
+
? runtimeLeaseHeartbeatIntervalFromExpiry({
|
|
7932
|
+
leaseExpiresAt: options?.receiptLeaseExpiresAt,
|
|
7933
|
+
fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
7934
|
+
})
|
|
7935
|
+
: null;
|
|
7936
|
+
const receiptHeartbeatSupervisor =
|
|
7937
|
+
heartbeatIntervalMs !== null && receiptHeartbeat
|
|
7938
|
+
? createRuntimeReceiptHeartbeatSupervisor({
|
|
7939
|
+
intervalMs: heartbeatIntervalMs,
|
|
7940
|
+
heartbeat: async () => {
|
|
7941
|
+
await receiptHeartbeat();
|
|
7942
|
+
return 'active';
|
|
7943
|
+
},
|
|
7944
|
+
isLeaseLost: (error) =>
|
|
7945
|
+
error instanceof RuntimeReceiptLeaseLostError,
|
|
7946
|
+
onLeaseLost: (error) => {
|
|
7947
|
+
heartbeatFailure ??= error;
|
|
7948
|
+
abortController?.abort(error);
|
|
7949
|
+
},
|
|
7950
|
+
onTransientFailure: (error) => {
|
|
7951
|
+
this.log(
|
|
7952
|
+
`Tool ${toolId} receipt heartbeat transport failed; retrying: ${error instanceof Error ? error.message : String(error)}`,
|
|
7953
|
+
);
|
|
7954
|
+
},
|
|
7955
|
+
})
|
|
7956
|
+
: null;
|
|
7957
|
+
receiptHeartbeatSupervisor?.start();
|
|
7958
|
+
try {
|
|
7959
|
+
response = await fetch(url, {
|
|
7960
|
+
method: 'POST',
|
|
7961
|
+
signal: abortController?.signal,
|
|
7962
|
+
headers: {
|
|
7963
|
+
'Content-Type': 'application/json',
|
|
7964
|
+
Authorization: `Bearer ${this.#options.executorToken}`,
|
|
7965
|
+
[EXECUTE_RESPONSE_CONTRACT_HEADER]:
|
|
7966
|
+
V2_EXECUTE_RESPONSE_CONTRACT,
|
|
7967
|
+
[EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
|
|
7968
|
+
[EXECUTE_TOOL_METADATA_HEADER]: 'true',
|
|
7969
|
+
...(deeplineRequestId
|
|
6591
7970
|
? {
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
offset: options.customerDbDataset.offset,
|
|
6595
|
-
page_size: options.customerDbDataset.pageSize,
|
|
6596
|
-
total_rows: options.customerDbDataset.totalRows,
|
|
6597
|
-
},
|
|
6598
|
-
...(isCustomerDbDatasetTool(toolId)
|
|
6599
|
-
? {
|
|
6600
|
-
customer_db_dataset: {
|
|
6601
|
-
limit: options.customerDbDataset.limit,
|
|
6602
|
-
offset: options.customerDbDataset.offset,
|
|
6603
|
-
page_size: options.customerDbDataset.pageSize,
|
|
6604
|
-
total_rows:
|
|
6605
|
-
options.customerDbDataset.totalRows,
|
|
6606
|
-
},
|
|
6607
|
-
}
|
|
6608
|
-
: {}),
|
|
7971
|
+
'x-deepline-request-id': deeplineRequestId,
|
|
7972
|
+
'x-deepline-idempotency-key': providerIdempotencyKey!,
|
|
6609
7973
|
}
|
|
6610
7974
|
: {}),
|
|
6611
|
-
...(
|
|
6612
|
-
? { provider_idempotency_key: providerIdempotencyKey }
|
|
6613
|
-
: {}),
|
|
7975
|
+
...(await this.vercelProtectionHeaders()),
|
|
6614
7976
|
},
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
: {
|
|
6618
|
-
|
|
6619
|
-
|
|
7977
|
+
body: JSON.stringify({
|
|
7978
|
+
payload: input,
|
|
7979
|
+
metadata: {
|
|
7980
|
+
parent_run_id: this.#options.runId,
|
|
7981
|
+
...(durableCallReceiptKey
|
|
7982
|
+
? {
|
|
7983
|
+
durable_call_receipt_key: durableCallReceiptKey,
|
|
7984
|
+
...(executionAuthScopeDigest
|
|
7985
|
+
? {
|
|
7986
|
+
execution_auth_scope_digest:
|
|
7987
|
+
executionAuthScopeDigest,
|
|
7988
|
+
}
|
|
7989
|
+
: {}),
|
|
7990
|
+
}
|
|
7991
|
+
: {}),
|
|
7992
|
+
...(options?.customerDbDataset
|
|
7993
|
+
? {
|
|
7994
|
+
query_result_dataset: {
|
|
7995
|
+
limit: options.customerDbDataset.limit,
|
|
7996
|
+
offset: options.customerDbDataset.offset,
|
|
7997
|
+
page_size: options.customerDbDataset.pageSize,
|
|
7998
|
+
total_rows: options.customerDbDataset.totalRows,
|
|
7999
|
+
},
|
|
8000
|
+
...(isCustomerDbDatasetTool(toolId)
|
|
8001
|
+
? {
|
|
8002
|
+
customer_db_dataset: {
|
|
8003
|
+
limit: options.customerDbDataset.limit,
|
|
8004
|
+
offset: options.customerDbDataset.offset,
|
|
8005
|
+
page_size:
|
|
8006
|
+
options.customerDbDataset.pageSize,
|
|
8007
|
+
total_rows:
|
|
8008
|
+
options.customerDbDataset.totalRows,
|
|
8009
|
+
},
|
|
8010
|
+
}
|
|
8011
|
+
: {}),
|
|
8012
|
+
}
|
|
8013
|
+
: {}),
|
|
8014
|
+
...(providerIdempotencyKey
|
|
8015
|
+
? { provider_idempotency_key: providerIdempotencyKey }
|
|
8016
|
+
: {}),
|
|
8017
|
+
},
|
|
8018
|
+
...(this.#options.integrationMode
|
|
8019
|
+
? { integration_mode: this.#options.integrationMode }
|
|
8020
|
+
: {}),
|
|
8021
|
+
}),
|
|
8022
|
+
});
|
|
8023
|
+
this.resourceGovernor.observe({
|
|
8024
|
+
providerResourceKey: `tool:${toolId}`,
|
|
8025
|
+
providerLatencyMs: Date.now() - providerCallStartedAt,
|
|
8026
|
+
providerSuccess: response.ok,
|
|
8027
|
+
provider429: response.status === 429,
|
|
8028
|
+
});
|
|
8029
|
+
} finally {
|
|
8030
|
+
receiptHeartbeatSupervisor?.stop();
|
|
8031
|
+
}
|
|
6620
8032
|
} catch (error) {
|
|
8033
|
+
if (heartbeatFailure) {
|
|
8034
|
+
throw heartbeatFailure;
|
|
8035
|
+
}
|
|
6621
8036
|
if (abortController?.signal.aborted) {
|
|
6622
8037
|
throw abortController.signal.reason instanceof Error
|
|
6623
8038
|
? abortController.signal.reason
|
|
@@ -6639,6 +8054,7 @@ export class PlayContextImpl {
|
|
|
6639
8054
|
this.log(
|
|
6640
8055
|
`Tool ${toolId} transport failed calling ${url} on attempt ${transportAttempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS}; retrying after ${retryAfterMs}ms: ${message}`,
|
|
6641
8056
|
);
|
|
8057
|
+
await options?.parkProviderCall?.();
|
|
6642
8058
|
await this.sleepWithCheckpointHeartbeat(retryAfterMs);
|
|
6643
8059
|
continue;
|
|
6644
8060
|
}
|
|
@@ -6702,6 +8118,7 @@ export class PlayContextImpl {
|
|
|
6702
8118
|
this.log(
|
|
6703
8119
|
`Tool ${toolId} returned ${response.status}; retrying after ${failure.retryDelayMs}ms`,
|
|
6704
8120
|
);
|
|
8121
|
+
await options?.parkProviderCall?.();
|
|
6705
8122
|
await this.sleepWithCheckpointHeartbeat(failure.retryDelayMs);
|
|
6706
8123
|
continue;
|
|
6707
8124
|
}
|
|
@@ -6730,8 +8147,8 @@ export class PlayContextImpl {
|
|
|
6730
8147
|
|
|
6731
8148
|
/**
|
|
6732
8149
|
* Resolve the provider backing a tool (from queue-hint metadata) and feed a
|
|
6733
|
-
* server-observed Retry-After back into the Governor's shared pacer.
|
|
6734
|
-
*
|
|
8150
|
+
* server-observed Retry-After back into the Governor's shared pacer. Tools
|
|
8151
|
+
* without hints use the Governor's default `tool:${toolId}` bucket.
|
|
6735
8152
|
*/
|
|
6736
8153
|
private async reportToolBackpressure(
|
|
6737
8154
|
toolId: string,
|
|
@@ -6740,11 +8157,11 @@ export class PlayContextImpl {
|
|
|
6740
8157
|
const hints: readonly PlayQueueHint[] = this.#options.getToolQueueHints
|
|
6741
8158
|
? await this.#options.getToolQueueHints(toolId)
|
|
6742
8159
|
: [];
|
|
6743
|
-
const provider = hints[0]?.provider
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
8160
|
+
const provider = hints[0]?.provider?.trim() || `tool:${toolId}`;
|
|
8161
|
+
this.resourceGovernor.reportProviderBackpressure({
|
|
8162
|
+
provider,
|
|
8163
|
+
retryAfterMs,
|
|
8164
|
+
});
|
|
6748
8165
|
}
|
|
6749
8166
|
|
|
6750
8167
|
private async sleepWithCheckpointHeartbeat(ms: number): Promise<void> {
|