@gajae-code/coding-agent 0.18.5 → 0.18.6

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 (91) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/types/cli-commands.d.ts +10 -0
  3. package/dist/types/cli-ordinary.d.ts +13 -0
  4. package/dist/types/cli.d.ts +12 -3
  5. package/dist/types/commands/contribution-prep.d.ts +0 -3
  6. package/dist/types/commands/sdk.d.ts +3 -0
  7. package/dist/types/commands/update.d.ts +1 -0
  8. package/dist/types/config/model-profile-activation.d.ts +19 -0
  9. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  10. package/dist/types/edit/modes/replace.d.ts +3 -3
  11. package/dist/types/edit/streaming.d.ts +2 -2
  12. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  13. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  14. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  15. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  16. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  17. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  18. package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
  19. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  20. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  21. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  22. package/dist/types/sdk/index.d.ts +1 -0
  23. package/dist/types/sdk/router/session-router.d.ts +2 -0
  24. package/dist/types/sdk/session.d.ts +13 -0
  25. package/dist/types/session/agent-session.d.ts +5 -1
  26. package/dist/types/tools/ask-contract.d.ts +75 -254
  27. package/dist/types/tools/bash.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/scripts/build-sdk-package-smoke.ts +5 -0
  30. package/src/cli/public-command-entry.ts +5 -0
  31. package/src/cli/ssh-cli.ts +6 -3
  32. package/src/cli/web-search-cli.ts +9 -3
  33. package/src/cli-commands.ts +14 -0
  34. package/src/cli-ordinary.ts +197 -0
  35. package/src/cli.ts +25 -182
  36. package/src/commands/contribution-prep.ts +1 -3
  37. package/src/commands/sdk.ts +59 -0
  38. package/src/commands/update.ts +15 -1
  39. package/src/config/file-lock.ts +91 -2
  40. package/src/config/model-profile-activation.ts +70 -4
  41. package/src/config/model-profile-contract.ts +4 -1
  42. package/src/config/model-profiles.ts +23 -31
  43. package/src/config/model-resolver.ts +9 -1
  44. package/src/coordinator-mcp/event-webhook.ts +19 -14
  45. package/src/coordinator-mcp/policy.ts +14 -4
  46. package/src/coordinator-mcp/server.ts +142 -7
  47. package/src/coordinator-mcp/session-reaper.ts +9 -1
  48. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  49. package/src/edit/modes/replace.ts +6 -6
  50. package/src/edit/streaming.ts +15 -15
  51. package/src/exec/bash-shell-supervisor.ts +60 -3
  52. package/src/internal-urls/docs-index.generated.ts +10 -9
  53. package/src/memories/index.ts +8 -2
  54. package/src/modes/acp/acp-agent.ts +649 -367
  55. package/src/modes/components/tool-execution.ts +1 -1
  56. package/src/prompts/tools/ask.md +2 -0
  57. package/src/sdk/broker/broker.ts +46 -0
  58. package/src/sdk/broker/daemon-entry.ts +67 -6
  59. package/src/sdk/broker/discovery.ts +8 -0
  60. package/src/sdk/broker/ensure.ts +156 -20
  61. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  62. package/src/sdk/broker/lifecycle.ts +225 -341
  63. package/src/sdk/broker/runtime.ts +5 -1
  64. package/src/sdk/broker/transport.ts +5 -1
  65. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  66. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  67. package/src/sdk/bus/telegram-daemon.ts +34 -1
  68. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  69. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  70. package/src/sdk/host/control/dispatch.ts +52 -4
  71. package/src/sdk/host/control/operations.ts +1 -1
  72. package/src/sdk/host/session-runtime.test.ts +40 -2
  73. package/src/sdk/host/session-runtime.ts +13 -5
  74. package/src/sdk/index.ts +1 -0
  75. package/src/sdk/lifecycle-session.ts +1 -0
  76. package/src/sdk/protocol/operation-registry.ts +1 -1
  77. package/src/sdk/router/session-router.ts +7 -1
  78. package/src/sdk/session.ts +37 -8
  79. package/src/session/agent-session.ts +114 -39
  80. package/src/session/blob-store.ts +78 -33
  81. package/src/session/internal/managed-session-storage.ts +96 -3
  82. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  83. package/src/setup/hermes-setup.ts +4 -1
  84. package/src/slash-commands/builtin-registry.ts +8 -5
  85. package/src/tools/ask-contract.test.ts +76 -1
  86. package/src/tools/ask-contract.ts +57 -6
  87. package/src/tools/ask.ts +16 -7
  88. package/src/tools/bash.ts +38 -29
  89. package/src/tools/descriptors.test.ts +29 -0
  90. package/src/tools/tool-catalog.generated.ts +7 -4
  91. package/src/web/search/index.ts +2 -2
