@gajae-code/coding-agent 0.18.7 → 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 (279) hide show
  1. package/CHANGELOG.md +279 -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 +2 -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/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -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
  }
@@ -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) &&
@@ -6,9 +6,14 @@ import path from "node:path";
6
6
  import type { NativeBrokerRestartIntent, NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
7
7
  import { logger, resolveEquivalentPath } from "@gajae-code/utils";
8
8
  import packageJson from "../../../package.json" with { type: "json" };
9
+ import { FileLockAcquireError } from "../../config/file-lock";
9
10
  import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
10
11
  import { planLaunchWorktree } from "../../gjc-runtime/launch-worktree";
11
12
  import { readExistingStateForMutation, withWorkflowStateLock } from "../../gjc-runtime/state-writer";
13
+ import type {
14
+ TaskArtifactOwnerDeletionEvidence,
15
+ TaskArtifactOwnerRetirementContinuation,
16
+ } from "../../session/task-artifact-owner-codec";
12
17
  import { SdkClient, SdkClientError } from "../client";
13
18
  import {
14
19
  BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
@@ -127,6 +132,7 @@ import {
127
132
  type SpawnSubstrateProvider,
128
133
  } from "./spawn-authority";
129
134
  import { createSpawnSubstrateProvider } from "./spawn-substrate";
135
+ import type { BrokerTaskArtifactOwnerRetirementDisposition } from "./task-artifact-owner-validation";
130
136
  import { BrokerTransport } from "./transport";
131
137
 
132
138
  export interface BrokerSettings {
@@ -158,6 +164,8 @@ export interface BrokerSettings {
158
164
  restartRequestId?: string;
159
165
  /** Cancel bootstrap before retained publication when the owning CLI receives a signal. */
160
166
  startupAbortSignal?: AbortSignal;
167
+ /** Monotonic checkpoint deadline, leaving time for retained publication. */
168
+ startupCheckpointDeadline?: number;
161
169
  /** Called synchronously when retained publication establishes broker readiness. */
162
170
  onStartupReady?: () => void;
163
171
  /** Test-only delay after session checkpoint to verify unpublished discovery ownership. */
@@ -293,6 +301,19 @@ export type BrokerCleanupEvidence = {
293
301
  retainedTranscriptSuccessorPath?: string;
294
302
  retainedTranscriptPlaceholderPath?: string;
295
303
  retainedTranscriptUnknownPath?: string;
304
+ /** Immutable task-artifact owner authority captured before the first deletion effect. */
305
+ taskArtifactOwnerDeletionEvidence?: TaskArtifactOwnerDeletionEvidence;
306
+ /** Latest exact native remnant; it never replaces the original deletion evidence. */
307
+ taskArtifactOwnerRetirementContinuation?: TaskArtifactOwnerRetirementContinuation;
308
+ /** Strict owner-only wire disposition, decoded only with its separately stored evidence. */
309
+ taskArtifactOwnerRetirementOutcome?: BrokerTaskArtifactOwnerRetirementDisposition;
310
+ taskArtifactOwnerPayloadRetired?: true;
311
+ taskArtifactOwnerNamespaceRetained?: true;
312
+ taskArtifactOwnerRetired?: true;
313
+ /** Durable fact that transcript retirement completed while the owner namespace remains. */
314
+ taskArtifactOwnerTranscriptDeleted?: true;
315
+ /** Stable artifacts-phase owner refusal diagnostic. */
316
+ taskArtifactOwnerCleanupError?: string;
296
317
  /** Durable proof that artifact cleanup completed before transcript mutation. */
297
318
  artifactsRemoved?: boolean;
298
319
  artifactsAbsentAtAuthorization?: true;
@@ -756,6 +777,17 @@ function normalizeAliasedString(
756
777
  return { value: values[0] };
757
778
  }
758
779
 
780
+ const BROKER_TASK_ARTIFACT_OWNER_CLEANUP_FIELDS = [
781
+ "taskArtifactOwnerDeletionEvidence",
782
+ "taskArtifactOwnerRetirementContinuation",
783
+ "taskArtifactOwnerRetirementOutcome",
784
+ "taskArtifactOwnerPayloadRetired",
785
+ "taskArtifactOwnerNamespaceRetained",
786
+ "taskArtifactOwnerRetired",
787
+ "taskArtifactOwnerTranscriptDeleted",
788
+ "taskArtifactOwnerCleanupError",
789
+ ] as const;
790
+
759
791
  export function normalizeBrokerInput(operation: string, input: Record<string, unknown>): InputNormalization {
760
792
  const normalized: Record<string, unknown> = { ...input };
761
793
  const session = normalizeAliasedString(input, "sessionId", ["id"]);
@@ -834,6 +866,18 @@ export function normalizeBrokerInput(operation: string, input: Record<string, un
834
866
  if (cwd.value !== undefined) normalized.stateRoot = path.join(cwd.value, ".gjc", "state");
835
867
  else if (stateRoot.value !== undefined) return error("invalid_input", "stateRoot requires cwd.");
836
868
 
869
+ if (operation === "session.delete") {
870
+ const hasOwnerField = (value: unknown): boolean =>
871
+ typeof value === "object" &&
872
+ value !== null &&
873
+ !Array.isArray(value) &&
874
+ BROKER_TASK_ARTIFACT_OWNER_CLEANUP_FIELDS.some(key => Object.hasOwn(value, key));
875
+ if (hasOwnerField(input) || hasOwnerField(target) || hasOwnerField(input.cleanup))
876
+ return error(
877
+ "invalid_input",
878
+ "Task-artifact owner cleanup state is broker-managed and cannot be supplied by clients.",
879
+ );
880
+ }
837
881
  if (target) {
838
882
  const normalizedTarget = { ...target };
839
883
  delete normalizedTarget.path;
@@ -1530,6 +1574,7 @@ export class Broker {
1530
1574
  #transport: BrokerTransport | null = null;
1531
1575
  #heartbeatTimer: NodeJS.Timeout | null = null;
1532
1576
  #startupAbortSignal: AbortSignal | undefined;
1577
+ #startupCheckpointDeadline: number | undefined;
1533
1578
  #onStartupReady: (() => void) | undefined;
1534
1579
  #startupPrePublicationDelayMs: number;
1535
1580
  #startupPrePublicationTestHook: (() => Promise<void>) | undefined;
@@ -1569,6 +1614,7 @@ export class Broker {
1569
1614
  this.#ownsResolveModelPin = settings.resolveModelPin === undefined;
1570
1615
  this.#resolveModelPin = settings.resolveModelPin ?? createDefaultSdkHostModelResolver(this.settings.agentDir);
1571
1616
  this.#startupAbortSignal = settings.startupAbortSignal;
1617
+ this.#startupCheckpointDeadline = settings.startupCheckpointDeadline;
1572
1618
  this.#onStartupReady = settings.onStartupReady;
1573
1619
  this.#startupPrePublicationDelayMs =
1574
1620
  Number.isSafeInteger(settings.startupPrePublicationDelayMs) &&
@@ -3898,7 +3944,7 @@ export class Broker {
3898
3944
  // checkpoint settles. The bootstrap watchdog owns this pre-publication
3899
3945
  // interval; publishing first allowed it to kill an endpoint already handed
3900
3946
  // to callers when a legitimate index-lock wait outlived the fence.
3901
- await this.#checkpointSessionHeartbeats();
3947
+ await this.#checkpointSessionHeartbeats(this.#startupAbortSignal, this.#startupCheckpointDeadline);
3902
3948
  this.#throwIfStartupAborted();
3903
3949
  await this.#startupPrePublicationTestHook?.();
3904
3950
  if (this.#startupPrePublicationDelayMs > 0) await Bun.sleep(this.#startupPrePublicationDelayMs);
@@ -4231,15 +4277,29 @@ export class Broker {
4231
4277
  if (publication) await this.#writeHeartbeat(publication);
4232
4278
  }
4233
4279
  /** Re-observes provably live session hosts and checkpoints their liveness. */
4234
- async heartbeatSessions(now = Date.now()): Promise<number> {
4235
- return await this.index.checkpointLiveHeartbeats(now);
4280
+ async heartbeatSessions(now = Date.now(), abortSignal?: AbortSignal, deadlineAt?: number): Promise<number> {
4281
+ return await this.index.checkpointLiveHeartbeats(now, abortSignal, deadlineAt);
4236
4282
  }
4237
- async #checkpointSessionHeartbeats(): Promise<void> {
4283
+ async #checkpointSessionHeartbeats(abortSignal?: AbortSignal, startupCheckpointDeadline?: number): Promise<void> {
4238
4284
  if (this.#checkpointInFlight || this.#stopping) return;
4239
4285
  this.#checkpointInFlight = true;
4240
4286
  try {
4241
- await this.heartbeatSessions();
4287
+ // The CLI reserves publication headroom in this startup deadline.
4288
+ // Periodic passes use their own budget, not a retired startup signal.
4289
+ await this.heartbeatSessions(Date.now(), abortSignal, startupCheckpointDeadline);
4242
4290
  } catch (error) {
4291
+ if (
4292
+ error instanceof FileLockAcquireError &&
4293
+ error.orphanPath &&
4294
+ this.#publication !== null &&
4295
+ this.#publicationState === "healthy-owned" &&
4296
+ this.#fenceReason === null &&
4297
+ this.#completionTask === null &&
4298
+ this.#provenOwnedRoot()
4299
+ ) {
4300
+ await this.#complete("owned-root", "heartbeat-renewal-blocked", null, error.orphanPath);
4301
+ return;
4302
+ }
4243
4303
  logger.warn(`sdk broker: session heartbeat checkpoint failed: ${String(error)}`);
4244
4304
  } finally {
4245
4305
  this.#checkpointInFlight = false;
@@ -4249,6 +4309,7 @@ export class Broker {
4249
4309
  mode: BrokerExitMode,
4250
4310
  reason: BrokerExitReason,
4251
4311
  signal: BrokerExitRecord["signal"] = null,
4312
+ blockingLockPath?: string,
4252
4313
  ): Promise<void> {
4253
4314
  if (this.#completionTask) return this.#completionTask;
4254
4315
  const now = process.hrtime.bigint();
@@ -4260,11 +4321,19 @@ export class Broker {
4260
4321
  fenceReason: this.#fenceReason,
4261
4322
  fencedForMs: fenceStartedAt === null ? 0 : Math.max(0, Number((now - fenceStartedAt) / 1_000_000n)),
4262
4323
  uptimeMs: this.#startedAt === null ? 0 : Math.max(0, Number((now - this.#startedAt) / 1_000_000n)),
4324
+ ...(blockingLockPath === undefined ? {} : { blockingLockPath }),
4263
4325
  pid: process.pid,
4264
4326
  signal,
4265
4327
  writtenAt: Date.now(),
4266
4328
  };
4267
- (mode === "lost-root" ? logger.warn : logger.info)("sdk broker: exiting", exitRecord);
4329
+ const message =
4330
+ reason === "heartbeat-renewal-blocked" && blockingLockPath
4331
+ ? `blocked by retained removal transition ${blockingLockPath}`
4332
+ : undefined;
4333
+ (mode === "lost-root" || message !== undefined ? logger.warn : logger.info)("sdk broker: exiting", {
4334
+ ...exitRecord,
4335
+ ...(message === undefined ? {} : { message }),
4336
+ });
4268
4337
  this.#stopping = true;
4269
4338
  this.#checkpointInFlight = false;
4270
4339
  this.#publicationState = "stopping";
@@ -41,6 +41,8 @@ function brokerStartupFailureReason(marker: BrokerStartupFailureMarker | undefin
41
41
 
42
42
  function brokerStartupExitReason(record: BrokerStartupExitRecord | undefined): string | undefined {
43
43
  if (!record) return undefined;
44
+ if (record.reason === "startup-lock-blocked")
45
+ return `SDK broker startup blocked by retained removal transition ${record.blockingLockPath}.`;
44
46
  if (record.reason === "startup-deadline")
45
47
  return `SDK broker startup exceeded its ${record.timeoutMs}ms fence deadline.`;
46
48
  return `SDK broker startup interrupted by ${record.signal} before readiness.`;