deepline 0.1.211 → 0.1.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +150 -11
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/customer-console.ts +23 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +1 -1
- package/dist/bundling-sources/sdk/src/client.ts +118 -3
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/types.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +253 -30
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +22 -20
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
- package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -5
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +21 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +128 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +37 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +63 -7
- package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
- package/dist/cli/index.js +637 -229
- package/dist/cli/index.mjs +637 -229
- package/dist/index.d.mts +76 -6
- package/dist/index.d.ts +76 -6
- package/dist/index.js +225 -43
- package/dist/index.mjs +225 -43
- package/dist/plays/bundle-play-file.mjs +65 -4
- package/package.json +1 -1
|
@@ -66,6 +66,22 @@ export type RuntimeStatusUpdate = {
|
|
|
66
66
|
* positional cursor stay correct once the buffer has rotated.
|
|
67
67
|
*/
|
|
68
68
|
liveLogTotalCount?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Monotonic scheduler attempt that owns the positional log channel. A new
|
|
71
|
+
* attempt starts at offset zero without colliding with earlier attempts.
|
|
72
|
+
*/
|
|
73
|
+
liveLogProducerAttempt?: number;
|
|
74
|
+
datasetLifecycleEvents?: Array<{
|
|
75
|
+
datasetId: string;
|
|
76
|
+
path: string;
|
|
77
|
+
tableNamespace: string;
|
|
78
|
+
phase: 'registered' | 'available' | 'failed';
|
|
79
|
+
persistedRows?: number;
|
|
80
|
+
succeededRows?: number;
|
|
81
|
+
failedRows?: number;
|
|
82
|
+
complete?: boolean;
|
|
83
|
+
at: number;
|
|
84
|
+
}>;
|
|
69
85
|
/**
|
|
70
86
|
* Explicit terminal-output replay for final runner logs when the caller must
|
|
71
87
|
* keep `status` nonterminal. Direct-worker/Postgres progress finalizers do this so
|
|
@@ -75,6 +91,8 @@ export type RuntimeStatusUpdate = {
|
|
|
75
91
|
terminalLogReplay?: {
|
|
76
92
|
lines: string[];
|
|
77
93
|
totalCount?: number;
|
|
94
|
+
dedupeMode?: 'semantic' | 'exact';
|
|
95
|
+
producerAttempt?: number;
|
|
78
96
|
} | null;
|
|
79
97
|
liveTimeline?: PlayRunTimelineEntry[];
|
|
80
98
|
liveNodeProgress?: PlayVisualNodeProgressMap;
|
|
@@ -347,12 +365,84 @@ function applyRetryJitter(delayMs: number): number {
|
|
|
347
365
|
const APP_RUNTIME_API_DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
348
366
|
const RUN_STATUS_LEDGER_SNAPSHOT_CACHE_LIMIT = 1_000;
|
|
349
367
|
const runStatusLedgerSnapshots = new Map<string, PlayRunLedgerSnapshot>();
|
|
350
|
-
// Positional log cursor per run: count of liveLogs lines already
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// positionally (exactly-once on the channel, repeated identical lines kept).
|
|
368
|
+
// Positional log cursor per run attempt: count of liveLogs lines already
|
|
369
|
+
// forwarded from this process. A fresh process re-sends from offset 0; durable
|
|
370
|
+
// ingestion skips overlap within that same attempt.
|
|
354
371
|
const runLogChannelSentCounts = new Map<string, number>();
|
|
355
372
|
const runStatusUpdateChains = new Map<string, Promise<void>>();
|
|
373
|
+
const APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES = 128 * 1024;
|
|
374
|
+
const APP_RUNTIME_EVENT_BATCH_MAX_JSON_BYTES = 512 * 1024;
|
|
375
|
+
const appRuntimeTextEncoder = new TextEncoder();
|
|
376
|
+
|
|
377
|
+
function splitRunEventForAppRuntime(
|
|
378
|
+
event: PlayRunLedgerEvent,
|
|
379
|
+
): PlayRunLedgerEvent[] {
|
|
380
|
+
if (event.type !== 'log.appended' || event.lines.length === 0) return [event];
|
|
381
|
+
const split: PlayRunLedgerEvent[] = [];
|
|
382
|
+
let index = 0;
|
|
383
|
+
while (index < event.lines.length) {
|
|
384
|
+
const start = index;
|
|
385
|
+
let bytes = 2;
|
|
386
|
+
while (index < event.lines.length) {
|
|
387
|
+
const lineBytes = appRuntimeTextEncoder.encode(
|
|
388
|
+
JSON.stringify(event.lines[index]!),
|
|
389
|
+
).length;
|
|
390
|
+
if (
|
|
391
|
+
index > start &&
|
|
392
|
+
bytes + lineBytes + 1 > APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES
|
|
393
|
+
) {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
bytes += lineBytes + 1;
|
|
397
|
+
index += 1;
|
|
398
|
+
}
|
|
399
|
+
split.push({
|
|
400
|
+
...event,
|
|
401
|
+
lines: event.lines.slice(start, index),
|
|
402
|
+
...(typeof event.channelOffset === 'number'
|
|
403
|
+
? { channelOffset: event.channelOffset + start }
|
|
404
|
+
: {}),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return split;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function partitionRunEventsForAppRuntime(
|
|
411
|
+
events: readonly PlayRunLedgerEvent[],
|
|
412
|
+
): PlayRunLedgerEvent[][] {
|
|
413
|
+
const batches: PlayRunLedgerEvent[][] = [];
|
|
414
|
+
let current: PlayRunLedgerEvent[] = [];
|
|
415
|
+
let currentBytes = 2;
|
|
416
|
+
for (const event of events.flatMap(splitRunEventForAppRuntime)) {
|
|
417
|
+
const eventBytes = appRuntimeTextEncoder.encode(
|
|
418
|
+
JSON.stringify(event),
|
|
419
|
+
).length;
|
|
420
|
+
if (
|
|
421
|
+
current.length > 0 &&
|
|
422
|
+
currentBytes + eventBytes + 1 > APP_RUNTIME_EVENT_BATCH_MAX_JSON_BYTES
|
|
423
|
+
) {
|
|
424
|
+
batches.push(current);
|
|
425
|
+
current = [];
|
|
426
|
+
currentBytes = 2;
|
|
427
|
+
}
|
|
428
|
+
current.push(event);
|
|
429
|
+
currentBytes += eventBytes + 1;
|
|
430
|
+
}
|
|
431
|
+
if (current.length > 0) batches.push(current);
|
|
432
|
+
return batches;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function appendRunEventBatchesViaAppRuntime(
|
|
436
|
+
context: WorkerRuntimeApiContext,
|
|
437
|
+
input: { playId: string; events: readonly PlayRunLedgerEvent[] },
|
|
438
|
+
): Promise<void> {
|
|
439
|
+
for (const events of partitionRunEventsForAppRuntime(input.events)) {
|
|
440
|
+
await appendRunEventsViaAppRuntime(context, {
|
|
441
|
+
playId: input.playId,
|
|
442
|
+
events,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
356
446
|
|
|
357
447
|
function isTerminalRuntimeStatus(status: string): boolean {
|
|
358
448
|
return (
|
|
@@ -364,6 +454,10 @@ function isTerminalRuntimeStatus(status: string): boolean {
|
|
|
364
454
|
);
|
|
365
455
|
}
|
|
366
456
|
|
|
457
|
+
function runLogChannelKey(runId: string, producerAttempt?: number): string {
|
|
458
|
+
return `${runId}:${producerAttempt ?? 'legacy'}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
367
461
|
function rememberRunStatusLedgerSnapshot(
|
|
368
462
|
runId: string,
|
|
369
463
|
snapshot: PlayRunLedgerSnapshot,
|
|
@@ -376,7 +470,11 @@ function rememberRunStatusLedgerSnapshot(
|
|
|
376
470
|
const oldestRunId = runStatusLedgerSnapshots.keys().next().value;
|
|
377
471
|
if (!oldestRunId) break;
|
|
378
472
|
runStatusLedgerSnapshots.delete(oldestRunId);
|
|
379
|
-
runLogChannelSentCounts.
|
|
473
|
+
for (const key of runLogChannelSentCounts.keys()) {
|
|
474
|
+
if (key.startsWith(`${oldestRunId}:`)) {
|
|
475
|
+
runLogChannelSentCounts.delete(key);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
380
478
|
runStatusUpdateChains.delete(oldestRunId);
|
|
381
479
|
}
|
|
382
480
|
}
|
|
@@ -399,12 +497,16 @@ function isRetryableAppRuntimeResponse(input: {
|
|
|
399
497
|
if (input.status === 500 && isTransientAppRuntimeFailureBody(input.body)) {
|
|
400
498
|
return true;
|
|
401
499
|
}
|
|
500
|
+
return isRetryableAppRuntimeResponseStatus(input.status);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function isRetryableAppRuntimeResponseStatus(status: number): boolean {
|
|
402
504
|
return (
|
|
403
|
-
|
|
404
|
-
(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
505
|
+
status === 429 ||
|
|
506
|
+
(status >= 520 && status <= 524) ||
|
|
507
|
+
status === 502 ||
|
|
508
|
+
status === 503 ||
|
|
509
|
+
status === 504
|
|
408
510
|
);
|
|
409
511
|
}
|
|
410
512
|
|
|
@@ -601,6 +703,38 @@ function sleep(ms: number): Promise<void> {
|
|
|
601
703
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
602
704
|
}
|
|
603
705
|
|
|
706
|
+
async function retryAppRuntimeBodyTimeoutOrThrow(input: {
|
|
707
|
+
action: RuntimeApiRequest['action'];
|
|
708
|
+
attempt: number;
|
|
709
|
+
maxAttempts: number;
|
|
710
|
+
error: unknown;
|
|
711
|
+
retryAfterMs?: number | null;
|
|
712
|
+
}): Promise<void> {
|
|
713
|
+
if (!isRequestTimeoutAbort(input.error)) {
|
|
714
|
+
throw input.error;
|
|
715
|
+
}
|
|
716
|
+
if (
|
|
717
|
+
input.attempt < input.maxAttempts &&
|
|
718
|
+
isRetryableAppRuntimeFetchError({
|
|
719
|
+
action: input.action,
|
|
720
|
+
error: input.error,
|
|
721
|
+
})
|
|
722
|
+
) {
|
|
723
|
+
await sleep(
|
|
724
|
+
Math.max(
|
|
725
|
+
applyRetryJitter(appRuntimeRetryDelayMs(input.action, input.attempt)),
|
|
726
|
+
input.retryAfterMs ?? 0,
|
|
727
|
+
),
|
|
728
|
+
);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
throw new AppRuntimeApiTransportError({
|
|
732
|
+
action: input.action,
|
|
733
|
+
attempts: input.attempt,
|
|
734
|
+
cause: input.error,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
604
738
|
export class AppRuntimeApiTransportError extends Error {
|
|
605
739
|
readonly action: RuntimeApiRequest['action'];
|
|
606
740
|
readonly attempts: number;
|
|
@@ -691,9 +825,40 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
691
825
|
});
|
|
692
826
|
}
|
|
693
827
|
if (response.ok) {
|
|
694
|
-
|
|
828
|
+
try {
|
|
829
|
+
return (await response.json()) as TResponse;
|
|
830
|
+
} catch (error) {
|
|
831
|
+
await retryAppRuntimeBodyTimeoutOrThrow({
|
|
832
|
+
action: body.action,
|
|
833
|
+
attempt,
|
|
834
|
+
maxAttempts,
|
|
835
|
+
error,
|
|
836
|
+
});
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
let responseText: string;
|
|
841
|
+
try {
|
|
842
|
+
responseText = await response.text();
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (
|
|
845
|
+
isRequestTimeoutAbort(error) &&
|
|
846
|
+
!isRetryableAppRuntimeResponseStatus(response.status)
|
|
847
|
+
) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`App runtime API ${body.action} failed with status ${response.status}: response body timed out`,
|
|
850
|
+
{ cause: error },
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
await retryAppRuntimeBodyTimeoutOrThrow({
|
|
854
|
+
action: body.action,
|
|
855
|
+
attempt,
|
|
856
|
+
maxAttempts,
|
|
857
|
+
error,
|
|
858
|
+
retryAfterMs: appRuntimeRetryAfterMs(response, ''),
|
|
859
|
+
});
|
|
860
|
+
continue;
|
|
695
861
|
}
|
|
696
|
-
const responseText = await response.text();
|
|
697
862
|
if (
|
|
698
863
|
attempt < maxAttempts &&
|
|
699
864
|
isRetryableAppRuntimeResponse({
|
|
@@ -917,7 +1082,9 @@ async function updateRunStatusViaAppRuntimeUnlocked(
|
|
|
917
1082
|
// durable stream can recover any progress flush that was lost in transit.
|
|
918
1083
|
const sentCount = isTerminalRuntimeStatus(update.status)
|
|
919
1084
|
? 0
|
|
920
|
-
: (runLogChannelSentCounts.get(
|
|
1085
|
+
: (runLogChannelSentCounts.get(
|
|
1086
|
+
runLogChannelKey(update.playId, update.liveLogProducerAttempt),
|
|
1087
|
+
) ?? 0);
|
|
921
1088
|
const logSlice = Array.isArray(update.liveLogs)
|
|
922
1089
|
? slicePositionalLogLines({
|
|
923
1090
|
bufferLines: update.liveLogs,
|
|
@@ -932,6 +1099,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
|
|
|
932
1099
|
? {
|
|
933
1100
|
lines: update.liveLogs,
|
|
934
1101
|
totalCount: update.liveLogTotalCount,
|
|
1102
|
+
producerAttempt: update.liveLogProducerAttempt,
|
|
935
1103
|
}
|
|
936
1104
|
: null;
|
|
937
1105
|
const statusPatchLogSlice = terminalLogReplay ? null : logSlice;
|
|
@@ -944,6 +1112,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
|
|
|
944
1112
|
lastCheckpointAt: update.lastCheckpointAt ?? null,
|
|
945
1113
|
liveLogs: statusPatchLogSlice?.lines ?? null,
|
|
946
1114
|
liveLogsChannelOffset: statusPatchLogSlice?.channelOffset ?? null,
|
|
1115
|
+
liveLogsProducerAttempt: update.liveLogProducerAttempt ?? null,
|
|
947
1116
|
liveNodeProgress: update.liveNodeProgress ?? null,
|
|
948
1117
|
result: update.result,
|
|
949
1118
|
},
|
|
@@ -951,33 +1120,87 @@ async function updateRunStatusViaAppRuntimeUnlocked(
|
|
|
951
1120
|
now: update.lastCheckpointAt ?? Date.now(),
|
|
952
1121
|
source: 'worker',
|
|
953
1122
|
});
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1123
|
+
for (const event of update.datasetLifecycleEvents ?? []) {
|
|
1124
|
+
const current = previousSnapshot.datasetsById[event.datasetId];
|
|
1125
|
+
const changesSnapshot =
|
|
1126
|
+
!current ||
|
|
1127
|
+
current.path !== event.path ||
|
|
1128
|
+
current.tableNamespace !== event.tableNamespace ||
|
|
1129
|
+
current.phase !== event.phase ||
|
|
1130
|
+
(event.persistedRows ?? 0) > current.persistedRows ||
|
|
1131
|
+
(event.succeededRows ?? 0) > current.succeededRows ||
|
|
1132
|
+
(event.failedRows ?? 0) > current.failedRows ||
|
|
1133
|
+
(event.complete === true && current.complete !== true);
|
|
1134
|
+
if (!changesSnapshot) continue;
|
|
1135
|
+
events.push({
|
|
1136
|
+
type: 'dataset.lifecycle',
|
|
1137
|
+
runId: update.playId,
|
|
1138
|
+
source: 'worker',
|
|
1139
|
+
occurredAt: event.at,
|
|
1140
|
+
datasetId: event.datasetId,
|
|
1141
|
+
path: event.path,
|
|
1142
|
+
tableNamespace: event.tableNamespace,
|
|
1143
|
+
phase: event.phase,
|
|
1144
|
+
...(event.persistedRows === undefined
|
|
1145
|
+
? {}
|
|
1146
|
+
: { persistedRows: event.persistedRows }),
|
|
1147
|
+
...(event.succeededRows === undefined
|
|
1148
|
+
? {}
|
|
1149
|
+
: { succeededRows: event.succeededRows }),
|
|
1150
|
+
...(event.failedRows === undefined
|
|
1151
|
+
? {}
|
|
1152
|
+
: { failedRows: event.failedRows }),
|
|
1153
|
+
...(event.complete === undefined ? {} : { complete: event.complete }),
|
|
1154
|
+
});
|
|
964
1155
|
}
|
|
965
|
-
|
|
1156
|
+
const terminalLogEvents =
|
|
1157
|
+
terminalLogReplay && terminalLogReplay.lines.length > 0
|
|
1158
|
+
? buildTerminalLogReplayEvents({
|
|
1159
|
+
runId: update.playId,
|
|
1160
|
+
source: 'worker',
|
|
1161
|
+
occurredAt: update.lastCheckpointAt ?? Date.now(),
|
|
1162
|
+
lines: terminalLogReplay.lines,
|
|
1163
|
+
liveLogTotalCount: terminalLogReplay.totalCount,
|
|
1164
|
+
dedupeMode: terminalLogReplay.dedupeMode,
|
|
1165
|
+
producerAttempt: terminalLogReplay.producerAttempt,
|
|
1166
|
+
})
|
|
1167
|
+
: [];
|
|
1168
|
+
if (events.length === 0 && terminalLogEvents.length === 0) {
|
|
966
1169
|
return;
|
|
967
1170
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1171
|
+
const combinedEvents = [...events, ...terminalLogEvents];
|
|
1172
|
+
if (partitionRunEventsForAppRuntime(combinedEvents).length === 1) {
|
|
1173
|
+
// Preserve the single-mutation fast path for ordinary bounded updates.
|
|
1174
|
+
await appendRunEventsViaAppRuntime(context, {
|
|
1175
|
+
playId: update.playId,
|
|
1176
|
+
events: combinedEvents,
|
|
1177
|
+
});
|
|
1178
|
+
} else {
|
|
1179
|
+
// Persist terminal lifecycle first so an oversized or interrupted
|
|
1180
|
+
// best-effort log heal can never prevent settlement. Each bounded log
|
|
1181
|
+
// batch is a separate mutation; retries are safe because ingestion owns
|
|
1182
|
+
// positional reconciliation.
|
|
1183
|
+
await appendRunEventBatchesViaAppRuntime(context, {
|
|
1184
|
+
playId: update.playId,
|
|
1185
|
+
events,
|
|
1186
|
+
});
|
|
1187
|
+
await appendRunEventBatchesViaAppRuntime(context, {
|
|
1188
|
+
playId: update.playId,
|
|
1189
|
+
events: terminalLogEvents,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
972
1192
|
if (statusPatchLogSlice) {
|
|
973
1193
|
runLogChannelSentCounts.set(
|
|
974
|
-
update.playId,
|
|
1194
|
+
runLogChannelKey(update.playId, update.liveLogProducerAttempt),
|
|
975
1195
|
statusPatchLogSlice.channelOffset + statusPatchLogSlice.lines.length,
|
|
976
1196
|
);
|
|
977
1197
|
}
|
|
978
1198
|
rememberRunStatusLedgerSnapshot(
|
|
979
1199
|
update.playId,
|
|
980
|
-
reducePlayRunLedgerEvents(previousSnapshot,
|
|
1200
|
+
reducePlayRunLedgerEvents(previousSnapshot, [
|
|
1201
|
+
...events,
|
|
1202
|
+
...terminalLogEvents,
|
|
1203
|
+
]),
|
|
981
1204
|
);
|
|
982
1205
|
}
|
|
983
1206
|
|
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
parseToolExecuteAuthScopeChangedError,
|
|
107
107
|
ToolExecuteAuthScopeChangedError,
|
|
108
108
|
} from './tool-execute-retry-policy';
|
|
109
|
+
import { ToolHttpError } from './tool-http-errors';
|
|
109
110
|
import {
|
|
110
111
|
buildDurableCtxCallCacheKey,
|
|
111
112
|
buildDurableRunPlayInvocationScope,
|
|
@@ -3486,7 +3487,8 @@ export class PlayContextImpl {
|
|
|
3486
3487
|
);
|
|
3487
3488
|
}
|
|
3488
3489
|
const finalWrapped =
|
|
3489
|
-
completed?.status === 'completed' || completed?.status === 'skipped'
|
|
3490
|
+
(completed?.status === 'completed' || completed?.status === 'skipped') &&
|
|
3491
|
+
completed.output !== undefined
|
|
3490
3492
|
? await this.wrapToolExecutionResult({
|
|
3491
3493
|
toolId,
|
|
3492
3494
|
status:
|
|
@@ -6319,11 +6321,6 @@ export class PlayContextImpl {
|
|
|
6319
6321
|
semanticKey: toolRequestIdentity,
|
|
6320
6322
|
force: toolCachePolicy.force,
|
|
6321
6323
|
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
6322
|
-
markSkipped: () => {
|
|
6323
|
-
this.log(
|
|
6324
|
-
`ctx.tools.execute(${toolId}): no-op due completed receipt ${toolRequestIdentity} (label: ${normalizedKey})`,
|
|
6325
|
-
);
|
|
6326
|
-
},
|
|
6327
6324
|
onClaimedResult: (output, receiptKey) =>
|
|
6328
6325
|
markToolExecuteResultExecutionOutcome(output, {
|
|
6329
6326
|
kind: 'live',
|
|
@@ -7277,11 +7274,6 @@ export class PlayContextImpl {
|
|
|
7277
7274
|
egressSlot.release();
|
|
7278
7275
|
}
|
|
7279
7276
|
},
|
|
7280
|
-
markSkipped: (output) => {
|
|
7281
|
-
this.log(
|
|
7282
|
-
`ctx.fetch(${output?.url ?? ''}): no-op due completed receipt`,
|
|
7283
|
-
);
|
|
7284
|
-
},
|
|
7285
7277
|
},
|
|
7286
7278
|
);
|
|
7287
7279
|
}
|
|
@@ -7348,7 +7340,6 @@ export class PlayContextImpl {
|
|
|
7348
7340
|
: options?.semanticKey,
|
|
7349
7341
|
staleAfterSeconds: options?.staleAfterSeconds,
|
|
7350
7342
|
markSkipped: (output) => {
|
|
7351
|
-
this.log(`ctx.step(${normalizedKey}): no-op due completed receipt`);
|
|
7352
7343
|
assertJsonSerializableStepOutput(normalizedKey, output);
|
|
7353
7344
|
},
|
|
7354
7345
|
execute: executeStep,
|
|
@@ -8500,6 +8491,8 @@ export class PlayContextImpl {
|
|
|
8500
8491
|
|
|
8501
8492
|
while (true) {
|
|
8502
8493
|
let response: Response;
|
|
8494
|
+
let providerCallStartedAt: number | null = null;
|
|
8495
|
+
let providerCallElapsedMs: number | null = null;
|
|
8503
8496
|
const durableCallReceiptKey =
|
|
8504
8497
|
options?.durableCallReceiptKey?.trim() || null;
|
|
8505
8498
|
const executionAuthScopeDigest =
|
|
@@ -8533,7 +8526,6 @@ export class PlayContextImpl {
|
|
|
8533
8526
|
}, timeoutMs)
|
|
8534
8527
|
: null;
|
|
8535
8528
|
try {
|
|
8536
|
-
const providerCallStartedAt = Date.now();
|
|
8537
8529
|
const ownershipStartedAt = Date.now();
|
|
8538
8530
|
await options?.beforeProviderCall?.();
|
|
8539
8531
|
if (runtimeReceiptReadTraceEnabled) {
|
|
@@ -8541,6 +8533,7 @@ export class PlayContextImpl {
|
|
|
8541
8533
|
`[perf] tool call id=${toolId} phase=before_provider_ownership elapsed_ms=${Date.now() - ownershipStartedAt}`,
|
|
8542
8534
|
);
|
|
8543
8535
|
}
|
|
8536
|
+
providerCallStartedAt = Date.now();
|
|
8544
8537
|
const heartbeatIntervalMs = hasReceiptHeartbeat
|
|
8545
8538
|
? runtimeLeaseHeartbeatIntervalFromExpiry({
|
|
8546
8539
|
leaseExpiresAt: options?.receiptLeaseExpiresAt,
|
|
@@ -8640,10 +8633,11 @@ export class PlayContextImpl {
|
|
|
8640
8633
|
`[perf] tool call id=${toolId} phase=integration_fetch_headers elapsed_ms=${Date.now() - integrationFetchStartedAt} status=${response.status}`,
|
|
8641
8634
|
);
|
|
8642
8635
|
}
|
|
8636
|
+
providerCallElapsedMs = Date.now() - providerCallStartedAt;
|
|
8643
8637
|
this.resourceGovernor.observe({
|
|
8644
8638
|
toolId,
|
|
8645
8639
|
providerResourceKey: `tool:${toolId}`,
|
|
8646
|
-
providerLatencyMs:
|
|
8640
|
+
providerLatencyMs: providerCallElapsedMs,
|
|
8647
8641
|
providerSuccess: response.ok,
|
|
8648
8642
|
provider429: response.status === 429,
|
|
8649
8643
|
});
|
|
@@ -8654,16 +8648,18 @@ export class PlayContextImpl {
|
|
|
8654
8648
|
if (heartbeatFailure) {
|
|
8655
8649
|
throw heartbeatFailure;
|
|
8656
8650
|
}
|
|
8657
|
-
|
|
8658
|
-
|
|
8651
|
+
const transportError = abortController?.signal.aborted
|
|
8652
|
+
? abortController.signal.reason instanceof Error
|
|
8659
8653
|
? abortController.signal.reason
|
|
8660
8654
|
: new Error(
|
|
8661
8655
|
`Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
|
|
8662
|
-
)
|
|
8663
|
-
|
|
8656
|
+
)
|
|
8657
|
+
: error;
|
|
8664
8658
|
transportAttempt += 1;
|
|
8665
8659
|
const message =
|
|
8666
|
-
|
|
8660
|
+
transportError instanceof Error
|
|
8661
|
+
? transportError.message
|
|
8662
|
+
: String(transportError);
|
|
8667
8663
|
if (transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS) {
|
|
8668
8664
|
await this.governor.chargeBudget('retry');
|
|
8669
8665
|
const retryAfterMs =
|
|
@@ -8679,8 +8675,11 @@ export class PlayContextImpl {
|
|
|
8679
8675
|
await this.sleepWithCheckpointHeartbeat(retryAfterMs);
|
|
8680
8676
|
continue;
|
|
8681
8677
|
}
|
|
8682
|
-
throw new
|
|
8678
|
+
throw new ToolHttpError(
|
|
8683
8679
|
`Tool ${toolId} transport failed calling ${url} after ${transportAttempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS} attempts: ${message}`,
|
|
8680
|
+
null,
|
|
8681
|
+
0,
|
|
8682
|
+
'repairable',
|
|
8684
8683
|
);
|
|
8685
8684
|
} finally {
|
|
8686
8685
|
if (timeoutHandle) {
|
|
@@ -8711,6 +8710,9 @@ export class PlayContextImpl {
|
|
|
8711
8710
|
bodyText: text,
|
|
8712
8711
|
retryAfterHeader: response.headers.get('retry-after'),
|
|
8713
8712
|
transientHttpRetrySafe: retrySafeTransientHttp,
|
|
8713
|
+
...(providerCallElapsedMs !== null
|
|
8714
|
+
? { providerLatencyMs: providerCallElapsedMs }
|
|
8715
|
+
: {}),
|
|
8714
8716
|
});
|
|
8715
8717
|
if (failure.backpressureDelayMs !== null) {
|
|
8716
8718
|
// Feed the server-observed Retry-After back into the shared
|
|
@@ -380,6 +380,18 @@ export interface MapExecutionScope {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
export type PlayExecutionEvent =
|
|
383
|
+
| {
|
|
384
|
+
type: 'dataset.lifecycle';
|
|
385
|
+
datasetId: string;
|
|
386
|
+
path: string;
|
|
387
|
+
tableNamespace: string;
|
|
388
|
+
phase: 'registered' | 'available' | 'failed';
|
|
389
|
+
persistedRows?: number;
|
|
390
|
+
succeededRows?: number;
|
|
391
|
+
failedRows?: number;
|
|
392
|
+
complete?: boolean;
|
|
393
|
+
at: number;
|
|
394
|
+
}
|
|
383
395
|
| {
|
|
384
396
|
type: 'map.preparing';
|
|
385
397
|
mapInvocationId: string;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
normalizePlayNameForSheet,
|
|
3
|
+
normalizeTableNamespace,
|
|
4
|
+
sha256Hex,
|
|
5
|
+
} from '../plays/row-identity';
|
|
2
6
|
|
|
3
7
|
export function createRuntimeDatasetId(
|
|
4
8
|
playName: string,
|
|
5
9
|
tableNamespace: string,
|
|
6
10
|
): string {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const normalizedPlayName = normalizePlayNameForSheet(playName);
|
|
12
|
+
const normalizedTableNamespace = normalizeTableNamespace(tableNamespace);
|
|
13
|
+
return sha256Hex(
|
|
14
|
+
`${normalizedPlayName}:${normalizedTableNamespace}`,
|
|
15
|
+
).slice(0, 40);
|
|
10
16
|
}
|
|
@@ -56,6 +56,7 @@ export type CreateDbSessionRequest = {
|
|
|
56
56
|
|
|
57
57
|
export type CreateDbSessionResponse = {
|
|
58
58
|
sessionId: string;
|
|
59
|
+
executionRole?: string;
|
|
59
60
|
postgresUrl?: string;
|
|
60
61
|
encryptedPostgresUrl?: EncryptedPostgresUrl;
|
|
61
62
|
postgres?: {
|
|
@@ -398,6 +399,14 @@ export const createDbSessionResponseSchema =
|
|
|
398
399
|
}
|
|
399
400
|
return {
|
|
400
401
|
sessionId: parseString(value.sessionId, 'sessionId'),
|
|
402
|
+
...(value.executionRole !== undefined
|
|
403
|
+
? {
|
|
404
|
+
executionRole: parseOptionalString(
|
|
405
|
+
value.executionRole,
|
|
406
|
+
'executionRole',
|
|
407
|
+
),
|
|
408
|
+
}
|
|
409
|
+
: {}),
|
|
401
410
|
...(value.postgresUrl !== undefined
|
|
402
411
|
? { postgresUrl: parseOptionalString(value.postgresUrl, 'postgresUrl') }
|
|
403
412
|
: {}),
|
|
@@ -412,9 +412,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
412
412
|
receipt: RuntimeStepReceipt,
|
|
413
413
|
source: DurableReceiptRecoverySource = 'cache',
|
|
414
414
|
): Promise<T> => {
|
|
415
|
-
input.log(
|
|
416
|
-
`ctx.${input.operation}(${input.id}): recovered result from receipt`,
|
|
417
|
-
);
|
|
415
|
+
input.log(`ctx.${input.operation}(${input.id}): reused completed work`);
|
|
418
416
|
if (receipt.output === undefined) {
|
|
419
417
|
return receipt.output as T;
|
|
420
418
|
}
|
|
@@ -447,7 +445,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
447
445
|
}
|
|
448
446
|
if (reclaimed?.status === 'failed') {
|
|
449
447
|
throw new Error(
|
|
450
|
-
`ctx.${input.operation}(${input.id}):
|
|
448
|
+
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${reclaimed.error ?? 'unknown error'}.`,
|
|
451
449
|
);
|
|
452
450
|
}
|
|
453
451
|
if (
|
|
@@ -509,7 +507,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
509
507
|
receipt: RuntimeStepReceipt,
|
|
510
508
|
): Promise<never> => {
|
|
511
509
|
throw new Error(
|
|
512
|
-
`ctx.${input.operation}(${input.id}):
|
|
510
|
+
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${receipt.error ?? 'unknown error'}.`,
|
|
513
511
|
);
|
|
514
512
|
};
|
|
515
513
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createSecretRedactionContext } from './secret-redaction';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertRuntimeReceiptOutputWithinLimit,
|
|
4
|
+
TERMINAL_RUN_RESULT_MAX_BYTES,
|
|
5
|
+
} from './output-size-limits';
|
|
3
6
|
|
|
4
|
-
export const MAX_LEDGER_RESULT_PAYLOAD_BYTES =
|
|
7
|
+
export const MAX_LEDGER_RESULT_PAYLOAD_BYTES = TERMINAL_RUN_RESULT_MAX_BYTES;
|
|
5
8
|
export const MAX_LEDGER_LOG_LINES_PER_EVENT = 500;
|
|
6
9
|
export const MAX_LEDGER_LOG_LINE_LENGTH = 2_000;
|
|
7
10
|
|
|
@@ -21,6 +24,15 @@ export function sanitizeLedgerPayload(value: unknown): unknown {
|
|
|
21
24
|
return ledgerIngressRedactor.redact(value);
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
/** Secret-redact a receipt payload without coupling its 10 MiB cell limit to the ledger. */
|
|
28
|
+
export function sanitizeRuntimeReceiptPayload(value: unknown): unknown {
|
|
29
|
+
assertRuntimeReceiptOutputWithinLimit({
|
|
30
|
+
output: value,
|
|
31
|
+
path: 'runtime receipt output',
|
|
32
|
+
});
|
|
33
|
+
return ledgerIngressRedactor.redact(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export function sanitizeLedgerLogLines(value: unknown): string[] {
|
|
25
37
|
if (!Array.isArray(value)) return [];
|
|
26
38
|
return value
|
|
@@ -3,8 +3,10 @@ const encoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder();
|
|
|
3
3
|
export const TERMINAL_RUN_RESULT_MAX_BYTES = 2_500_000;
|
|
4
4
|
export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
|
|
5
5
|
export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
|
|
6
|
-
|
|
7
|
-
export const
|
|
6
|
+
/** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */
|
|
7
|
+
export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
/** One completion request may contain multiple receipts up to 32 MiB total. */
|
|
9
|
+
export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
8
10
|
|
|
9
11
|
export class OutputTooLargeError extends Error {
|
|
10
12
|
readonly code = 'OUTPUT_TOO_LARGE';
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { RECEIPT_STATUS_CODE } from './receipt-status';
|
|
2
2
|
import { workReceiptFailureKindCode } from './work-receipts';
|
|
3
3
|
|
|
4
|
+
export function workReceiptRepairableFailurePredicateSql(
|
|
5
|
+
receiptTable: string,
|
|
6
|
+
): string {
|
|
7
|
+
return `(
|
|
8
|
+
${receiptTable}.failure_kind = ${workReceiptFailureKindCode('repairable')}::smallint
|
|
9
|
+
OR (
|
|
10
|
+
${receiptTable}.failure_kind = ${workReceiptFailureKindCode('terminal')}::smallint
|
|
11
|
+
AND ${receiptTable}.error IS NOT NULL
|
|
12
|
+
AND lower(${receiptTable}.error) NOT LIKE '%billing cap%'
|
|
13
|
+
AND lower(${receiptTable}.error) NOT LIKE '%insufficient credits%'
|
|
14
|
+
AND lower(${receiptTable}.error) NOT LIKE '%monthly billing limit%'
|
|
15
|
+
AND (
|
|
16
|
+
${receiptTable}.error ~* '(^|[^0-9])5[0-9]{2}([^0-9]|$)'
|
|
17
|
+
OR lower(${receiptTable}.error) LIKE '%transport failed calling%'
|
|
18
|
+
OR lower(${receiptTable}.error) LIKE '%runtime api call timed out%'
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
)`;
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
export function workReceiptClaimableStatusCodes(input: {
|
|
5
25
|
forceRefresh?: boolean;
|
|
6
26
|
forceFailedRefresh?: boolean;
|
|
@@ -47,7 +67,7 @@ export function workReceiptClaimConflictPredicateSql(input: {
|
|
|
47
67
|
)
|
|
48
68
|
OR ${table}.status <> ${RECEIPT_STATUS_CODE.failed}::smallint
|
|
49
69
|
OR (
|
|
50
|
-
${table
|
|
70
|
+
${workReceiptRepairableFailurePredicateSql(table)}
|
|
51
71
|
AND (
|
|
52
72
|
${table}.run_id IS DISTINCT FROM ${input.claimantRunIdSql}
|
|
53
73
|
OR COALESCE(${table}.lease_owner_attempt, 0) < ${claimantRunAttemptSql}::integer
|