deepline 0.3.284 → 0.3.286

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.
Files changed (46) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +1 -2
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +26 -2
  4. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +7 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +836 -2234
  6. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +19 -6
  7. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +127 -789
  8. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/job.ts +18 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +16 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +180 -14
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +3 -22
  13. package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +26 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +59 -8
  15. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +122 -18
  16. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +458 -130
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +60 -9
  18. package/dist/bundling-sources/shared_libs/play-runtime/test-async-batching.ts +4 -1
  19. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +13 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +181 -326
  21. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +38 -151
  22. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +8 -0
  24. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +10 -1
  25. package/dist/bundling-sources/shared_libs/plays/enrich-compat-adapter.ts +2 -0
  26. package/dist/cli/index.js +30579 -34364
  27. package/dist/cli/index.mjs +30647 -34443
  28. package/dist/{client-BCXQ_Ftc.d.ts → client-B7ic-kY5.d.ts} +6 -3
  29. package/dist/{client-B0AjIkgb.d.mts → client-Dzf_KovW.d.mts} +6 -3
  30. package/dist/{compiler-manifest-B1zo3igW.d.mts → compiler-manifest-9SQtUuKb.d.mts} +1 -1
  31. package/dist/{compiler-manifest-B1zo3igW.d.ts → compiler-manifest-9SQtUuKb.d.ts} +1 -1
  32. package/dist/index.d.mts +4 -4
  33. package/dist/index.d.ts +4 -4
  34. package/dist/index.js +34 -4
  35. package/dist/index.mjs +34 -4
  36. package/dist/install-integrity.json +4 -4
  37. package/dist/plays/bundle-play-file.d.mts +2 -2
  38. package/dist/plays/bundle-play-file.d.ts +2 -2
  39. package/dist/plays/bundle-play-file.mjs +8 -1
  40. package/dist/plays/run-request.d.mts +2 -2
  41. package/dist/plays/run-request.d.ts +2 -2
  42. package/dist/release.d.mts +1 -1
  43. package/dist/release.d.ts +1 -1
  44. package/dist/release.js +1 -1
  45. package/dist/release.mjs +1 -1
  46. 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
- // A running receipt owned elsewhere is re-read with exponential backoff up to
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 before re-reading a running receipt on the given attempt (1-based). */
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 throwIfReceiptWaitAborted(signal?: AbortSignal): void {
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, reject) => {
774
- const timeout = setTimeout(finish, delayMs);
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
- /** New receipt-cache path: read completed facts, execute without creating an
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, source)
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.completedCacheOnly === true) {
1127
- const withCompletedReceiptHydration = async <Result>(
1128
- hydrate: () => Promise<Result>,
1129
- ): Promise<Result> =>
1130
- input.withCompletedReceiptHydration
1131
- ? await input.withCompletedReceiptHydration(hydrate)
1132
- : await hydrate();
1133
- const inspectCompletedReceipt = async (
1134
- read: () => Promise<RuntimeStepReceipt | null>,
1135
- source: DurableReceiptRecoverySource = 'cache',
1136
- ): Promise<{ kind: 'recovered'; output: T } | { kind: 'unresolved' }> => {
1137
- const inspect = async () => {
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
- if (input.force !== true) {
1150
- const cached = await inspectCompletedReceipt(() =>
1151
- input.store.get(input.receiptKey),
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 needsLock = input.requiresExecutionLock === true;
1156
- const ownerExecutionId = `${input.runId}:${crypto.randomUUID()}`;
1157
- let ownsLock = false;
1158
- let lockSupervisor: ReturnType<
1159
- typeof createRuntimeReceiptHeartbeatSupervisor
1160
- > | null = null;
1161
- const executionAbortController = needsLock ? new AbortController() : null;
1162
- if (needsLock) {
1163
- if (
1164
- !input.store.acquireExecutionLock ||
1165
- !input.store.releaseExecutionLock
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
- const deadline = Date.now() + DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS;
1172
- let completedWaitAttempt = 0;
1173
- while (!ownsLock) {
1174
- const lock = await input.store.acquireExecutionLock({
1175
- receiptKey: input.receiptKey,
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: input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS,
893
+ ttlMs: lockTtlMs,
1178
894
  });
1179
- ownsLock = lock?.ownerExecutionId === ownerExecutionId;
1180
- if (ownsLock) break;
1181
- if (input.force !== true) {
1182
- const winner = await inspectCompletedReceipt(
1183
- () => input.store.get(input.receiptKey),
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
- const lockTtlMs =
1213
- input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS;
1214
- lockSupervisor = createRuntimeReceiptHeartbeatSupervisor({
1215
- intervalMs:
1216
- input.heartbeatIntervalMs ??
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
- const winner = await inspectCompletedReceipt(
1300
- () => input.store.get(input.receiptKey),
1301
- 'owner',
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
- let ownedLeaseId: string | null = null;
1313
- let ownedLeaseExpiresAt: string | null = null;
1314
-
1315
- const reclaimReceipt = async (): Promise<
1316
- { kind: 'recovered'; output: T } | { kind: 'claimed' }
1317
- > => {
1318
- const reclaimed = await input.store.claim(
1319
- input.receiptKey,
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
- // New runners treat a claimed, nonterminal receipt as active. Keep the
1473
- // mark-running transition as an explicit compatibility option for older
1474
- // stores during their migration window, but do not pay the extra durable
1475
- // mutation on the normal claim -> terminal path.
1476
- if (input.markRunningBeforeExecute === true && input.store.markRunning) {
1477
- const markRunningStartedAt = Date.now();
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
- } catch (error) {
1522
- if (input.shouldPersistFailure?.(error) === false) {
1523
- // This error is deliberately not a durable execution result (for
1524
- // example, an HTTP non-2xx response under the current ctx.fetch
1525
- // contract). Release only the lease we still own so a later run can
1526
- // make a fresh, idempotency-protected attempt rather than inheriting a
1527
- // running or failed receipt.
1528
- const released = await input.store.release(
1529
- input.receiptKey,
1530
- input.runId,
1531
- ownedLeaseId,
1532
- );
1533
- if (
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
- input.formatError(error),
1574
- ownedLeaseId,
1575
- runtimeReceiptFailureKindForError(error),
1576
- serializeToolExecutionFailure(error),
950
+ isToolExecuteResult(result) ? serializeToolExecuteResult(result) : result,
951
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
1577
952
  );
1578
- if (!failed && input.store.canPersistFailure) {
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}): execution failed and failed receipt could not be persisted: ${input.formatError(error)}`,
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
- if (!completed && input.store.canPersistCompletion) {
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
  }