@@ -4833,7 +4833,6 @@ async function terminateSpawnedChild(
4833
4833
  terminationStartDeadlineAt: number,
4834
4834
  expected: EffectMarker | undefined,
4835
4835
  timing: LifecycleTiming,
4836
- reapUnregisteredCutoff = false,
4837
4836
  ): Promise<boolean> {
4838
4837
  const pid = child.pid;
4839
4838
  if (!pid || (expected && pid !== expected.pid)) return false;
@@ -4847,226 +4846,100 @@ async function terminateSpawnedChild(
4847
4846
  await broker.index.refresh();
4848
4847
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4849
4848
  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;
4849
+ // Keep one poll interval inside the lifecycle deadline for the final exact
4850
+ // cleanup proof. Registered sessions retain the original deadline partition;
4851
+ // only an unregistered child needs this extra bounded margin.
4852
+ const processExitDeadlineAt = expected && !registered ? deadline - POLL_MS : deadline;
4854
4853
  const unregisteredTerminationDeadlineAt =
4855
4854
  processExitDeadlineAt - Math.max(POLL_MS, Math.floor((processExitDeadlineAt - terminationStartDeadlineAt) / 2));
4856
4855
  const observe = (): ProcessObservation =>
4857
- child.exitCode !== null || child.signalCode !== null
4856
+ child.exitCode !== null
4858
4857
  ? "exited"
4859
4858
  : 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
-
4859
+ const waitForExit = async (until: number): Promise<ProcessObservation> => {
4882
4860
  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
- }
4861
+ while (observation !== "exited" && timing.now() < until) {
4862
+ await timing.sleep(Math.max(0, Math.min(POLL_MS, until - timing.now())));
4902
4863
  observation = observe();
4903
4864
  }
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);
4865
+
4866
+ return observation;
4867
+ };
4868
+
4869
+ let observation = observe();
4870
+ const recheckOwnedExitObservation = async (): Promise<void> => {
4871
+ if (observation !== "uncertain") return;
4872
+ // This direct ChildProcess is owned by this broker invocation. A process-incarnation
4873
+ // read can briefly lag its exit event, so recheck only this owned child before failing.
4874
+ observation = await waitForExit(processExitDeadlineAt);
4875
+ };
4876
+ if (observation === "alive") {
4877
+ if (expected && !registered) {
4878
+ // A child that has not registered yet owns the cutoff receipt. Give it
4879
+ // the bounded pre-registration window to publish that proof, but reserve
4880
+ // the final proof interval for post-signal observation inside the request
4881
+ // deadline. A valid receipt does not interrupt the child's own rollback.
4882
+ while (timing.now() < unregisteredTerminationDeadlineAt) {
4883
+ if (await readLifecycleFailureArtifact(lifecycleFailurePath(root, id, expected.effectMarker), expected))
4884
+ await waitUntil(timing, unregisteredTerminationDeadlineAt);
4885
+ else await timing.sleep(Math.max(0, Math.min(POLL_MS, unregisteredTerminationDeadlineAt - timing.now())));
4922
4886
  }
4887
+ } else {
4888
+ await waitUntil(timing, terminationStartDeadlineAt);
4923
4889
  }
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);
4890
+ observation = observe();
4891
+ }
4892
+ if (observation === "alive") {
4893
+ if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGTERM", expected))) {
4894
+ observation = observe();
4895
+ await recheckOwnedExitObservation();
4896
+ if (observation !== "exited") {
4897
+ await recordTerminalUncertain(broker, id, root, pid);
4898
+ return false;
4938
4899
  }
4900
+ } else {
4901
+ const remaining = Math.max(0, processExitDeadlineAt - timing.now());
4902
+ const gracefulDeadline = timing.now() + Math.min(CLOSE_TIMEOUT_MS, Math.floor(remaining / 2));
4903
+ observation = await waitForExit(gracefulDeadline);
4939
4904
  }
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
- }
4905
+ }
4906
+ if (observation === "alive") {
4907
+ if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGKILL", expected))) {
4908
+ observation = observe();
4909
+ await recheckOwnedExitObservation();
4910
+ if (observation !== "exited") {
4911
+ await recordTerminalUncertain(broker, id, root, pid);
4912
+ return false;
4978
4913
  }
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
- }
4914
+ } else {
4915
+ observation = await waitForExit(processExitDeadlineAt);
4916
+ }
4917
+ }
4918
+ await recheckOwnedExitObservation();
4919
+ if (observation !== "exited" || !lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4920
+ let rollbackGeneration: number | null | undefined;
4921
+ if (expected) {
4922
+ const failure = await readLifecycleFailureArtifact(
4923
+ lifecycleFailurePath(root, id, expected.effectMarker),
4924
+ expected,
4925
+ );
4926
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4927
+ const rollbackComplete =
4928
+ failure?.artifact.rollback.fenced === true &&
4929
+ failure.artifact.rollback.runtimeRemoved &&
4930
+ failure.artifact.rollback.hostStopped &&
4931
+ failure.artifact.rollback.brokerRegistrationReleased;
4932
+ if (!rollbackComplete) {
4933
+ if (!readyThenExitToleranceEnabled()) {
4934
+ // A child that exited with a non-zero status before publishing any
4935
+ // lifecycle receipt has no durable rollback to wait for. Prove the
4936
+ // owned artifacts are gone and release its registration, but retain
4937
+ // terminal uncertainty for timeouts, clean exits, and child-authored
4938
+ // startup receipts whose rollback is incomplete.
4939
+ if (failure || child.exitCode === null || child.exitCode === 0) return failClosed();
5067
4940
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5068
4941
  const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
5069
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4942
+ if (publishedReady) return failClosed();
5070
4943
  const artifactsRemoved = await removeOwnedLifecycleArtifacts(
5071
4944
  root,
5072
4945
  id,
@@ -5081,50 +4954,21 @@ async function terminateSpawnedChild(
5081
4954
  const registered = broker.index
5082
4955
  .listSessions()
5083
4956
  .sessions.find(session => session.sessionId === id && session.pid === pid);
5084
- const registeredRowTerminal = registered?.terminal === true || registered?.terminalUncertain === true;
5085
4957
  let registrationReleased =
5086
- !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) || registeredRowTerminal;
4958
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) ||
4959
+ registered?.terminal === true ||
4960
+ registered?.terminalUncertain === true;
5087
4961
  if (registered && !registrationReleased) {
5088
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5089
4962
  registrationReleased = await broker.index.unregisterIfCurrent(registered);
5090
4963
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5091
4964
  await broker.index.refresh();
5092
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5093
4965
  registrationReleased =
5094
4966
  registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
5095
4967
  }
5096
4968
  const stillExited =
5097
4969
  observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
5098
4970
  "exited";
5099
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5100
4971
  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
4972
  if (
5129
4973
  stillExited &&
5130
4974
  artifactsRemoved &&
@@ -5135,35 +4979,102 @@ async function terminateSpawnedChild(
5135
4979
  return true;
5136
4980
  return failClosed();
5137
4981
  }
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) {
4982
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4983
+ const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
4984
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4985
+ const artifactsRemoved = await removeOwnedLifecycleArtifacts(
4986
+ root,
4987
+ id,
4988
+ expected,
4989
+ undefined,
4990
+ undefined,
4991
+ proofBudget,
4992
+ );
4993
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4994
+ await broker.index.refresh();
4995
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4996
+ const registered = broker.index
4997
+ .listSessions()
4998
+ .sessions.find(session => session.sessionId === id && session.pid === pid);
4999
+ const registeredRowTerminal = registered?.terminal === true || registered?.terminalUncertain === true;
5000
+ let registrationReleased =
5001
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) || registeredRowTerminal;
5002
+ if (registered && !registrationReleased) {
5003
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5004
+ registrationReleased = await broker.index.unregisterIfCurrent(registered);
5005
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5006
+ await broker.index.refresh();
5007
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5008
+ registrationReleased =
5009
+ registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
5010
+ }
5011
+ const stillExited =
5012
+ observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) === "exited";
5013
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5014
+ const endpointGone = await endpointRemoved(root, id);
5015
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5016
+ try {
5017
+ if (publishedReady && !failure) {
5018
+ await writeSessionLifecycleFailure(
5019
+ root,
5020
+ id,
5021
+ expected.effectMarker,
5022
+ {
5023
+ phase: "startup",
5024
+ reason: "failed",
5025
+ message: `Session ${id} ${READY_THEN_EXIT_MESSAGE}.`,
5026
+ },
5027
+ {
5028
+ endpointGeneration: registered?.endpointGeneration ?? null,
5029
+ fenced: stillExited && registrationReleased,
5030
+ runtimeRemoved: artifactsRemoved && endpointGone,
5031
+ hostStopped: stillExited,
5032
+ brokerRegistrationReleased: registrationReleased,
5033
+ },
5034
+ undefined,
5035
+ expected.incarnation,
5036
+ expected.pid,
5037
+ );
5038
+ }
5039
+ } catch {
5040
+ // A missing broker-authored receipt must not hide a proven dead child.
5041
+ }
5042
+ if (
5043
+ stillExited &&
5044
+ artifactsRemoved &&
5045
+ endpointGone &&
5046
+ registrationReleased &&
5047
+ lifecycleProofWithinDeadline(proofBudget)
5048
+ )
5049
+ return true;
5159
5050
  return failClosed();
5160
5051
  }
5161
- if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5162
- const endpointGone = await endpointRemoved(root, id);
5163
- return endpointGone && lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5164
- } finally {
5165
- cleanupOwnedExitListeners();
5052
+ rollbackGeneration = failure.artifact.rollback.endpointGeneration;
5053
+ }
5054
+ if (expected && !(await removeOwnedLifecycleArtifacts(root, id, expected, undefined, undefined, proofBudget)))
5055
+ return failClosed();
5056
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5057
+ await broker.index.refresh();
5058
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5059
+ if (
5060
+ rollbackGeneration === null &&
5061
+ expected &&
5062
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker)
5063
+ )
5064
+ return lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5065
+ const registeredBeforeTermination =
5066
+ rollbackGeneration === undefined || rollbackGeneration === null
5067
+ ? undefined
5068
+ : broker.index.findHostRegistration(id, rollbackGeneration, pid, expected?.effectMarker);
5069
+ const unregistered = registeredBeforeTermination
5070
+ ? broker.index.hostUnregisteredAfter(registeredBeforeTermination)
5071
+ : undefined;
5072
+ if (!registeredBeforeTermination || !unregistered) {
5073
+ return failClosed();
5166
5074
  }
