@gajae-code/coding-agent 0.16.3 → 0.16.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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -13,6 +13,7 @@ import type {
13
13
  JsonRpcRequest,
14
14
  JsonRpcResponse,
15
15
  MCPRequestOptions,
16
+ MCPStdioPreparedLaunch,
16
17
  MCPStdioServerConfig,
17
18
  MCPTransport,
18
19
  } from "../../runtime-mcp/types";
@@ -56,6 +57,22 @@ function buildMinimalStdioEnv(explicit?: Record<string, string>): Record<string,
56
57
  return { ...env, ...explicit };
57
58
  }
58
59
 
60
+ function combinedOperationAndCleanupFailure(operationError: unknown, cleanupError: unknown): MCPExpectedFailure {
61
+ return new MCPExpectedFailure(
62
+ new AggregateError(
63
+ [operationError, cleanupError],
64
+ "MCP stdio operation failed and subprocess cleanup also failed",
65
+ ),
66
+ );
67
+ }
68
+
69
+ type StdioConnectAttempt = {
70
+ generation: number;
71
+ revoked: boolean;
72
+ settled: boolean;
73
+ completion: Promise<void>;
74
+ };
75
+
59
76
  export class StdioTransport implements MCPTransport {
60
77
  #process: OwnedProcess | null = null;
61
78
  #pendingRequests = new Map<
@@ -69,6 +86,10 @@ export class StdioTransport implements MCPTransport {
69
86
  #readLoop: Promise<void> | null = null;
70
87
  #stderrLoop: Promise<void> | null = null;
71
88
  #closePromise: Promise<void> | null = null;
89
+ #connectGeneration = 0;
90
+ #connectAttempt: StdioConnectAttempt | null = null;
91
+ /** Retained until the per-attempt cleanup resolves successfully. */
92
+ #afterProcessExit: (() => Promise<void>) | null = null;
72
93
 
73
94
  onClose?: () => void;
74
95
  onError?: (error: Error) => void;
@@ -85,33 +106,122 @@ export class StdioTransport implements MCPTransport {
85
106
  return true;
86
107
  }
87
108
 
109
+ #registerCleanup(attempt: StdioConnectAttempt, cleanup: () => Promise<void>): void {
110
+ if (this.#connectAttempt !== attempt) {
111
+ throw new Error("MCP stdio attempt registered cleanup after its ownership ended");
112
+ }
113
+ if (this.#afterProcessExit && this.#afterProcessExit !== cleanup) {
114
+ throw new Error("MCP stdio attempt registered conflicting cleanup owners");
115
+ }
116
+ this.#afterProcessExit = cleanup;
117
+ }
118
+
119
+ #assertConnectAttemptActive(attempt: StdioConnectAttempt): void {
120
+ if (attempt.revoked || this.#connectAttempt !== attempt || this.#connectGeneration !== attempt.generation) {
121
+ throw new Error("MCP stdio connection attempt was closed");
122
+ }
123
+ }
124
+
125
+ #revokeConnectAttempt(): StdioConnectAttempt | null {
126
+ const attempt = this.#connectAttempt;
127
+ if (attempt && !attempt.revoked) {
128
+ attempt.revoked = true;
129
+ this.#connectGeneration++;
130
+ }
131
+ return attempt;
132
+ }
133
+
88
134
  /**
89
135
  * Start the subprocess and begin reading.
90
136
  */
