deepline 0.3.284 → 0.3.285
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/activity-observation.ts +26 -2
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +817 -2236
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +19 -6
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +127 -789
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +180 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +3 -22
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +50 -8
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +122 -18
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +218 -41
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +20 -2
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +13 -2
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +181 -326
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +38 -151
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +8 -0
- package/dist/cli/index.js +28 -3
- package/dist/cli/index.mjs +28 -3
- package/dist/{client-B0AjIkgb.d.mts → client-Cl-zj2oS.d.mts} +4 -0
- package/dist/{client-BCXQ_Ftc.d.ts → client-T2jVnZrq.d.ts} +4 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +25 -1
- package/dist/index.mjs +25 -1
- package/dist/install-integrity.json +2 -2
- package/dist/plays/run-request.d.mts +1 -1
- package/dist/plays/run-request.d.ts +1 -1
- 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
|
@@ -16,42 +16,22 @@ import type {
|
|
|
16
16
|
RuntimeMemoryOccupancyReader,
|
|
17
17
|
} from './map-memory-limits';
|
|
18
18
|
import type { DurableReceiptRecoverySource } from './tool-execution-outcome';
|
|
19
|
-
import {
|
|
20
|
-
isPlayExecutionSuspendedError,
|
|
21
|
-
isPlayRowExecutionSuspendedError,
|
|
22
|
-
PlayRowProviderBackpressureError,
|
|
23
|
-
} from './suspension';
|
|
24
19
|
import type { WorkReceiptFailureKind } from './work-receipts';
|
|
25
|
-
import {
|
|
26
|
-
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
27
|
-
runtimeLeaseHeartbeatIntervalFromExpiry,
|
|
28
|
-
runtimeLeaseHeartbeatIntervalMs,
|
|
29
|
-
} from './lease-policy';
|
|
30
20
|
import { createRuntimeReceiptHeartbeatSupervisor } from './receipt-heartbeat-supervisor';
|
|
31
|
-
import {
|
|
32
|
-
deserializeToolExecutionFailure,
|
|
33
|
-
serializeToolExecutionFailure,
|
|
34
|
-
TOOL_EXECUTION_ERROR_SCHEMA_VERSION,
|
|
35
|
-
} from '../plays/tool-execution-error';
|
|
36
21
|
import type {
|
|
37
22
|
ToolExecutionErrorSchemaVersion,
|
|
38
23
|
ToolExecutionFailureV1,
|
|
39
24
|
} from '../plays/tool-execution-error';
|
|
40
25
|
|
|
41
|
-
export const DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS = 240;
|
|
42
26
|
export const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
|
|
43
|
-
//
|
|
44
|
-
// this cap. Fixed 250 ms polls from every waiting group were most of the
|
|
45
|
-
// gateway read traffic in a 1k map. Because the delay grows, the attempt count
|
|
46
|
-
// alone no longer bounds wall time: every backoff loop must clamp itself to
|
|
47
|
-
// `runtimeReceiptWaitBudgetMs`.
|
|
27
|
+
// Invocation execution-lock retries back off within their wall-clock deadline.
|
|
48
28
|
export const DURABLE_RECEIPT_WAIT_MAX_DELAY_MS = 2_000;
|
|
49
29
|
// The hydration lane re-examines its queue on this cadence while turns are
|
|
50
30
|
// waiting, so headroom freed outside the lane reaches a waiter promptly. It is
|
|
51
31
|
// a probe interval, not a width: nothing about it bounds concurrency.
|
|
52
32
|
const RUNTIME_RECEIPT_HYDRATION_PROBE_MS = 25;
|
|
53
33
|
|
|
54
|
-
/** Backoff delay
|
|
34
|
+
/** Backoff delay for an invocation execution-lock retry (1-based). */
|
|
55
35
|
export function runtimeReceiptWaitDelayMs(attempt: number): number {
|
|
56
36
|
const exponent = Math.max(0, Math.min(attempt, 8) - 1);
|
|
57
37
|
return Math.min(
|
|
@@ -60,30 +40,6 @@ export function runtimeReceiptWaitDelayMs(attempt: number): number {
|
|
|
60
40
|
);
|
|
61
41
|
}
|
|
62
42
|
|
|
63
|
-
/**
|
|
64
|
-
* Whether a claim that came back `existing` is actually blocked by live work.
|
|
65
|
-
*
|
|
66
|
-
* A non-forced claim's status set is `('pending','failed')`, so a `running`
|
|
67
|
-
* row returns `existing` whether its owner is alive or long dead. Only an
|
|
68
|
-
* unexpired lease proves the former; anything else must be reclaimed and
|
|
69
|
-
* re-executed rather than waited on.
|
|
70
|
-
*/
|
|
71
|
-
export function runtimeReceiptClaimLeaseIsLive(
|
|
72
|
-
claim: Pick<RuntimeStepReceipt, 'leaseExpiresAt'>,
|
|
73
|
-
nowMs: number = Date.now(),
|
|
74
|
-
): boolean {
|
|
75
|
-
const expiresAtMs = Date.parse(claim.leaseExpiresAt ?? '');
|
|
76
|
-
return !Number.isFinite(expiresAtMs) || expiresAtMs > nowMs;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Wall-clock budget that a fixed-delay attempt count used to express. */
|
|
80
|
-
export function runtimeReceiptWaitBudgetMs(
|
|
81
|
-
maxAttempts: number,
|
|
82
|
-
delayMs: number = DURABLE_RECEIPT_WAIT_DELAY_MS,
|
|
83
|
-
): number {
|
|
84
|
-
return Math.max(delayMs, maxAttempts * delayMs);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
43
|
/**
|
|
88
44
|
* Settled per-key hydration samples the lane keeps to price a turn. Bounded,
|
|
89
45
|
* so past this many the reservoir below chooses which to keep.
|
|
@@ -702,18 +658,6 @@ export class RuntimeReceiptLeaseLostError extends Error {
|
|
|
702
658
|
}
|
|
703
659
|
}
|
|
704
660
|
|
|
705
|
-
function isInFlightRuntimeReceipt(
|
|
706
|
-
receipt: RuntimeStepReceipt | null | undefined,
|
|
707
|
-
): receipt is RuntimeStepReceipt & {
|
|
708
|
-
status: 'queued' | 'pending' | 'running';
|
|
709
|
-
} {
|
|
710
|
-
return (
|
|
711
|
-
receipt?.status === 'queued' ||
|
|
712
|
-
receipt?.status === 'pending' ||
|
|
713
|
-
receipt?.status === 'running'
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
661
|
export function runtimeReceiptFailureKindForError(
|
|
718
662
|
_error: unknown,
|
|
719
663
|
): WorkReceiptFailureKind {
|
|
@@ -753,45 +697,11 @@ export function resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
|
753
697
|
);
|
|
754
698
|
}
|
|
755
699
|
|
|
756
|
-
function
|
|
757
|
-
if (!signal?.aborted) return;
|
|
758
|
-
throw signal.reason instanceof Error
|
|
759
|
-
? signal.reason
|
|
760
|
-
: new Error(
|
|
761
|
-
typeof signal.reason === 'string' && signal.reason.trim()
|
|
762
|
-
? signal.reason
|
|
763
|
-
: 'Durable receipt wait aborted.',
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
async function sleepReceiptWait(
|
|
768
|
-
delayMs: number,
|
|
769
|
-
signal?: AbortSignal,
|
|
770
|
-
): Promise<void> {
|
|
771
|
-
throwIfReceiptWaitAborted(signal);
|
|
700
|
+
async function sleepReceiptWait(delayMs: number): Promise<void> {
|
|
772
701
|
if (delayMs <= 0) return;
|
|
773
|
-
await new Promise<void>((resolve
|
|
774
|
-
|
|
775
|
-
const abort = () => {
|
|
776
|
-
clearTimeout(timeout);
|
|
777
|
-
reject(
|
|
778
|
-
signal?.reason instanceof Error
|
|
779
|
-
? signal.reason
|
|
780
|
-
: new Error(
|
|
781
|
-
typeof signal?.reason === 'string' && signal.reason.trim()
|
|
782
|
-
? signal.reason
|
|
783
|
-
: 'Durable receipt wait aborted.',
|
|
784
|
-
),
|
|
785
|
-
);
|
|
786
|
-
};
|
|
787
|
-
function finish() {
|
|
788
|
-
signal?.removeEventListener('abort', abort);
|
|
789
|
-
resolve();
|
|
790
|
-
}
|
|
791
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
792
|
-
if (signal?.aborted) abort();
|
|
702
|
+
await new Promise<void>((resolve) => {
|
|
703
|
+
setTimeout(resolve, delayMs);
|
|
793
704
|
});
|
|
794
|
-
throwIfReceiptWaitAborted(signal);
|
|
795
705
|
}
|
|
796
706
|
|
|
797
707
|
export type DurableReceiptOperation = 'step' | 'tool' | 'fetch' | 'runPlay';
|
|
@@ -849,206 +759,12 @@ export type DurableReceiptExecutionStore = {
|
|
|
849
759
|
}) => Promise<boolean>;
|
|
850
760
|
};
|
|
851
761
|
|
|
852
|
-
async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
|
|
853
|
-
receiptKey: string;
|
|
854
|
-
runId: string;
|
|
855
|
-
leaseId: string;
|
|
856
|
-
heartbeatIntervalMs?: number;
|
|
857
|
-
store: DurableReceiptExecutionStore;
|
|
858
|
-
onTerminalHeartbeat?: (receipt: RuntimeStepReceipt) => void;
|
|
859
|
-
execute: () => Promise<T>;
|
|
860
|
-
}): Promise<T> {
|
|
861
|
-
if (!input.store.heartbeat) {
|
|
862
|
-
return await input.execute();
|
|
863
|
-
}
|
|
864
|
-
const heartbeatIntervalMs =
|
|
865
|
-
input.heartbeatIntervalMs ??
|
|
866
|
-
runtimeLeaseHeartbeatIntervalMs(PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS);
|
|
867
|
-
if (!Number.isFinite(heartbeatIntervalMs) || heartbeatIntervalMs <= 0) {
|
|
868
|
-
throw new Error('Runtime receipt heartbeat interval must be positive.');
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
let rejectLeaseLost!: (error: RuntimeReceiptLeaseLostError) => void;
|
|
872
|
-
const leaseLost = new Promise<never>((_, reject) => {
|
|
873
|
-
rejectLeaseLost = reject;
|
|
874
|
-
});
|
|
875
|
-
const heartbeatOnce = async (): Promise<'active' | 'terminal'> => {
|
|
876
|
-
const receipt = await input.store.heartbeat!(
|
|
877
|
-
input.receiptKey,
|
|
878
|
-
input.runId,
|
|
879
|
-
input.leaseId,
|
|
880
|
-
);
|
|
881
|
-
const isTerminal =
|
|
882
|
-
receipt?.status === 'completed' || receipt?.status === 'skipped';
|
|
883
|
-
if (receipt && isTerminal) {
|
|
884
|
-
input.onTerminalHeartbeat?.(receipt);
|
|
885
|
-
return 'terminal';
|
|
886
|
-
}
|
|
887
|
-
const stillOwnsReceipt =
|
|
888
|
-
receipt &&
|
|
889
|
-
(receipt.status === 'running' || receipt.status === 'queued') &&
|
|
890
|
-
receipt.leaseId === input.leaseId;
|
|
891
|
-
if (!stillOwnsReceipt) {
|
|
892
|
-
const latest = await input.store.get(input.receiptKey);
|
|
893
|
-
if (
|
|
894
|
-
latest &&
|
|
895
|
-
(latest.status === 'completed' || latest.status === 'skipped')
|
|
896
|
-
) {
|
|
897
|
-
input.onTerminalHeartbeat?.(latest);
|
|
898
|
-
return 'terminal';
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
if (!stillOwnsReceipt) {
|
|
902
|
-
throw new RuntimeReceiptLeaseLostError(input);
|
|
903
|
-
}
|
|
904
|
-
return 'active';
|
|
905
|
-
};
|
|
906
|
-
const supervisor = createRuntimeReceiptHeartbeatSupervisor({
|
|
907
|
-
intervalMs: heartbeatIntervalMs,
|
|
908
|
-
heartbeat: heartbeatOnce,
|
|
909
|
-
isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
|
|
910
|
-
onLeaseLost: (error) =>
|
|
911
|
-
rejectLeaseLost(error as RuntimeReceiptLeaseLostError),
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// The claim is the authoritative pre-dispatch ownership fence. Renewal is
|
|
915
|
-
// only needed when the lease approaches expiry; an immediate read/renewal
|
|
916
|
-
// adds a round trip to every short tool call without extending ownership.
|
|
917
|
-
// Completion remains conditional on this lease, and long work still renews.
|
|
918
|
-
supervisor.start();
|
|
919
|
-
try {
|
|
920
|
-
return await Promise.race([input.execute(), leaseLost]);
|
|
921
|
-
} finally {
|
|
922
|
-
supervisor.stop();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
762
|
export function runtimeReceiptOutput<T>(receipt: RuntimeStepReceipt): T {
|
|
927
763
|
return isSerializedToolExecuteResult(receipt.output)
|
|
928
764
|
? (deserializeToolExecuteResult(receipt.output) as T)
|
|
929
765
|
: (receipt.output as T);
|
|
930
766
|
}
|
|
931
767
|
|
|
932
|
-
export function runtimeReceiptFailureError(
|
|
933
|
-
receipt: RuntimeStepReceipt,
|
|
934
|
-
legacyPrefix: string,
|
|
935
|
-
_toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion,
|
|
936
|
-
): Error {
|
|
937
|
-
const message = receipt.error ?? 'unknown error';
|
|
938
|
-
return (
|
|
939
|
-
deserializeToolExecutionFailure(
|
|
940
|
-
message,
|
|
941
|
-
receipt.errorPayload,
|
|
942
|
-
TOOL_EXECUTION_ERROR_SCHEMA_VERSION,
|
|
943
|
-
) ?? new Error(`${legacyPrefix}: ${message}`)
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
export async function waitForCompletedRuntimeReceipt(input: {
|
|
948
|
-
receiptKey: string;
|
|
949
|
-
store: Pick<DurableReceiptExecutionStore, 'getMany'>;
|
|
950
|
-
maxAttempts?: number;
|
|
951
|
-
delayMs?: number;
|
|
952
|
-
abortSignal?: AbortSignal;
|
|
953
|
-
toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion;
|
|
954
|
-
}): Promise<RuntimeStepReceipt> {
|
|
955
|
-
const maxAttempts = input.maxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
|
|
956
|
-
const delayMs = input.delayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
|
|
957
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
958
|
-
throwIfReceiptWaitAborted(input.abortSignal);
|
|
959
|
-
if (attempt > 0) {
|
|
960
|
-
await sleepReceiptWait(delayMs, input.abortSignal);
|
|
961
|
-
}
|
|
962
|
-
const receipt = (await input.store.getMany([input.receiptKey])).get(
|
|
963
|
-
input.receiptKey,
|
|
964
|
-
);
|
|
965
|
-
if (receipt?.status === 'completed' || receipt?.status === 'skipped') {
|
|
966
|
-
return receipt;
|
|
967
|
-
}
|
|
968
|
-
if (receipt?.status === 'failed') {
|
|
969
|
-
throw runtimeReceiptFailureError(
|
|
970
|
-
receipt,
|
|
971
|
-
`Durable tool call ${input.receiptKey} failed`,
|
|
972
|
-
input.toolErrorSchemaVersion,
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
export async function waitForCompletedRuntimeReceipts(input: {
|
|
980
|
-
receiptKeys: string[];
|
|
981
|
-
store: Pick<DurableReceiptExecutionStore, 'getMany'>;
|
|
982
|
-
maxAttempts?: number;
|
|
983
|
-
delayMs?: number;
|
|
984
|
-
abortSignal?: AbortSignal;
|
|
985
|
-
log?: (message: string) => void;
|
|
986
|
-
toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion;
|
|
987
|
-
}): Promise<{
|
|
988
|
-
completed: Map<string, RuntimeStepReceipt>;
|
|
989
|
-
failed: Map<string, Error>;
|
|
990
|
-
timedOut: Set<string>;
|
|
991
|
-
}> {
|
|
992
|
-
const uniqueKeys = [
|
|
993
|
-
...new Set(input.receiptKeys.map((key) => key.trim()).filter(Boolean)),
|
|
994
|
-
];
|
|
995
|
-
const pending = new Set(uniqueKeys);
|
|
996
|
-
const completed = new Map<string, RuntimeStepReceipt>();
|
|
997
|
-
const failed = new Map<string, Error>();
|
|
998
|
-
const maxAttempts = input.maxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
|
|
999
|
-
const delayMs = input.delayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
|
|
1000
|
-
|
|
1001
|
-
for (
|
|
1002
|
-
let attempt = 0;
|
|
1003
|
-
attempt < maxAttempts && pending.size > 0;
|
|
1004
|
-
attempt += 1
|
|
1005
|
-
) {
|
|
1006
|
-
throwIfReceiptWaitAborted(input.abortSignal);
|
|
1007
|
-
if (attempt > 0) {
|
|
1008
|
-
await sleepReceiptWait(delayMs, input.abortSignal);
|
|
1009
|
-
}
|
|
1010
|
-
const receipts = await input.store.getMany([...pending]);
|
|
1011
|
-
const statuses = [...receipts.values()].reduce<Record<string, number>>(
|
|
1012
|
-
(counts, receipt) => {
|
|
1013
|
-
counts[receipt.status] = (counts[receipt.status] ?? 0) + 1;
|
|
1014
|
-
return counts;
|
|
1015
|
-
},
|
|
1016
|
-
{},
|
|
1017
|
-
);
|
|
1018
|
-
input.log?.(
|
|
1019
|
-
`[perf] runtime-receipt-wait attempt=${attempt} requested=${pending.size} indexed=${receipts.size} statuses=${
|
|
1020
|
-
Object.entries(statuses)
|
|
1021
|
-
.map(([status, count]) => `${status}:${count}`)
|
|
1022
|
-
.join(',') || 'none'
|
|
1023
|
-
}`,
|
|
1024
|
-
);
|
|
1025
|
-
for (const key of [...pending]) {
|
|
1026
|
-
const receipt = receipts.get(key);
|
|
1027
|
-
if (receipt?.status === 'completed' || receipt?.status === 'skipped') {
|
|
1028
|
-
completed.set(key, receipt);
|
|
1029
|
-
pending.delete(key);
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (receipt?.status === 'failed') {
|
|
1033
|
-
failed.set(
|
|
1034
|
-
key,
|
|
1035
|
-
runtimeReceiptFailureError(
|
|
1036
|
-
receipt,
|
|
1037
|
-
`Durable tool call ${key} failed`,
|
|
1038
|
-
input.toolErrorSchemaVersion,
|
|
1039
|
-
),
|
|
1040
|
-
);
|
|
1041
|
-
pending.delete(key);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
input.log?.(
|
|
1045
|
-
`[perf] runtime-receipt-wait attempt=${attempt} remaining=${pending.size}`,
|
|
1046
|
-
);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
return { completed, failed, timedOut: pending };
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
768
|
export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
1053
769
|
operation: DurableReceiptOperation;
|
|
1054
770
|
id: string;
|
|
@@ -1071,14 +787,14 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
1071
787
|
shouldPersistFailure?: (error: unknown) => boolean;
|
|
1072
788
|
heartbeatIntervalMs?: number;
|
|
1073
789
|
markRunningBeforeExecute?: boolean;
|
|
1074
|
-
/**
|
|
1075
|
-
* in-flight receipt, then atomically insert the completed fact. */
|
|
790
|
+
/** Historical caller option; all receipts now cache completed values only. */
|
|
1076
791
|
completedCacheOnly?: boolean;
|
|
1077
792
|
withCompletedReceiptHydration?: <Result>(
|
|
1078
793
|
hydrate: () => Promise<Result>,
|
|
1079
794
|
) => Promise<Result>;
|
|
1080
795
|
requiresExecutionLock?: boolean;
|
|
1081
796
|
executionLockTtlMs?: number;
|
|
797
|
+
executionLockKey?: string;
|
|
1082
798
|
toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion;
|
|
1083
799
|
formatError: (error: unknown) => string;
|
|
1084
800
|
log: (message: string) => void;
|
|
@@ -1087,35 +803,20 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
1087
803
|
signal?: AbortSignal;
|
|
1088
804
|
}) => Promise<T>;
|
|
1089
805
|
}): Promise<T> {
|
|
1090
|
-
const lifecycleStartedAt = Date.now();
|
|
1091
|
-
const logPhase = (phase: string, startedAt: number): void => {
|
|
1092
|
-
// Keep this at the semantic receipt layer. Transport timing below this
|
|
1093
|
-
// function explains HTTP cost; these markers expose gaps before/after the
|
|
1094
|
-
// store calls and prove whether user code or receipt orchestration owns the
|
|
1095
|
-
// elapsed time. Do not log receipt keys because they encode tool inputs.
|
|
1096
|
-
input.log(
|
|
1097
|
-
`[perf] durable receipt operation=${input.operation} id=${input.id} phase=${phase} elapsed_ms=${Date.now() - startedAt} lifecycle_ms=${Date.now() - lifecycleStartedAt}`,
|
|
1098
|
-
);
|
|
1099
|
-
};
|
|
1100
806
|
if (!input.store.enabled) {
|
|
1101
807
|
return await input.execute({ leaseId: null });
|
|
1102
808
|
}
|
|
1103
809
|
|
|
1104
810
|
const recoverCompletedReceipt = async (
|
|
1105
811
|
receipt: RuntimeStepReceipt,
|
|
1106
|
-
source: DurableReceiptRecoverySource = 'cache',
|
|
1107
812
|
): Promise<T> => {
|
|
1108
|
-
input.log(
|
|
1109
|
-
source === 'owner'
|
|
1110
|
-
? `ctx.${input.operation}(${input.id}): executed and converged through immutable publication`
|
|
1111
|
-
: `ctx.${input.operation}(${input.id}): reused completed work`,
|
|
1112
|
-
);
|
|
813
|
+
input.log(`ctx.${input.operation}(${input.id}): reused completed work`);
|
|
1113
814
|
if (receipt.output === undefined) {
|
|
1114
815
|
return receipt.output as T;
|
|
1115
816
|
}
|
|
1116
817
|
const output = runtimeReceiptOutput<T>(receipt);
|
|
1117
818
|
const recovered = input.onRecovered
|
|
1118
|
-
? input.onRecovered(output, receipt,
|
|
819
|
+
? input.onRecovered(output, receipt, 'cache')
|
|
1119
820
|
: output;
|
|
1120
821
|
if (input.markSkipped) {
|
|
1121
822
|
await input.markSkipped(recovered);
|
|
@@ -1123,509 +824,146 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
1123
824
|
return recovered;
|
|
1124
825
|
};
|
|
1125
826
|
|
|
1126
|
-
if (input.
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
input.
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const receipt = await read();
|
|
1139
|
-
if (receipt?.status === 'completed' || receipt?.status === 'skipped') {
|
|
1140
|
-
return {
|
|
1141
|
-
kind: 'recovered' as const,
|
|
1142
|
-
output: await recoverCompletedReceipt(receipt, source),
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
return { kind: 'unresolved' as const };
|
|
1146
|
-
};
|
|
1147
|
-
return await withCompletedReceiptHydration(inspect);
|
|
827
|
+
if (input.force !== true) {
|
|
828
|
+
const readCompleted = async (): Promise<
|
|
829
|
+
{ kind: 'recovered'; output: T } | { kind: 'unresolved' }
|
|
830
|
+
> => {
|
|
831
|
+
const receipt = await input.store.get(input.receiptKey);
|
|
832
|
+
if (receipt?.status === 'completed' || receipt?.status === 'skipped') {
|
|
833
|
+
return {
|
|
834
|
+
kind: 'recovered',
|
|
835
|
+
output: await recoverCompletedReceipt(receipt),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return { kind: 'unresolved' };
|
|
1148
839
|
};
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
840
|
+
const cached = input.withCompletedReceiptHydration
|
|
841
|
+
? await input.withCompletedReceiptHydration(readCompleted)
|
|
842
|
+
: await readCompleted();
|
|
843
|
+
if (cached.kind === 'recovered') return cached.output;
|
|
844
|
+
}
|
|
845
|
+
const needsLock = input.requiresExecutionLock === true;
|
|
846
|
+
const ownerExecutionId = `${input.runId}:${crypto.randomUUID()}`;
|
|
847
|
+
const executionLockKey =
|
|
848
|
+
input.executionLockKey?.trim() ||
|
|
849
|
+
`${input.receiptKey}:invocation:${ownerExecutionId}`;
|
|
850
|
+
let ownsLock = false;
|
|
851
|
+
let lockSupervisor: ReturnType<
|
|
852
|
+
typeof createRuntimeReceiptHeartbeatSupervisor
|
|
853
|
+
> | null = null;
|
|
854
|
+
const executionAbortController = needsLock ? new AbortController() : null;
|
|
855
|
+
if (needsLock) {
|
|
856
|
+
if (
|
|
857
|
+
!input.store.acquireExecutionLock ||
|
|
858
|
+
!input.store.releaseExecutionLock
|
|
859
|
+
) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`ctx.${input.operation}(${input.id}): non-idempotent provider requires the receipt execution-lock backend.`,
|
|
1152
862
|
);
|
|
1153
|
-
if (cached.kind === 'recovered') return cached.output;
|
|
1154
863
|
}
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
throw new Error(
|
|
1168
|
-
`ctx.${input.operation}(${input.id}): non-idempotent provider requires the receipt execution-lock backend.`,
|
|
1169
|
-
);
|
|
864
|
+
const deadline = Date.now() + DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS;
|
|
865
|
+
let completedWaitAttempt = 0;
|
|
866
|
+
while (!ownsLock) {
|
|
867
|
+
const lock = await input.store.acquireExecutionLock({
|
|
868
|
+
receiptKey: executionLockKey,
|
|
869
|
+
ownerExecutionId,
|
|
870
|
+
ttlMs: input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS,
|
|
871
|
+
});
|
|
872
|
+
ownsLock = lock?.ownerExecutionId === ownerExecutionId;
|
|
873
|
+
if (ownsLock) break;
|
|
874
|
+
if (Date.now() >= deadline) {
|
|
875
|
+
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
1170
876
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
877
|
+
completedWaitAttempt += 1;
|
|
878
|
+
await sleepReceiptWait(
|
|
879
|
+
Math.min(
|
|
880
|
+
runtimeReceiptWaitDelayMs(completedWaitAttempt),
|
|
881
|
+
Math.max(0, deadline - Date.now()),
|
|
882
|
+
),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const lockTtlMs = input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS;
|
|
886
|
+
lockSupervisor = createRuntimeReceiptHeartbeatSupervisor({
|
|
887
|
+
intervalMs:
|
|
888
|
+
input.heartbeatIntervalMs ?? Math.max(1_000, Math.floor(lockTtlMs / 3)),
|
|
889
|
+
heartbeat: async () => {
|
|
890
|
+
const renewed = await input.store.acquireExecutionLock!({
|
|
891
|
+
receiptKey: executionLockKey,
|
|
1176
892
|
ownerExecutionId,
|
|
1177
|
-
ttlMs:
|
|
893
|
+
ttlMs: lockTtlMs,
|
|
1178
894
|
});
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
'in_flight',
|
|
1185
|
-
);
|
|
1186
|
-
if (winner.kind === 'recovered') return winner.output;
|
|
1187
|
-
}
|
|
1188
|
-
if (Date.now() >= deadline) {
|
|
1189
|
-
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
1190
|
-
}
|
|
1191
|
-
completedWaitAttempt += 1;
|
|
1192
|
-
await sleepReceiptWait(
|
|
1193
|
-
Math.min(
|
|
1194
|
-
runtimeReceiptWaitDelayMs(completedWaitAttempt),
|
|
1195
|
-
Math.max(0, deadline - Date.now()),
|
|
1196
|
-
),
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
if (input.force !== true) {
|
|
1200
|
-
const afterLock = await inspectCompletedReceipt(
|
|
1201
|
-
() => input.store.get(input.receiptKey),
|
|
1202
|
-
'in_flight',
|
|
1203
|
-
);
|
|
1204
|
-
if (afterLock.kind === 'recovered') {
|
|
1205
|
-
await input.store.releaseExecutionLock({
|
|
1206
|
-
receiptKey: input.receiptKey,
|
|
1207
|
-
ownerExecutionId,
|
|
895
|
+
if (renewed?.ownerExecutionId !== ownerExecutionId) {
|
|
896
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
897
|
+
receiptKey: executionLockKey,
|
|
898
|
+
runId: input.runId,
|
|
899
|
+
leaseId: ownerExecutionId,
|
|
1208
900
|
});
|
|
1209
|
-
return afterLock.output;
|
|
1210
901
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
input.
|
|
1217
|
-
Math.max(1_000, Math.floor(lockTtlMs / 3)),
|
|
1218
|
-
heartbeat: async () => {
|
|
1219
|
-
const renewed = await input.store.acquireExecutionLock!({
|
|
1220
|
-
receiptKey: input.receiptKey,
|
|
1221
|
-
ownerExecutionId,
|
|
1222
|
-
ttlMs: lockTtlMs,
|
|
1223
|
-
});
|
|
1224
|
-
if (renewed?.ownerExecutionId !== ownerExecutionId) {
|
|
1225
|
-
throw new RuntimeReceiptLeaseLostError({
|
|
1226
|
-
receiptKey: input.receiptKey,
|
|
1227
|
-
runId: input.runId,
|
|
1228
|
-
leaseId: ownerExecutionId,
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
return 'active';
|
|
1232
|
-
},
|
|
1233
|
-
isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
|
|
1234
|
-
onLeaseLost: (error) => {
|
|
1235
|
-
input.log(
|
|
1236
|
-
`ctx.${input.operation}(${input.id}): dangerous side-effect execution fence lost: ${error instanceof Error ? error.message : String(error)}`,
|
|
1237
|
-
);
|
|
1238
|
-
executionAbortController?.abort(error);
|
|
1239
|
-
},
|
|
1240
|
-
});
|
|
1241
|
-
lockSupervisor.start();
|
|
1242
|
-
}
|
|
1243
|
-
const releaseOwnLock = async (): Promise<void> => {
|
|
1244
|
-
lockSupervisor?.stop();
|
|
1245
|
-
lockSupervisor = null;
|
|
1246
|
-
if (ownsLock) {
|
|
1247
|
-
ownsLock = false;
|
|
1248
|
-
await input.store.releaseExecutionLock!({
|
|
1249
|
-
receiptKey: input.receiptKey,
|
|
1250
|
-
ownerExecutionId,
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
let executed: T;
|
|
1255
|
-
try {
|
|
1256
|
-
executed = await input.execute({
|
|
1257
|
-
leaseId: null,
|
|
1258
|
-
signal: executionAbortController?.signal,
|
|
1259
|
-
});
|
|
1260
|
-
executionAbortController?.signal.throwIfAborted();
|
|
1261
|
-
} catch (error) {
|
|
1262
|
-
await releaseOwnLock();
|
|
1263
|
-
throw error;
|
|
1264
|
-
}
|
|
1265
|
-
try {
|
|
1266
|
-
const result = input.onClaimedResult
|
|
1267
|
-
? input.onClaimedResult(executed, input.receiptKey)
|
|
1268
|
-
: executed;
|
|
1269
|
-
assertNoSecretTaint(result, `ctx.${input.operation} result`);
|
|
1270
|
-
// Force bypasses the immutable cache. It does not grant permission to
|
|
1271
|
-
// replace the completed fact already published under this semantic key.
|
|
1272
|
-
if (input.force === true) {
|
|
1273
|
-
return result;
|
|
1274
|
-
}
|
|
1275
|
-
// The hydration window bounds how many stored payloads expand at once.
|
|
1276
|
-
// A completion is a write, and on wire format 3 it answers with a
|
|
1277
|
-
// compact acknowledgement carrying no payload at all — nothing expands,
|
|
1278
|
-
// so nothing needs a turn. Holding a slot of the 8-wide run-global
|
|
1279
|
-
// window across the whole gateway round trip only queued unrelated rows
|
|
1280
|
-
// behind this row's write. Only a response that really carries a stored
|
|
1281
|
-
// payload decodes under the gate.
|
|
1282
|
-
const completed = await input.store.complete(
|
|
1283
|
-
input.receiptKey,
|
|
1284
|
-
input.runId,
|
|
1285
|
-
isToolExecuteResult(result)
|
|
1286
|
-
? serializeToolExecuteResult(result)
|
|
1287
|
-
: result,
|
|
1288
|
-
COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
|
|
1289
|
-
);
|
|
1290
|
-
if (
|
|
1291
|
-
completed?.status === 'completed' ||
|
|
1292
|
-
completed?.status === 'skipped'
|
|
1293
|
-
) {
|
|
1294
|
-
if (completed.output === undefined) return result;
|
|
1295
|
-
return await withCompletedReceiptHydration(
|
|
1296
|
-
async () => await recoverCompletedReceipt(completed, 'owner'),
|
|
902
|
+
return 'active';
|
|
903
|
+
},
|
|
904
|
+
isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
|
|
905
|
+
onLeaseLost: (error) => {
|
|
906
|
+
input.log(
|
|
907
|
+
`ctx.${input.operation}(${input.id}): dangerous side-effect execution fence lost: ${error instanceof Error ? error.message : String(error)}`,
|
|
1297
908
|
);
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
);
|
|
1303
|
-
if (winner.kind === 'recovered') return winner.output;
|
|
1304
|
-
throw new Error(
|
|
1305
|
-
`ctx.${input.operation}(${input.id}): completed receipt cache insert failed.`,
|
|
1306
|
-
);
|
|
1307
|
-
} finally {
|
|
1308
|
-
await releaseOwnLock();
|
|
1309
|
-
}
|
|
909
|
+
executionAbortController?.abort(error);
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
lockSupervisor.start();
|
|
1310
913
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
input.runId,
|
|
1321
|
-
true,
|
|
1322
|
-
);
|
|
1323
|
-
if (reclaimed?.status === 'completed' || reclaimed?.status === 'skipped') {
|
|
1324
|
-
return {
|
|
1325
|
-
kind: 'recovered',
|
|
1326
|
-
output: await recoverCompletedReceipt(reclaimed, 'cache'),
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
if (reclaimed?.status === 'failed') {
|
|
1330
|
-
throw runtimeReceiptFailureError(
|
|
1331
|
-
reclaimed,
|
|
1332
|
-
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused`,
|
|
1333
|
-
input.toolErrorSchemaVersion,
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
if (
|
|
1337
|
-
isInFlightRuntimeReceipt(reclaimed) &&
|
|
1338
|
-
reclaimed.claimState === 'existing'
|
|
1339
|
-
) {
|
|
1340
|
-
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
1341
|
-
}
|
|
1342
|
-
ownedLeaseId = reclaimed?.leaseId ?? null;
|
|
1343
|
-
ownedLeaseExpiresAt = reclaimed?.leaseExpiresAt ?? null;
|
|
1344
|
-
return { kind: 'claimed' };
|
|
1345
|
-
};
|
|
1346
|
-
const shouldRepairSameRunRunningReceipt = (
|
|
1347
|
-
receipt: RuntimeStepReceipt,
|
|
1348
|
-
): boolean =>
|
|
1349
|
-
input.repairRunningReceiptForSameRun === true &&
|
|
1350
|
-
isInFlightRuntimeReceipt(receipt) &&
|
|
1351
|
-
typeof receipt.runId === 'string' &&
|
|
1352
|
-
receipt.runId.trim() === input.runId;
|
|
1353
|
-
const waitForRunningReceiptOrRelease = async (): Promise<
|
|
1354
|
-
{ kind: 'recovered'; output: T } | { kind: 'claimed' }
|
|
1355
|
-
> => {
|
|
1356
|
-
const maxAttempts =
|
|
1357
|
-
input.runningReceiptWaitMaxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
|
|
1358
|
-
const delayMs =
|
|
1359
|
-
input.runningReceiptWaitDelayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
|
|
1360
|
-
// An explicit delay keeps the caller's fixed cadence; the default backs off.
|
|
1361
|
-
const backoff = input.runningReceiptWaitDelayMs == null;
|
|
1362
|
-
const waitDeadline =
|
|
1363
|
-
Date.now() + runtimeReceiptWaitBudgetMs(maxAttempts, delayMs);
|
|
1364
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1365
|
-
if (attempt > 0) {
|
|
1366
|
-
if (backoff && Date.now() >= waitDeadline) break;
|
|
1367
|
-
await sleepReceiptWait(
|
|
1368
|
-
backoff
|
|
1369
|
-
? Math.min(
|
|
1370
|
-
runtimeReceiptWaitDelayMs(attempt),
|
|
1371
|
-
Math.max(0, waitDeadline - Date.now()),
|
|
1372
|
-
)
|
|
1373
|
-
: delayMs,
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
const current = await input.store.get(input.receiptKey);
|
|
1377
|
-
if (current?.status === 'completed' || current?.status === 'skipped') {
|
|
1378
|
-
return {
|
|
1379
|
-
kind: 'recovered',
|
|
1380
|
-
output: await recoverCompletedReceipt(current, 'in_flight'),
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
if (current?.status === 'failed') {
|
|
1384
|
-
throw runtimeReceiptFailureError(
|
|
1385
|
-
current,
|
|
1386
|
-
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused`,
|
|
1387
|
-
input.toolErrorSchemaVersion,
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
if (
|
|
1391
|
-
(current?.status === 'queued' || current?.status === 'pending') &&
|
|
1392
|
-
!current.leaseId &&
|
|
1393
|
-
!current.leaseExpiresAt
|
|
1394
|
-
) {
|
|
1395
|
-
return await reclaimReceipt();
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
1399
|
-
};
|
|
1400
|
-
const repairOrWaitForRunningReceipt = async (
|
|
1401
|
-
receipt: RuntimeStepReceipt,
|
|
1402
|
-
): Promise<{ kind: 'recovered'; output: T } | { kind: 'claimed' }> => {
|
|
1403
|
-
if (shouldRepairSameRunRunningReceipt(receipt)) {
|
|
1404
|
-
const recovered = await reclaimReceipt();
|
|
1405
|
-
if (recovered.kind === 'recovered') return recovered;
|
|
1406
|
-
return { kind: 'claimed' };
|
|
1407
|
-
}
|
|
1408
|
-
try {
|
|
1409
|
-
return await waitForRunningReceiptOrRelease();
|
|
1410
|
-
} catch (error) {
|
|
1411
|
-
if (error instanceof RuntimeReceiptWaitTimeoutError) {
|
|
1412
|
-
const recovered = await reclaimReceipt();
|
|
1413
|
-
if (recovered.kind === 'recovered') return recovered;
|
|
1414
|
-
return { kind: 'claimed' };
|
|
1415
|
-
}
|
|
1416
|
-
throw error;
|
|
914
|
+
const releaseOwnLock = async (): Promise<void> => {
|
|
915
|
+
lockSupervisor?.stop();
|
|
916
|
+
lockSupervisor = null;
|
|
917
|
+
if (ownsLock) {
|
|
918
|
+
ownsLock = false;
|
|
919
|
+
await input.store.releaseExecutionLock!({
|
|
920
|
+
receiptKey: executionLockKey,
|
|
921
|
+
ownerExecutionId,
|
|
922
|
+
});
|
|
1417
923
|
}
|
|
1418
924
|
};
|
|
1419
|
-
const repairFailedReceipt = async (
|
|
1420
|
-
receipt: RuntimeStepReceipt,
|
|
1421
|
-
): Promise<never> => {
|
|
1422
|
-
throw runtimeReceiptFailureError(
|
|
1423
|
-
receipt,
|
|
1424
|
-
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused`,
|
|
1425
|
-
input.toolErrorSchemaVersion,
|
|
1426
|
-
);
|
|
1427
|
-
};
|
|
1428
|
-
|
|
1429
|
-
const claimStartedAt = Date.now();
|
|
1430
|
-
const claimed = await input.store.claim(
|
|
1431
|
-
input.receiptKey,
|
|
1432
|
-
input.runId,
|
|
1433
|
-
input.reclaimRunning === true || input.force === true,
|
|
1434
|
-
input.force === true,
|
|
1435
|
-
);
|
|
1436
|
-
logPhase('claim', claimStartedAt);
|
|
1437
|
-
if (
|
|
1438
|
-
input.force !== true &&
|
|
1439
|
-
(claimed?.status === 'completed' || claimed?.status === 'skipped')
|
|
1440
|
-
) {
|
|
1441
|
-
return await recoverCompletedReceipt(claimed);
|
|
1442
|
-
}
|
|
1443
|
-
if (!claimed) {
|
|
1444
|
-
const latest = await input.store.get(input.receiptKey);
|
|
1445
|
-
if (latest?.status === 'completed' || latest?.status === 'skipped') {
|
|
1446
|
-
return await recoverCompletedReceipt(latest);
|
|
1447
|
-
}
|
|
1448
|
-
if (isInFlightRuntimeReceipt(latest)) {
|
|
1449
|
-
const recovered = await repairOrWaitForRunningReceipt(latest);
|
|
1450
|
-
if (recovered.kind === 'recovered') return recovered.output;
|
|
1451
|
-
} else if (latest?.status === 'failed') {
|
|
1452
|
-
await repairFailedReceipt(latest);
|
|
1453
|
-
} else {
|
|
1454
|
-
throw new Error(
|
|
1455
|
-
`ctx.${input.operation}(${input.id}): receipt claim did not return execution ownership.`,
|
|
1456
|
-
);
|
|
1457
|
-
}
|
|
1458
|
-
} else if (isInFlightRuntimeReceipt(claimed)) {
|
|
1459
|
-
if (claimed.claimState === 'existing') {
|
|
1460
|
-
const recovered = await repairOrWaitForRunningReceipt(claimed);
|
|
1461
|
-
if (recovered.kind === 'recovered') return recovered.output;
|
|
1462
|
-
} else {
|
|
1463
|
-
ownedLeaseId = claimed.leaseId ?? null;
|
|
1464
|
-
ownedLeaseExpiresAt = claimed.leaseExpiresAt ?? null;
|
|
1465
|
-
}
|
|
1466
|
-
} else if (claimed.status === 'failed') {
|
|
1467
|
-
await repairFailedReceipt(claimed);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
let result: T;
|
|
1471
925
|
try {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const running = await input.store.markRunning(
|
|
1479
|
-
input.receiptKey,
|
|
1480
|
-
input.runId,
|
|
1481
|
-
ownedLeaseId,
|
|
1482
|
-
);
|
|
1483
|
-
logPhase('mark_running', markRunningStartedAt);
|
|
1484
|
-
if (!running || running.status !== 'running') {
|
|
1485
|
-
throw new RuntimeReceiptLeaseLostError({
|
|
1486
|
-
receiptKey: input.receiptKey,
|
|
1487
|
-
runId: input.runId,
|
|
1488
|
-
leaseId: ownedLeaseId ?? '',
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
ownedLeaseId = running.leaseId ?? ownedLeaseId;
|
|
1492
|
-
ownedLeaseExpiresAt = running.leaseExpiresAt ?? ownedLeaseExpiresAt;
|
|
1493
|
-
}
|
|
1494
|
-
const executeStartedAt = Date.now();
|
|
1495
|
-
const executed =
|
|
1496
|
-
ownedLeaseId && input.store.heartbeat
|
|
1497
|
-
? await executeWithDurableRuntimeReceiptHeartbeat({
|
|
1498
|
-
receiptKey: input.receiptKey,
|
|
1499
|
-
runId: input.runId,
|
|
1500
|
-
leaseId: ownedLeaseId,
|
|
1501
|
-
heartbeatIntervalMs:
|
|
1502
|
-
input.heartbeatIntervalMs ??
|
|
1503
|
-
runtimeLeaseHeartbeatIntervalFromExpiry({
|
|
1504
|
-
leaseExpiresAt: ownedLeaseExpiresAt,
|
|
1505
|
-
fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
1506
|
-
}),
|
|
1507
|
-
store: input.store,
|
|
1508
|
-
onTerminalHeartbeat: (receipt) => {
|
|
1509
|
-
input.log(
|
|
1510
|
-
`ctx.${input.operation}(${input.id}): receipt heartbeat observed terminal ${receipt.status}; treating inner gateway completion as ownership handoff`,
|
|
1511
|
-
);
|
|
1512
|
-
},
|
|
1513
|
-
execute: () => input.execute({ leaseId: ownedLeaseId }),
|
|
1514
|
-
})
|
|
1515
|
-
: await input.execute({ leaseId: ownedLeaseId });
|
|
1516
|
-
logPhase('execute', executeStartedAt);
|
|
1517
|
-
result = input.onClaimedResult
|
|
926
|
+
const executed = await input.execute({
|
|
927
|
+
leaseId: null,
|
|
928
|
+
signal: executionAbortController?.signal,
|
|
929
|
+
});
|
|
930
|
+
executionAbortController?.signal.throwIfAborted();
|
|
931
|
+
const result = input.onClaimedResult
|
|
1518
932
|
? input.onClaimedResult(executed, input.receiptKey)
|
|
1519
933
|
: executed;
|
|
1520
934
|
assertNoSecretTaint(result, `ctx.${input.operation} result`);
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
!released ||
|
|
1535
|
-
(released.status !== 'queued' && released.status !== 'pending')
|
|
1536
|
-
) {
|
|
1537
|
-
throw new Error(
|
|
1538
|
-
`ctx.${input.operation}(${input.id}): non-cacheable execution failed but receipt ownership could not be released: ${input.formatError(error)}.`,
|
|
1539
|
-
);
|
|
1540
|
-
}
|
|
1541
|
-
throw error;
|
|
1542
|
-
}
|
|
1543
|
-
// The ownership is uncertain, so neither `fail` nor `release` is safe.
|
|
1544
|
-
// Let the attempt-level cleanup and the receipt lease fence reconcile this
|
|
1545
|
-
// on the next Play Run attempt. In particular, never replace a possibly
|
|
1546
|
-
// completed invocation with a terminal error from the old owner.
|
|
1547
|
-
if (error instanceof RuntimeReceiptLeaseLostError) {
|
|
1548
|
-
throw error;
|
|
1549
|
-
}
|
|
1550
|
-
if (
|
|
1551
|
-
isPlayExecutionSuspendedError(error) ||
|
|
1552
|
-
isPlayRowExecutionSuspendedError(error) ||
|
|
1553
|
-
error instanceof PlayRowProviderBackpressureError
|
|
1554
|
-
) {
|
|
1555
|
-
const released = await input.store.release(
|
|
1556
|
-
input.receiptKey,
|
|
1557
|
-
input.runId,
|
|
1558
|
-
ownedLeaseId,
|
|
1559
|
-
);
|
|
1560
|
-
if (
|
|
1561
|
-
!released ||
|
|
1562
|
-
(released.status !== 'queued' && released.status !== 'pending')
|
|
1563
|
-
) {
|
|
1564
|
-
throw new Error(
|
|
1565
|
-
`ctx.${input.operation}(${input.id}): receipt suspended but ownership could not be released: ${input.formatError(error)}.`,
|
|
1566
|
-
);
|
|
1567
|
-
}
|
|
1568
|
-
throw error;
|
|
1569
|
-
}
|
|
1570
|
-
const failed = await input.store.fail(
|
|
935
|
+
// Force bypasses the immutable cache. It does not grant permission to
|
|
936
|
+
// replace the completed fact already published under this semantic key.
|
|
937
|
+
if (input.force === true) {
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
// The hydration window bounds how many stored payloads expand at once.
|
|
941
|
+
// A completion is a write, and on wire format 3 it answers with a
|
|
942
|
+
// compact acknowledgement carrying no payload at all — nothing expands,
|
|
943
|
+
// so nothing needs a turn. Holding a slot of the 8-wide run-global
|
|
944
|
+
// window across the whole gateway round trip only queued unrelated rows
|
|
945
|
+
// behind this row's write. Only a response that really carries a stored
|
|
946
|
+
// payload decodes under the gate.
|
|
947
|
+
const completed = await input.store.complete(
|
|
1571
948
|
input.receiptKey,
|
|
1572
949
|
input.runId,
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
runtimeReceiptFailureKindForError(error),
|
|
1576
|
-
serializeToolExecutionFailure(error),
|
|
950
|
+
isToolExecuteResult(result) ? serializeToolExecuteResult(result) : result,
|
|
951
|
+
COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
|
|
1577
952
|
);
|
|
1578
|
-
|
|
953
|
+
const stored =
|
|
954
|
+
completed?.status === 'completed' || completed?.status === 'skipped'
|
|
955
|
+
? completed
|
|
956
|
+
: await input.store.get(input.receiptKey);
|
|
957
|
+
if (stored?.status !== 'completed' && stored?.status !== 'skipped') {
|
|
1579
958
|
throw new Error(
|
|
1580
|
-
`ctx.${input.operation}(${input.id}):
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
throw error;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
const completeStartedAt = Date.now();
|
|
1587
|
-
const completed = await input.store.complete(
|
|
1588
|
-
input.receiptKey,
|
|
1589
|
-
input.runId,
|
|
1590
|
-
isToolExecuteResult(result) ? serializeToolExecuteResult(result) : result,
|
|
1591
|
-
ownedLeaseId,
|
|
1592
|
-
);
|
|
1593
|
-
logPhase('complete', completeStartedAt);
|
|
1594
|
-
if (
|
|
1595
|
-
!completed ||
|
|
1596
|
-
(completed.status !== 'completed' && completed.status !== 'skipped')
|
|
1597
|
-
) {
|
|
1598
|
-
const latest = await input.store.get(input.receiptKey);
|
|
1599
|
-
if (
|
|
1600
|
-
latest &&
|
|
1601
|
-
(latest.status === 'completed' || latest.status === 'skipped')
|
|
1602
|
-
) {
|
|
1603
|
-
input.log(
|
|
1604
|
-
`ctx.${input.operation}(${input.id}): receipt completion response reconciled from durable store read-back`,
|
|
959
|
+
`ctx.${input.operation}(${input.id}): completed receipt cache insert failed.`,
|
|
1605
960
|
);
|
|
1606
|
-
return await recoverCompletedReceipt(latest, 'owner');
|
|
1607
961
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
throw new Error(
|
|
1611
|
-
`ctx.${input.operation}(${input.id}): lost durable receipt ownership before completion.`,
|
|
1612
|
-
);
|
|
1613
|
-
}
|
|
1614
|
-
if (
|
|
1615
|
-
completed &&
|
|
1616
|
-
(completed.status === 'completed' || completed.status === 'skipped')
|
|
1617
|
-
) {
|
|
1618
|
-
logPhase('total', lifecycleStartedAt);
|
|
1619
|
-
// Completion endpoints acknowledge ownership and status only. The caller
|
|
1620
|
-
// already owns the live value; replay reads are the only path that needs
|
|
1621
|
-
// the receipt payload returned over the transport.
|
|
1622
|
-
if (completed.output === undefined) return result;
|
|
1623
|
-
return await recoverCompletedReceipt(completed, 'owner');
|
|
1624
|
-
}
|
|
1625
|
-
if (input.store.canPersistCompletion) {
|
|
1626
|
-
throw new Error(
|
|
1627
|
-
`ctx.${input.operation}(${input.id}): lost durable receipt ownership before completion.`,
|
|
962
|
+
input.log(
|
|
963
|
+
`ctx.${input.operation}(${input.id}): executed and converged through immutable publication`,
|
|
1628
964
|
);
|
|
965
|
+
return result;
|
|
966
|
+
} finally {
|
|
967
|
+
await releaseOwnLock();
|
|
1629
968
|
}
|
|
1630
|
-
return result;
|
|
1631
969
|
}
|