@gajae-code/coding-agent 0.18.5 → 0.18.7

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 (133) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli-commands.d.ts +10 -0
  4. package/dist/types/cli-ordinary.d.ts +13 -0
  5. package/dist/types/cli.d.ts +12 -3
  6. package/dist/types/commands/contribution-prep.d.ts +0 -3
  7. package/dist/types/commands/sdk.d.ts +3 -0
  8. package/dist/types/commands/update.d.ts +1 -0
  9. package/dist/types/config/model-profile-activation.d.ts +19 -0
  10. package/dist/types/config/settings.d.ts +4 -0
  11. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  12. package/dist/types/discovery/helpers.d.ts +2 -2
  13. package/dist/types/edit/modes/replace.d.ts +3 -3
  14. package/dist/types/edit/streaming.d.ts +2 -2
  15. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  16. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  17. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  18. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  20. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  21. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  22. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  23. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  24. package/dist/types/sdk/bus/telegram-daemon.d.ts +4 -2
  25. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  26. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  27. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  28. package/dist/types/sdk/host/host.d.ts +6 -0
  29. package/dist/types/sdk/index.d.ts +1 -0
  30. package/dist/types/sdk/router/session-router.d.ts +9 -0
  31. package/dist/types/sdk/session.d.ts +13 -0
  32. package/dist/types/session/agent-session.d.ts +5 -1
  33. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  34. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  35. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  36. package/dist/types/session/session-retirement.d.ts +47 -0
  37. package/dist/types/session/session-storage.d.ts +0 -46
  38. package/dist/types/session/session-transcript-header.d.ts +2 -0
  39. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  40. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  41. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  42. package/dist/types/tools/ask-contract.d.ts +75 -254
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/package.json +7 -7
  45. package/scripts/build-sdk-package-smoke.ts +5 -0
  46. package/scripts/resident-memory-bench.ts +174 -20
  47. package/src/capability/index.ts +1 -0
  48. package/src/capability/types.ts +4 -0
  49. package/src/cli/config-cli.ts +8 -18
  50. package/src/cli/public-command-entry.ts +5 -0
  51. package/src/cli/ssh-cli.ts +6 -3
  52. package/src/cli/web-search-cli.ts +9 -3
  53. package/src/cli-commands.ts +14 -0
  54. package/src/cli-main.ts +7 -8
  55. package/src/cli-ordinary.ts +197 -0
  56. package/src/cli.ts +25 -182
  57. package/src/commands/contribution-prep.ts +1 -3
  58. package/src/commands/sdk.ts +171 -21
  59. package/src/commands/update.ts +15 -1
  60. package/src/config/file-lock.ts +91 -2
  61. package/src/config/model-profile-activation.ts +70 -4
  62. package/src/config/model-profile-contract.ts +4 -1
  63. package/src/config/model-profiles.ts +23 -31
  64. package/src/config/model-resolver.ts +9 -1
  65. package/src/config/settings.ts +44 -4
  66. package/src/coordinator-mcp/event-webhook.ts +19 -14
  67. package/src/coordinator-mcp/policy.ts +14 -4
  68. package/src/coordinator-mcp/server.ts +142 -7
  69. package/src/coordinator-mcp/session-reaper.ts +9 -1
  70. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  71. package/src/discovery/helpers.ts +9 -3
  72. package/src/discovery/ssh.ts +8 -3
  73. package/src/edit/modes/replace.ts +6 -6
  74. package/src/edit/streaming.ts +15 -15
  75. package/src/exec/bash-shell-supervisor.ts +60 -3
  76. package/src/gjc-runtime/gc-runtime.ts +2 -1
  77. package/src/hindsight/config.ts +9 -3
  78. package/src/internal-urls/docs-index.generated.ts +11 -10
  79. package/src/memories/index.ts +8 -2
  80. package/src/modes/acp/acp-agent.ts +724 -370
  81. package/src/modes/components/tool-execution.ts +1 -1
  82. package/src/prompts/tools/ask.md +2 -0
  83. package/src/runtime-mcp/client.ts +13 -4
  84. package/src/runtime-mcp/elicitation.ts +142 -0
  85. package/src/sdk/broker/broker.ts +54 -1
  86. package/src/sdk/broker/daemon-entry.ts +67 -6
  87. package/src/sdk/broker/discovery.ts +8 -0
  88. package/src/sdk/broker/ensure.ts +156 -20
  89. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  90. package/src/sdk/broker/lifecycle.ts +334 -348
  91. package/src/sdk/broker/runtime.ts +5 -1
  92. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  93. package/src/sdk/broker/transport.ts +5 -1
  94. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  95. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  96. package/src/sdk/bus/index.ts +3 -3
  97. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  98. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  99. package/src/sdk/bus/telegram-daemon.ts +38 -3
  100. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  101. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  102. package/src/sdk/host/control/dispatch.ts +52 -4
  103. package/src/sdk/host/control/operations.ts +1 -1
  104. package/src/sdk/host/host.ts +6 -0
  105. package/src/sdk/host/session-runtime.test.ts +83 -4
  106. package/src/sdk/host/session-runtime.ts +15 -7
  107. package/src/sdk/index.ts +1 -0
  108. package/src/sdk/lifecycle-session.ts +1 -0
  109. package/src/sdk/mcp/server.ts +15 -3
  110. package/src/sdk/protocol/operation-registry.ts +1 -1
  111. package/src/sdk/router/session-router.ts +26 -3
  112. package/src/sdk/session.ts +51 -8
  113. package/src/session/agent-session.ts +169 -43
  114. package/src/session/blob-store.ts +78 -33
  115. package/src/session/internal/managed-session-storage.ts +203 -4
  116. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  117. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  118. package/src/session/session-retirement.ts +155 -0
  119. package/src/session/session-storage.ts +1 -160
  120. package/src/session/session-transcript-header.ts +17 -0
  121. package/src/session/task-artifact-owner-codec.ts +578 -0
  122. package/src/session/task-artifact-owner-retirement.ts +475 -0
  123. package/src/session/task-artifact-owner.ts +289 -0
  124. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  125. package/src/setup/hermes-setup.ts +4 -1
  126. package/src/slash-commands/builtin-registry.ts +8 -5
  127. package/src/tools/ask-contract.test.ts +76 -1
  128. package/src/tools/ask-contract.ts +57 -6
  129. package/src/tools/ask.ts +16 -7
  130. package/src/tools/bash.ts +38 -29
  131. package/src/tools/descriptors.test.ts +29 -0
  132. package/src/tools/tool-catalog.generated.ts +7 -4
  133. package/src/web/search/index.ts +2 -2
