deepline 0.3.184 → 0.3.185
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/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +53 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +751 -193
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +437 -150
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +47 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +22 -2
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +102 -12
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +136 -28
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +80 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +897 -207
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +120 -34
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +82 -2
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +7 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +1 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -104,12 +104,12 @@ import {
|
|
|
104
104
|
type PacingResolver,
|
|
105
105
|
type PlayExecutionGovernor,
|
|
106
106
|
} from './governor/governor';
|
|
107
|
-
import { resolveExecutionPolicy } from './governor/policy';
|
|
108
107
|
import {
|
|
109
108
|
createRuntimeResourceGovernor,
|
|
110
109
|
type RuntimeResourceGovernor,
|
|
111
110
|
type RuntimeResourceObservation,
|
|
112
111
|
type RuntimeResourceSnapshot,
|
|
112
|
+
type RuntimeProviderPermit,
|
|
113
113
|
} from './resource-governor';
|
|
114
114
|
import { CTX_FETCH_EGRESS_TOOL_ID } from './builtin-pacing';
|
|
115
115
|
import {
|
|
@@ -192,6 +192,8 @@ import {
|
|
|
192
192
|
TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS,
|
|
193
193
|
TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER,
|
|
194
194
|
TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
|
|
195
|
+
TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER,
|
|
196
|
+
TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER,
|
|
195
197
|
classifyToolExecuteHttpFailure,
|
|
196
198
|
createToolExecuteHttpFailureAttemptTracker,
|
|
197
199
|
parseToolExecuteRetryAfterMs,
|
|
@@ -358,6 +360,7 @@ import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from '../plays/runtime-constra
|
|
|
358
360
|
import {
|
|
359
361
|
PlayExecutionSuspendedError,
|
|
360
362
|
PlayRowExecutionSuspendedError,
|
|
363
|
+
PlayRowProviderBackpressureError,
|
|
361
364
|
isPlayExecutionSuspendedError,
|
|
362
365
|
isPlayRowExecutionSuspendedError,
|
|
363
366
|
type PlayExecutionSuspension,
|
|
@@ -1374,10 +1377,11 @@ export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
|
|
|
1374
1377
|
expectedRows: number;
|
|
1375
1378
|
currentRows: Record<string, unknown>[];
|
|
1376
1379
|
failedRowCount: number;
|
|
1380
|
+
rowMode?: 'all' | 'output';
|
|
1377
1381
|
readPersistedRows: (input: {
|
|
1378
1382
|
limit: number;
|
|
1379
1383
|
offset: number;
|
|
1380
|
-
rowMode: 'all';
|
|
1384
|
+
rowMode: 'all' | 'output';
|
|
1381
1385
|
}) => Promise<Record<string, unknown>[]>;
|
|
1382
1386
|
log?: (line: string) => void;
|
|
1383
1387
|
}): Promise<Record<string, unknown>[]> {
|
|
@@ -1395,7 +1399,7 @@ export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
|
|
|
1395
1399
|
const page = await input.readPersistedRows({
|
|
1396
1400
|
limit: Math.min(10_000, input.expectedRows - persistedRows.length),
|
|
1397
1401
|
offset,
|
|
1398
|
-
rowMode: 'all',
|
|
1402
|
+
rowMode: input.rowMode ?? 'all',
|
|
1399
1403
|
});
|
|
1400
1404
|
if (page.length === 0) break;
|
|
1401
1405
|
persistedRows.push(...page);
|
|
@@ -1924,6 +1928,8 @@ type ToolExecutionApiOptions = {
|
|
|
1924
1928
|
* inside a row's async scope, where the scope is read from there instead.
|
|
1925
1929
|
*/
|
|
1926
1930
|
memoryLedgerRowScopes?: readonly (MemoryLedgerRowScope | null | undefined)[];
|
|
1931
|
+
/** Runner-local map ownership; this must never be serialized to the gateway. */
|
|
1932
|
+
providerBackpressureMapRows?: boolean;
|
|
1927
1933
|
/**
|
|
1928
1934
|
* Present only on a native batch's one physical call. The members share a
|
|
1929
1935
|
* single body whose per-member sizes the splitter has not produced yet, so
|
|
@@ -2512,6 +2518,7 @@ class RuntimeDatasetBuilder<T extends Record<string, unknown>> {
|
|
|
2512
2518
|
}
|
|
2513
2519
|
|
|
2514
2520
|
const WAITING_ROW = Symbol('deepline.waiting_row');
|
|
2521
|
+
|
|
2515
2522
|
const FAILED_ROW = Symbol('deepline.failed_row');
|
|
2516
2523
|
const COMPLETED_ROW = Symbol('deepline.completed_row');
|
|
2517
2524
|
type MapRowExecutionResult =
|
|
@@ -2604,6 +2611,9 @@ function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
|
|
|
2604
2611
|
suspendedInputRows: frame.suspendedInputRows.map((row) => ({
|
|
2605
2612
|
inputIndex: row.inputIndex,
|
|
2606
2613
|
data: cloneCsvAliasedRow(row.data),
|
|
2614
|
+
...(row.authoredInputDigest
|
|
2615
|
+
? { authoredInputDigest: row.authoredInputDigest }
|
|
2616
|
+
: {}),
|
|
2607
2617
|
})),
|
|
2608
2618
|
}
|
|
2609
2619
|
: {}),
|
|
@@ -2613,7 +2623,8 @@ function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
|
|
|
2613
2623
|
/**
|
|
2614
2624
|
* Adapt this runtime's per-tool queue hints into the Play Execution Governor's
|
|
2615
2625
|
* pacing resolver. The Governor owns per-(org, provider) pacing; this only maps
|
|
2616
|
-
*
|
|
2626
|
+
* declared hints into `PacingRule`s. An empty rule list deliberately leaves
|
|
2627
|
+
* the substrate-specific default to the Governor.
|
|
2617
2628
|
*/
|
|
2618
2629
|
function createPacingResolver(
|
|
2619
2630
|
getToolQueueHints: ContextOptions['getToolQueueHints'],
|
|
@@ -2627,10 +2638,7 @@ function createPacingResolver(
|
|
|
2627
2638
|
if (declared) return declared;
|
|
2628
2639
|
const provider = (await getToolProvider?.(toolId))?.trim();
|
|
2629
2640
|
if (!provider) return null;
|
|
2630
|
-
return {
|
|
2631
|
-
...defaultPacingForTool(toolId, resolveExecutionPolicy('cjs_node20')),
|
|
2632
|
-
provider,
|
|
2633
|
-
};
|
|
2641
|
+
return { provider, rules: [] };
|
|
2634
2642
|
};
|
|
2635
2643
|
}
|
|
2636
2644
|
|
|
@@ -2817,6 +2825,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
2817
2825
|
boundaryId: string;
|
|
2818
2826
|
eventKey: string;
|
|
2819
2827
|
timeoutMs: number;
|
|
2828
|
+
eventExpiresAtMs?: number;
|
|
2820
2829
|
messageRef?: { channel: string; ts: string };
|
|
2821
2830
|
}> = [];
|
|
2822
2831
|
private processedRowCount = 0;
|
|
@@ -6726,6 +6735,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6726
6735
|
fieldName: input.request.fieldName,
|
|
6727
6736
|
},
|
|
6728
6737
|
],
|
|
6738
|
+
providerBackpressureMapRows: Boolean(
|
|
6739
|
+
input.request.tableNamespace && input.request.rowKey,
|
|
6740
|
+
),
|
|
6729
6741
|
// Same reason, for the delivery clock: this request really did
|
|
6730
6742
|
// make a provider call, so its row's delivery site has a body
|
|
6731
6743
|
// completion to measure from. A retry re-stamps; the last body is
|
|
@@ -8578,10 +8590,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
8578
8590
|
// before a stateful source query can decide that a sibling action is no
|
|
8579
8591
|
// longer eligible because an earlier action already completed.
|
|
8580
8592
|
const replayMapInvocationId = `${normalizedMapNamespace}:${this.mapInvocationIndex}`;
|
|
8593
|
+
const replayMapFrame = this.checkpoint.mapFrames?.[replayMapInvocationId];
|
|
8594
|
+
const canReadPersistedMapResults = Boolean(
|
|
8595
|
+
this.#options.onMapRowsCompleted &&
|
|
8596
|
+
this.#options.dbSessionStrategy &&
|
|
8597
|
+
this.#options.baseUrl &&
|
|
8598
|
+
this.#options.executorToken &&
|
|
8599
|
+
this.#options.playName &&
|
|
8600
|
+
this.#options.runId,
|
|
8601
|
+
);
|
|
8602
|
+
// A suspended map's admitted pending inputs remain authoritative even
|
|
8603
|
+
// when a stateful authored source returns fewer rows after a wake.
|
|
8581
8604
|
const suspendedInputRows = shouldStreamRuntimeBackedDataset
|
|
8582
8605
|
? null
|
|
8583
|
-
: (
|
|
8584
|
-
|
|
8606
|
+
: (replayMapFrame?.suspendedInputRows ?? null);
|
|
8607
|
+
// Completed siblings require either the Runtime Sheet or a complete
|
|
8608
|
+
// authored replay. Never return only the pending subset as a full result.
|
|
8609
|
+
const replayWithoutSheet = Boolean(
|
|
8610
|
+
suspendedInputRows?.length && !canReadPersistedMapResults,
|
|
8611
|
+
);
|
|
8612
|
+
const resumedPendingRowsOnly = Boolean(
|
|
8613
|
+
suspendedInputRows?.length &&
|
|
8614
|
+
(canReadPersistedMapResults ||
|
|
8615
|
+
(replayMapFrame?.completedRowKeys.length ?? 0) === 0),
|
|
8616
|
+
);
|
|
8617
|
+
const previouslyCompletedRowKeys = resumedPendingRowsOnly
|
|
8618
|
+
? (replayMapFrame?.completedRowKeys ?? [])
|
|
8619
|
+
: [];
|
|
8620
|
+
const previouslyFailedRowsCount = resumedPendingRowsOnly
|
|
8621
|
+
? (replayMapFrame?.failedRowsCount ?? 0)
|
|
8622
|
+
: 0;
|
|
8585
8623
|
if (shouldStreamRuntimeBackedDataset) {
|
|
8586
8624
|
// Net-new admission happens one source page at a time. Its progress and
|
|
8587
8625
|
// returned dataset describe admitted rows, not every provider/CSV row
|
|
@@ -9392,18 +9430,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9392
9430
|
},
|
|
9393
9431
|
});
|
|
9394
9432
|
}
|
|
9395
|
-
const rawMaterializedEntries =
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9433
|
+
const rawMaterializedEntries =
|
|
9434
|
+
resumedPendingRowsOnly && suspendedInputRows
|
|
9435
|
+
? suspendedInputRows.map((row) => ({
|
|
9436
|
+
item: cloneCsvAliasedRow(row.data) as T,
|
|
9437
|
+
inputIndex: row.inputIndex,
|
|
9438
|
+
}))
|
|
9439
|
+
: (
|
|
9440
|
+
await materializePlayDatasetInput(items, {
|
|
9441
|
+
onRow: (row) => {
|
|
9442
|
+
materializationMemoryTracker.track(row);
|
|
9443
|
+
},
|
|
9444
|
+
})
|
|
9445
|
+
).map((item, inputIndex) => ({ item, inputIndex }));
|
|
9407
9446
|
const rawMaterializedItems = rawMaterializedEntries.map(
|
|
9408
9447
|
(entry) => entry.item,
|
|
9409
9448
|
);
|
|
@@ -9434,7 +9473,105 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9434
9473
|
);
|
|
9435
9474
|
}
|
|
9436
9475
|
const materializedItems = dedupedMaterialized.rows;
|
|
9437
|
-
|
|
9476
|
+
const priorAuthoredDigestsByIndex = new Map(
|
|
9477
|
+
(suspendedInputRows ?? []).flatMap((row) =>
|
|
9478
|
+
row.authoredInputDigest
|
|
9479
|
+
? [[row.inputIndex, row.authoredInputDigest] as const]
|
|
9480
|
+
: [],
|
|
9481
|
+
),
|
|
9482
|
+
);
|
|
9483
|
+
const authoredInputDigestsByIndex = new Map<number, string>();
|
|
9484
|
+
if (!canReadPersistedMapResults) {
|
|
9485
|
+
for (const [index, item] of materializedItems.entries()) {
|
|
9486
|
+
const inputIndex = originalInputIndexByItem.get(item) ?? index;
|
|
9487
|
+
const priorDigest = priorAuthoredDigestsByIndex.get(inputIndex);
|
|
9488
|
+
// A legacy pending-only checkpoint contains post-merge row data, not
|
|
9489
|
+
// the original authored input. Do not manufacture a digest from it.
|
|
9490
|
+
if (resumedPendingRowsOnly) {
|
|
9491
|
+
if (priorDigest)
|
|
9492
|
+
authoredInputDigestsByIndex.set(inputIndex, priorDigest);
|
|
9493
|
+
continue;
|
|
9494
|
+
}
|
|
9495
|
+
const authored = Object.fromEntries(
|
|
9496
|
+
Object.entries(
|
|
9497
|
+
toSerializableCsvAliasedRow(
|
|
9498
|
+
this.toOutputRow(item as Record<string, unknown>),
|
|
9499
|
+
),
|
|
9500
|
+
).filter(([field]) => !field.startsWith('__deepline')),
|
|
9501
|
+
);
|
|
9502
|
+
authoredInputDigestsByIndex.set(
|
|
9503
|
+
inputIndex,
|
|
9504
|
+
stableDigest(stableStringify(authored)),
|
|
9505
|
+
);
|
|
9506
|
+
}
|
|
9507
|
+
}
|
|
9508
|
+
if (replayWithoutSheet && !resumedPendingRowsOnly) {
|
|
9509
|
+
// A no-sheet replay can only rebuild completed output by executing the
|
|
9510
|
+
// same admitted input again. The previous failed row's identity/output
|
|
9511
|
+
// is not in the checkpoint, so re-executing it would change its terminal
|
|
9512
|
+
// outcome; require a readable sheet instead of silently reclassifying it.
|
|
9513
|
+
if ((replayMapFrame?.failedRowsCount ?? 0) > 0) {
|
|
9514
|
+
throw new Error(
|
|
9515
|
+
`Cannot reconstruct the complete result for ctx.dataset("${normalizedMapNamespace}") after suspension: ` +
|
|
9516
|
+
'a prior failed row has no readable Runtime Sheet.',
|
|
9517
|
+
);
|
|
9518
|
+
}
|
|
9519
|
+
const authoredRowsByKey = new Map(
|
|
9520
|
+
materializedItems.map((item, index) => {
|
|
9521
|
+
const inputIndex = originalInputIndexByItem.get(item) ?? index;
|
|
9522
|
+
const row = this.toOutputRow(item as Record<string, unknown>);
|
|
9523
|
+
return [
|
|
9524
|
+
rowIdentity(row, inputIndex),
|
|
9525
|
+
this.toPersistedOutputRow(row),
|
|
9526
|
+
] as const;
|
|
9527
|
+
}),
|
|
9528
|
+
);
|
|
9529
|
+
const missingCompleted = replayMapFrame!.completedRowKeys.some(
|
|
9530
|
+
(key) => !authoredRowsByKey.has(key),
|
|
9531
|
+
);
|
|
9532
|
+
const changedPending = suspendedInputRows!.some(
|
|
9533
|
+
({ data, inputIndex, authoredInputDigest }) => {
|
|
9534
|
+
const authored = authoredRowsByKey.get(rowIdentity(data, inputIndex));
|
|
9535
|
+
if (!authored) return true;
|
|
9536
|
+
if (authoredInputDigest) {
|
|
9537
|
+
return (
|
|
9538
|
+
authoredInputDigestsByIndex.get(inputIndex) !==
|
|
9539
|
+
authoredInputDigest
|
|
9540
|
+
);
|
|
9541
|
+
}
|
|
9542
|
+
// A saved waiting row may include output columns restored by
|
|
9543
|
+
// onMapStart. Legacy checkpoints lack the original input digest,
|
|
9544
|
+
// so compare their stable input projection and supplied fields.
|
|
9545
|
+
if (
|
|
9546
|
+
stableStringify(stableInputRow(authored)) !==
|
|
9547
|
+
stableStringify(stableInputRow(data))
|
|
9548
|
+
) {
|
|
9549
|
+
return true;
|
|
9550
|
+
}
|
|
9551
|
+
return Object.entries(authored).some(
|
|
9552
|
+
([field, value]) =>
|
|
9553
|
+
!field.startsWith('__deepline') &&
|
|
9554
|
+
(!(field in data) ||
|
|
9555
|
+
stableStringify(value) !== stableStringify(data[field])),
|
|
9556
|
+
);
|
|
9557
|
+
},
|
|
9558
|
+
);
|
|
9559
|
+
if (
|
|
9560
|
+
missingCompleted ||
|
|
9561
|
+
changedPending ||
|
|
9562
|
+
materializedItems.length !== replayMapFrame!.totalRows
|
|
9563
|
+
) {
|
|
9564
|
+
throw new Error(
|
|
9565
|
+
`Cannot reconstruct the complete result for ctx.dataset("${normalizedMapNamespace}") after suspension: ` +
|
|
9566
|
+
'the authored input changed and this run has no readable Runtime Sheet.',
|
|
9567
|
+
);
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
// A suspended checkpoint retains only waiting inputs, but its original
|
|
9571
|
+
// total and completed keys still describe the full result of this run.
|
|
9572
|
+
totalInputCount = resumedPendingRowsOnly
|
|
9573
|
+
? Math.max(replayMapFrame?.totalRows ?? 0, materializedItems.length)
|
|
9574
|
+
: materializedItems.length;
|
|
9438
9575
|
rawItems = materializedItems.map((item) => this.toOutputRow(item));
|
|
9439
9576
|
itemsToProcess = materializedItems.map((item) =>
|
|
9440
9577
|
this.toOutputRow(item as Record<string, unknown>),
|
|
@@ -9546,7 +9683,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9546
9683
|
rawItems = admittedItems.map((item) => item.row);
|
|
9547
9684
|
itemsToProcess = admittedItems.map((item) => item.row);
|
|
9548
9685
|
itemOriginalIndexes = admittedItems.map((item) => item.index);
|
|
9549
|
-
totalInputCount =
|
|
9686
|
+
totalInputCount = resumedPendingRowsOnly
|
|
9687
|
+
? Math.max(replayMapFrame?.totalRows ?? 0, admittedItems.length)
|
|
9688
|
+
: admittedItems.length;
|
|
9550
9689
|
}
|
|
9551
9690
|
|
|
9552
9691
|
const mapScope = this.createMapExecutionScope({
|
|
@@ -9554,7 +9693,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9554
9693
|
artifactTableNamespace: resolvedTableNamespace,
|
|
9555
9694
|
explicitKey: explicitKeyResolver,
|
|
9556
9695
|
});
|
|
9557
|
-
const completedRowKeys: string[] = [];
|
|
9696
|
+
const completedRowKeys: string[] = [...previouslyCompletedRowKeys];
|
|
9558
9697
|
const pendingRowKeys = itemsToProcess.map((item, index) =>
|
|
9559
9698
|
rowIdentity(this.toOutputRow(item), itemOriginalIndexes[index] ?? index),
|
|
9560
9699
|
);
|
|
@@ -9567,8 +9706,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9567
9706
|
totalRows: totalInputCount,
|
|
9568
9707
|
completedRowKeys,
|
|
9569
9708
|
pendingRowKeys,
|
|
9570
|
-
failedRowsCount:
|
|
9571
|
-
startedAt:
|
|
9709
|
+
failedRowsCount: previouslyFailedRowsCount,
|
|
9710
|
+
startedAt: resumedPendingRowsOnly
|
|
9711
|
+
? (replayMapFrame?.startedAt ?? Date.now())
|
|
9712
|
+
: Date.now(),
|
|
9572
9713
|
updatedAt: Date.now(),
|
|
9573
9714
|
});
|
|
9574
9715
|
this.emitExecutionEvent({
|
|
@@ -9690,7 +9831,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9690
9831
|
options?.description,
|
|
9691
9832
|
{
|
|
9692
9833
|
totalRows: totalInputCount,
|
|
9693
|
-
completedRows:
|
|
9834
|
+
completedRows:
|
|
9835
|
+
duplicateReuseCount + previouslyCompletedRowKeys.length,
|
|
9694
9836
|
},
|
|
9695
9837
|
{
|
|
9696
9838
|
emitTerminalEvent: false,
|
|
@@ -9699,8 +9841,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9699
9841
|
executionRowIndexes: rowsToExecuteEntries.map(
|
|
9700
9842
|
(entry) => entry.originalIndex,
|
|
9701
9843
|
),
|
|
9844
|
+
...(!canReadPersistedMapResults
|
|
9845
|
+
? {
|
|
9846
|
+
authoredInputDigests: rowsToExecuteEntries.map((entry) =>
|
|
9847
|
+
authoredInputDigestsByIndex.get(entry.originalIndex),
|
|
9848
|
+
),
|
|
9849
|
+
}
|
|
9850
|
+
: {}),
|
|
9702
9851
|
retainedRowsMemoryBudgetBytes:
|
|
9703
9852
|
mapMemoryLimits.materializedBudgetBytes,
|
|
9853
|
+
progressFailedOffset: previouslyFailedRowsCount,
|
|
9704
9854
|
incrementalPersistence,
|
|
9705
9855
|
},
|
|
9706
9856
|
);
|
|
@@ -9751,7 +9901,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9751
9901
|
this.toPublicOutputRow(resultsByKey.get(rowKey) ?? rawItem),
|
|
9752
9902
|
];
|
|
9753
9903
|
});
|
|
9754
|
-
const executedCount =
|
|
9904
|
+
const executedCount =
|
|
9905
|
+
rowsToExecute.length +
|
|
9906
|
+
previouslyCompletedRowKeys.length +
|
|
9907
|
+
previouslyFailedRowsCount;
|
|
9755
9908
|
const reusedCount = duplicateReuseCount;
|
|
9756
9909
|
|
|
9757
9910
|
// Persist executed rows to the tenant runtime sheet — the sheet is the
|
|
@@ -9794,9 +9947,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9794
9947
|
|
|
9795
9948
|
const durableCompletedRows = Math.max(
|
|
9796
9949
|
0,
|
|
9797
|
-
|
|
9950
|
+
previouslyCompletedRowKeys.length +
|
|
9951
|
+
reusedCount +
|
|
9952
|
+
visibleCompletedRows.length,
|
|
9798
9953
|
);
|
|
9799
|
-
const durableFailedRows =
|
|
9954
|
+
const durableFailedRows =
|
|
9955
|
+
previouslyFailedRowsCount + visibleFailedRows.length;
|
|
9800
9956
|
const terminalFrame = this.checkpoint.mapFrames?.[mapScope.mapInvocationId];
|
|
9801
9957
|
if (terminalFrame) {
|
|
9802
9958
|
this.setMapFrame({
|
|
@@ -9829,6 +9985,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9829
9985
|
});
|
|
9830
9986
|
|
|
9831
9987
|
if (this.#options.onMapRowsCompleted) {
|
|
9988
|
+
// Output mode selects enriched rows when this replay's earlier
|
|
9989
|
+
// invocation also persisted isolated failures.
|
|
9990
|
+
const finalizationRowMode = resumedPendingRowsOnly ? 'output' : 'all';
|
|
9832
9991
|
const finalizationPageReader = createRuntimeSheetDatasetPageReader({
|
|
9833
9992
|
context: {
|
|
9834
9993
|
baseUrl: this.#options.baseUrl!,
|
|
@@ -9840,15 +9999,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9840
9999
|
},
|
|
9841
10000
|
tableNamespace: resolvedTableNamespace,
|
|
9842
10001
|
runId: this.#options.runId!,
|
|
9843
|
-
rowMode:
|
|
10002
|
+
rowMode: finalizationRowMode,
|
|
9844
10003
|
});
|
|
9845
10004
|
results = await reconcileNodeRuntimeMapResultsWithPersistedSheet({
|
|
9846
10005
|
mapName: normalizedMapNamespace,
|
|
9847
10006
|
tableNamespace: resolvedTableNamespace,
|
|
9848
10007
|
runId: this.#options.runId,
|
|
9849
|
-
expectedRows: Math.max(
|
|
10008
|
+
expectedRows: Math.max(
|
|
10009
|
+
0,
|
|
10010
|
+
totalInputCount -
|
|
10011
|
+
staleCompletionKeys.size -
|
|
10012
|
+
(resumedPendingRowsOnly ? durableFailedRows : 0),
|
|
10013
|
+
),
|
|
9850
10014
|
currentRows: results,
|
|
9851
|
-
failedRowCount: visibleFailedRows.length,
|
|
10015
|
+
failedRowCount: resumedPendingRowsOnly ? 0 : visibleFailedRows.length,
|
|
10016
|
+
rowMode: finalizationRowMode,
|
|
9852
10017
|
log: (line) => this.runtimeLog(line),
|
|
9853
10018
|
readPersistedRows: async (readInput) => {
|
|
9854
10019
|
if (
|
|
@@ -9968,7 +10133,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9968
10133
|
reused: reusedCount,
|
|
9969
10134
|
skipped: reusedCount,
|
|
9970
10135
|
pending: 0,
|
|
9971
|
-
failed:
|
|
10136
|
+
failed: durableFailedRows,
|
|
9972
10137
|
...(duplicateReuseCount > 0
|
|
9973
10138
|
? { duplicates: { exact: duplicateReuseCount } }
|
|
9974
10139
|
: {}),
|
|
@@ -10008,6 +10173,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10008
10173
|
onRowError?: 'isolate' | 'fail';
|
|
10009
10174
|
executionRowKeys?: string[];
|
|
10010
10175
|
executionRowIndexes?: number[];
|
|
10176
|
+
authoredInputDigests?: Array<string | undefined>;
|
|
10011
10177
|
/**
|
|
10012
10178
|
* Opt-in cap on how many row resolvers run their body concurrently. When
|
|
10013
10179
|
* unset, the Governor's rowDefault is used. Excess rows stay pending as
|
|
@@ -10552,6 +10718,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10552
10718
|
);
|
|
10553
10719
|
let nextRowIndex = 0;
|
|
10554
10720
|
let firstRowError: unknown = null;
|
|
10721
|
+
const providerRetryDeadlines: Array<{
|
|
10722
|
+
provider: string;
|
|
10723
|
+
retryAtMs: number;
|
|
10724
|
+
}> = [];
|
|
10725
|
+
let providerBackpressureRowCount = 0;
|
|
10726
|
+
const waitingBaseRows = new Map<number, Record<string, unknown>>();
|
|
10555
10727
|
|
|
10556
10728
|
const runRow = async (
|
|
10557
10729
|
idx: number,
|
|
@@ -10644,7 +10816,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10644
10816
|
} catch (error) {
|
|
10645
10817
|
if (
|
|
10646
10818
|
isPlayRowExecutionSuspendedError(error) ||
|
|
10647
|
-
error instanceof PlayExecutionSuspendedError
|
|
10819
|
+
error instanceof PlayExecutionSuspendedError ||
|
|
10820
|
+
error instanceof PlayRowProviderBackpressureError
|
|
10648
10821
|
) {
|
|
10649
10822
|
throw error;
|
|
10650
10823
|
}
|
|
@@ -10831,6 +11004,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10831
11004
|
return COMPLETED_ROW;
|
|
10832
11005
|
} catch (error) {
|
|
10833
11006
|
if (isPlayRowExecutionSuspendedError(error)) {
|
|
11007
|
+
waitingBaseRows.set(idx, baseRow);
|
|
10834
11008
|
this.pendingRowEventBoundaries.push(error.boundary);
|
|
10835
11009
|
updateMapFrameProgress({
|
|
10836
11010
|
pendingRowKey: rowKey,
|
|
@@ -10852,6 +11026,47 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10852
11026
|
});
|
|
10853
11027
|
return WAITING_ROW;
|
|
10854
11028
|
}
|
|
11029
|
+
if (error instanceof PlayRowProviderBackpressureError) {
|
|
11030
|
+
waitingBaseRows.set(idx, baseRow);
|
|
11031
|
+
// Each row retains its own deadline. Provider names are not rate
|
|
11032
|
+
// scopes: two credentials for one provider can recover separately.
|
|
11033
|
+
providerRetryDeadlines.push({
|
|
11034
|
+
provider: error.provider,
|
|
11035
|
+
retryAtMs: error.retryAtMs,
|
|
11036
|
+
});
|
|
11037
|
+
providerBackpressureRowCount += 1;
|
|
11038
|
+
updateMapFrameProgress({ pendingRowKey: rowKey });
|
|
11039
|
+
const fieldName = activeFieldName;
|
|
11040
|
+
if (!fieldName) {
|
|
11041
|
+
throw new Error(
|
|
11042
|
+
'Provider backpressure escaped a mapped row without an active field.',
|
|
11043
|
+
{ cause: error },
|
|
11044
|
+
);
|
|
11045
|
+
}
|
|
11046
|
+
this.emitScopedFieldMetaUpdate({
|
|
11047
|
+
rowId: idx,
|
|
11048
|
+
key: rowKey,
|
|
11049
|
+
tableNamespace: normalizedTableNamespace,
|
|
11050
|
+
fieldName,
|
|
11051
|
+
status: 'running',
|
|
11052
|
+
rowStatus: 'running',
|
|
11053
|
+
stage: 'waiting_for_provider',
|
|
11054
|
+
provider: error.provider,
|
|
11055
|
+
error: null,
|
|
11056
|
+
dataPatch: {},
|
|
11057
|
+
});
|
|
11058
|
+
this.runtimeLog(
|
|
11059
|
+
`[runtime.provider.row_wait] ${JSON.stringify({
|
|
11060
|
+
provider: error.provider,
|
|
11061
|
+
retry_at: new Date(error.retryAtMs).toISOString(),
|
|
11062
|
+
wait_ms: Math.max(0, error.retryAtMs - Date.now()),
|
|
11063
|
+
row_key_digest: stableDigest(rowKey),
|
|
11064
|
+
field: fieldName,
|
|
11065
|
+
})}`,
|
|
11066
|
+
{ level: 'warn' },
|
|
11067
|
+
);
|
|
11068
|
+
return WAITING_ROW;
|
|
11069
|
+
}
|
|
10855
11070
|
if (isPlayExecutionSuspendedError(error)) {
|
|
10856
11071
|
const completedKeys = new Set(
|
|
10857
11072
|
completedRowsToPersist.map((row) => row.key),
|
|
@@ -10866,6 +11081,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10866
11081
|
data: this.toPersistedOutputRow(
|
|
10867
11082
|
this.toOutputRow(item as Record<string, unknown>),
|
|
10868
11083
|
),
|
|
11084
|
+
...(runtimeOptions?.authoredInputDigests?.[index]
|
|
11085
|
+
? {
|
|
11086
|
+
authoredInputDigest:
|
|
11087
|
+
runtimeOptions.authoredInputDigests[index],
|
|
11088
|
+
}
|
|
11089
|
+
: {}),
|
|
10869
11090
|
},
|
|
10870
11091
|
];
|
|
10871
11092
|
});
|
|
@@ -11089,8 +11310,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11089
11310
|
settled = await Promise.allSettled(workers);
|
|
11090
11311
|
}
|
|
11091
11312
|
} finally {
|
|
11092
|
-
datasetStep.items =
|
|
11093
|
-
this.processedRowCount =
|
|
11313
|
+
datasetStep.items = currentTotalRows();
|
|
11314
|
+
this.processedRowCount = currentTotalRows();
|
|
11094
11315
|
mapStallObserver?.stopHeartbeat();
|
|
11095
11316
|
unsubscribePersistenceLatch();
|
|
11096
11317
|
this.#options.rowAdmissionSignal?.removeEventListener(
|
|
@@ -11166,36 +11387,44 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11166
11387
|
accountedRowIndexes.add(row.inputIndex);
|
|
11167
11388
|
}
|
|
11168
11389
|
}
|
|
11169
|
-
if (
|
|
11170
|
-
|
|
11390
|
+
if (
|
|
11391
|
+
this.pendingRowEventBoundaries.length > 0 ||
|
|
11392
|
+
providerRetryDeadlines.length > 0
|
|
11393
|
+
) {
|
|
11394
|
+
const waitingRowKeys = new Set<string>();
|
|
11395
|
+
const previousSuspendedInputs =
|
|
11396
|
+
this.checkpoint.mapFrames?.[mapScope.mapInvocationId]
|
|
11397
|
+
?.suspendedInputRows;
|
|
11398
|
+
const suspendedInputRows =
|
|
11399
|
+
rowFeed ||
|
|
11400
|
+
(providerRetryDeadlines.length === 0 && !previousSuspendedInputs)
|
|
11401
|
+
? undefined
|
|
11402
|
+
: rowResults.flatMap((result, index) => {
|
|
11403
|
+
if (result !== WAITING_ROW) return [];
|
|
11404
|
+
const baseRow = waitingBaseRows.get(index);
|
|
11405
|
+
if (!baseRow) {
|
|
11406
|
+
throw new Error(
|
|
11407
|
+
`Waiting row ${index} lost its resumable input before map suspension.`,
|
|
11408
|
+
);
|
|
11409
|
+
}
|
|
11410
|
+
return [
|
|
11411
|
+
{
|
|
11412
|
+
inputIndex: executionRowIndex(index),
|
|
11413
|
+
data: this.toPersistedOutputRow(baseRow),
|
|
11414
|
+
...(runtimeOptions?.authoredInputDigests?.[index]
|
|
11415
|
+
? {
|
|
11416
|
+
authoredInputDigest:
|
|
11417
|
+
runtimeOptions.authoredInputDigests[index],
|
|
11418
|
+
}
|
|
11419
|
+
: {}),
|
|
11420
|
+
},
|
|
11421
|
+
];
|
|
11422
|
+
});
|
|
11171
11423
|
for (let index = 0; index < rowResults.length; index += 1) {
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
if (result === WAITING_ROW) {
|
|
11175
|
-
if (key) {
|
|
11176
|
-
completedRowKeys.add(key);
|
|
11177
|
-
}
|
|
11178
|
-
accountedRowIndexes.add(executionRowIndex(index));
|
|
11179
|
-
continue;
|
|
11424
|
+
if (rowResults[index] === WAITING_ROW) {
|
|
11425
|
+
waitingRowKeys.add(keyForIndex(index));
|
|
11180
11426
|
}
|
|
11181
|
-
if (key) completedRowKeys.add(key);
|
|
11182
11427
|
}
|
|
11183
|
-
this.setMapFrame({
|
|
11184
|
-
...(this.checkpoint.mapFrames?.[mapScope.mapInvocationId] ?? {
|
|
11185
|
-
mapInvocationId: mapScope.mapInvocationId,
|
|
11186
|
-
logicalNamespace: mapScope.logicalNamespace,
|
|
11187
|
-
artifactTableNamespace: mapScope.artifactTableNamespace,
|
|
11188
|
-
status: 'suspended' as const,
|
|
11189
|
-
totalRows: currentTotalRows(),
|
|
11190
|
-
completedRowKeys: [...completedRowKeys],
|
|
11191
|
-
pendingRowKeys: items.map((_item, index) => keyForIndex(index)),
|
|
11192
|
-
startedAt: Date.now(),
|
|
11193
|
-
updatedAt: Date.now(),
|
|
11194
|
-
}),
|
|
11195
|
-
status: 'suspended',
|
|
11196
|
-
completedRowKeys: [...completedRowKeys],
|
|
11197
|
-
updatedAt: Date.now(),
|
|
11198
|
-
});
|
|
11199
11428
|
const uniqueBoundaries = [
|
|
11200
11429
|
...new Map(
|
|
11201
11430
|
this.pendingRowEventBoundaries.map((boundary) => [
|
|
@@ -11204,19 +11433,69 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11204
11433
|
]),
|
|
11205
11434
|
).values(),
|
|
11206
11435
|
];
|
|
11436
|
+
const earliestHold = providerRetryDeadlines.reduce<
|
|
11437
|
+
(typeof providerRetryDeadlines)[number] | null
|
|
11438
|
+
>(
|
|
11439
|
+
(earliest, next) =>
|
|
11440
|
+
!earliest || next.retryAtMs < earliest.retryAtMs ? next : earliest,
|
|
11441
|
+
null,
|
|
11442
|
+
);
|
|
11443
|
+
const providerWakeAtMs = earliestHold
|
|
11444
|
+
? Math.max(Date.now() + 1_000, earliestHold.retryAtMs)
|
|
11445
|
+
: null;
|
|
11207
11446
|
const checkpointUpdates = this.activeMapCheckpointUpdatesForTable(
|
|
11208
11447
|
normalizedTableNamespace,
|
|
11209
11448
|
);
|
|
11210
|
-
//
|
|
11211
|
-
//
|
|
11449
|
+
// Both terminal siblings and partial waiting-cell patches must cross
|
|
11450
|
+
// the writer's durability barrier before this map becomes parked.
|
|
11212
11451
|
await incrementalPersistence?.checkpoint(checkpointUpdates);
|
|
11213
11452
|
await incrementalPersistence?.flush();
|
|
11214
11453
|
await this.runtimeStepReceiptCompletionWriter.flush();
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11454
|
+
updateMapFrameProgress({
|
|
11455
|
+
status: 'suspended',
|
|
11456
|
+
activeBoundaryId: null,
|
|
11457
|
+
emitEventType: 'map.suspended',
|
|
11219
11458
|
});
|
|
11459
|
+
const frame = this.checkpoint.mapFrames?.[mapScope.mapInvocationId];
|
|
11460
|
+
if (!frame) {
|
|
11461
|
+
throw new Error('Suspended map lost its durable execution frame.');
|
|
11462
|
+
}
|
|
11463
|
+
this.setMapFrame({
|
|
11464
|
+
...frame,
|
|
11465
|
+
completedRowKeys: frame.completedRowKeys.filter(
|
|
11466
|
+
(key) => !waitingRowKeys.has(key),
|
|
11467
|
+
),
|
|
11468
|
+
pendingRowKeys: rowFeed ? [] : [...waitingRowKeys],
|
|
11469
|
+
suspendedInputRows,
|
|
11470
|
+
activeBoundaryId: null,
|
|
11471
|
+
updatedAt: Date.now(),
|
|
11472
|
+
});
|
|
11473
|
+
if (earliestHold && providerWakeAtMs !== null) {
|
|
11474
|
+
this.runtimeLog(
|
|
11475
|
+
`[runtime.provider.map_wait] ${JSON.stringify({
|
|
11476
|
+
provider: earliestHold.provider,
|
|
11477
|
+
retry_at: new Date(providerWakeAtMs).toISOString(),
|
|
11478
|
+
wait_ms: Math.max(0, providerWakeAtMs - Date.now()),
|
|
11479
|
+
waiting_rows: providerBackpressureRowCount,
|
|
11480
|
+
table_namespace: normalizedTableNamespace,
|
|
11481
|
+
})}`,
|
|
11482
|
+
{ level: 'warn' },
|
|
11483
|
+
);
|
|
11484
|
+
}
|
|
11485
|
+
this.pendingRowEventBoundaries = [];
|
|
11486
|
+
if (uniqueBoundaries.length > 0) {
|
|
11487
|
+
this.suspendDurably({
|
|
11488
|
+
kind: 'integration_event_batch',
|
|
11489
|
+
boundaries: uniqueBoundaries,
|
|
11490
|
+
...(providerWakeAtMs !== null ? { providerWakeAtMs } : {}),
|
|
11491
|
+
});
|
|
11492
|
+
}
|
|
11493
|
+
if (earliestHold && providerWakeAtMs !== null) {
|
|
11494
|
+
await this.waitForProviderBackpressure(
|
|
11495
|
+
earliestHold.provider,
|
|
11496
|
+
providerWakeAtMs,
|
|
11497
|
+
);
|
|
11498
|
+
}
|
|
11220
11499
|
}
|
|
11221
11500
|
for (let index = 0; index < rowResults.length; index += 1) {
|
|
11222
11501
|
if (rowResults[index] !== WAITING_ROW) {
|
|
@@ -13491,6 +13770,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13491
13770
|
throw error;
|
|
13492
13771
|
}
|
|
13493
13772
|
if (suspendAfterTimeoutMs !== undefined) {
|
|
13773
|
+
const finalizedAtMs = existing.completedAt ?? Date.now();
|
|
13774
|
+
if (existing.completedAt === undefined) {
|
|
13775
|
+
this.checkpoint.resolvedBoundaries = {
|
|
13776
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
13777
|
+
[preparedBoundary.boundaryId]: {
|
|
13778
|
+
...existing,
|
|
13779
|
+
completedAt: finalizedAtMs,
|
|
13780
|
+
},
|
|
13781
|
+
};
|
|
13782
|
+
}
|
|
13494
13783
|
this.runtimeLog(
|
|
13495
13784
|
`Integration event ${preparedBoundary.boundaryId}: timeout finalized externally; keeping Play parked`,
|
|
13496
13785
|
);
|
|
@@ -13499,6 +13788,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13499
13788
|
boundaryId: preparedBoundary.boundaryId,
|
|
13500
13789
|
eventKey: preparedBoundary.eventKey,
|
|
13501
13790
|
timeoutMs: suspendAfterTimeoutMs,
|
|
13791
|
+
eventExpiresAtMs: finalizedAtMs + suspendAfterTimeoutMs,
|
|
13792
|
+
postTimeoutPark: true,
|
|
13502
13793
|
...(existing.messageRef ? { messageRef: existing.messageRef } : {}),
|
|
13503
13794
|
});
|
|
13504
13795
|
}
|
|
@@ -13530,6 +13821,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13530
13821
|
boundaryId: preparedBoundary.boundaryId,
|
|
13531
13822
|
eventKey: existing.eventKey,
|
|
13532
13823
|
timeoutMs: existing.timeoutMs,
|
|
13824
|
+
...(existing.eventExpiresAtMs !== undefined
|
|
13825
|
+
? { eventExpiresAtMs: existing.eventExpiresAtMs }
|
|
13826
|
+
: {}),
|
|
13533
13827
|
...(existing.messageRef ? { messageRef: existing.messageRef } : {}),
|
|
13534
13828
|
};
|
|
13535
13829
|
if (rowScope) {
|
|
@@ -13544,6 +13838,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13544
13838
|
boundary: preparedBoundary,
|
|
13545
13839
|
...(force ? { force: true } : {}),
|
|
13546
13840
|
});
|
|
13841
|
+
// An unanswered checkpoint suspends above without re-arming the provider.
|
|
13842
|
+
// New boundaries receive one absolute expiry, persisted for later replays.
|
|
13843
|
+
const eventExpiresAtMs = Date.now() + boundary.timeoutMs;
|
|
13547
13844
|
|
|
13548
13845
|
if (boundary.resolvedOutput) {
|
|
13549
13846
|
this.checkpoint.resolvedBoundaries = {
|
|
@@ -13552,6 +13849,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13552
13849
|
kind: 'integration_event',
|
|
13553
13850
|
eventKey: boundary.eventKey,
|
|
13554
13851
|
timeoutMs: boundary.timeoutMs,
|
|
13852
|
+
eventExpiresAtMs,
|
|
13555
13853
|
provider: boundary.provider,
|
|
13556
13854
|
toolId: boundary.toolId,
|
|
13557
13855
|
output: boundary.resolvedOutput,
|
|
@@ -13597,6 +13895,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13597
13895
|
kind: 'integration_event',
|
|
13598
13896
|
eventKey: boundary.eventKey,
|
|
13599
13897
|
timeoutMs: boundary.timeoutMs,
|
|
13898
|
+
eventExpiresAtMs,
|
|
13600
13899
|
provider: boundary.provider,
|
|
13601
13900
|
toolId: boundary.toolId,
|
|
13602
13901
|
...(boundary.requestKey ? { requestKey: boundary.requestKey } : {}),
|
|
@@ -13626,6 +13925,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13626
13925
|
boundaryId: boundary.boundaryId,
|
|
13627
13926
|
eventKey: boundary.eventKey,
|
|
13628
13927
|
timeoutMs: boundary.timeoutMs,
|
|
13928
|
+
eventExpiresAtMs,
|
|
13629
13929
|
...(boundary.messageRef ? { messageRef: boundary.messageRef } : {}),
|
|
13630
13930
|
});
|
|
13631
13931
|
}
|
|
@@ -13635,6 +13935,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13635
13935
|
boundaryId: boundary.boundaryId,
|
|
13636
13936
|
eventKey: boundary.eventKey,
|
|
13637
13937
|
timeoutMs: boundary.timeoutMs,
|
|
13938
|
+
eventExpiresAtMs,
|
|
13638
13939
|
...(boundary.messageRef ? { messageRef: boundary.messageRef } : {}),
|
|
13639
13940
|
});
|
|
13640
13941
|
}
|
|
@@ -14176,6 +14477,60 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
14176
14477
|
);
|
|
14177
14478
|
}
|
|
14178
14479
|
|
|
14480
|
+
private async waitForProviderBackpressure(
|
|
14481
|
+
provider: string,
|
|
14482
|
+
retryAtMs: number,
|
|
14483
|
+
): Promise<void> {
|
|
14484
|
+
this.assertNoPendingDurableSuspension();
|
|
14485
|
+
const prefix = this.durableBoundaryId(
|
|
14486
|
+
`provider-backpressure-${stableDigest(provider)}-`,
|
|
14487
|
+
);
|
|
14488
|
+
let completedWaits = 0;
|
|
14489
|
+
for (const [boundaryId, value] of Object.entries(
|
|
14490
|
+
this.checkpoint.resolvedBoundaries ?? {},
|
|
14491
|
+
)) {
|
|
14492
|
+
if (!boundaryId.startsWith(prefix)) continue;
|
|
14493
|
+
const boundary = assertKnownResolvedBoundary(boundaryId, value);
|
|
14494
|
+
if (boundary?.kind !== 'sleep') {
|
|
14495
|
+
throw new Error(
|
|
14496
|
+
`Provider backpressure boundary ${boundaryId} is not a sleep.`,
|
|
14497
|
+
);
|
|
14498
|
+
}
|
|
14499
|
+
if (boundary.completedAt !== undefined) completedWaits += 1;
|
|
14500
|
+
}
|
|
14501
|
+
// Count completed waits, not this process's call order. A parked Play is
|
|
14502
|
+
// re-executed by a new sandbox with fresh in-memory counters, and another
|
|
14503
|
+
// provider quota window must not reuse the previous completed sleep.
|
|
14504
|
+
const boundaryId = `${prefix}${completedWaits}`;
|
|
14505
|
+
const existing = assertKnownResolvedBoundary(
|
|
14506
|
+
boundaryId,
|
|
14507
|
+
this.checkpoint.resolvedBoundaries?.[boundaryId],
|
|
14508
|
+
);
|
|
14509
|
+
if (existing?.kind === 'sleep' && existing.completedAt !== undefined) {
|
|
14510
|
+
throw new Error(
|
|
14511
|
+
`Provider backpressure boundary ${boundaryId} was already completed.`,
|
|
14512
|
+
);
|
|
14513
|
+
}
|
|
14514
|
+
const delayMs =
|
|
14515
|
+
existing?.kind === 'sleep'
|
|
14516
|
+
? existing.delayMs
|
|
14517
|
+
: Math.max(1_000, Math.ceil(retryAtMs - Date.now()));
|
|
14518
|
+
this.checkpoint.resolvedBoundaries = {
|
|
14519
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
14520
|
+
[boundaryId]: { kind: 'sleep', delayMs },
|
|
14521
|
+
};
|
|
14522
|
+
this.runtimeLog(
|
|
14523
|
+
`[runtime.provider.wait] ${JSON.stringify({
|
|
14524
|
+
provider,
|
|
14525
|
+
retry_at: new Date(Date.now() + delayMs).toISOString(),
|
|
14526
|
+
wait_ms: delayMs,
|
|
14527
|
+
boundary_id: boundaryId,
|
|
14528
|
+
})}`,
|
|
14529
|
+
{ level: 'warn' },
|
|
14530
|
+
);
|
|
14531
|
+
this.suspendDurably({ kind: 'sleep', boundaryId, delayMs });
|
|
14532
|
+
}
|
|
14533
|
+
|
|
14179
14534
|
async sleep(ms: number): Promise<void> {
|
|
14180
14535
|
if (!rowContext.getStore()) this.assertRowAdmissionOpen();
|
|
14181
14536
|
this.assertInlineChildContract('suspending_child');
|
|
@@ -15834,6 +16189,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
15834
16189
|
rowKey: member.rowKey,
|
|
15835
16190
|
fieldName: member.fieldName,
|
|
15836
16191
|
})),
|
|
16192
|
+
providerBackpressureMapRows: detail.members.some(
|
|
16193
|
+
(member) =>
|
|
16194
|
+
Boolean(member.tableNamespace && member.rowKey),
|
|
16195
|
+
),
|
|
15837
16196
|
nativeBatchBodyAttribution: bodyAttribution,
|
|
15838
16197
|
playNodeScope: playNodeScopeForToolCallRequest(
|
|
15839
16198
|
detail.members[0]!,
|
|
@@ -16109,7 +16468,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16109
16468
|
// provider. Keep infrastructure and contract failures loud.
|
|
16110
16469
|
if (
|
|
16111
16470
|
isProviderExhaustedFailure(entry.error) ||
|
|
16112
|
-
isProviderUnavailable(entry.error)
|
|
16471
|
+
isProviderUnavailable(entry.error) ||
|
|
16472
|
+
entry.error instanceof PlayRowProviderBackpressureError
|
|
16113
16473
|
) {
|
|
16114
16474
|
continue;
|
|
16115
16475
|
}
|
|
@@ -16295,6 +16655,50 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16295
16655
|
): Promise<void> => {
|
|
16296
16656
|
const followers = liveFollowersByOwnerCallId.get(owner.callId) ?? [];
|
|
16297
16657
|
liveFollowersByOwnerCallId.delete(owner.callId);
|
|
16658
|
+
if (error instanceof PlayRowProviderBackpressureError) {
|
|
16659
|
+
let releaseFailure: unknown = null;
|
|
16660
|
+
const receiptKey = owner.receiptKey?.trim() || null;
|
|
16661
|
+
try {
|
|
16662
|
+
if (receiptKey && this.durableMappedToolResultsBackedByReceipts) {
|
|
16663
|
+
const leaseId = owner.receiptLeaseId?.trim();
|
|
16664
|
+
if (!leaseId) {
|
|
16665
|
+
throw new Error(
|
|
16666
|
+
`Provider wait cannot release receipt ${receiptKey} without its owned lease.`,
|
|
16667
|
+
);
|
|
16668
|
+
}
|
|
16669
|
+
const released = await this.releaseRuntimeStepReceipt(
|
|
16670
|
+
receiptKey,
|
|
16671
|
+
this.currentReceiptOwnerRunId,
|
|
16672
|
+
leaseId,
|
|
16673
|
+
);
|
|
16674
|
+
if (
|
|
16675
|
+
!released ||
|
|
16676
|
+
(released.status !== 'queued' &&
|
|
16677
|
+
released.status !== 'pending')
|
|
16678
|
+
) {
|
|
16679
|
+
throw new Error(
|
|
16680
|
+
`Provider wait could not release owned receipt ${receiptKey}.`,
|
|
16681
|
+
);
|
|
16682
|
+
}
|
|
16683
|
+
}
|
|
16684
|
+
} catch (failure) {
|
|
16685
|
+
releaseFailure = failure;
|
|
16686
|
+
}
|
|
16687
|
+
// A provider deadline is a nonterminal control signal. The row
|
|
16688
|
+
// catch records waiting metadata after the resolver unwinds; no
|
|
16689
|
+
// follower may see or persist a failed receipt in between. If the
|
|
16690
|
+
// lease could not be released, wake every follower with that loud
|
|
16691
|
+
// infrastructure failure rather than stranding their promises.
|
|
16692
|
+
for (const request of [owner, ...followers]) {
|
|
16693
|
+
const resolver = this.toolCallResolvers.get(request.callId);
|
|
16694
|
+
if (resolver) {
|
|
16695
|
+
resolver.reject(releaseFailure ?? error);
|
|
16696
|
+
this.toolCallResolvers.delete(request.callId);
|
|
16697
|
+
}
|
|
16698
|
+
}
|
|
16699
|
+
if (releaseFailure) throw releaseFailure;
|
|
16700
|
+
return;
|
|
16701
|
+
}
|
|
16298
16702
|
const shouldPersistReceiptFailure =
|
|
16299
16703
|
!(error instanceof ToolExecuteAuthScopeChangedError) &&
|
|
16300
16704
|
!this.persistenceLatch.tripped;
|
|
@@ -17363,9 +17767,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
17363
17767
|
onChunkComplete: async (chunkResults) => {
|
|
17364
17768
|
for (const entry of chunkResults) {
|
|
17365
17769
|
if (entry.error !== undefined) {
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17770
|
+
const settlements = await Promise.allSettled(
|
|
17771
|
+
entry.request.memberRequests.map((request) =>
|
|
17772
|
+
rejectWithLiveFollowers(request, entry.error),
|
|
17773
|
+
),
|
|
17774
|
+
);
|
|
17775
|
+
const rejected = settlements.find(
|
|
17776
|
+
(settlement): settlement is PromiseRejectedResult =>
|
|
17777
|
+
settlement.status === 'rejected',
|
|
17778
|
+
);
|
|
17779
|
+
if (rejected) throw rejected.reason;
|
|
17369
17780
|
continue;
|
|
17370
17781
|
}
|
|
17371
17782
|
const batchExecution = entry.result;
|
|
@@ -17543,11 +17954,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
17543
17954
|
const failedEntries = dispatchResults.filter(
|
|
17544
17955
|
(entry) => entry.error !== undefined,
|
|
17545
17956
|
);
|
|
17546
|
-
await Promise.allSettled(
|
|
17957
|
+
const failedSettlements = await Promise.allSettled(
|
|
17547
17958
|
failedEntries.map((entry) =>
|
|
17548
17959
|
rejectWithLiveFollowers(entry.job, entry.error),
|
|
17549
17960
|
),
|
|
17550
17961
|
);
|
|
17962
|
+
const failedRejection = failedSettlements.find(
|
|
17963
|
+
(settlement): settlement is PromiseRejectedResult =>
|
|
17964
|
+
settlement.status === 'rejected',
|
|
17965
|
+
);
|
|
17966
|
+
if (failedRejection) throw failedRejection.reason;
|
|
17551
17967
|
recordToolStep(pendingRequests);
|
|
17552
17968
|
this.#options.onBatchComplete?.(this.checkpoint);
|
|
17553
17969
|
}
|
|
@@ -18500,6 +18916,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18500
18916
|
|
|
18501
18917
|
let providerConcurrencyGrant: ProviderConcurrencyGrant | null = null;
|
|
18502
18918
|
let providerPacingGrant: ProviderPacingGrant | null = null;
|
|
18919
|
+
let providerGatewayDispatchProof: NonNullable<
|
|
18920
|
+
RuntimeProviderPermit['gatewayDispatchProof']
|
|
18921
|
+
> | null = null;
|
|
18503
18922
|
// The permit returns the canonical pacing bucket. Keep it alongside
|
|
18504
18923
|
// the tool-keyed dispatch identity so terminal attribution can join
|
|
18505
18924
|
// provider admission with the response residence it admitted.
|
|
@@ -18509,9 +18928,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18509
18928
|
const permit = await this.resourceGovernor.acquireProviderPermit({
|
|
18510
18929
|
toolId,
|
|
18511
18930
|
signal,
|
|
18931
|
+
yieldProviderHolds: this.#options.durableBoundaries === true,
|
|
18512
18932
|
});
|
|
18513
18933
|
providerConcurrencyGrant = permit.concurrencyGrant ?? null;
|
|
18514
18934
|
providerPacingGrant = permit.pacingGrant ?? null;
|
|
18935
|
+
providerGatewayDispatchProof = permit.gatewayDispatchProof ?? null;
|
|
18515
18936
|
providerTelemetryKey = permit.provider;
|
|
18516
18937
|
return permit;
|
|
18517
18938
|
};
|
|
@@ -18529,6 +18950,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18529
18950
|
providerConcurrencyGrant = null;
|
|
18530
18951
|
providerPacingGrant = null;
|
|
18531
18952
|
let response: Response | null = null;
|
|
18953
|
+
let providerRetryAtMs: number | undefined;
|
|
18532
18954
|
let responseData: Record<string, unknown> | null = null;
|
|
18533
18955
|
let responseErrorText: string | null = null;
|
|
18534
18956
|
let providerCallStartedAt: number | null = null;
|
|
@@ -18647,6 +19069,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18647
19069
|
if (!permit) return null;
|
|
18648
19070
|
providerConcurrencyGrant = permit.concurrencyGrant ?? null;
|
|
18649
19071
|
providerPacingGrant = permit.pacingGrant ?? null;
|
|
19072
|
+
providerGatewayDispatchProof =
|
|
19073
|
+
permit.gatewayDispatchProof ?? null;
|
|
18650
19074
|
providerTelemetryKey = permit.provider;
|
|
18651
19075
|
return permit;
|
|
18652
19076
|
},
|
|
@@ -18674,6 +19098,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18674
19098
|
providerResourceKey: `tool:${toolId}`,
|
|
18675
19099
|
signal: abortController?.signal,
|
|
18676
19100
|
requiresTransportPermit: !fixtureExecution,
|
|
19101
|
+
deferProviderPermitValidation: true,
|
|
18677
19102
|
onTransportAdmitted: (waitMs) => {
|
|
18678
19103
|
transportSlotWaitMs = waitMs;
|
|
18679
19104
|
},
|
|
@@ -18702,117 +19127,151 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18702
19127
|
};
|
|
18703
19128
|
const releaseProviderExecution = () => {
|
|
18704
19129
|
const execution = providerExecution.current;
|
|
19130
|
+
providerExecution.current = null;
|
|
18705
19131
|
if (execution) execution.release();
|
|
18706
19132
|
};
|
|
18707
|
-
|
|
18708
|
-
|
|
18709
|
-
|
|
18710
|
-
|
|
18711
|
-
|
|
18712
|
-
|
|
18713
|
-
|
|
18714
|
-
|
|
18715
|
-
|
|
18716
|
-
|
|
18717
|
-
|
|
18718
|
-
|
|
18719
|
-
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
|
|
18726
|
-
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
)
|
|
18739
|
-
|
|
18740
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
|
|
19133
|
+
try {
|
|
19134
|
+
return await request(
|
|
19135
|
+
async (beforeProviderDispatch) => {
|
|
19136
|
+
// Fixture delay models provider residence. The shared
|
|
19137
|
+
// permit may still need re-admission afterwards, so
|
|
19138
|
+
// pause this budget around that queueing boundary.
|
|
19139
|
+
let fixtureTimeoutUsedMs = 0;
|
|
19140
|
+
const armProviderTimeout = () => {
|
|
19141
|
+
const controller = abortController;
|
|
19142
|
+
if (
|
|
19143
|
+
!timeoutMs ||
|
|
19144
|
+
!controller ||
|
|
19145
|
+
controller.signal.aborted
|
|
19146
|
+
)
|
|
19147
|
+
return;
|
|
19148
|
+
timeoutHandle = setTimeout(
|
|
19149
|
+
() => {
|
|
19150
|
+
controller.abort(
|
|
19151
|
+
new Error(
|
|
19152
|
+
`Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
|
|
19153
|
+
),
|
|
19154
|
+
);
|
|
19155
|
+
},
|
|
19156
|
+
Math.max(1, timeoutMs - fixtureTimeoutUsedMs),
|
|
19157
|
+
);
|
|
19158
|
+
};
|
|
19159
|
+
const fixtureBehavior = this.#options.fixtureBehavior;
|
|
19160
|
+
if (
|
|
19161
|
+
fixtureExecution &&
|
|
19162
|
+
fixtureBehavior &&
|
|
19163
|
+
shouldRouteFixtureToolId(toolId)
|
|
19164
|
+
) {
|
|
19165
|
+
const canonicalProvider =
|
|
19166
|
+
(
|
|
19167
|
+
await this.#options.getToolProvider?.(toolId)
|
|
19168
|
+
)?.trim() || provider;
|
|
19169
|
+
if (shouldRouteFixtureProvider(canonicalProvider)) {
|
|
19170
|
+
const canonicalOperation =
|
|
19171
|
+
(
|
|
19172
|
+
await this.#options.getToolOperation?.(toolId)
|
|
19173
|
+
)?.trim() || toolId;
|
|
19174
|
+
// A fixture schedule needs a stable logical-call
|
|
19175
|
+
// identity, not a cache receipt. Ordinary /execute
|
|
19176
|
+
// calls deliberately do not create durable receipts;
|
|
19177
|
+
// deeplineRequestId remains stable across physical
|
|
19178
|
+
// transport retries.
|
|
19179
|
+
const fixtureRequestIdentity =
|
|
19180
|
+
durableCallReceiptKey ?? deeplineRequestId;
|
|
19181
|
+
const stableRequestKey = `${canonicalProvider}:${canonicalOperation}:${fixtureRequestIdentity}`;
|
|
19182
|
+
const behaviorDigest = stableDigest(
|
|
19183
|
+
JSON.stringify(fixtureBehavior),
|
|
19184
|
+
);
|
|
19185
|
+
const requestKeyDigest =
|
|
19186
|
+
stableDigest(stableRequestKey);
|
|
19187
|
+
let selected: {
|
|
19188
|
+
delayMs: number;
|
|
19189
|
+
sampleIndex: number;
|
|
19190
|
+
} | null = null;
|
|
19191
|
+
beginProviderExecution();
|
|
19192
|
+
const delayStartedAt = Date.now();
|
|
19193
|
+
providerCallStartedAt = delayStartedAt;
|
|
19194
|
+
armProviderTimeout();
|
|
19195
|
+
try {
|
|
19196
|
+
selected = await waitForFixtureResponseDelay({
|
|
19197
|
+
behavior: fixtureBehavior,
|
|
19198
|
+
stableRequestKey,
|
|
19199
|
+
signal: abortController?.signal,
|
|
19200
|
+
onSelected: (value) => {
|
|
19201
|
+
selected = value;
|
|
19202
|
+
this.runtimeLog(
|
|
19203
|
+
`[fixture.response_delay] ${JSON.stringify(
|
|
19204
|
+
{
|
|
19205
|
+
outcome: 'scheduled',
|
|
19206
|
+
provider: canonicalProvider,
|
|
19207
|
+
operation: canonicalOperation,
|
|
19208
|
+
behavior_digest: `sha256_${behaviorDigest}`,
|
|
19209
|
+
request_key_digest: `sha256_${requestKeyDigest}`,
|
|
19210
|
+
delay_ms: value.delayMs,
|
|
19211
|
+
sample_index: value.sampleIndex,
|
|
19212
|
+
physical_attempt: physicalAttempt,
|
|
19213
|
+
},
|
|
19214
|
+
)}`,
|
|
19215
|
+
);
|
|
19216
|
+
},
|
|
19217
|
+
});
|
|
19218
|
+
} catch (error) {
|
|
19219
|
+
this.runtimeLog(
|
|
19220
|
+
`[fixture.response_delay] ${JSON.stringify({
|
|
19221
|
+
outcome: 'aborted',
|
|
19222
|
+
provider: canonicalProvider,
|
|
19223
|
+
operation: canonicalOperation,
|
|
19224
|
+
behavior_digest: `sha256_${behaviorDigest}`,
|
|
19225
|
+
request_key_digest: `sha256_${requestKeyDigest}`,
|
|
19226
|
+
delay_ms: selected?.delayMs ?? null,
|
|
19227
|
+
sample_index: selected?.sampleIndex ?? null,
|
|
19228
|
+
physical_attempt: physicalAttempt,
|
|
19229
|
+
elapsed_ms: Date.now() - delayStartedAt,
|
|
19230
|
+
})}`,
|
|
19231
|
+
);
|
|
19232
|
+
releaseProviderExecution();
|
|
19233
|
+
throw error;
|
|
19234
|
+
}
|
|
19235
|
+
fixtureDelayMs = Date.now() - delayStartedAt;
|
|
19236
|
+
fixtureTimeoutUsedMs = fixtureDelayMs;
|
|
18765
19237
|
this.runtimeLog(
|
|
18766
19238
|
`[fixture.response_delay] ${JSON.stringify({
|
|
18767
|
-
outcome: '
|
|
19239
|
+
outcome: 'completed',
|
|
18768
19240
|
provider: canonicalProvider,
|
|
18769
19241
|
operation: canonicalOperation,
|
|
18770
19242
|
behavior_digest: `sha256_${behaviorDigest}`,
|
|
18771
19243
|
request_key_digest: `sha256_${requestKeyDigest}`,
|
|
18772
|
-
delay_ms:
|
|
18773
|
-
sample_index:
|
|
19244
|
+
delay_ms: selected.delayMs,
|
|
19245
|
+
sample_index: selected.sampleIndex,
|
|
18774
19246
|
physical_attempt: physicalAttempt,
|
|
19247
|
+
elapsed_ms: Date.now() - delayStartedAt,
|
|
18775
19248
|
})}`,
|
|
18776
19249
|
);
|
|
18777
|
-
}
|
|
18778
|
-
}
|
|
18779
|
-
|
|
18780
|
-
|
|
18781
|
-
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
behavior_digest: `sha256_${behaviorDigest}`,
|
|
18786
|
-
request_key_digest: `sha256_${requestKeyDigest}`,
|
|
18787
|
-
delay_ms: selected?.delayMs ?? null,
|
|
18788
|
-
sample_index: selected?.sampleIndex ?? null,
|
|
18789
|
-
physical_attempt: physicalAttempt,
|
|
18790
|
-
elapsed_ms: Date.now() - delayStartedAt,
|
|
18791
|
-
})}`,
|
|
19250
|
+
}
|
|
19251
|
+
}
|
|
19252
|
+
if (timeoutHandle) {
|
|
19253
|
+
clearTimeout(timeoutHandle);
|
|
19254
|
+
timeoutHandle = null;
|
|
19255
|
+
}
|
|
19256
|
+
const dispatchBoundary = beforeProviderDispatch(
|
|
19257
|
+
releaseProviderExecution,
|
|
18792
19258
|
);
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
provider
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
);
|
|
18810
|
-
}
|
|
18811
|
-
}
|
|
18812
|
-
beginProviderExecution();
|
|
18813
|
-
try {
|
|
18814
|
-
return await request(
|
|
18815
|
-
async () => {
|
|
19259
|
+
const permitReacquired =
|
|
19260
|
+
typeof dispatchBoundary === 'boolean'
|
|
19261
|
+
? dispatchBoundary
|
|
19262
|
+
: await dispatchBoundary;
|
|
19263
|
+
if (permitReacquired) {
|
|
19264
|
+
// A shared Retry-After may have arrived while
|
|
19265
|
+
// fixture/request preparation was suspended. The
|
|
19266
|
+
// old residence was not provider work; restart the
|
|
19267
|
+
// latency clock with the fresh permit.
|
|
19268
|
+
providerCallStartedAt = Date.now();
|
|
19269
|
+
}
|
|
19270
|
+
// A stale permit can take arbitrarily long to
|
|
19271
|
+
// replace. Only fixture residence and the physical
|
|
19272
|
+
// HTTP exchange spend the tool execution deadline.
|
|
19273
|
+
armProviderTimeout();
|
|
19274
|
+
beginProviderExecution();
|
|
18816
19275
|
const fetchedAt = Date.now();
|
|
18817
19276
|
const dispatchedProviderCallStartedAt =
|
|
18818
19277
|
providerCallStartedAt ?? fetchedAt;
|
|
@@ -18842,6 +19301,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18842
19301
|
// settle the row as failed instead of pacing.
|
|
18843
19302
|
[TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER]:
|
|
18844
19303
|
TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
|
|
19304
|
+
...(providerGatewayDispatchProof
|
|
19305
|
+
? {
|
|
19306
|
+
[TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER]:
|
|
19307
|
+
providerGatewayDispatchProof.rateScopeToken,
|
|
19308
|
+
[TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER]:
|
|
19309
|
+
String(
|
|
19310
|
+
providerGatewayDispatchProof.retryAtMsAtGrant,
|
|
19311
|
+
),
|
|
19312
|
+
}
|
|
19313
|
+
: {}),
|
|
18845
19314
|
'x-deepline-request-id': deeplineRequestId,
|
|
18846
19315
|
...(providerExecutionFenceKey
|
|
18847
19316
|
? {
|
|
@@ -18868,6 +19337,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18868
19337
|
if (dispatchedResponse.status === 429) {
|
|
18869
19338
|
providerRequestsInFlightAt429 =
|
|
18870
19339
|
activeProviderRequest?.inFlightCount() ?? null;
|
|
19340
|
+
const observedAt = Date.now();
|
|
19341
|
+
const retryAfter =
|
|
19342
|
+
dispatchedResponse.headers.get('retry-after');
|
|
19343
|
+
// Without a provider deadline the classifier owns
|
|
19344
|
+
// the conservative bare-429 cooldown. Do not replace
|
|
19345
|
+
// it with the generic one-second HTTP retry fallback.
|
|
19346
|
+
providerRetryAtMs = retryAfter?.trim()
|
|
19347
|
+
? observedAt +
|
|
19348
|
+
parseToolExecuteRetryAfterMs(
|
|
19349
|
+
retryAfter,
|
|
19350
|
+
observedAt,
|
|
19351
|
+
)
|
|
19352
|
+
: undefined;
|
|
18871
19353
|
}
|
|
18872
19354
|
const dispatchedFetchToHeadersMs =
|
|
18873
19355
|
Date.now() - fetchedAt;
|
|
@@ -19056,6 +19538,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19056
19538
|
? null
|
|
19057
19539
|
: Date.now() - providerCallStartedAt,
|
|
19058
19540
|
providerConcurrencyGrant,
|
|
19541
|
+
providerRetryAtMs,
|
|
19059
19542
|
);
|
|
19060
19543
|
providerBackpressureReported = true;
|
|
19061
19544
|
} else if (
|
|
@@ -19076,6 +19559,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19076
19559
|
? null
|
|
19077
19560
|
: Date.now() - providerCallStartedAt,
|
|
19078
19561
|
providerConcurrencyGrant,
|
|
19562
|
+
providerRetryAtMs,
|
|
19079
19563
|
);
|
|
19080
19564
|
providerBackpressureReported = true;
|
|
19081
19565
|
}
|
|
@@ -19144,6 +19628,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19144
19628
|
}
|
|
19145
19629
|
if (error instanceof ProviderExhaustedError) {
|
|
19146
19630
|
const retryAtMs = Date.parse(error.retryAt);
|
|
19631
|
+
if (!this.#options.durableBoundaries) {
|
|
19632
|
+
await this.sleepWithCheckpointHeartbeat(
|
|
19633
|
+
Math.max(
|
|
19634
|
+
1_000,
|
|
19635
|
+
Number.isFinite(retryAtMs)
|
|
19636
|
+
? Math.ceil(retryAtMs - Date.now())
|
|
19637
|
+
: 1_000,
|
|
19638
|
+
),
|
|
19639
|
+
);
|
|
19640
|
+
continue;
|
|
19641
|
+
}
|
|
19642
|
+
if (this.#options.durableBoundaries) {
|
|
19643
|
+
const rowScope = rowContext.getStore();
|
|
19644
|
+
if (options?.providerBackpressureMapRows || rowScope) {
|
|
19645
|
+
throw new PlayRowProviderBackpressureError(
|
|
19646
|
+
error.provider,
|
|
19647
|
+
Number.isFinite(retryAtMs) ? retryAtMs : Date.now(),
|
|
19648
|
+
);
|
|
19649
|
+
}
|
|
19650
|
+
// This is a provider admission hold, not a row outcome.
|
|
19651
|
+
// Release the sandbox and resume the same Play after the
|
|
19652
|
+
// shared pacer's deadline. The receipt executor releases
|
|
19653
|
+
// this call's claim on suspension, so completed work remains
|
|
19654
|
+
// reusable and this tool can re-admit safely.
|
|
19655
|
+
await this.waitForProviderBackpressure(
|
|
19656
|
+
error.provider,
|
|
19657
|
+
Number.isFinite(retryAtMs) ? retryAtMs : Date.now(),
|
|
19658
|
+
);
|
|
19659
|
+
continue;
|
|
19660
|
+
}
|
|
19147
19661
|
const providerFailure =
|
|
19148
19662
|
error.cause instanceof ToolExecutionError
|
|
19149
19663
|
? error.cause
|
|
@@ -19411,22 +19925,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19411
19925
|
failure.backpressureDelayMs,
|
|
19412
19926
|
providerCallElapsedMs,
|
|
19413
19927
|
providerConcurrencyGrant,
|
|
19928
|
+
providerRetryAtMs,
|
|
19414
19929
|
);
|
|
19415
19930
|
providerBackpressureReported = true;
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19931
|
+
}
|
|
19932
|
+
// The normal 429 path reports its hold before releasing the
|
|
19933
|
+
// provider lease. A mapped row must now yield even for a short
|
|
19934
|
+
// hold: sleeping here would occupy its row slot and prevent
|
|
19935
|
+
// unrelated rows from making progress.
|
|
19936
|
+
const durableProviderWaitMs =
|
|
19937
|
+
failure.providerPacingWaitMs ?? failure.backpressureDelayMs;
|
|
19938
|
+
if (
|
|
19939
|
+
this.#options.durableBoundaries &&
|
|
19940
|
+
durableProviderWaitMs !== null &&
|
|
19941
|
+
durableProviderWaitMs > 0
|
|
19942
|
+
) {
|
|
19943
|
+
const retryAtMs =
|
|
19944
|
+
(failure.providerPacingWaitMs !== null
|
|
19945
|
+
? Date.now() + failure.providerPacingWaitMs
|
|
19946
|
+
: providerRetryAtMs) ?? Date.now() + durableProviderWaitMs;
|
|
19947
|
+
const rowScope = rowContext.getStore();
|
|
19421
19948
|
if (
|
|
19422
|
-
|
|
19949
|
+
this.#options.durableBoundaries &&
|
|
19950
|
+
(options?.providerBackpressureMapRows || rowScope)
|
|
19423
19951
|
) {
|
|
19424
|
-
throw new
|
|
19952
|
+
throw new PlayRowProviderBackpressureError(
|
|
19425
19953
|
provider,
|
|
19426
|
-
retryAtMs
|
|
19427
|
-
|
|
19428
|
-
});
|
|
19954
|
+
retryAtMs,
|
|
19955
|
+
);
|
|
19429
19956
|
}
|
|
19957
|
+
await this.waitForProviderBackpressure(provider, retryAtMs);
|
|
19958
|
+
throw new ProviderExhaustedError({
|
|
19959
|
+
provider,
|
|
19960
|
+
retryAtMs,
|
|
19961
|
+
cause: surfacedError,
|
|
19962
|
+
});
|
|
19430
19963
|
}
|
|
19431
19964
|
// A Deepline concurrency shed is issued by our own gateway/relay
|
|
19432
19965
|
// before it opens any provider connection and before any durable
|
|
@@ -19438,18 +19971,25 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19438
19971
|
// 5xx retries safe — otherwise a protective limit on the relay
|
|
19439
19972
|
// turns straight into customer row failures.
|
|
19440
19973
|
const shedKnownNotDispatched =
|
|
19441
|
-
failure.reason === 'concurrency_backpressure'
|
|
19974
|
+
failure.reason === 'concurrency_backpressure' ||
|
|
19975
|
+
failure.reason === 'provider_pacing_wait';
|
|
19442
19976
|
if (
|
|
19443
19977
|
failure.shouldRetry &&
|
|
19444
19978
|
(failure.knownNotDispatched ||
|
|
19445
19979
|
failure.reason === 'gateway_invocation_in_progress' ||
|
|
19446
19980
|
ambiguousTransportReplayAllowed ||
|
|
19447
19981
|
retrySafeTransientHttp ||
|
|
19448
|
-
fixtureReplaySafe
|
|
19982
|
+
fixtureReplaySafe ||
|
|
19983
|
+
// An explicit provider-owned 429 is a rejected operation,
|
|
19984
|
+
// independently of whether generic 5xx replay is safe.
|
|
19985
|
+
// The gateway attests the prior 429 before accepting a
|
|
19986
|
+
// fresh fenced invocation attempt; never extend this to
|
|
19987
|
+
// platform/caller 429s or ambiguous transport failures.
|
|
19988
|
+
failure.backpressureDelayMs !== null)
|
|
19449
19989
|
) {
|
|
19450
19990
|
// A gateway follower is retrying its owner's durable
|
|
19451
|
-
// invocation, while a concurrency
|
|
19452
|
-
// all. Both
|
|
19991
|
+
// invocation, while a concurrency or provider-pacing refusal
|
|
19992
|
+
// wrote no invocation at all. Both reuse this logical attempt: advancing it
|
|
19453
19993
|
// either bypasses the live-owner replay fence or makes the
|
|
19454
19994
|
// prior-attempt sweep see an intentionally absent receipt.
|
|
19455
19995
|
if (
|
|
@@ -19466,9 +20006,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19466
20006
|
if (failure.chargeRetryBudget) {
|
|
19467
20007
|
await this.governor.chargeBudget('retry');
|
|
19468
20008
|
}
|
|
19469
|
-
const retryAttributePrefix =
|
|
19470
|
-
|
|
19471
|
-
|
|
20009
|
+
const retryAttributePrefix =
|
|
20010
|
+
failure.reason === 'provider_pacing_wait'
|
|
20011
|
+
? 'provider_pacing_wait'
|
|
20012
|
+
: failure.isRateLimit
|
|
20013
|
+
? 'rate_limit'
|
|
20014
|
+
: 'transient_http';
|
|
19472
20015
|
span.setAttribute(
|
|
19473
20016
|
`plays.${retryAttributePrefix}_retry_after_ms`,
|
|
19474
20017
|
failure.retryDelayMs,
|
|
@@ -19499,7 +20042,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19499
20042
|
reason: failure.isRateLimit
|
|
19500
20043
|
? 'rate_limit'
|
|
19501
20044
|
: 'provider_error',
|
|
19502
|
-
retryAt:
|
|
20045
|
+
retryAt:
|
|
20046
|
+
failure.providerPacingWaitMs !== null
|
|
20047
|
+
? Date.now() + failure.providerPacingWaitMs
|
|
20048
|
+
: Date.now() + failure.retryDelayMs,
|
|
19503
20049
|
attempt: httpFailureAttempt,
|
|
19504
20050
|
},
|
|
19505
20051
|
observedAt: Date.now(),
|
|
@@ -19614,6 +20160,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19614
20160
|
retryAfterMs: number,
|
|
19615
20161
|
latencyMs: number | null = null,
|
|
19616
20162
|
concurrencyGrant: ProviderConcurrencyGrant | null = null,
|
|
20163
|
+
retryAtMs = Date.now() + Math.max(0, retryAfterMs),
|
|
19617
20164
|
): Promise<void> {
|
|
19618
20165
|
const hints: readonly PlayQueueHint[] = this.#options.getToolQueueHints
|
|
19619
20166
|
? await this.#options.getToolQueueHints(toolId)
|
|
@@ -19623,9 +20170,20 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19623
20170
|
provider,
|
|
19624
20171
|
toolId,
|
|
19625
20172
|
retryAfterMs,
|
|
20173
|
+
retryAtMs,
|
|
19626
20174
|
latencyMs,
|
|
19627
20175
|
concurrencyGrant,
|
|
19628
20176
|
});
|
|
20177
|
+
this.runtimeLog(
|
|
20178
|
+
`[runtime.provider.backpressure_report] ${JSON.stringify({
|
|
20179
|
+
toolId,
|
|
20180
|
+
provider,
|
|
20181
|
+
retryAfterMs,
|
|
20182
|
+
retryAtMs,
|
|
20183
|
+
outcome: 'recorded',
|
|
20184
|
+
})}`,
|
|
20185
|
+
{ level: 'warn' },
|
|
20186
|
+
);
|
|
19629
20187
|
}
|
|
19630
20188
|
|
|
19631
20189
|
private async sleepWithCheckpointHeartbeat(ms: number): Promise<void> {
|