91
137
  async connect(): Promise<void> {
92
138
  if (this.#connected) return;
93
- if (this.#closePromise || this.#process) {
139
+ if (this.#closePromise) {
140
+ throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
141
+ }
142
+ if (this.#connectAttempt) {
143
+ if (!this.#connectAttempt.settled) return this.#connectAttempt.completion;
144
+ if (this.#process || this.#afterProcessExit) {
145
+ throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
146
+ }
147
+ this.#connectAttempt = null;
148
+ }
149
+ if (this.#process || this.#afterProcessExit) {
94
150
  throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
95
151
  }
96
152
 
97
- const args = this.config.args ?? [];
153
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
154
+ const attempt: StdioConnectAttempt = {
155
+ generation: ++this.#connectGeneration,
156
+ revoked: false,
157
+ settled: false,
158
+ completion: promise,
159
+ };
160
+ this.#connectAttempt = attempt;
161
+ void this.#finishConnect(attempt).then(
162
+ () => {
163
+ attempt.settled = true;
164
+ resolve();
165
+ },
166
+ error => {
167
+ attempt.settled = true;
168
+ reject(error);
169
+ },
170
+ );
171
+ void promise.catch(() => {});
172
+ return promise;
173
+ }
174
+
175
+ async #finishConnect(attempt: StdioConnectAttempt): Promise<void> {
176
+ let launch: MCPStdioPreparedLaunch = {
177
+ command: this.config.command,
178
+ args: Object.freeze([...(this.config.args ?? [])]),
179
+ cwd: this.config.cwd ?? process.cwd(),
180
+ registerCleanup: cleanup => this.#registerCleanup(attempt, cleanup),
181
+ };
98
182
  const env = this.config.noInheritEnv
99
183
  ? buildMinimalStdioEnv(this.config.env)
100
184
  : {
101
185
  ...Bun.env,
102
186
  ...this.config.env,
103
187
  };
104
- const cwd = this.config.cwd ?? process.cwd();
105
-
106
188
  try {
107
- this.#process = spawnOwnedProcess([this.config.command, ...args], {
108
- cwd,
189
+ this.#assertConnectAttemptActive(attempt);
190
+ if (this.config.prepareSpawn) launch = await this.config.prepareSpawn(launch);
191
+ if (launch.afterProcessExit) this.#registerCleanup(attempt, launch.afterProcessExit);
192
+ else if (!this.#afterProcessExit && this.config.afterProcessExit) {
193
+ this.#registerCleanup(attempt, this.config.afterProcessExit);
194
+ }
195
+ this.#assertConnectAttemptActive(attempt);
196
+ await this.config.spawnGuard?.(launch);
197
+ this.#assertConnectAttemptActive(attempt);
198
+ await this.config.afterSpawnGuardForTest?.();
199
+ this.#assertConnectAttemptActive(attempt);
200
+ const process = spawnOwnedProcess([launch.command, ...launch.args], {
201
+ cwd: launch.cwd,
109
202
  env,
110
203
  stdin: "pipe",
111
204
  gracefulMs: CLOSE_WAIT_MS,
112
- name: `mcp-stdio:${this.config.command}`,
205
+ name: `mcp-stdio:${launch.command}`,
113
206
  });
207
+ if (attempt.revoked) {
208
+ const teardown = await process.dispose();
209
+ await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
210
+ if (teardown.status !== "terminated") {
211
+ this.#process = process;
212
+ throw new MCPExpectedFailure(new Error(`stdio child teardown ${teardown.status}`));
213
+ }
214
+ throw new Error("MCP stdio connection attempt was closed");
215
+ }
216
+ this.#process = process;
114
217
  } catch (error) {
218
+ if (!this.#process) {
219
+ try {
220
+ await this.#runAfterProcessExit();
221
+ } catch (cleanupError) {
222
+ throw combinedOperationAndCleanupFailure(error, cleanupError);
223
+ }
224
+ }
115
225
  throw new MCPExpectedFailure(error);
116
226
  }
117
227
 
@@ -367,19 +477,32 @@ export class StdioTransport implements MCPTransport {
367
477
  }
368
478
  }
369
479
 
480
+ async #runAfterProcessExit(): Promise<void> {
481
+ const cleanup = this.#afterProcessExit;
482
+ if (!cleanup) return;
483
+ await cleanup();
484
+ if (this.#afterProcessExit === cleanup) this.#afterProcessExit = null;
485
+ }
486
+
370
487
  async close(): Promise<void> {
371
488
  await this.#closeInternal(false);
372
489
  }
373
490
 
374
491
  #closeInternal(fromReadLoop: boolean, failure?: MCPExpectedFailure): Promise<void> {
375
492
  if (this.#closePromise) return this.#closePromise;
376
- this.#closePromise = this.#finishClose(fromReadLoop, failure).finally(() => {
493
+ const connectAttempt = this.#revokeConnectAttempt();
494
+ this.#closePromise = this.#finishClose(fromReadLoop, failure, connectAttempt).finally(() => {
495
+ if (this.#connectAttempt === connectAttempt) this.#connectAttempt = null;
377
496
  this.#closePromise = null;
378
497
  });
379
498
  return this.#closePromise;
380
499
  }