@@ -1667,9 +1667,9 @@ export async function reapDeadLifecycleMarkers(
1667
1667
  let inspected = 0;
1668
1668
  for await (const entry of directoryHandle) {
1669
1669
  if (inspected >= inspectionLimit) break;
1670
- inspected += 1;
1671
1670
  const name = entry.name;
1672
1671
  if (!entry.isFile() || !name.endsWith(".lifecycle.json")) continue;
1672
+ inspected += 1;
1673
1673
  const id = name.slice(0, -".lifecycle.json".length);
1674
1674
  if (!isCanonicalSessionId(id)) continue;
1675
1675
  const markerPath = path.join(directory, name);
@@ -1741,6 +1741,108 @@ export async function reapDeadLifecycleMarkers(
1741
1741
  return reaped;
1742
1742
  }
1743
1743
 
1744
+ /**
1745
+ * Retires one launch id's lifecycle marker pair after its recorded owner has
1746
+ * exited. Unlike the bounded background sweep, this launch-local repair is
1747
+ * not age-gated and does not require the ready sibling to contain the same
1748
+ * effect marker: the primary marker is the owner authority for this id.
1749
+ */
1750
+ type RetireExitedLifecycleMarkerPairHook = () => void | Promise<void>;
1751
+
1752
+ async function retireExitedLifecycleMarkerPairImpl(
1753
+ root: string,
1754
+ id: string,
1755
+ afterObservation?: RetireExitedLifecycleMarkerPairHook,
1756
+ ): Promise<boolean> {
1757
+ if (!isCanonicalSessionId(id)) return false;
1758
+ let directory: string;
1759
+ let directoryIdentity: { dev: bigint; ino: bigint };
1760
+ try {
1761
+ const canonicalRoot = fsSync.realpathSync(root);
1762
+ directory = path.join(canonicalRoot, "sdk");
1763
+ const directoryStat = fsSync.lstatSync(directory, { bigint: true });
1764
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) return false;
1765
+ directoryIdentity = { dev: directoryStat.dev, ino: directoryStat.ino };
1766
+ } catch {
1767
+ return false;
1768
+ }
1769
+ const markerPath = lifecycleMarkerPath(path.dirname(directory), id);
1770
+ const primary = captureLifecycleFile(markerPath, true, true);
1771
+ if (!primary) return false;
1772
+ let marker: EffectMarker;
1773
+ try {
1774
+ const parsed = parseLifecycleJson(primary.bytes);
1775
+ if (!isExactEffectMarker(parsed)) return false;
1776
+ marker = parsed;
1777
+ } catch {
1778
+ return false;
1779
+ }
1780
+ if (observeProcess(marker.pid, marker.incarnation) !== "exited") return false;
1781
+ await afterObservation?.();
1782
+ const readyPath = lifecycleReadyPath(path.dirname(directory), id);
1783
+ const ready = captureLifecycleFile(readyPath, true, true);
1784
+ try {
1785
+ const currentParent = lifecycleParentIdentity(directory);
1786
+ if (
1787
+ !currentParent ||
1788
+ BigInt(currentParent.dev) !== directoryIdentity.dev ||
1789
+ BigInt(currentParent.ino) !== directoryIdentity.ino
1790
+ )
1791
+ return false;
1792
+ const currentPrimary = captureLifecycleFile(markerPath, true, true);
1793
+ await afterObservation?.();
1794
+ if (
1795
+ !currentPrimary ||
1796
+ !sameLifecycleCleanupIdentity(
1797
+ currentPrimary.identity,
1798
+ serializeCleanupIdentity({ ...primary.identity, size: Number(primary.identity.size) }),
1799
+ )
1800
+ )
1801
+ return false;
1802
+ const currentMarker = parseLifecycleJson(currentPrimary.bytes);
1803
+ if (!isExactEffectMarker(currentMarker) || !sameEffectMarker(currentMarker, marker)) return false;
1804
+ if (ready) {
1805
+ const currentReady = captureLifecycleFile(readyPath, true, true);
1806
+ if (
1807
+ !currentReady ||
1808
+ !sameLifecycleCleanupIdentity(
1809
+ currentReady.identity,
1810
+ serializeCleanupIdentity({ ...ready.identity, size: Number(ready.identity.size) }),
1811
+ )
1812
+ )
1813
+ return false;
1814
+ const removedReady = nativeLifecycle().exactUnlinkDirect(readyPath, {
1815
+ ...ready.identity,
1816
+ parentDev: directoryIdentity.dev,
1817
+ parentIno: directoryIdentity.ino,
1818
+ quarantineName: `.gjc-reap-${randomUUID()}-${path.basename(readyPath)}`,
1819
+ });
1820
+ if (!removedReady.ok && removedReady.code !== "not_found") return false;
1821
+ }
1822
+ const removedMarker = nativeLifecycle().exactUnlinkDirect(markerPath, {
1823
+ ...primary.identity,
1824
+ parentDev: directoryIdentity.dev,
1825
+ parentIno: directoryIdentity.ino,
1826
+ quarantineName: `.gjc-reap-${randomUUID()}-${path.basename(markerPath)}`,
1827
+ });
1828
+ return removedMarker.ok || removedMarker.code === "not_found";
1829
+ } catch {
1830
+ return false;
1831
+ }
1832
+ }
1833
+
1834
+ export async function retireExitedLifecycleMarkerPair(root: string, id: string): Promise<boolean> {
1835
+ return retireExitedLifecycleMarkerPairImpl(root, id);
1836
+ }
1837
+
1838
+ export async function retireExitedLifecycleMarkerPairForTest(
1839
+ root: string,
1840
+ id: string,
1841
+ afterObservation: RetireExitedLifecycleMarkerPairHook,
1842
+ ): Promise<boolean> {
1843
+ return retireExitedLifecycleMarkerPairImpl(root, id, afterObservation);
1844
+ }
1845
+
1744
1846
  export async function writeEffectMarker(root: string, id: string, marker: EffectMarker): Promise<void> {
1745
1847
  const directory = path.join(root, "sdk");
1746
1848
  await fs.mkdir(directory, { recursive: true, mode: 0o700 });
@@ -4833,7 +4935,6 @@ async function terminateSpawnedChild(
4833
4935
  terminationStartDeadlineAt: number,
4834
4936
  expected: EffectMarker | undefined,
4835
4937
  timing: LifecycleTiming,
4836
- reapUnregisteredCutoff = false,
4837
4938
  ): Promise<boolean> {
4838
4939
  const pid = child.pid;
4839
4940
  if (!pid || (expected && pid !== expected.pid)) return false;
@@ -4847,226 +4948,100 @@ async function terminateSpawnedChild(
4847
4948
  await broker.index.refresh();
4848
4949
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4849
4950
  const registered = expected ? broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) : false;
4850
- // Reserve a quarter of the termination phase for the exact post-exit proof
4851
- // and ledger-backed artifact cleanup. Registered hosts retain their deadline.
4852
- const postExitProofReserveMs = Math.max(POLL_MS, Math.floor((deadline - terminationStartDeadlineAt) / 4));
4853
- const processExitDeadlineAt = expected && !registered ? deadline - postExitProofReserveMs : deadline;
4951
+ // Keep one poll interval inside the lifecycle deadline for the final exact
4952
+ // cleanup proof. Registered sessions retain the original deadline partition;
4953
+ // only an unregistered child needs this extra bounded margin.
4954
+ const processExitDeadlineAt = expected && !registered ? deadline - POLL_MS : deadline;
4854
4955
  const unregisteredTerminationDeadlineAt =
4855
4956
  processExitDeadlineAt - Math.max(POLL_MS, Math.floor((processExitDeadlineAt - terminationStartDeadlineAt) / 2));
4856
4957
  const observe = (): ProcessObservation =>
4857
- child.exitCode !== null || child.signalCode !== null
4958
+ child.exitCode !== null
4858
4959
  ? "exited"
4859
4960
  : observeProcess(pid, incarnation, value => processIncarnationForBroker(broker, value));
4860
- let ownedExitObserved = child.exitCode !== null || child.signalCode !== null;
4861
- const ownedExit = Promise.withResolvers<void>();
4862
- const observeOwnedExit = (): void => {
4863
- ownedExitObserved = true;
4864
- ownedExit.resolve();
4865
- };
4866
- child.once("exit", observeOwnedExit);
4867
- const cleanupOwnedExitListeners = (): void => {
4868
- child.removeListener("exit", observeOwnedExit);
4869
- };
4870
- try {
4871
- const waitForExit = async (until: number): Promise<ProcessObservation> => {
4872
- let observation = observe();
4873
- while (observation !== "exited" && timing.now() < until) {
4874
- await Promise.race([ownedExit.promise, timing.sleep(Math.max(0, Math.min(POLL_MS, until - timing.now())))]);
4875
- if (ownedExitObserved) observation = "exited";
4876
- else observation = observe();
4877
- }
4878
-
4879
- return observation;
4880
- };
4881
-
4961
+ const waitForExit = async (until: number): Promise<ProcessObservation> => {
4882
4962
  let observation = observe();
4883
- const recheckOwnedExitObservation = async (): Promise<void> => {
4884
- if (observation !== "uncertain") return;
4885
- // This direct ChildProcess is owned by this broker invocation. A process-incarnation
4886
- // read can briefly lag its exit event, so recheck only this owned child before failing.
4887
- observation = await waitForExit(processExitDeadlineAt);
4888
- };
4889
- if (observation === "alive") {
4890
- if (expected && !registered) {
4891
- await waitUntil(timing, terminationStartDeadlineAt);
4892
- // A child with a published cutoff receipt owns its rollback. Without one,
4893
- // waiting for a blocked module to publish is wasted signal/exit time.
4894
- const failure = await readLifecycleFailureArtifact(
4895
- lifecycleFailurePath(root, id, expected.effectMarker),
4896
- expected,
4897
- );
4898
- if (failure) await waitUntil(timing, unregisteredTerminationDeadlineAt);
4899
- } else {
4900
- await waitUntil(timing, terminationStartDeadlineAt);
4901
- }
4963
+ while (observation !== "exited" && timing.now() < until) {
4964
+ await timing.sleep(Math.max(0, Math.min(POLL_MS, until - timing.now())));
4902
4965
  observation = observe();
4903
4966
  }
4904
- if (observation === "alive") {
4905
- const signalResult =
4906
- expected && !registered
4907
- ? child.kill("SIGTERM")
4908
- : await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGTERM", expected);
4909
- if (!signalResult) {
4910
- observation = observe();
4911
- await recheckOwnedExitObservation();
4912
- if (observation !== "exited") {
4913
- await recordTerminalUncertain(broker, id, root, pid);
4914
- return false;
4915
- }
4916
- } else {
4917
- const remaining = Math.max(0, processExitDeadlineAt - timing.now());
4918
- const gracefulDeadline =
4919
- timing.now() +
4920
- Math.min(CLOSE_TIMEOUT_MS, Math.floor(remaining * (expected && !registered ? 0.15 : 0.5)));
4921
- observation = await waitForExit(gracefulDeadline);
4967
+
4968
+ return observation;
4969
+ };
4970
+
4971
+ let observation = observe();
4972
+ const recheckOwnedExitObservation = async (): Promise<void> => {
4973
+ if (observation !== "uncertain") return;
4974
+ // This direct ChildProcess is owned by this broker invocation. A process-incarnation
4975
+ // read can briefly lag its exit event, so recheck only this owned child before failing.
4976
+ observation = await waitForExit(processExitDeadlineAt);
4977
+ };
4978
+ if (observation === "alive") {
4979
+ if (expected && !registered) {
4980
+ // A child that has not registered yet owns the cutoff receipt. Give it
4981
+ // the bounded pre-registration window to publish that proof, but reserve
4982
+ // the final proof interval for post-signal observation inside the request
4983
+ // deadline. A valid receipt does not interrupt the child's own rollback.
4984
+ while (timing.now() < unregisteredTerminationDeadlineAt) {
4985
+ if (await readLifecycleFailureArtifact(lifecycleFailurePath(root, id, expected.effectMarker), expected))
4986
+ await waitUntil(timing, unregisteredTerminationDeadlineAt);
4987
+ else await timing.sleep(Math.max(0, Math.min(POLL_MS, unregisteredTerminationDeadlineAt - timing.now())));
4922
4988
  }
4989
+ } else {
4990
+ await waitUntil(timing, terminationStartDeadlineAt);
4923
4991
  }
4924
- if (observation === "alive") {
4925
- const signalResult =
4926
- expected && !registered
4927
- ? child.kill("SIGKILL")
4928
- : await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGKILL", expected);
4929
- if (!signalResult) {
4930
- observation = observe();
4931
- await recheckOwnedExitObservation();
4932
- if (observation !== "exited") {
4933
- await recordTerminalUncertain(broker, id, root, pid);
4934
- return false;
4935
- }
4936
- } else {
4937
- observation = await waitForExit(processExitDeadlineAt);
4992
+ observation = observe();
4993
+ }
4994
+ if (observation === "alive") {
4995
+ if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGTERM", expected))) {
4996
+ observation = observe();
4997
+ await recheckOwnedExitObservation();
4998
+ if (observation !== "exited") {
4999
+ await recordTerminalUncertain(broker, id, root, pid);
5000
+ return false;
4938
5001
  }
5002
+ } else {
5003
+ const remaining = Math.max(0, processExitDeadlineAt - timing.now());
5004
+ const gracefulDeadline = timing.now() + Math.min(CLOSE_TIMEOUT_MS, Math.floor(remaining / 2));
5005
+ observation = await waitForExit(gracefulDeadline);
4939
5006
  }
4940
- await recheckOwnedExitObservation();
4941
- if (observation !== "exited" || !lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4942
- let rollbackGeneration: number | null | undefined;
4943
- if (expected) {
4944
- const failure = await readLifecycleFailureArtifact(
4945
- lifecycleFailurePath(root, id, expected.effectMarker),
4946
- expected,
4947
- );
4948
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4949
- const rollbackComplete =
4950
- failure?.artifact.rollback.fenced === true &&
4951
- failure.artifact.rollback.runtimeRemoved &&
4952
- failure.artifact.rollback.hostStopped &&
4953
- failure.artifact.rollback.brokerRegistrationReleased;
4954
- if (failure && expected && !rollbackComplete) {
4955
- await broker.index.refresh();
4956
- const hasRegistration = broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
4957
- if (!hasRegistration) {
4958
- const artifactsRemoved = await removeOwnedLifecycleArtifacts(
4959
- root,
4960
- id,
4961
- expected,
4962
- undefined,
4963
- undefined,
4964
- proofBudget,
4965
- );
4966
- const endpointGone = await endpointRemoved(root, id);
4967
- // A signal-delivery error can be emitted while the child still runs. Recheck
4968
- // exact process absence after cleanup before reporting a proven reap.
4969
- if (
4970
- artifactsRemoved &&
4971
- endpointGone &&
4972
- (observe() === "exited" || ownedExitObserved) &&
4973
- lifecycleProofWithinDeadline(proofBudget)
4974
- )
4975
- return true;
4976
- return failClosed();
4977
- }
5007
+ }
5008
+ if (observation === "alive") {
5009
+ if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGKILL", expected))) {
5010
+ observation = observe();
5011
+ await recheckOwnedExitObservation();
5012
+ if (observation !== "exited") {
5013
+ await recordTerminalUncertain(broker, id, root, pid);
5014
+ return false;
4978
5015
  }
4979
- if (!rollbackComplete) {
4980
- if (!readyThenExitToleranceEnabled()) {
4981
- // A child that exited with a non-zero status before publishing any
4982
- // lifecycle receipt has no durable rollback to wait for. Prove the
4983
- // owned artifacts are gone and release its registration, but retain
4984
- // terminal uncertainty for timeouts, clean exits, and child-authored
4985
- // startup receipts whose rollback is incomplete.
4986
- await broker.index.refresh();
4987
- const uncertainRegistration = broker.index
4988
- .listSessions()
4989
- .sessions.some(
4990
- session => session.sessionId === id && session.pid === pid && session.terminalUncertain === true,
4991
- );
4992
- if (
4993
- failure ||
4994
- uncertainRegistration ||
4995
- (child.exitCode === null && child.signalCode === null) ||
4996
- child.exitCode === 0
4997
- )
4998
- return failClosed();
4999
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5000
- const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
5001
- if (publishedReady) return failClosed();
5002
- if (reapUnregisteredCutoff && !(await endpointRemoved(root, id))) return failClosed();
5003
- const artifactsRemoved = await removeOwnedLifecycleArtifacts(
5004
- root,
5005
- id,
5006
- expected,
5007
- undefined,
5008
- undefined,
5009
- proofBudget,
5010
- );
5011
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5012
- await broker.index.refresh();
5013
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5014
- const registered = broker.index
5015
- .listSessions()
5016
- .sessions.find(session => session.sessionId === id && session.pid === pid);
5017
- let registrationReleased =
5018
- !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) ||
5019
- registered?.terminal === true ||
5020
- registered?.terminalUncertain === true;
5021
- if (registered && !registrationReleased) {
5022
- registrationReleased = await broker.index.unregisterIfCurrent(registered);
5023
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5024
- await broker.index.refresh();
5025
- registrationReleased =
5026
- registrationReleased ||
5027
- !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
5028
- }
5029
- const stillExited =
5030
- observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
5031
- "exited";
5032
- const endpointGone = await endpointRemoved(root, id);
5033
- if (
5034
- stillExited &&
5035
- artifactsRemoved &&
5036
- endpointGone &&
5037
- registrationReleased &&
5038
- registered?.terminalUncertain !== true &&
5039
- lifecycleProofWithinDeadline(proofBudget)
5040
- ) {
5041
- if (!reapUnregisteredCutoff) return true;
5042
- try {
5043
- await writeSessionLifecycleFailure(
5044
- root,
5045
- id,
5046
- expected.effectMarker,
5047
- { phase: "startup", reason: "failed", message: `Session ${id} exited at readiness cutoff.` },
5048
- {
5049
- endpointGeneration: null,
5050
- fenced: true,
5051
- runtimeRemoved: true,
5052
- hostStopped: true,
5053
- brokerRegistrationReleased: true,
5054
- },
5055
- undefined,
5056
- expected.incarnation,
5057
- expected.pid,
5058
- );
5059
- } catch {
5060
- return failClosed();
5061
- }
5062
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5063
- return true;
5064
- }
5065
- return failClosed();
5066
- }
5016
+ } else {
5017
+ observation = await waitForExit(processExitDeadlineAt);
5018
+ }
5019
+ }
5020
+ await recheckOwnedExitObservation();
5021
+ if (observation !== "exited" || !lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5022
+ let rollbackGeneration: number | null | undefined;
5023
+ if (expected) {
5024
+ const failure = await readLifecycleFailureArtifact(
5025
+ lifecycleFailurePath(root, id, expected.effectMarker),
5026
+ expected,
5027
+ );
5028
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5029
+ const rollbackComplete =
5030
+ failure?.artifact.rollback.fenced === true &&
5031
+ failure.artifact.rollback.runtimeRemoved &&
5032
+ failure.artifact.rollback.hostStopped &&
5033
+ failure.artifact.rollback.brokerRegistrationReleased;
5034
+ if (!rollbackComplete) {
5035
+ if (!readyThenExitToleranceEnabled()) {
5036
+ // A child that exited with a non-zero status before publishing any
5037
+ // lifecycle receipt has no durable rollback to wait for. Prove the
5038
+ // owned artifacts are gone and release its registration, but retain
5039
+ // terminal uncertainty for timeouts, clean exits, and child-authored
5040
+ // startup receipts whose rollback is incomplete.
5041
+ if (failure || child.exitCode === null || child.exitCode === 0) return failClosed();
5067
5042
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5068
5043
  const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
5069
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5044
+ if (publishedReady) return failClosed();
5070
5045
  const artifactsRemoved = await removeOwnedLifecycleArtifacts(
5071
5046
  root,
5072
5047
  id,
@@ -5081,50 +5056,21 @@ async function terminateSpawnedChild(
5081
5056
  const registered = broker.index
5082
5057
  .listSessions()
5083
5058
  .sessions.find(session => session.sessionId === id && session.pid === pid);
5084
- const registeredRowTerminal = registered?.terminal === true || registered?.terminalUncertain === true;
5085
5059
  let registrationReleased =
5086
- !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) || registeredRowTerminal;
5060
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) ||
5061
+ registered?.terminal === true ||
5062
+ registered?.terminalUncertain === true;
5087
5063
  if (registered && !registrationReleased) {
5088
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5089
5064
  registrationReleased = await broker.index.unregisterIfCurrent(registered);
5090
5065
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5091
5066
  await broker.index.refresh();
5092
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5093
5067
  registrationReleased =
5094
5068
  registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
5095
5069
  }
5096
5070
  const stillExited =
5097
5071
  observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
5098
5072
  "exited";
5099
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5100
5073
  const endpointGone = await endpointRemoved(root, id);
5101
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5102
- try {
5103
- if (publishedReady && !failure) {
5104
- await writeSessionLifecycleFailure(
5105
- root,
5106
- id,
5107
- expected.effectMarker,
5108
- {
5109
- phase: "startup",
5110
- reason: "failed",
5111
- message: `Session ${id} ${READY_THEN_EXIT_MESSAGE}.`,
5112
- },
5113
- {
5114
- endpointGeneration: registered?.endpointGeneration ?? null,
5115
- fenced: stillExited && registrationReleased,
5116
- runtimeRemoved: artifactsRemoved && endpointGone,
5117
- hostStopped: stillExited,
5118
- brokerRegistrationReleased: registrationReleased,
5119
- },
5120
- undefined,
5121
- expected.incarnation,
5122
- expected.pid,
5123
- );
5124
- }
5125
- } catch {
5126
- // A missing broker-authored receipt must not hide a proven dead child.
5127
- }
5128
5074
  if (
5129
5075
  stillExited &&
5130
5076
  artifactsRemoved &&
@@ -5135,35 +5081,102 @@ async function terminateSpawnedChild(
5135
5081
  return true;
5136
5082
  return failClosed();
5137
5083
  }
5138
- rollbackGeneration = failure.artifact.rollback.endpointGeneration;
5139
- }
5140
- if (expected && !(await removeOwnedLifecycleArtifacts(root, id, expected, undefined, undefined, proofBudget)))
5141
- return failClosed();
5142
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5143
- await broker.index.refresh();
5144
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5145
- if (
5146
- rollbackGeneration === null &&
5147
- expected &&
5148
- !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker)
5149
- )
5150
- return lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5151
- const registeredBeforeTermination =
5152
- rollbackGeneration === undefined || rollbackGeneration === null
5153
- ? undefined
5154
- : broker.index.findHostRegistration(id, rollbackGeneration, pid, expected?.effectMarker);
5155
- const unregistered = registeredBeforeTermination
5156
- ? broker.index.hostUnregisteredAfter(registeredBeforeTermination)
5157
- : undefined;
5158
- if (!registeredBeforeTermination || !unregistered) {
5084
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5085
+ const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
5086
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5087
+ const artifactsRemoved = await removeOwnedLifecycleArtifacts(
5088
+ root,
5089
+ id,
5090
+ expected,
5091
+ undefined,
5092
+ undefined,
5093
+ proofBudget,
5094
+ );
5095
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5096
+ await broker.index.refresh();
5097
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5098
+ const registered = broker.index
5099
+ .listSessions()
5100
+ .sessions.find(session => session.sessionId === id && session.pid === pid);
5101
+ const registeredRowTerminal = registered?.terminal === true || registered?.terminalUncertain === true;
5102
+ let registrationReleased =
5103
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) || registeredRowTerminal;
5104
+ if (registered && !registrationReleased) {
5105
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5106
+ registrationReleased = await broker.index.unregisterIfCurrent(registered);
5107
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5108
+ await broker.index.refresh();
5109
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5110
+ registrationReleased =
5111
+ registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
5112
+ }
5113
+ const stillExited =
5114
+ observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) === "exited";
5115
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5116
+ const endpointGone = await endpointRemoved(root, id);
5117
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5118
+ try {
5119
+ if (publishedReady && !failure) {
5120
+ await writeSessionLifecycleFailure(
5121
+ root,
5122
+ id,
5123
+ expected.effectMarker,
5124
+ {
5125
+ phase: "startup",
5126
+ reason: "failed",
5127
+ message: `Session ${id} ${READY_THEN_EXIT_MESSAGE}.`,
5128
+ },
5129
+ {
5130
+ endpointGeneration: registered?.endpointGeneration ?? null,
5131
+ fenced: stillExited && registrationReleased,
5132
+ runtimeRemoved: artifactsRemoved && endpointGone,
5133
+ hostStopped: stillExited,
5134
+ brokerRegistrationReleased: registrationReleased,
5135
+ },
5136
+ undefined,
5137
+ expected.incarnation,
5138
+ expected.pid,
5139
+ );
5140
+ }
5141
+ } catch {
5142
+ // A missing broker-authored receipt must not hide a proven dead child.
5143
+ }
5144
+ if (
5145
+ stillExited &&
5146
+ artifactsRemoved &&
5147
+ endpointGone &&
5148
+ registrationReleased &&
5149
+ lifecycleProofWithinDeadline(proofBudget)
5150
+ )
5151
+ return true;
5159
5152
  return failClosed();
5160
5153
  }
5161
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5162
- const endpointGone = await endpointRemoved(root, id);
5163
- return endpointGone && lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5164
- } finally {
5165
- cleanupOwnedExitListeners();
5154
+ rollbackGeneration = failure.artifact.rollback.endpointGeneration;
5166
5155
  }
5156
+ if (expected && !(await removeOwnedLifecycleArtifacts(root, id, expected, undefined, undefined, proofBudget)))
5157
+ return failClosed();
5158
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5159
+ await broker.index.refresh();
5160
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5161
+ if (
5162
+ rollbackGeneration === null &&
5163
+ expected &&
5164
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker)
5165
+ )
5166
+ return lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5167
+ const registeredBeforeTermination =
5168
+ rollbackGeneration === undefined || rollbackGeneration === null
5169
+ ? undefined
5170
+ : broker.index.findHostRegistration(id, rollbackGeneration, pid, expected?.effectMarker);
5171
+ const unregistered = registeredBeforeTermination
5172
+ ? broker.index.hostUnregisteredAfter(registeredBeforeTermination)
5173
+ : undefined;
5174
+ if (!registeredBeforeTermination || !unregistered) {
5175
+ return failClosed();
5176
+ }
5177
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5178
+ const endpointGone = await endpointRemoved(root, id);
5179
+ return endpointGone && lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5167
5180
  }
5168
5181
 
5169
5182
  async function signalVerifiedSession(
@@ -5481,12 +5494,11 @@ async function currentReadyAuthority(
5481
5494
  token?: unknown;
5482
5495
  pid?: unknown;
5483
5496
  };
5484
- // Native-alive owned readiness is the admission authority. `record.live`
5485
- // also requires a fresh index heartbeat projection, which can lag a just-
5486
- // registered detached Windows host. Never admit a terminal/uncertain or
5487
- // native-dead child; do not refuse a native-alive ready host for a stale live bit.
5488
- await broker.heartbeatSessions();
5489
- await broker.index.refresh();
5497
+ // Native-alive owned readiness is the admission authority. The indexed
5498
+ // projection can lag a just-registered detached Windows host, so the
5499
+ // change-stamp refresh is deliberately separate from heartbeat liveness.
5500
+ // Never admit a terminal/uncertain or native-dead child.
5501
+ await broker.index.refreshIfChanged();
5490
5502
  const record = broker.index
5491
5503
  .listSessions()
5492
5504
  .sessions.find(
@@ -6739,6 +6751,7 @@ async function executeLifecycleResponse(
6739
6751
  terminationStartDeadline = childDeadlines.terminationStartDeadlineAt;
6740
6752
  lifecycleDeadline = childDeadlines.lifecycleCleanupDeadlineAt;
6741
6753
  }
6754
+ await retireExitedLifecycleMarkerPair(launch.root, launch.id);
6742
6755
  await reapDeadLifecycleMarkers(launch.root);
6743
6756
  } catch (error) {
6744
6757
  if (launch.worktreePlan && worktreeReceipt?.created && !worktreeReceipt.reused) {
@@ -6993,7 +7006,6 @@ async function executeLifecycleResponse(
6993
7006
  knownSecrets,
6994
7007
  )
6995
7008
  : undefined;
6996
- const ledgerUncertain = broker.ledger.get(identity)?.state === "terminal_uncertain";
6997
7009
  const terminated = await terminateSpawnedChild(
6998
7010
  child,
6999
7011
  broker,
@@ -7003,8 +7015,8 @@ async function executeLifecycleResponse(
7003
7015
  terminationStartDeadline,
7004
7016
  spawnedAuthority,
7005
7017
  timing,
7006
- readiness.kind === "timeout",
7007
7018
  );
7019
+
7008
7020
  if (!terminated)
7009
7021
  return readiness.kind === "ready_probe_failed"
7010
7022
  ? fail(
@@ -7015,11 +7027,6 @@ async function executeLifecycleResponse(
7015
7027
  "terminal_uncertain",
7016
7028
  `Session ${launch.id} did not become ready and its spawned process could not be verified dead. ${diagnostic ?? "stage=readiness waiting_for=session_ready child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
7017
7029
  );
7018
- if (ledgerUncertain)
7019
- return fail(
7020
- "terminal_uncertain",
7021
- `Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation. Original launch failure: Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
7022
- );
7023
7030
  return readiness.kind === "startup_failed"
7024
7031
  ? fail(
7025
7032
  readiness.failure.code ?? "spawn_failed",
@@ -8016,7 +8023,6 @@ async function exactCleanupProof(
8016
8023
  evidence: { artifact: LifecycleFailureArtifact } | undefined,
8017
8024
  proofBudget?: LifecycleProofBudget,
8018
8025
  allowObservedWindowsReadyExit = false,
8019
- allowObservedUnregisteredCutoff = false,
8020
8026
  ): Promise<LifecycleCleanupProof | undefined> {
8021
8027
  const rollback = evidence?.artifact.rollback;
8022
8028
  const durableRollbackComplete =
@@ -8024,16 +8030,6 @@ async function exactCleanupProof(
8024
8030
  rollback.runtimeRemoved &&
8025
8031
  rollback.hostStopped &&
8026
8032
  rollback.brokerRegistrationReleased;
8027
- // An owned child reaped before registration cannot finish its cutoff receipt.
8028
- // The broker may retire that receipt only after independently proving process,
8029
- // endpoint, and registration absence through this exact cleanup transaction.
8030
- const observedUnregisteredCutoff =
8031
- allowObservedUnregisteredCutoff &&
8032
- rollback?.endpointGeneration === null &&
8033
- rollback.fenced === false &&
8034
- rollback.runtimeRemoved === false &&
8035
- rollback.hostStopped === false &&
8036
- rollback.brokerRegistrationReleased === false;
8037
8033
  // Windows deliberately keeps the failure receipt incomplete because its
8038
8034
  // directory-entry publication cannot be flushed. A broker-observed
8039
8035
  // ready-then-exit can still authorize the separate ledger-backed exact
@@ -8051,7 +8047,7 @@ async function exactCleanupProof(
8051
8047
  !root ||
8052
8048
  !id ||
8053
8049
  !expected ||
8054
- (!durableRollbackComplete && !observedWindowsReadyExit && !observedUnregisteredCutoff) ||
8050
+ (!durableRollbackComplete && !observedWindowsReadyExit) ||
8055
8051
  observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) !==
8056
8052
  "exited"
8057
8053
  )
@@ -8266,28 +8262,28 @@ export async function executeLifecycle(
8266
8262
  expected,
8267
8263
  evidence,
8268
8264
  proofBudget,
8269
- !response.ok && response.error.code === "ready_then_exited",
8270
- !response.ok && response.error.code === "readiness_timeout",
8265
+ !response.ok ? response.error.code === "ready_then_exited" : false,
8271
8266
  );
8272
- const startupFailure: LifecycleStartupFailureReceipt | undefined = evidence
8273
- ? {
8274
- artifactDigest: evidence.digest,
8275
- phase: evidence.artifact.phase,
8276
- reason: evidence.artifact.reason,
8277
- message: evidence.artifact.message,
8278
- ...(evidence.artifact.code === undefined
8279
- ? {}
8280
- : { code: evidence.artifact.code, details: evidence.artifact.details }),
8281
- rollback: {
8282
- endpointGeneration: evidence.artifact.rollback.endpointGeneration,
8283
- fenced: evidence.artifact.rollback.fenced,
8284
- runtimeRemoved: evidence.artifact.rollback.runtimeRemoved,
8285
- hostStopped: evidence.artifact.rollback.hostStopped,
8286
- brokerRegistrationReleased: evidence.artifact.rollback.brokerRegistrationReleased,
8287
- },
8288
- ...(cleanupProof ? { cleanupProof } : {}),
8289
- }
8290
- : undefined;
8267
+ const startupFailure: LifecycleStartupFailureReceipt | undefined =
8268
+ evidence && !response.ok
8269
+ ? {
8270
+ artifactDigest: evidence.digest,
8271
+ message: evidence.artifact.message,
8272
+ phase: evidence.artifact.phase,
8273
+ reason: evidence.artifact.reason,
8274
+ ...(evidence.artifact.code === undefined
8275
+ ? {}
8276
+ : { code: evidence.artifact.code, details: evidence.artifact.details }),
8277
+ rollback: {
8278
+ endpointGeneration: evidence.artifact.rollback.endpointGeneration,
8279
+ fenced: evidence.artifact.rollback.fenced,
8280
+ runtimeRemoved: evidence.artifact.rollback.runtimeRemoved,
8281
+ hostStopped: evidence.artifact.rollback.hostStopped,
8282
+ brokerRegistrationReleased: evidence.artifact.rollback.brokerRegistrationReleased,
8283
+ },
8284
+ ...(cleanupProof ? { cleanupProof } : {}),
8285
+ }
8286
+ : undefined;
8291
8287
  const durableEffectsBody: Omit<LifecycleDurableEffectsReceipt, "digest"> = {
8292
8288
  ...(priorDurableEffects?.worktree ? { worktree: priorDurableEffects.worktree } : {}),
8293
8289
  ...(evidence?.artifact.transcript
@@ -8367,54 +8363,44 @@ export async function executeLifecycle(
8367
8363
  ? response
8368
8364
  : response.error.code === "endpoint_unreadable"
8369
8365
  ? response
8370
- : response.error.code === "readiness_timeout" &&
8371
- cleanupProof &&
8372
- lifecycleCleanupResponse &&
8373
- !lifecycleCleanupResponse.ok &&
8374
- lifecycleCleanupResponse.error.code === "spawn_failed"
8366
+ : // An unreadable/corrupt endpoint is already the fail-closed honest
8367
+ // terminal classification (it names the artifact, not the
8368
+ // child); the reconciliation wrapper must not erase it.
8369
+ response.error.code === "ready_then_exited" && (cleanupProof || provenDeadCleanup)
8375
8370
  ? {
8376
8371
  ...response,
8377
8372
  ...(durableEffects ? { durableEffects } : {}),
8378
8373
  ...(startupFailure ? { startupFailure } : {}),
8379
8374
  }
8380
- : // An unreadable/corrupt endpoint is already the fail-closed honest
8381
- // terminal classification (it names the artifact, not the
8382
- // child); the reconciliation wrapper must not erase it.
8383
- response.error.code === "ready_then_exited" && (cleanupProof || provenDeadCleanup)
8375
+ : startupFailure && cleanupProof
8384
8376
  ? {
8385
- ...response,
8377
+ ok: false,
8378
+ error: startupFailure.code
8379
+ ? {
8380
+ code: startupFailure.code,
8381
+ message: startupFailure.message,
8382
+ details: startupFailure.details!,
8383
+ endpoint: "unavailable" as const,
8384
+ }
8385
+ : {
8386
+ code: "spawn_failed",
8387
+ message: startupFailure.message,
8388
+ endpoint: "unavailable" as const,
8389
+ },
8386
8390
  ...(durableEffects ? { durableEffects } : {}),
8387
- ...(startupFailure ? { startupFailure } : {}),
8391
+ startupFailure,
8388
8392
  }
8389
- : startupFailure && cleanupProof
8390
- ? {
8393
+ : provenDeadCleanup && response.error.code !== "terminal_uncertain"
8394
+ ? response
8395
+ : {
8391
8396
  ok: false,
8392
- error: startupFailure.code
8393
- ? {
8394
- code: startupFailure.code,
8395
- message: startupFailure.message,
8396
- details: startupFailure.details!,
8397
- endpoint: "unavailable" as const,
8398
- }
8399
- : {
8400
- code: "spawn_failed",
8401
- message: startupFailure.message,
8402
- endpoint: "unavailable" as const,
8403
- },
8397
+ error: {
8398
+ code: "terminal_uncertain",
8399
+ message: terminalUncertainStartupMessage(response),
8400
+ },
8404
8401
  ...(durableEffects ? { durableEffects } : {}),
8405
- startupFailure,
8402
+ ...(startupFailure ? { startupFailure } : {}),
8406
8403
  }
8407
- : provenDeadCleanup && response.error.code !== "terminal_uncertain"
8408
- ? response
8409
- : {
8410
- ok: false,
8411
- error: {
8412
- code: "terminal_uncertain",
8413
- message: terminalUncertainStartupMessage(response),
8414
- },
8415
- ...(durableEffects ? { durableEffects } : {}),
8416
- ...(startupFailure ? { startupFailure } : {}),
8417
- }
8418
8404
  : response;
8419
8405
  // The extended proof window may reconcile artifacts after the original
8420
8406
  // admission deadline, but cannot retroactively prove a timely spawn failure.