@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. package/src/web/search/providers/codex.ts +3 -2
@@ -451,6 +451,9 @@ function isModernMethodNotFound(error: unknown): boolean {
451
451
  * The original request is retried with `inputResponses` and a verbatim
452
452
  * `requestState` echo under a fresh JSON-RPC id, at most MAX_MRTR_RETRIES times;
453
453
  * the retry is fenced to the originating exchange by a stable correlation id.
454
+ * `roots/list` input requests are answered locally; every other input request
455
+ * requires `options.inputHandler`, and its absence fails the exchange before any
456
+ * retry is sent (no write replay, no implicit accept).
454
457
  */
455
458
  async function requestWithInputHandling<T>(
456
459
  connection: MCPServerConnection,
@@ -474,17 +477,20 @@ async function requestWithInputHandling<T>(
474
477
  if (!inputRequired) return stripResultType(initial);
475
478
 
476
479
  const handler = options?.inputHandler;
477
- if (!handler) {
478
- throw new MCPExpectedFailure(
480
+ const correlationId = crypto.randomUUID();
481
+
482
+ const missingHandler = () =>
483
+ new MCPExpectedFailure(
479
484
  new Error(
480
485
  `MCP server "${connection.name}" requested additional input (input_required) for ${method}, but no interactive input handler is available`,
481
486
  ),
482
487
  );
483
- }
484
- const correlationId = crypto.randomUUID();
485
488
 
486
489
  for (let attempt = 0; attempt < MAX_MRTR_RETRIES; attempt++) {
487
490
  const inputResponses: Record<string, unknown> = {};
491
+ // A requestState-only round has no local answer to give; without an
492
+ // interactive handler it is never replayed unattended.
493
+ if (!handler && Object.keys(inputRequired.inputRequests).length === 0) throw missingHandler();
488
494
  for (const [key, request] of Object.entries(inputRequired.inputRequests)) {
489
495
  if (options?.signal?.aborted) {
490
496
  throw options.signal.reason instanceof Error ? options.signal.reason : new Error("Aborted");
@@ -494,6 +500,9 @@ async function requestWithInputHandling<T>(
494
500
  inputResponses[key] = await defaultRequestHandler("roots/list", undefined);
495
501
  continue;
496
502
  }
503
+ // Everything else (elicitation, sampling, ...) needs a real interactive
504
+ // handler. It is never answered implicitly; fail before any retry is sent.
505
+ if (!handler) throw missingHandler();
497
506
  const outcome = await handler(key, request, {
498
507
  serverName: connection.name,
499
508
  originMethod: method,
@@ -50,13 +50,17 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
50
50
  const dir = path.dirname(filePath);
51
51
  await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
52
52
 
53
- // Write to temp file first (atomic write)
54
- const tmpPath = `${filePath}.tmp`;
53
+ // A predictable `${file}.tmp` can already be a symlink planted in the project.
54
+ // Write a unique sibling and fail closed if that path exists.
55
+ const tmpPath = `${filePath}.${crypto.randomUUID()}.tmp`;
55
56
  const content = JSON.stringify(withSchema(config), null, 2);
56
- await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
57
-
58
- // Rename to final path (atomic on most systems)
59
- await fs.promises.rename(tmpPath, filePath);
57
+ try {
58
+ await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600, flag: "wx" });
59
+ await fs.promises.rename(tmpPath, filePath);
60
+ } catch (error) {
61
+ await fs.promises.rm(tmpPath, { force: true });
62
+ throw error;
63
+ }
60
64
  // Invalidate the capability fs cache so subsequent reads see the new content
61
65
  invalidateFsCache(filePath);
62
66
  }
@@ -103,6 +103,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
103
103
  url: server.url ?? "",
104
104
  };
105
105
  if (server.headers) config.headers = server.headers;
106
+ if (server.publicNetwork === true) config.publicNetwork = true;
106
107
  canonicalizeMCPEndpoint(config.url);
107
108
  return config;
108
109
  }
@@ -0,0 +1,142 @@
1
+ import { untilAborted } from "@gajae-code/utils";
2
+ import type { ExtensionUIContext } from "../extensibility/extensions/types";
3
+ import type { AskAnswerSource, AskRemoteReceipt } from "../tools";
4
+ import type { MCPInputRequestHandler } from "./types";
5
+
6
+ export interface MCPFormInputDependencies {
7
+ getUi(): { ui?: Pick<ExtensionUIContext, "select" | "input">; hasUI: boolean };
8
+ getAskAnswerSource(): AskAnswerSource | undefined;
9
+ }
10
+
11
+ interface FormField {
12
+ key: string;
13
+ label: string;
14
+ type: "string" | "boolean";
15
+ choices?: string[];
16
+ }
17
+
18
+ function isRecord(value: unknown): value is Record<string, unknown> {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+
22
+ function displayText(value: string): string {
23
+ return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ").slice(0, 4000);
24
+ }
25
+
26
+ /** Unsupported constraints are rejected, never silently discarded. */
27
+ function decodeField(schema: unknown): FormField | undefined {
28
+ if (!isRecord(schema) || schema.type !== "object" || !isRecord(schema.properties)) return undefined;
29
+ const allowed = new Set(["type", "properties", "required", "additionalProperties", "title", "description"]);
30
+ if (Object.keys(schema).some(key => !allowed.has(key))) return undefined;
31
+ if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== "boolean") return undefined;
32
+ const entries = Object.entries(schema.properties);
33
+ if (entries.length !== 1) return undefined;
34
+ const [key, property] = entries[0]!;
35
+ if (!isRecord(property) || (property.type !== "string" && property.type !== "boolean")) return undefined;
36
+ if (
37
+ schema.required !== undefined &&
38
+ (!Array.isArray(schema.required) || schema.required.some(value => value !== key))
39
+ )
40
+ return undefined;
41
+ const propertyKeys = new Set(["type", "title", "description", "enum"]);
42
+ if (Object.keys(property).some(name => !propertyKeys.has(name))) return undefined;
43
+ if (property.title !== undefined && typeof property.title !== "string") return undefined;
44
+ if (property.description !== undefined && typeof property.description !== "string") return undefined;
45
+ let choices: string[] | undefined;
46
+ if (property.enum !== undefined) {
47
+ if (
48
+ property.type !== "string" ||
49
+ !Array.isArray(property.enum) ||
50
+ property.enum.length === 0 ||
51
+ !property.enum.every(value => typeof value === "string") ||
52
+ new Set(property.enum).size !== property.enum.length ||
53
+ property.enum.some(value => displayText(value) !== value)
54
+ )
55
+ return undefined;
56
+ choices = property.enum;
57
+ }
58
+ const description = typeof property.description === "string" ? ` — ${property.description}` : "";
59
+ return { key, label: displayText(`${property.title ?? key}${description}`), type: property.type, choices };
60
+ }
61
+
62
+ /** Register on an owned manager; dependencies are resolved per call for late UI registration. */
63
+ export function createMCPFormInputHandler(deps: MCPFormInputDependencies): MCPInputRequestHandler {
64
+ return async (_key, request, context) => {
65
+ if (context.signal?.aborted) return { kind: "failed", reason: "cancelled" };
66
+ if (request.method !== "elicitation/create") return { kind: "failed", reason: "unavailable" };
67
+ const params = request.params;
68
+ if (!params || typeof params.message !== "string" || (params.mode !== undefined && params.mode !== "form"))
69
+ return { kind: "failed", reason: "unavailable", message: "Unsupported MCP form request" };
70
+ const field = decodeField(params.requestedSchema);
71
+ if (!field) return { kind: "failed", reason: "unavailable", message: "Unsupported MCP form schema" };
72
+ const local = deps.getUi();
73
+ const ui = local.hasUI ? local.ui : undefined;
74
+ const source = ui ? undefined : deps.getAskAnswerSource();
75
+ if (!ui && !source)
76
+ return { kind: "failed", reason: "unavailable", message: "No user input surface is available" };
77
+ const signal = context.signal;
78
+ const ask = async (question: string, choices?: string[]): Promise<string | undefined> => {
79
+ return untilAborted(signal, async () => {
80
+ if (ui)
81
+ return choices ? ui.select(question, choices, { signal }) : ui.input(question, undefined, { signal });
82
+ if (!source) return undefined;
83
+ const answer = source.awaitAnswerRequest
84
+ ? await source.awaitAnswerRequest(
85
+ {
86
+ question,
87
+ options: choices ?? [],
88
+ interaction: choices ? "selector" : "custom_editor",
89
+ controls: [],
90
+ },
91
+ signal,
92
+ )
93
+ : await source.awaitAnswer(question, choices ?? [], signal);
94
+ let receipt: AskRemoteReceipt | undefined;
95
+ let value: string | undefined;
96
+ if (typeof answer === "string") value = answer;
97
+ else if (answer) {
98
+ receipt = answer;
99
+ if (answer.interaction.kind === "value") value = answer.interaction.value;
100
+ }
101
+ if (signal?.aborted) {
102
+ await receipt?.settle({ kind: "resolve_without_commit", reason: "aborted" });
103
+ return undefined;
104
+ }
105
+ if (receipt?.interaction.kind === "control") {
106
+ await receipt.settle({ kind: "invalid", reason: "invalid_control" });
107
+ throw new Error("Invalid MCP form control");
108
+ }
109
+ if (value !== undefined && choices && !choices.includes(value)) {
110
+ await receipt?.settle({ kind: "invalid", reason: "invalid_option" });
111
+ throw new Error("Invalid MCP form answer");
112
+ }
113
+ const settlement = await receipt?.settle(
114
+ value === undefined ? { kind: "resolve_without_commit", reason: "cancelled" } : { kind: "commit" },
115
+ );
116
+ if (receipt && value !== undefined && settlement?.kind !== "committed") return undefined;
117
+ return value;
118
+ });
119
+ };
120
+ try {
121
+ const title = `MCP server ${displayText(context.serverName)} requests input (untrusted server text): ${displayText(params.message)}`;
122
+ const action = await ask(title, ["Accept", "Decline"]);
123
+ if (signal?.aborted) return { kind: "failed", reason: "cancelled" };
124
+ if (action === undefined) return { kind: "result", result: { action: "cancel" } };
125
+ if (action === "Decline") return { kind: "result", result: { action: "decline" } };
126
+ if (action !== "Accept") return { kind: "failed", reason: "error", message: "Invalid MCP form action" };
127
+ const choices = field.type === "boolean" ? ["Yes", "No"] : field.choices;
128
+ const answer = await ask(
129
+ `MCP server ${displayText(context.serverName)} (untrusted server text): ${field.label}`,
130
+ choices,
131
+ );
132
+ if (signal?.aborted) return { kind: "failed", reason: "cancelled" };
133
+ if (answer === undefined) return { kind: "result", result: { action: "cancel" } };
134
+ if (choices && !choices.includes(answer))
135
+ return { kind: "failed", reason: "error", message: "Invalid MCP form answer" };
136
+ const value = field.type === "boolean" ? answer === "Yes" : answer;
137
+ return { kind: "result", result: { action: "accept", content: { [field.key]: value } } };
138
+ } catch {
139
+ return { kind: "failed", reason: signal?.aborted ? "cancelled" : "error" };
140
+ }
141
+ };
142
+ }
@@ -8,6 +8,7 @@
8
8
  import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callback-server";
9
9
  import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
10
10
  import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
11
+ import { assertPublicOAuthUrl } from "./oauth-public-url";
11
12
 
12
13
  const DEFAULT_PORT = 3000;
13
14
  const CALLBACK_PATH = "/callback";
@@ -261,8 +262,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
261
262
  params.set("client_secret", clientSecret);
262
263
  }
263
264
 
265
+ await assertPublicOAuthUrl(this.config.tokenUrl);
264
266
  const response = await fetch(this.config.tokenUrl, {
265
267
  method: "POST",
268
+ redirect: "error",
266
269
  headers: {
267
270
  "Content-Type": "application/x-www-form-urlencoded",
268
271
  },
@@ -340,8 +343,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
340
343
  if (!registrationEndpoint) return;
341
344
 
342
345
  try {
346
+ await assertPublicOAuthUrl(registrationEndpoint);
343
347
  const response = await fetch(registrationEndpoint, {
344
348
  method: "POST",
349
+ redirect: "error",
345
350
  headers: {
346
351
  "Content-Type": "application/json",
347
352
  Accept: "application/json",
@@ -0,0 +1,8 @@
1
+ import { validatePublicHttpUrl } from "../web/insane/url-guard";
2
+
3
+ export async function assertPublicOAuthUrl(url: string): Promise<void> {
4
+ const checked = await validatePublicHttpUrl(url);
5
+ if (!checked.ok) {
6
+ throw new Error(`Refusing non-public OAuth endpoint: ${checked.reason ?? url}`);
7
+ }
8
+ }
@@ -19,6 +19,10 @@ export function bindPluginMcpToPublicNetwork<T extends MCPHttpServerConfig | MCP
19
19
  }
20
20
 
21
21
  /** The marker is an internal symbol, so persisted user MCP configs cannot opt into or forge this path. */
22
+ export function usesPublicNetworkMcpFetch(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
23
+ return ("publicNetwork" in config && config.publicNetwork === true) || isPluginMcpPublicNetworkBound(config);
24
+ }
25
+
22
26
  export function isPluginMcpPublicNetworkBound(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
23
27
  return (
24
28
  (config as MCPHttpServerConfig & { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]?: boolean })[
@@ -1,4 +1,5 @@
1
1
  import { logger } from "@gajae-code/utils";
2
+ import { type AddressResolver, validatePublicHttpUrl } from "../web/insane/url-guard";
2
3
  import type { MCPServerConfig } from "./types";
3
4
 
4
5
  const SMITHERY_REGISTRY_BASE_URL = "https://registry.smithery.ai";
@@ -289,6 +290,20 @@ function chooseConnection(
289
290
  return null;
290
291
  }
291
292
 
293
+ export async function resolveSmitheryServerConfig(
294
+ qualifiedName: string,
295
+ selected: { connection: SmitheryConnection; useDirectHttp: boolean },
296
+ options?: { resolver?: AddressResolver },
297
+ ): Promise<MCPServerConfig | null> {
298
+ let useDirectHttp = selected.useDirectHttp;
299
+ const deploymentUrl = selected.connection.deploymentUrl;
300
+ if (useDirectHttp && selected.connection.type === "http" && deploymentUrl) {
301
+ const checked = await validatePublicHttpUrl(deploymentUrl, { resolver: options?.resolver });
302
+ if (!checked.ok) useDirectHttp = false;
303
+ }
304
+ return createConfig(qualifiedName, { connection: selected.connection, useDirectHttp });
305
+ }
306
+
292
307
  function createConfig(
293
308
  qualifiedName: string,
294
309
  selected: { connection: SmitheryConnection; useDirectHttp: boolean },
@@ -297,6 +312,7 @@ function createConfig(
297
312
  return {
298
313
  type: "http",
299
314
  url: selected.connection.deploymentUrl,
315
+ publicNetwork: true,
300
316
  };
301
317
  }
302
318
 
@@ -335,7 +351,10 @@ async function fetchServerDetailsFromEntry(
335
351
  return null;
336
352
  }
337
353
 
338
- function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetails): SmitherySearchResult | null {
354
+ async function toSearchResult(
355
+ entry: SmitherySearchEntry,
356
+ details: SmitheryServerDetails,
357
+ ): Promise<SmitherySearchResult | null> {
339
358
  if (!entry.id) return null;
340
359
  const qualifiedName = normalizeQualifiedName(
341
360
  details.qualifiedName ?? entry.qualifiedName ?? `${entry.namespace}/${entry.slug}`,
@@ -343,7 +362,7 @@ function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetai
343
362
  const selected = chooseConnection(details);
344
363
  if (!selected) return null;
345
364
 
346
- const config = createConfig(qualifiedName, selected);
365
+ const config = await resolveSmitheryServerConfig(qualifiedName, selected);
347
366
  if (!config) return null;
348
367
 
349
368
  const requiredInputs = getSchemaInputs(selected.connection.configSchema);
@@ -450,7 +469,7 @@ export async function searchSmitheryRegistry(
450
469
  try {
451
470
  const details = await fetchServerDetailsFromEntry(entry, { apiKey: options?.apiKey });
452
471
  if (!details) return null;
453
- return toSearchResult(entry, details);
472
+ return await toSearchResult(entry, details);
454
473
  } catch (error) {
455
474
  detailFailures.push({
456
475
  identity: getEntryIdentityKey(entry) ?? entry.id ?? "unknown",
@@ -28,7 +28,7 @@ import {
28
28
  MCP_MAX_SSE_REQUEST_MESSAGES,
29
29
  readMCPResponseText,
30
30
  } from "../content-limits";
31
- import { fetchPluginMcpRequest, isPluginMcpPublicNetworkBound } from "../plugin-network-boundary";
31
+ import { fetchPluginMcpRequest, usesPublicNetworkMcpFetch } from "../plugin-network-boundary";
32
32
  import { buildModernMcpHeaders, type MCPModernClientContext, type MCPProtocolEra, withModernMeta } from "../protocol";
33
33
 
34
34
  /**
@@ -155,7 +155,7 @@ export class HttpTransport implements MCPTransport {
155
155
  }
156
156
 
157
157
  #fetch(init: BunFetchRequestInit): Promise<Response> {
158
- return isPluginMcpPublicNetworkBound(this.config)
158
+ return usesPublicNetworkMcpFetch(this.config)
159
159
  ? fetchPluginMcpRequest(this.config.url, init)
160
160
  : fetch(this.config.url, init);
161
161
  }
@@ -139,6 +139,12 @@ export interface MCPHttpServerConfig extends MCPServerConfigBase {
139
139
  type: "http";
140
140
  url: string;
141
141
  headers?: Record<string, string>;
142
+ /**
143
+ * When true, every request and redirect for this URL is rechecked against
144
+ * the public-network policy. Persisted so a later process cannot fall back
145
+ * to an unguarded fetch. Ordinary localhost MCP servers leave this unset.
146
+ */
147
+ publicNetwork?: boolean;
142
148
  }
143
149
 
144
150
  /** SSE server configuration (deprecated, use HTTP) */
@@ -1,7 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { logger } from "@gajae-code/utils";
3
3
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
4
- import { type SdkClient, SdkClientError } from "../client";
4
+ import {
5
+ DEFAULT_SDK_REQUEST_TIMEOUT_MS,
6
+ type SdkClient,
7
+ SdkClientError,
8
+ type SdkDispatchContext,
9
+ type SdkSentRecord,
10
+ } from "../client";
5
11
  import type { AbortScope } from "../host/control/operations";
6
12
  import { assertReverseResponseFrame, ReverseLeaseError } from "../host/reverse-leases";
7
13
  import {
@@ -143,6 +149,7 @@ const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
143
149
  "readiness_timeout",
144
150
  "spawn_failed",
145
151
  "worktree_in_use",
152
+ "uncertain_after_send",
146
153
  ]);
147
154
 
148
155
  /**
@@ -531,7 +538,11 @@ export class AcpSdkAdapter {
531
538
  return envelope?.result ?? response;
532
539
  }
533
540
 
534
- async #requestSession(frame: JsonObject, raw = false, options?: { timeoutMs: number }): Promise<unknown> {
541
+ async #requestSession(
542
+ frame: JsonObject,
543
+ raw = false,
544
+ options?: { timeoutMs: number; deadline?: number },
545
+ ): Promise<unknown> {
535
546
  const router = this.#router;
536
547
  if (!router)
537
548
  throw new AcpSdkAdapterError(
@@ -603,11 +614,68 @@ export class AcpSdkAdapter {
603
614
  // request that named no readiness budget is queued for the default one, so it
604
615
  // needs the same extension rather than the client's generic request deadline.
605
616
  const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
606
- const response = await this.#client.global(operation, input, {
617
+ const budgetMs = timeoutMs ?? DEFAULT_SDK_REQUEST_TIMEOUT_MS;
618
+ const deadline = Date.now() + budgetMs;
619
+ let sent = false;
620
+ let sentRecord: SdkSentRecord | undefined;
621
+ let uncertainty: SdkClientError | undefined;
622
+ const deadlineFailure = () =>
623
+ uncertainty ??
624
+ (sent
625
+ ? new SdkClientError(
626
+ "uncertain_after_send",
627
+ "ACP lifecycle deadline elapsed after dispatch.",
628
+ sentRecord ?? {
629
+ operation,
630
+ idempotencyKey,
631
+ },
632
+ )
633
+ : new SdkClientError("timeout", "ACP lifecycle deadline elapsed before dispatch.", undefined, undefined, {
634
+ transport: true,
635
+ }));
636
+ const options = {
607
637
  idempotencyKey,
608
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
609
- });
610
- return response;
638
+ deadline,
639
+ // SdkClient reads this after connecting, so reconnect and replay spend the same allowance.
640
+ get timeoutMs() {
641
+ return Math.max(0, deadline - Date.now());
642
+ },
643
+ beforeDispatch: () => {
644
+ if (Date.now() >= deadline) throw deadlineFailure();
645
+ },
646
+ onDispatch: (context: SdkDispatchContext) => {
647
+ sent = true;
648
+ if (typeof context.frame.id === "string") sentRecord = client.getSentRecord(context.frame.id);
649
+ },
650
+ };
651
+ const expired = Promise.withResolvers<never>();
652
+ const timer = setTimeout(() => expired.reject(deadlineFailure()), budgetMs);
653
+ const recover = async () => {
654
+ try {
655
+ const result = await client.global(operation, input, options);
656
+ if (Date.now() >= deadline) throw deadlineFailure();
657
+ return result;
658
+ } catch (firstError) {
659
+ if (!(firstError instanceof SdkClientError) || firstError.code !== "uncertain_after_send") throw firstError;
660
+ uncertainty = firstError;
661
+ // Same-key replay joins the original durable claim, within the original deadline.
662
+ // Failure to recover does not prove that the committed operation failed.
663
+ if (Date.now() >= deadline) throw firstError;
664
+ try {
665
+ const result = await client.global(operation, input, options);
666
+ if (Date.now() >= deadline) throw firstError;
667
+ return result;
668
+ } catch (replayError) {
669
+ if (replayError !== firstError) Object.assign(firstError, { recovery: replayError });
670
+ throw firstError;
671
+ }
672
+ }
673
+ };
674
+ try {
675
+ return await Promise.race([recover(), expired.promise]);
676
+ } finally {
677
+ clearTimeout(timer);
678
+ }
611
679
  }
612
680
 
613
681
  async sdkControl(params: { operation: string; input?: JsonObject }): Promise<unknown> {
@@ -646,7 +714,7 @@ export class AcpSdkAdapter {
646
714
  throw new AcpSdkAdapterError("method_not_found", `Unsupported ACP SDK method: ${method}`);
647
715
  }
648
716
 
649
- async registerProvider(provider: AcpProviderRegistration): Promise<void> {
717
+ async registerProvider(provider: AcpProviderRegistration, timeoutMs?: number, deadline?: number): Promise<void> {
650
718
  if (!this.#router)
651
719
  throw new AcpSdkAdapterError(
652
720
  "operation_prohibited",
@@ -665,10 +733,15 @@ export class AcpSdkAdapter {
665
733
  ...(previousLeaseId ? { expectedLeaseId: previousLeaseId } : {}),
666
734
  },
667
735
  true,
736
+ timeoutMs === undefined ? undefined : { timeoutMs, ...(deadline === undefined ? {} : { deadline }) },
668
737
  );
669
738
  const result = object(object(response)?.result) ?? object(response) ?? {};
670
739
  if (typeof result.leaseId !== "string")
671
740
  throw new AcpSdkAdapterError("invalid_reverse_frame", "Provider registration omitted leaseId.");
741
+ if (this.#closed)
742
+ throw new AcpSdkAdapterError("connection_closed", "ACP provider activation closed before acknowledgement.");
743
+ if (deadline !== undefined && Date.now() >= deadline)
744
+ throw new AcpSdkAdapterError("provider_activation_exhausted", "ACP provider activation deadline elapsed.");
672
745
  this.#leases.set(provider.capability, result.leaseId);
673
746
  if (previousLeaseId && previousLeaseId !== result.leaseId) this.#abortReverseForCapability(provider.capability);
674
747
  }
@@ -694,9 +767,20 @@ export class AcpSdkAdapter {
694
767
  const connectionId = attachment?.connectionId;
695
768
  try {
696
769
  for (const provider of this.#providers) {
770
+ const providerRemainingMs = PROVIDER_ACTIVATION_BUDGET_MS - (Date.now() - startedAt);
771
+ if (providerRemainingMs <= 0) throw this.#providerActivationExhausted(attempt, startedAt);
697
772
  try {
698
- await this.registerProvider(provider);
773
+ await this.registerProvider(
774
+ provider,
775
+ Math.max(1, providerRemainingMs),
776
+ startedAt + PROVIDER_ACTIVATION_BUDGET_MS,
777
+ );
778
+ if (Date.now() - startedAt >= PROVIDER_ACTIVATION_BUDGET_MS)
779
+ throw this.#providerActivationExhausted(attempt, startedAt);
699
780
  } catch (error) {
781
+ if (providerErrorCode(error) === "uncertain_after_send") throw error;
782
+ if (Date.now() - startedAt >= PROVIDER_ACTIVATION_BUDGET_MS)
783
+ throw this.#providerActivationExhausted(attempt, startedAt);
700
784
  if (providerErrorCode(error) === "provider_lease_conflict") {
701
785
  this.#leases.delete(provider.capability);
702
786
  this.#abortReverseForCapability(provider.capability);
@@ -725,6 +809,7 @@ export class AcpSdkAdapter {
725
809
  this.#providersActivated = true;
726
810
  return;
727
811
  } catch (error) {
812
+ if (providerErrorCode(error) === "uncertain_after_send") throw error;
728
813
  if (this.#router && attachment !== this.#attachment && !this.#closed) {
729
814
  this.#providersActivated = false;
730
815
  continue;
@@ -22,6 +22,7 @@ export type BrokerFenceReason = "suspect-unpublished" | "observation-ambiguous"
22
22
  export type BrokerExitReason =
23
23
  | "ownership-fence-expired"
24
24
  | "publication-liveness-timeout"
25
+ | "heartbeat-renewal-blocked"
25
26
  | "restart-committed"
26
27
  | "shutdown-request"
27
28
  | "startup-failure"
@@ -39,6 +40,8 @@ export interface BrokerExitRecord {
39
40
  fenceReason: BrokerFenceReason | null;
40
41
  fencedForMs: number;
41
42
  uptimeMs: number;
43
+ /** For a blocked heartbeat renewal: retained removal transition path. */
44
+ blockingLockPath?: string;
42
45
  pid: number;
43
46
  signal: "SIGINT" | "SIGTERM" | null;
44
47
  writtenAt: number;
@@ -47,7 +50,7 @@ export interface BrokerExitRecord {
47
50
  export interface BrokerStartupExitRecord {
48
51
  version: 1;
49
52
  mode: "startup";
50
- reason: "startup-deadline" | "startup-signal";
53
+ reason: "startup-deadline" | "startup-signal" | "startup-lock-blocked";
51
54
  fenceReason: null;
52
55
  fencedForMs: 0;
53
56
  uptimeMs: number;
@@ -56,6 +59,8 @@ export interface BrokerStartupExitRecord {
56
59
  exitCode: 1 | 130 | 143;
57
60
  timeoutMs: number | null;
58
61
  writtenAt: number;
62
+ /** For lock-blocked reason: path to the retained removal transition. */
63
+ blockingLockPath?: string;
59
64
  }
60
65
 
61
66
  export type BrokerStartupExitWriteStatus =
@@ -71,6 +76,7 @@ const lastWriteGenerationByPath = new Map<string, number>();
71
76
  const EXIT_REASONS = new Set<BrokerExitReason>([
72
77
  "ownership-fence-expired",
73
78
  "publication-liveness-timeout",
79
+ "heartbeat-renewal-blocked",
74
80
  "restart-committed",
75
81
  "shutdown-request",
76
82
  "startup-failure",
@@ -94,11 +100,20 @@ function isBrokerExitRecord(value: unknown): value is BrokerExitRecord {
94
100
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
95
101
  const record = value as Partial<BrokerExitRecord>;
96
102
  const keys = Object.keys(record).sort();
103
+ const requiredKeys = [
104
+ "fenceReason",
105
+ "fencedForMs",
106
+ "mode",
107
+ "pid",
108
+ "reason",
109
+ "signal",
110
+ "uptimeMs",
111
+ "version",
112
+ "writtenAt",
113
+ ];
97
114
  return (
98
- keys.join(",") ===
99
- ["fenceReason", "fencedForMs", "mode", "pid", "reason", "signal", "uptimeMs", "version", "writtenAt"]
100
- .sort()
101
- .join(",") &&
115
+ keys.every(key => requiredKeys.includes(key) || key === "blockingLockPath") &&
116
+ requiredKeys.every(key => keys.includes(key)) &&
102
117
  record.version === 1 &&
103
118
  (record.mode === "owned-root" || record.mode === "lost-root") &&
104
119
  typeof record.reason === "string" &&
@@ -115,6 +130,14 @@ function isBrokerExitRecord(value: unknown): value is BrokerExitRecord {
115
130
  (record.pid as number) > 0 &&
116
131
  (record.signal === null || record.signal === "SIGINT" || record.signal === "SIGTERM") &&
117
132
  (record.reason === "signal") === (record.signal !== null) &&
133
+ (record.reason === "heartbeat-renewal-blocked"
134
+ ? record.mode === "owned-root" &&
135
+ record.fenceReason === null &&
136
+ record.fencedForMs === 0 &&
137
+ record.signal === null &&
138
+ typeof record.blockingLockPath === "string" &&
139
+ record.blockingLockPath.length > 0
140
+ : record.blockingLockPath === undefined) &&
118
141
  Number.isSafeInteger(record.writtenAt) &&
119
142
  (record.writtenAt as number) > 0
120
143
  );
@@ -125,26 +148,29 @@ function isBrokerStartupExitRecord(value: unknown): value is BrokerStartupExitRe
125
148
  const record = value as Partial<BrokerStartupExitRecord>;
126
149
  const signalExitCode = record.signal === "SIGINT" ? 130 : record.signal === "SIGTERM" ? 143 : null;
127
150
  const keys = Object.keys(record).sort();
151
+ const requiredKeys = [
152
+ "exitCode",
153
+ "fenceReason",
154
+ "fencedForMs",
155
+ "mode",
156
+ "pid",
157
+ "reason",
158
+ "signal",
159
+ "timeoutMs",
160
+ "uptimeMs",
161
+ "version",
162
+ "writtenAt",
163
+ ];
164
+ const isValidShape =
165
+ keys.every(k => requiredKeys.includes(k) || k === "blockingLockPath") &&
166
+ requiredKeys.every(k => keys.includes(k));
128
167
  return (
129
- keys.join(",") ===
130
- [
131
- "exitCode",
132
- "fenceReason",
133
- "fencedForMs",
134
- "mode",
135
- "pid",
136
- "reason",
137
- "signal",
138
- "timeoutMs",
139
- "uptimeMs",
140
- "version",
141
- "writtenAt",
142
- ]
143
- .sort()
144
- .join(",") &&
168
+ isValidShape &&
145
169
  record.version === 1 &&
146
170
  record.mode === "startup" &&
147
- (record.reason === "startup-deadline" || record.reason === "startup-signal") &&
171
+ (record.reason === "startup-deadline" ||
172
+ record.reason === "startup-signal" ||
173
+ record.reason === "startup-lock-blocked") &&
148
174
  record.fenceReason === null &&
149
175
  record.fencedForMs === 0 &&
150
176
  Number.isSafeInteger(record.uptimeMs) &&
@@ -154,7 +180,10 @@ function isBrokerStartupExitRecord(value: unknown): value is BrokerStartupExitRe
154
180
  (record.signal === null || record.signal === "SIGINT" || record.signal === "SIGTERM") &&
155
181
  (record.exitCode === 1 || record.exitCode === 130 || record.exitCode === 143) &&
156
182
  (record.timeoutMs === null || (Number.isSafeInteger(record.timeoutMs) && (record.timeoutMs as number) > 0)) &&
157
- (record.reason === "startup-deadline"
183
+ (record.reason === "startup-lock-blocked"
184
+ ? typeof record.blockingLockPath === "string" && record.blockingLockPath.length > 0
185
+ : record.blockingLockPath === undefined) &&
186
+ (record.reason === "startup-deadline" || record.reason === "startup-lock-blocked"
158
187
  ? record.exitCode === 1 && record.signal === null && record.timeoutMs !== null
159
188
  : record.exitCode === signalExitCode && signalExitCode !== null && record.timeoutMs === null) &&
160
189
  Number.isSafeInteger(record.writtenAt) &&