5075
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
5076
+ const endpointGone = await endpointRemoved(root, id);
5077
+ return endpointGone && lifecycleProofWithinDeadline(proofBudget) ? true : failClosed();
5167
5078
  }
5168
5079
 
5169
5080
  async function signalVerifiedSession(
@@ -6993,7 +6904,6 @@ async function executeLifecycleResponse(
6993
6904
  knownSecrets,
6994
6905
  )
6995
6906
  : undefined;
6996
- const ledgerUncertain = broker.ledger.get(identity)?.state === "terminal_uncertain";
6997
6907
  const terminated = await terminateSpawnedChild(
6998
6908
  child,
6999
6909
  broker,
@@ -7003,8 +6913,8 @@ async function executeLifecycleResponse(
7003
6913
  terminationStartDeadline,
7004
6914
  spawnedAuthority,
7005
6915
  timing,
7006
- readiness.kind === "timeout",
7007
6916
  );
6917
+
7008
6918
  if (!terminated)
7009
6919
  return readiness.kind === "ready_probe_failed"
7010
6920
  ? fail(
@@ -7015,11 +6925,6 @@ async function executeLifecycleResponse(
7015
6925
  "terminal_uncertain",
7016
6926
  `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
6927
  );
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
6928
  return readiness.kind === "startup_failed"
7024
6929
  ? fail(
7025
6930
  readiness.failure.code ?? "spawn_failed",
@@ -8016,7 +7921,6 @@ async function exactCleanupProof(
8016
7921
  evidence: { artifact: LifecycleFailureArtifact } | undefined,
8017
7922
  proofBudget?: LifecycleProofBudget,
8018
7923
  allowObservedWindowsReadyExit = false,
8019
- allowObservedUnregisteredCutoff = false,
8020
7924
  ): Promise<LifecycleCleanupProof | undefined> {
8021
7925
  const rollback = evidence?.artifact.rollback;
8022
7926
  const durableRollbackComplete =
@@ -8024,16 +7928,6 @@ async function exactCleanupProof(
8024
7928
  rollback.runtimeRemoved &&
8025
7929
  rollback.hostStopped &&
8026
7930
  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
7931
  // Windows deliberately keeps the failure receipt incomplete because its
8038
7932
  // directory-entry publication cannot be flushed. A broker-observed
8039
7933
  // ready-then-exit can still authorize the separate ledger-backed exact
@@ -8051,7 +7945,7 @@ async function exactCleanupProof(
8051
7945
  !root ||
8052
7946
  !id ||
8053
7947
  !expected ||
8054
- (!durableRollbackComplete && !observedWindowsReadyExit && !observedUnregisteredCutoff) ||
7948
+ (!durableRollbackComplete && !observedWindowsReadyExit) ||
8055
7949
  observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) !==
8056
7950
  "exited"
8057
7951
  )
@@ -8266,28 +8160,28 @@ export async function executeLifecycle(
8266
8160
  expected,
8267
8161
  evidence,
8268
8162
  proofBudget,
8269
- !response.ok && response.error.code === "ready_then_exited",
8270
- !response.ok && response.error.code === "readiness_timeout",
8163
+ !response.ok ? response.error.code === "ready_then_exited" : false,
8271
8164
  );
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;
8165
+ const startupFailure: LifecycleStartupFailureReceipt | undefined =
8166
+ evidence && !response.ok
8167
+ ? {
8168
+ artifactDigest: evidence.digest,
8169
+ message: evidence.artifact.message,
8170
+ phase: evidence.artifact.phase,
8171
+ reason: evidence.artifact.reason,
8172
+ ...(evidence.artifact.code === undefined
8173
+ ? {}
8174
+ : { code: evidence.artifact.code, details: evidence.artifact.details }),
8175
+ rollback: {
8176
+ endpointGeneration: evidence.artifact.rollback.endpointGeneration,
8177
+ fenced: evidence.artifact.rollback.fenced,
8178
+ runtimeRemoved: evidence.artifact.rollback.runtimeRemoved,
8179
+ hostStopped: evidence.artifact.rollback.hostStopped,
8180
+ brokerRegistrationReleased: evidence.artifact.rollback.brokerRegistrationReleased,
8181
+ },
8182
+ ...(cleanupProof ? { cleanupProof } : {}),
8183
+ }
8184
+ : undefined;
8291
8185
  const durableEffectsBody: Omit<LifecycleDurableEffectsReceipt, "digest"> = {
8292
8186
  ...(priorDurableEffects?.worktree ? { worktree: priorDurableEffects.worktree } : {}),
8293
8187
  ...(evidence?.artifact.transcript
@@ -8367,54 +8261,44 @@ export async function executeLifecycle(
8367
8261
  ? response
8368
8262
  : response.error.code === "endpoint_unreadable"
8369
8263
  ? response
8370
- : response.error.code === "readiness_timeout" &&
8371
- cleanupProof &&
8372
- lifecycleCleanupResponse &&
8373
- !lifecycleCleanupResponse.ok &&
8374
- lifecycleCleanupResponse.error.code === "spawn_failed"
8264
+ : // An unreadable/corrupt endpoint is already the fail-closed honest
8265
+ // terminal classification (it names the artifact, not the
8266
+ // child); the reconciliation wrapper must not erase it.
8267
+ response.error.code === "ready_then_exited" && (cleanupProof || provenDeadCleanup)
8375
8268
  ? {
8376
8269
  ...response,
8377
8270
  ...(durableEffects ? { durableEffects } : {}),
8378
8271
  ...(startupFailure ? { startupFailure } : {}),
8379
8272
  }
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)
8273
+ : startupFailure && cleanupProof
8384
8274
  ? {
8385
- ...response,
8275
+ ok: false,
8276
+ error: startupFailure.code
8277
+ ? {
8278
+ code: startupFailure.code,
8279
+ message: startupFailure.message,
8280
+ details: startupFailure.details!,
8281
+ endpoint: "unavailable" as const,
8282
+ }
8283
+ : {
8284
+ code: "spawn_failed",
8285
+ message: startupFailure.message,
8286
+ endpoint: "unavailable" as const,
8287
+ },
8386
8288
  ...(durableEffects ? { durableEffects } : {}),
8387
- ...(startupFailure ? { startupFailure } : {}),
8289
+ startupFailure,
8388
8290
  }
8389
- : startupFailure && cleanupProof
8390
- ? {
8291
+ : provenDeadCleanup && response.error.code !== "terminal_uncertain"
8292
+ ? response
8293
+ : {
8391
8294
  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
- },
8295
+ error: {
8296
+ code: "terminal_uncertain",
8297
+ message: terminalUncertainStartupMessage(response),
8298
+ },
8404
8299
  ...(durableEffects ? { durableEffects } : {}),
8405
- startupFailure,
8300
+ ...(startupFailure ? { startupFailure } : {}),
8406
8301
  }
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
8302
  : response;
8419
8303
  // The extended proof window may reconcile artifacts after the original
8420
8304
  // admission deadline, but cannot retroactively prove a timely spawn failure.