381
500
 
382
- async #finishClose(fromReadLoop: boolean, failure?: MCPExpectedFailure): Promise<void> {
501
+ async #finishClose(
502
+ fromReadLoop: boolean,
503
+ failure: MCPExpectedFailure | undefined,
504
+ connectAttempt: StdioConnectAttempt | null,
505
+ ): Promise<void> {
383
506
  const wasConnected = this.#connected;
384
507
  this.#connected = false;
385
508
 
@@ -388,16 +511,36 @@ export class StdioTransport implements MCPTransport {
388
511
  }
389
512
  this.#pendingRequests.clear();
390
513
 
514
+ let primaryFailure: unknown = failure;
515
+ if (connectAttempt) {
516
+ try {
517
+ await connectAttempt.completion;
518
+ } catch (error) {
519
+ if (primaryFailure === undefined && this.#process) primaryFailure = error;
520
+ }
521
+ }
391
522
  const stdin = this.#getStdin();
392
523
  const process = this.#process;
393
524
  if (process) {
394
- stdin?.end();
395
- const teardown = await process.dispose();
396
- await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
397
- if (teardown.status !== "terminated") {
398
- throw new MCPExpectedFailure(new Error(`stdio child teardown ${teardown.status}`));
525
+ try {
526
+ stdin?.end();
527
+ const teardown = await process.dispose();
528
+ await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
529
+ if (teardown.status !== "terminated") {
530
+ throw new MCPExpectedFailure(new Error(`stdio child teardown ${teardown.status}`));
531
+ }
532
+ this.#process = null;
533
+ } catch (cleanupError) {
534
+ if (primaryFailure) throw combinedOperationAndCleanupFailure(primaryFailure, cleanupError);
535
+ throw cleanupError;
399
536
  }
400
- this.#process = null;
537
+ }
538
+
539
+ try {
540
+ await this.#runAfterProcessExit();
541
+ } catch (cleanupError) {
542
+ if (primaryFailure) throw combinedOperationAndCleanupFailure(primaryFailure, cleanupError);
543
+ throw cleanupError;
401
544
  }
402
545
 
403
546
  if (!fromReadLoop && this.#readLoop) {
@@ -413,12 +556,3 @@ export class StdioTransport implements MCPTransport {
413
556
  if (wasConnected) this.onClose?.();
414
557
  }
415
558
  }
416
-
417
- /**
418
- * Create and connect a stdio transport.
419
- */
420
- export async function createStdioTransport(config: MCPStdioServerConfig): Promise<StdioTransport> {
421
- const transport = new StdioTransport(config);
422
- await transport.connect();
423
- return transport;
424
- }
@@ -95,6 +95,22 @@ export interface MCPServerConfigBase {
95
95
  }
96
96
 
97
97
  /** Stdio server configuration */
98
+ export interface MCPStdioSpawnLaunch {
99
+ command: string;
100
+ args: readonly string[];
101
+ cwd: string;
102
+ /** Transport-supplied registrar for retryable cleanup before preparation allocates resources. */
103
+ registerCleanup?: (cleanup: () => Promise<void>) => void;
104
+ }
105
+
106
+ export interface MCPStdioPreparedLaunch extends MCPStdioSpawnLaunch {
107
+ /**
108
+ * Per-attempt cleanup run after confirmed child exit or failed spawn. The same
109
+ * callback may already have been registered during preparation.
110
+ */
111
+ afterProcessExit?: () => Promise<void>;
112
+ }
113
+
98
114
  export interface MCPStdioServerConfig extends MCPServerConfigBase {
99
115
  type?: "stdio"; // Default if not specified
100
116
  command: string;
@@ -108,6 +124,14 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
108
124
  */
109
125
  noInheritEnv?: boolean;
110
126
  cwd?: string;
127
+ /** Internal trusted-boundary preparation of immutable per-spawn authority. */
128
+ prepareSpawn?: (launch: MCPStdioSpawnLaunch) => Promise<MCPStdioPreparedLaunch>;
129
+ /** Internal trusted-boundary check run immediately before process creation. */
130
+ spawnGuard?: (launch: MCPStdioSpawnLaunch) => Promise<void>;
131
+ /** Internal cleanup run only after the owned stdio child is confirmed exited. */
132
+ afterProcessExit?: () => Promise<void>;
133
+ /** Test seam for deterministic mutation after the guard and before process creation. */
134
+ afterSpawnGuardForTest?: () => Promise<void>;
111
135
  }
112
136
 
113
137
  /** HTTP server configuration (Streamable HTTP transport) */
@@ -41,7 +41,9 @@ import {
41
41
  executeLifecycle,
42
42
  isCanonicalSessionId,
43
43
  prepareSpawnChildHostLaunch,
44
+ readSessionLifecycleFailure,
44
45
  validateBrokerModelPresetSync,
46
+ writeEffectMarker,
45
47
  } from "./lifecycle";
46
48
  import {
47
49
  type LifecycleDurableEffectsReceipt,
@@ -724,6 +726,7 @@ function credentialFreeLifecycleResponse(value: unknown): unknown {
724
726
  type LifecycleReplayEndpoint = {
725
727
  endpoint: Record<string, unknown>;
726
728
  endpointGeneration: number;
729
+ endpointIncarnation: string;
727
730
  pid: number;
728
731
  endpointMtimeMs: number;
729
732
  };
@@ -1588,12 +1591,34 @@ export class Broker {
1588
1591
  ).claim;
1589
1592
  return spawnFailureError(failure);
1590
1593
  }
1594
+ const { pid, processIncarnation: incarnation } = launched.proof;
1595
+ if (pid === undefined || incarnation === undefined) {
1596
+ const failure: SpawnSubstrateFailure = {
1597
+ substrateKind: launched.proof.substrateKind,
1598
+ code: "substrate_proof_failed",
1599
+ message: "session.spawn substrate lacks lifecycle process authority",
1600
+ };
1601
+ await this.#releaseUnownedSubstrate(provider, launchedProof);
1602
+ launchedProof = undefined;
1603
+ current = (
1604
+ await store.persistTransition(lifecycleIdentity, {
1605
+ claimId: current.claimId,
1606
+ from: "substrate_starting",
1607
+ to: "pre_send_rejected",
1608
+ failure,
1609
+ })
1610
+ ).claim;
1611
+ return spawnFailureError(failure);
1612
+ }
1613
+ const marker = { pid, incarnation, effectMarker: prep.effectMarker };
1614
+ await writeEffectMarker(prep.stateRoot, prep.childId, marker);
1591
1615
  const registration = await this.#spawnPromptLayer.awaitRegistration({
1592
1616
  childId: prep.childId,
1593
1617
  cwd: prep.cwd,
1594
1618
  stateRoot: prep.stateRoot,
1595
1619
  });
1596
1620
  if (!registration.ok) {
1621
+ const startupFailure = await readSessionLifecycleFailure(prep.stateRoot, prep.childId, marker);
1597
1622
  await this.#releaseUnownedSubstrate(provider, launchedProof);
1598
1623
  launchedProof = undefined;
1599
1624
  current = (
@@ -1603,7 +1628,11 @@ export class Broker {
1603
1628
  to: "uncertain",
1604
1629
  })
1605
1630
  ).claim;
1606
- return error("terminal_uncertain", "session.spawn child registration is uncertain");
1631
+ return error(
1632
+ "terminal_uncertain",
1633
+ "session.spawn child registration is uncertain" +
1634
+ (startupFailure ? ` (${startupFailure.phase}/${startupFailure.reason})` : ""),
1635
+ );
1607
1636
  }
1608
1637
  pinnedRegistration = registration.registration;
1609
1638
  const now = Date.now();
@@ -2404,26 +2433,35 @@ export class Broker {
2404
2433
  )
2405
2434
  return;
2406
2435
 
2407
- // Snapshot validation and the deterministic instance suffix protect successor locks.
2408
- const tombstone = path.join(
2409
- path.dirname(this.#lock),
2410
- `.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}`,
2411
- );
2412
- try {
2413
- await fs.rename(this.#lock, tombstone);
2414
- } catch (e) {
2415
- const code = (e as NodeJS.ErrnoException).code;
2416
- if (["ENOENT", "EEXIST", "ENOTEMPTY", "EISDIR", "ENOTDIR"].includes(code ?? "")) return;
2417
- if (code === "EPERM") {
2418
- try {
2419
- await fs.lstat(tombstone);
2420
- return;
2421
- } catch (statError) {
2422
- if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
2436
+ // Give every reclaimed generation a fresh quarantine name. Older brokers
2437
+ // used only the lock inode hash; if that tombstone survived a crash, the
2438
+ // deterministic name collided forever and left the dead canonical lock in
2439
+ // place. The random suffix preserves no-replace rename semantics without
2440
+ // letting retained cleanup debris block takeover.
2441
+ for (let attempt = 0; attempt < 8; attempt++) {
2442
+ const tombstone = path.join(
2443
+ path.dirname(this.#lock),
2444
+ `.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}-${randomBytes(8).toString("hex")}`,
2445
+ );
2446
+ try {
2447
+ await fs.rename(this.#lock, tombstone);
2448
+ return;
2449
+ } catch (e) {
2450
+ const code = (e as NodeJS.ErrnoException).code;
2451
+ if (code === "EEXIST" || code === "ENOTEMPTY") continue;
2452
+ if (["ENOENT", "EISDIR", "ENOTDIR"].includes(code ?? "")) return;
2453
+ if (code === "EPERM") {
2454
+ try {
2455
+ await fs.lstat(tombstone);
2456
+ continue;
2457
+ } catch (statError) {
2458
+ if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
2459
+ }
2423
2460
  }
2461
+ throw e;
2424
2462
  }
2425
- throw e;
2426
2463
  }
2464
+ throw new Error(`Broker lock quarantine namespace is saturated for ${this.#lock}`);
2427
2465
  }
2428
2466
  async #releaseOwnedLock(): Promise<void> {
2429
2467
  try {
@@ -2549,6 +2587,11 @@ export class Broker {
2549
2587
  startedAt: now,
2550
2588
  heartbeatAt: now,
2551
2589
  };
2590
+ // Readiness must not be externally visible until the initial session
2591
+ // checkpoint settles. The bootstrap watchdog owns this pre-publication
2592
+ // interval; publishing first allowed it to kill an endpoint already handed
2593
+ // to callers when a legitimate index-lock wait outlived the fence.
2594
+ await this.#checkpointSessionHeartbeats();
2552
2595
  this.#publication = await publishBrokerDiscovery(this.settings.agentDir, this.discovery);
2553
2596
  this.#publicationState = "healthy-owned";
2554
2597
  this.#publishedAt = process.hrtime.bigint();
@@ -2560,7 +2603,6 @@ export class Broker {
2560
2603
  void this.#watchPublication();
2561
2604
  void this.#reapSpawnOrphans();
2562
2605
  }, cadenceMs);
2563
- await this.#checkpointSessionHeartbeats();
2564
2606
  return this.discovery;
2565
2607
  } catch (error) {
2566
2608
  await this.#transport?.stop();
@@ -2862,6 +2904,8 @@ export class Broker {
2862
2904
  endpointMtimeMs <= 0
2863
2905
  )
2864
2906
  return error("endpoint_stale", "session endpoint authority is incomplete");
2907
+ const currentIncarnation = endpointIncarnation(record, sessionId);
2908
+ if (!currentIncarnation) return error("endpoint_stale", "session endpoint incarnation is unavailable");
2865
2909
  const endpoint = await this.#readEndpoint(record, {});
2866
2910
  if (!endpoint.ok) return endpoint;
2867
2911
  if (endpoint.result === null || typeof endpoint.result !== "object" || Array.isArray(endpoint.result))
@@ -2869,6 +2913,7 @@ export class Broker {
2869
2913
  return {
2870
2914
  endpoint: endpoint.result as Record<string, unknown>,
2871
2915
  endpointGeneration: record.endpointGeneration,
2916
+ endpointIncarnation: currentIncarnation,
2872
2917
  pid: record.pid,
2873
2918
  endpointMtimeMs,
2874
2919
  };
@@ -3041,16 +3086,21 @@ export class Broker {
3041
3086
  if (this.#chains.get(target) === chain) this.#chains.delete(target);
3042
3087
  }
3043
3088
  }
3044
- #storeSessionListCursor(cursor: SessionListCursor, replacingToken?: string): string | BrokerResponse {
3089
+ #storeSessionListCursor(cursor: SessionListCursor, replacingToken?: string): string {
3045
3090
  const now = Date.now();
3046
3091
  for (const [token, stored] of this.#sessionListCursors) {
3047
3092
  if (stored.expiresAt <= now) this.#sessionListCursors.delete(token);
3048
3093
  }
3049
- const replacing = replacingToken !== undefined && this.#sessionListCursors.has(replacingToken);
3050
- if (!replacing && this.#sessionListCursors.size >= SESSION_LIST_MAX_CURSORS)
3051
- return error("invalid_input", "session.list cursor capacity is exhausted");
3052
- const token = randomBytes(24).toString("base64url");
3053
3094
  if (replacingToken !== undefined) this.#sessionListCursors.delete(replacingToken);
3095
+ // Pagination cursors are a paging convenience, not durable state (#5370).
3096
+ // Evict the oldest cursor when the budget is full so abandoned or partial
3097
+ // paginations degrade gracefully instead of failing unrelated session ops.
3098
+ while (this.#sessionListCursors.size >= SESSION_LIST_MAX_CURSORS) {
3099
+ const oldest = this.#sessionListCursors.keys().next();
3100
+ if (oldest.done) break;
3101
+ this.#sessionListCursors.delete(oldest.value);
3102
+ }
3103
+ const token = randomBytes(24).toString("base64url");
3054
3104
  this.#sessionListCursors.set(token, cursor);
3055
3105
  return token;
3056
3106
  }
@@ -3095,11 +3145,12 @@ export class Broker {
3095
3145
  }
3096
3146
  const limit = stored?.limit ?? requestedLimit ?? SESSION_LIST_DEFAULT_LIMIT;
3097
3147
  const observedAt = stored?.observedAt ?? (scope === undefined ? undefined : new Date().toISOString());
3148
+ const resolveSessionId = typeof input.resolveSessionId === "string" ? input.resolveSessionId : undefined;
3098
3149
  const snapshot = stored ?? {
3099
- sessions:
3100
- scope === undefined
3101
- ? [...result.sessions]
3102
- : result.sessions.filter(session => scopeMatchesLocator(scope, session.locator)),
3150
+ sessions: (scope === undefined
3151
+ ? [...result.sessions]
3152
+ : result.sessions.filter(session => scopeMatchesLocator(scope, session.locator))
3153
+ ).filter(session => resolveSessionId === undefined || session.sessionId === resolveSessionId),
3103
3154
  indexSeq: result.indexSeq,
3104
3155
  warnings: [...result.warnings],
3105
3156
  limit,
@@ -3120,7 +3171,6 @@ export class Broker {
3120
3171
  { ...snapshot, offset, expiresAt: Date.now() + SESSION_LIST_CURSOR_TTL_MS },
3121
3172
  typeof cursor === "string" ? cursor : undefined,
3122
3173
  );
3123
- if (isBrokerResponse(continuationCursor)) return continuationCursor;
3124
3174
  return {
3125
3175
  ok: true,
3126
3176
  result: {
@@ -3325,15 +3375,32 @@ export class Broker {
3325
3375
  (operation === "session.create" || operation === "session.fork" || operation === "session.resume") &&
3326
3376
  typeof (replay.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
3327
3377
  ) {
3328
- const refreshed = await this.#readLifecycleReplayEndpoint(
3329
- (replay.result as { sessionId: string }).sessionId,
3330
- );
3378
+ const replayResult = objectRecord(replay.result);
3379
+ const replaySessionId = (replay.result as { sessionId: string }).sessionId;
3380
+ const replayIncarnation =
3381
+ typeof replayResult?.endpointIncarnation === "string" &&
3382
+ /^[a-f0-9]{64}$/.test(replayResult.endpointIncarnation)
3383
+ ? replayResult.endpointIncarnation
3384
+ : endpointIncarnation(
3385
+ {
3386
+ endpointGeneration: replayResult?.endpointGeneration as number,
3387
+ endpointMtimeMs: replayResult?.endpointMtimeMs as number,
3388
+ pid: replayResult?.pid as number,
3389
+ },
3390
+ replaySessionId,
3391
+ );
3392
+ if (!replayIncarnation)
3393
+ return error("endpoint_stale", "lifecycle replay lacks original endpoint authority");
3394
+ const refreshed = await this.#readLifecycleReplayEndpoint(replaySessionId);
3331
3395
  if (isBrokerResponse(refreshed)) return refreshed;
3396
+ if (refreshed.endpointIncarnation !== replayIncarnation)
3397
+ return error("endpoint_stale", "lifecycle replay target was replaced");
3332
3398
  return {
3333
3399
  ok: true,
3334
3400
  result: {
3335
3401
  ...(replay.result as Record<string, unknown>),
3336
3402
  endpointGeneration: refreshed.endpointGeneration,
3403
+ endpointIncarnation: refreshed.endpointIncarnation,
3337
3404
  pid: refreshed.pid,
3338
3405
  endpointMtimeMs: refreshed.endpointMtimeMs,
3339
3406
  endpoint: refreshed.endpoint,
@@ -3346,6 +3413,12 @@ export class Broker {
3346
3413
  const response = outcome.response;
3347
3414
  const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
3348
3415
  await this.ledger.transition(identity, lifecycleResponseState(response), {
3416
+ ...(operation === "session.delete" &&
3417
+ typeof input.sessionId === "string" &&
3418
+ lifecycleResponseState(response) === "terminal_uncertain" &&
3419
+ !pendingCleanupSessionId(response)
3420
+ ? { intendedSessionId: input.sessionId }
3421
+ : {}),
3349
3422
  response: storedResponse,
3350
3423
  responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
3351
3424
  ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
@@ -3365,6 +3438,12 @@ export class Broker {
3365
3438
  const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
3366
3439
  await this.ledger.transition(identity, lifecycleResponseState(response), {
3367
3440
  ...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
3441
+ ...(operation === "session.delete" &&
3442
+ typeof input.sessionId === "string" &&
3443
+ lifecycleResponseState(response) === "terminal_uncertain" &&
3444
+ !pendingCleanupSessionId(response)
3445
+ ? { intendedSessionId: input.sessionId }
3446
+ : {}),
3368
3447
  response: storedResponse,
3369
3448
  responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
3370
3449
  ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
@@ -3376,8 +3455,21 @@ export class Broker {
3376
3455
  const outcome = await executeLifecycle(this, operation, input, identity);
3377
3456
  const response = outcome.response;
3378
3457
  const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
3458
+ // Record the refusal's own target session so the fence it may leave
3459
+ // is scoped to that session. Without this, a session.delete refusal
3460
+ // for X persists as an unbound terminal_uncertain row and fences
3461
+ // every later delete for unrelated sessions (#5364). A refusal that
3462
+ // truly cannot name its target fences nothing (see
3463
+ // hasUncertainCleanupForSession).
3464
+ const refusalSessionId =
3465
+ operation === "session.delete" && typeof input.sessionId === "string" ? input.sessionId : undefined;
3379
3466
  await this.ledger.transition(identity, lifecycleResponseState(response), {
3380
3467
  ...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
3468
+ ...(refusalSessionId !== undefined &&
3469
+ lifecycleResponseState(response) === "terminal_uncertain" &&
3470
+ !pendingCleanupSessionId(response)
3471
+ ? { intendedSessionId: refusalSessionId }
3472
+ : {}),
3381
3473
  resultSessionId:
3382
3474
  response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
3383
3475
  ? (response.result as { sessionId: string }).sessionId