@gajae-code/coding-agent 0.16.7 → 0.17.1

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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -25,6 +25,7 @@ import {
25
25
  import { listMcpDelegateHostContexts } from "../hooks/mcp-delegate-host-context";
26
26
  import type { WorkflowGate, WorkflowGateQueryRecord } from "../modes/shared/agent-wire/workflow-gate-types";
27
27
  import type { BrokerDiscovery } from "../sdk/broker/discovery";
28
+ import { endpointIncarnation } from "../sdk/broker/endpoint-authority";
28
29
  import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
29
30
  import { lifecycleRequestTimeoutMs } from "../sdk/broker/startup-budget";
30
31
  import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
@@ -139,6 +140,7 @@ import {
139
140
  reconcileCreationRemoteVerifier,
140
141
  recordCreationRetirementBrokerProof,
141
142
  recordCreationRetirementIntent,
143
+ recordDeletionIntent,
142
144
  recoverExpiredPublicDelivery,
143
145
  releasePublicDeliveryClaim,
144
146
  removeSessionTransaction,
@@ -542,7 +544,10 @@ function textResult(
542
544
  };
543
545
  }
544
546
 
545
- function toolSchema(name: CoordinatorToolName): {
547
+ function toolSchema(
548
+ name: CoordinatorToolName,
549
+ platform: NodeJS.Platform,
550
+ ): {
546
551
  name: CoordinatorToolName;
547
552
  description: string;
548
553
  inputSchema: Record<string, unknown>;
@@ -684,7 +689,7 @@ function toolSchema(name: CoordinatorToolName): {
684
689
  return {
685
690
  name,
686
691
  description:
687
- "Close and reap a coordinator delegate-created (ephemeral) SDK session through broker lifecycle control. Non-ephemeral user-registered sessions require both force and the force-stop capability.",
692
+ "Close and reap a coordinator-created (ephemeral) SDK session through broker lifecycle control. Non-ephemeral user-registered sessions require both force and the force-stop capability.",
688
693
  inputSchema: {
689
694
  type: "object",
690
695
  properties: {
@@ -803,7 +808,9 @@ function toolSchema(name: CoordinatorToolName): {
803
808
  if (name === "gjc_coordinator_read_artifact") {
804
809
  return {
805
810
  name,
806
- description: coordinatorArtifactCapability().available
811
+ // Controllers detect capability from tools/list (docs/hermes-mcp-bridge.md), so the
812
+ // advertisement must use the same resolved platform as the handler refusal.
813
+ description: coordinatorArtifactCapability(platform).available
807
814
  ? "Read one bounded artifact from configured safe roots."
808
815
  : "Unavailable on this platform: artifact reads require Linux identity-bound handle authorization.",
809
816
  inputSchema: { type: "object", properties: { path: pathField }, required: ["path"] },
@@ -1376,7 +1383,7 @@ function boundedPublicValue(value: unknown, budget: { remaining: number }, depth
1376
1383
  const output: Record<string, unknown> = {};
1377
1384
  for (const key of Object.keys(value as Record<string, unknown>).slice(0, 128)) {
1378
1385
  const field = (value as Record<string, unknown>)[key];
1379
- if (key === "error") {
1386
+ if (key === "error" && field !== null) {
1380
1387
  const rawCode =
1381
1388
  field && typeof field === "object" && !Array.isArray(field)
1382
1389
  ? (field as Record<string, unknown>).code
@@ -4438,6 +4445,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4438
4445
  eventId: string;
4439
4446
  }> = [];
4440
4447
  let q12Admitted = true;
4448
+ let liveInFlightAskAdmitted = false;
4449
+ let liveInFlightCandidate = false;
4441
4450
  await withAdmittedSessionTransaction(
4442
4451
  questionPaths,
4443
4452
  sessionId,
@@ -4445,6 +4454,39 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4445
4454
  const liveState = await readSessionState(namespaceDir, sessionId);
4446
4455
  const waitingToken = admissionToken;
4447
4456
  const waitingTurn = waitingToken ? transaction.canonical.turns[waitingToken.coordinator_turn_id] : null;
4457
+ const liveTurn = admission.active_turn_id
4458
+ ? transaction.canonical.turns[admission.active_turn_id]
4459
+ : undefined;
4460
+ // The Q12 query runs outside this transaction. Re-check the turn that
4461
+ // admission observed so cancellation or terminal reconciliation cannot
4462
+ // make an in-flight empty snapshot look like a healthy running result.
4463
+ if (
4464
+ admission.active_turn_id &&
4465
+ (!liveTurn || TERMINAL_TURN_STATUSES.has(liveTurn.status as TurnStatus))
4466
+ ) {
4467
+ q12Admitted = false;
4468
+ return;
4469
+ }
4470
+ // A headless ask opens a durable Q12 gate before the runtime has a
4471
+ // distinct needs_user_input lifecycle marker. Admit that narrow,
4472
+ // observable in-flight state only when the sidecar, active turn, and
4473
+ // accepted runtime receipt all identify the same turn. This is waiting
4474
+ // evidence, not a terminal observation; all terminal paths remain
4475
+ // subject to the existing receipt and terminal-fence checks.
4476
+ liveInFlightCandidate = Boolean(
4477
+ liveState?.schema_version === 1 &&
4478
+ liveState.session_id === sessionId &&
4479
+ liveState?.source === "agent_session_event" &&
4480
+ liveState.live === true &&
4481
+ liveState.state === "running" &&
4482
+ liveState.current_turn_id !== null &&
4483
+ Number.isFinite(Date.parse(liveState.updated_at)) &&
4484
+ hasExactRuntimeWriterIdentity(liveState, transaction.canonical.session.broker) &&
4485
+ liveState.current_turn_id === admission.active_turn_id &&
4486
+ liveTurn &&
4487
+ ACTIVE_TURN_STATUSES.has(liveTurn.status as TurnStatus) &&
4488
+ hasAcceptedRuntimeReceipt(liveTurn),
4489
+ );
4448
4490
  if (
4449
4491
  waitingToken &&
4450
4492
  (!liveState ||
@@ -4464,10 +4506,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4464
4506
  q12Admitted = false;
4465
4507
  return;
4466
4508
  }
4509
+ if (
4510
+ liveState?.source === "agent_session_event" &&
4511
+ (liveState.state === "completed" || liveState.state === "errored") &&
4512
+ !admission.terminal
4513
+ ) {
4514
+ q12Admitted = false;
4515
+ return;
4516
+ }
4467
4517
  if (
4468
4518
  liveState?.source === "agent_session_event" &&
4469
4519
  liveState.live === true &&
4470
- (liveState.state !== "needs_user_input" || liveState.current_turn_id !== admission.active_turn_id)
4520
+ (liveState.state !== "needs_user_input" || liveState.current_turn_id !== admission.active_turn_id) &&
4521
+ !liveInFlightCandidate
4471
4522
  ) {
4472
4523
  q12Admitted = false;
4473
4524
  return;
@@ -4581,6 +4632,13 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4581
4632
  };
4582
4633
  authority.outcome = { state: "deferred_link", first_seen_at: authority.first_seen_at };
4583
4634
  authority.updated_at = observedAt;
4635
+ // The runtime already published this gate, but no coordinator turn owns
4636
+ // it yet, so the question stays unmaterialized. Reporting nothing here
4637
+ // made a blocked agent indistinguishable from an idle one: `list_questions`
4638
+ // answered a healthy empty snapshot for the whole deferral window while
4639
+ // the ask behind the gate was waiting for an answer no operator could see
4640
+ // (#5475). The deferral itself is unchanged — only the silence is.
4641
+ diagnostic("pending_registration", null, gate.gate_id);
4584
4642
  continue;
4585
4643
  }
4586
4644
  authority.outcome =
@@ -4596,6 +4654,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4596
4654
  continue;
4597
4655
  }
4598
4656
  const turn = owners[0]!;
4657
+ if (liveInFlightCandidate && turn.turn_id !== admission.active_turn_id) {
4658
+ authority.outcome = { state: "ownership_conflict", reason: "ownership_conflict" };
4659
+ authority.updated_at = observedAt;
4660
+ diagnostic("ownership_conflict", turn.turn_id, gate.gate_id);
4661
+ continue;
4662
+ }
4599
4663
  const provenance = runtimeProvenanceToken(sessionId, transaction, turn, gate);
4600
4664
  if (!turn.runtime_provenance || canonicalJson(turn.runtime_provenance) !== canonicalJson(provenance))
4601
4665
  turn.runtime_provenance = provenance;
@@ -4691,6 +4755,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4691
4755
  },
4692
4756
  };
4693
4757
  }
4758
+ if (liveInFlightCandidate && turn.turn_id === admission.active_turn_id)
4759
+ liveInFlightAskAdmitted = true;
4694
4760
  }
4695
4761
  if (complete)
4696
4762
  for (const [authorityId, authority] of Object.entries(transaction.canonical.gate_authorities))
@@ -4712,7 +4778,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4712
4778
  },
4713
4779
  options,
4714
4780
  );
4715
- if (!q12Admitted)
4781
+ // A complete empty Q12 snapshot proves no pending gate at this revision,
4782
+ // even while an authenticated, acknowledged turn is still running.
4783
+ // Nonempty snapshots still require an admitted ask; all writer, waiting
4784
+ // token and terminal fences above remain authoritative.
4785
+ if (!q12Admitted || (liveInFlightCandidate && items.length > 0 && !liveInFlightAskAdmitted))
4716
4786
  return {
4717
4787
  ok: true,
4718
4788
  schema_version: 1,
@@ -5633,9 +5703,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5633
5703
  endpointMtimeMs <= 0
5634
5704
  )
5635
5705
  return null;
5636
- return createHash("sha256")
5637
- .update(canonicalJson({ endpointGeneration, endpointMtimeMs, pid, sessionId }))
5638
- .digest("hex");
5706
+ // Single canonical producer lives in endpoint-authority.ts (#5376
5707
+ // follow-up): never reimplement the digest recipe here.
5708
+ return (
5709
+ endpointIncarnation(
5710
+ {
5711
+ endpointGeneration,
5712
+ endpointMtimeMs,
5713
+ pid,
5714
+ ...(typeof session.endpointFileId === "string" ? { endpointFileId: session.endpointFileId } : {}),
5715
+ },
5716
+ sessionId,
5717
+ ) ?? null
5718
+ );
5639
5719
  }
5640
5720
 
5641
5721
  type BrokerSessionAuthority = {
@@ -5701,13 +5781,31 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5701
5781
 
5702
5782
  async function listSessions(cwd?: string): Promise<Array<Record<string, unknown>>> {
5703
5783
  const roots = cwd ? [cwd] : config.allowedRoots;
5784
+ const uniqueRoots = [
5785
+ ...new Map(
5786
+ roots.map(root => {
5787
+ let canonical = path.resolve(root);
5788
+ try {
5789
+ canonical = nodeFs.realpathSync.native(canonical);
5790
+ } catch {
5791
+ // Preserve the configured path when the root is not materialized yet.
5792
+ }
5793
+ return [normalizePathForComparison(canonical, platform), root] as const;
5794
+ }),
5795
+ ).values(),
5796
+ ];
5704
5797
  const listings = await Promise.all(
5705
- roots.map(async root => {
5798
+ uniqueRoots.map(async root => {
5706
5799
  const listing = await paginatedBrokerSessionList(root, { cwd: root });
5707
5800
  return scopedBrokerSessions(Array.isArray(listing.sessions) ? listing.sessions : [], root, platform);
5708
5801
  }),
5709
5802
  );
5710
- return listings.flat();
5803
+ const uniqueSessions = new Map<string, Record<string, unknown>>();
5804
+ for (const session of listings.flat()) {
5805
+ const id = brokerSessionId(session);
5806
+ if (id !== null) uniqueSessions.set(id, session);
5807
+ }
5808
+ return [...uniqueSessions.values()];
5711
5809
  }
5712
5810
  function sessionFile(sessionId: unknown): string {
5713
5811
  return path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`);
@@ -5848,6 +5946,41 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5848
5946
  );
5849
5947
  return true;
5850
5948
  }
5949
+ /**
5950
+ * True when the broker row for this session is the terminal-uncertain claim a
5951
+ * forced stale-worktree release recorded against exactly this authority. The
5952
+ * claim is only accepted as close proof while the row is still that identity:
5953
+ * a rotated successor, an ambiguous row, or an unreadable listing all fall
5954
+ * back to the ordinary close.
5955
+ */
5956
+ async function forcedStaleReleaseProven(
5957
+ sessionId: string,
5958
+ workspace: string | null,
5959
+ endpointGeneration: number,
5960
+ endpointIncarnation: string,
5961
+ ): Promise<boolean> {
5962
+ if (!workspace) return false;
5963
+ let matches: Array<Record<string, unknown>>;
5964
+ try {
5965
+ const canonicalWorkspace = await canonicalBrokerWorkspace(workspace);
5966
+ const listing = await paginatedBrokerSessionList(canonicalWorkspace, { cwd: canonicalWorkspace });
5967
+ matches = jsonRecords(Array.isArray(listing.sessions) ? listing.sessions : []).filter(
5968
+ row => brokerSessionId(row) === sessionId,
5969
+ );
5970
+ } catch {
5971
+ return false;
5972
+ }
5973
+ if (matches.length !== 1) return false;
5974
+ const row = matches[0]!;
5975
+ return (
5976
+ row.ambiguous !== true &&
5977
+ (row.terminalUncertain === true || row.terminal_uncertain === true) &&
5978
+ (row.forcedStaleRelease === true || row.forced_stale_release === true) &&
5979
+ brokerEndpointGeneration(row) === endpointGeneration &&
5980
+ brokerEndpointIncarnation(row, sessionId) === endpointIncarnation
5981
+ );
5982
+ }
5983
+
5851
5984
  async function recoverIntentDeletion(entry: NamespaceDeletionEntryV1): Promise<void> {
5852
5985
  const session = asRecord(await readJsonFile(sessionFile(entry.session_id)));
5853
5986
  if (!session) throw new SdkClientError("state_corrupt", "Close intent has no session authority record.");
@@ -5860,19 +5993,33 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5860
5993
  : null;
5861
5994
  if (!cwd || endpointGeneration === null)
5862
5995
  throw new SdkClientError("state_corrupt", "Close intent authority is incomplete.");
5863
- strictBrokerSessionClose(
5864
- await brokerSession(
5865
- cwd,
5866
- "session.close",
5867
- {
5868
- sessionId: entry.session_id,
5869
- endpointGeneration,
5870
- endpointIncarnation: entry.endpoint_incarnation,
5871
- },
5872
- `coordinator-reap:${entry.session_id}:${entry.endpoint_incarnation}`,
5873
- ),
5874
- entry.session_id,
5875
- );
5996
+ // #5581: a forced stop that admitted this deletion and then lost its endpoint
5997
+ // already released the worktree by recording a terminal-uncertain claim bound
5998
+ // to this exact authority. The broker refuses an ordinary `session.close`
5999
+ // against such a row (`terminal_uncertain`), so retrying one here would park
6000
+ // the manifest at `intent` forever. The claim is itself the teardown proof:
6001
+ // advance on it instead, exactly as the success path does.
6002
+ if (
6003
+ !(await forcedStaleReleaseProven(
6004
+ entry.session_id,
6005
+ optionalString(session.broker_workspace),
6006
+ endpointGeneration,
6007
+ entry.endpoint_incarnation,
6008
+ ))
6009
+ )
6010
+ strictBrokerSessionClose(
6011
+ await brokerSession(
6012
+ cwd,
6013
+ "session.close",
6014
+ {
6015
+ sessionId: entry.session_id,
6016
+ endpointGeneration,
6017
+ endpointIncarnation: entry.endpoint_incarnation,
6018
+ },
6019
+ `coordinator-reap:${entry.session_id}:${entry.endpoint_incarnation}`,
6020
+ ),
6021
+ entry.session_id,
6022
+ );
5876
6023
  await advanceDeletion(questionPaths, entry.deletion_id, "broker_closed", undefined, {
5877
6024
  ok: true,
5878
6025
  closed: true,
@@ -5969,22 +6116,49 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5969
6116
  const persistedIncarnation = optionalString(session.endpoint_incarnation);
5970
6117
  if (!cwd || !persistedWorkspace || persistedGeneration === null || !persistedIncarnation)
5971
6118
  return { ok: false, reason: "endpoint_stale", closed: false };
6119
+ // #5581: a forced stop of a session whose endpoint has gone stale (its
6120
+ // coordinator service vanished mid-run) cannot prove teardown, so it
6121
+ // returns endpoint_stale below. The worktree release must still happen
6122
+ // first: ask the broker to record a terminal-uncertain claim so the row
6123
+ // stops holding its checkout. Best-effort — the endpoint_stale signal the
6124
+ // caller depends on is returned regardless of whether release succeeds.
6125
+ // The persisted endpoint authority is passed so the broker acts on exactly
6126
+ // this stale identity: a live successor incarnation fails the close
6127
+ // authority check and is never touched, so a legitimately re-held worktree
6128
+ // stays occupied.
6129
+ const releaseStaleWorktreeOnForce = async (): Promise<void> => {
6130
+ if (opts.force !== true) return;
6131
+ try {
6132
+ await brokerSession(cwd, "session.close", {
6133
+ sessionId: id,
6134
+ endpointGeneration: persistedGeneration,
6135
+ endpointIncarnation: persistedIncarnation,
6136
+ forceReleaseStaleWorktree: true,
6137
+ });
6138
+ } catch {
6139
+ // Advisory release; a later launch simply re-observes the row and retries.
6140
+ }
6141
+ };
5972
6142
  // Endpoint identity is the authority for any lifecycle mutation. Check it
5973
6143
  // before consulting a possibly stale active-turn projection so a successor
5974
6144
  // incarnation cannot be blocked by old local state.
5975
6145
  let preflightWorkspace: string;
5976
6146
  try {
5977
- preflightWorkspace = await canonicalBrokerWorkspace(cwd);
6147
+ preflightWorkspace = await canonicalBrokerWorkspace(persistedWorkspace);
5978
6148
  const authority = await exactBrokerSessionAuthority(id, preflightWorkspace);
5979
6149
  if (
5980
6150
  !sameCanonicalPath(authority.workspace, persistedWorkspace, platform) ||
5981
6151
  authority.endpointGeneration !== persistedGeneration ||
5982
6152
  authority.endpointIncarnation !== persistedIncarnation
5983
- )
6153
+ ) {
6154
+ await releaseStaleWorktreeOnForce();
5984
6155
  return { ok: false, reason: "endpoint_stale", closed: false };
6156
+ }
5985
6157
  } catch (error) {
5986
- if (error instanceof SdkClientError && (error.code === "not_found" || error.code === "endpoint_stale"))
6158
+ if (error instanceof SdkClientError && (error.code === "not_found" || error.code === "endpoint_stale")) {
6159
+ await releaseStaleWorktreeOnForce();
5987
6160
  return { ok: false, reason: "endpoint_stale", closed: false };
6161
+ }
5988
6162
  throw error;
5989
6163
  }
5990
6164
  if (session.ephemeral !== true && opts.force !== true)
@@ -6078,7 +6252,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6078
6252
  const closeAlreadyProven = deletionPhase === "broker_closed" || deletionPhase === "cleanup_pending";
6079
6253
  let workspace = "";
6080
6254
  try {
6081
- workspace = await canonicalBrokerWorkspace(cwd);
6255
+ workspace = await canonicalBrokerWorkspace(persistedWorkspace);
6082
6256
  let authority: BrokerSessionAuthority | null = null;
6083
6257
  if (!closeAlreadyProven) {
6084
6258
  authority = await exactBrokerSessionAuthority(id, workspace);
@@ -6086,8 +6260,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6086
6260
  !sameCanonicalPath(authority.workspace, persistedWorkspace, platform) ||
6087
6261
  authority.endpointGeneration !== persistedGeneration ||
6088
6262
  authority.endpointIncarnation !== persistedIncarnation
6089
- )
6263
+ ) {
6264
+ await releaseStaleWorktreeOnForce();
6090
6265
  return { ok: false, reason: "endpoint_stale", closed: false };
6266
+ }
6091
6267
  }
6092
6268
  await ensureQuestionStateReady();
6093
6269
  await ensureQuestionTransaction(id);
@@ -6127,6 +6303,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6127
6303
  });
6128
6304
  }
6129
6305
  } catch (error) {
6306
+ // #5581: a forced stop can clear the second authority check yet lose the
6307
+ // endpoint before `session.close` reaches the broker, so the close throws
6308
+ // a stale/absent code. The deletion is already admitted as remote_started
6309
+ // but never proven closed, leaving the indexed row holding its worktree.
6310
+ // Record the same identity-bound terminal-uncertain claim before surfacing
6311
+ // the unproven close. Only stale/absent codes qualify: a live endpoint
6312
+ // failing transiently must keep its checkout.
6313
+ if (error instanceof SdkClientError && (error.code === "endpoint_stale" || error.code === "not_found"))
6314
+ await releaseStaleWorktreeOnForce();
6130
6315
  return {
6131
6316
  ok: false,
6132
6317
  reason: "close_failed",
@@ -6211,6 +6396,164 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6211
6396
  });
6212
6397
  }
6213
6398
 
6399
+ /**
6400
+ * Prove a stored endpoint identity (workspace + generation + incarnation) is no
6401
+ * longer current against the live broker index. Returns true only when the
6402
+ * broker row is absent (not_found) or its generation/incarnation/workspace has
6403
+ * rotated away from the stored triple. A transient authority-read failure
6404
+ * returns false: we cannot prove the endpoint recovered, but we also must not
6405
+ * force-evict on a flake — the normal reap path will retry.
6406
+ */
6407
+ async function brokerEndpointIdentityStale(
6408
+ id: string,
6409
+ persistedWorkspace: string,
6410
+ persistedGeneration: number,
6411
+ persistedIncarnation: string,
6412
+ ): Promise<boolean> {
6413
+ try {
6414
+ const workspace = await canonicalBrokerWorkspace(persistedWorkspace);
6415
+ const authority = await exactBrokerSessionAuthority(id, workspace);
6416
+ return (
6417
+ !sameCanonicalPath(authority.workspace, persistedWorkspace, platform) ||
6418
+ authority.endpointGeneration !== persistedGeneration ||
6419
+ authority.endpointIncarnation !== persistedIncarnation
6420
+ );
6421
+ } catch (error) {
6422
+ if (error instanceof SdkClientError && (error.code === "not_found" || error.code === "endpoint_stale"))
6423
+ return true;
6424
+ return false;
6425
+ }
6426
+ }
6427
+
6428
+ /**
6429
+ * True when the session's endpoint authority can no longer be proven current.
6430
+ * Mirrors the reap preflight so force eviction only fires against a genuinely
6431
+ * stale endpoint. When the projection carries a full endpoint identity we prove
6432
+ * staleness against it directly. When the projection identity is missing/corrupt
6433
+ * (or the projection is absent), an absent field is NOT itself proof of a stale
6434
+ * endpoint: a partial write can drop `endpoint_incarnation` while the broker
6435
+ * session is still live. We therefore recover the canonical WAL's own broker
6436
+ * identity and prove absence/mismatch against that authority instead. If neither
6437
+ * the projection nor the WAL yields a usable identity, we return false and leave
6438
+ * the state for the safe repair path rather than destructively evicting a
6439
+ * possibly-live session.
6440
+ */
6441
+ async function isSessionEndpointStale(session: Record<string, unknown> | null, id: string): Promise<boolean> {
6442
+ const persistedWorkspace = optionalString(session?.broker_workspace);
6443
+ const persistedGeneration =
6444
+ typeof session?.endpoint_generation === "number" &&
6445
+ Number.isSafeInteger(session.endpoint_generation) &&
6446
+ session.endpoint_generation > 0
6447
+ ? session.endpoint_generation
6448
+ : null;
6449
+ const persistedIncarnation = optionalString(session?.endpoint_incarnation);
6450
+ if (persistedWorkspace && persistedGeneration !== null && persistedIncarnation)
6451
+ return await brokerEndpointIdentityStale(id, persistedWorkspace, persistedGeneration, persistedIncarnation);
6452
+ // Projection identity is missing — fall back to the canonical WAL authority.
6453
+ const canonicalTransaction = await readSessionTransaction(questionPaths, id);
6454
+ const broker = canonicalTransaction?.canonical.session.broker;
6455
+ const walWorkspace = optionalString(broker?.workspace);
6456
+ const walGeneration =
6457
+ typeof broker?.endpoint_generation === "number" &&
6458
+ Number.isSafeInteger(broker.endpoint_generation) &&
6459
+ broker.endpoint_generation > 0
6460
+ ? broker.endpoint_generation
6461
+ : null;
6462
+ const walIncarnation = optionalString(broker?.endpoint_incarnation);
6463
+ if (!walWorkspace || walGeneration === null || !walIncarnation) return false;
6464
+ return await brokerEndpointIdentityStale(id, walWorkspace, walGeneration, walIncarnation);
6465
+ }
6466
+
6467
+ /**
6468
+ * Confirm no turn is active for the session, reading the durable authority
6469
+ * (canonical WAL) and the legacy active-turn projection. `listSessions`
6470
+ * captures reap candidates before the per-session transition lock is held, so
6471
+ * a turn can begin in the gap before eviction — this must be re-checked under
6472
+ * the lock immediately before any irreversible removal.
6473
+ */
6474
+ async function sessionHasActiveTurn(id: string): Promise<boolean> {
6475
+ const canonicalTransaction = await readSessionTransaction(questionPaths, id);
6476
+ const canonicalActiveTurn = canonicalTransaction
6477
+ ? Object.values(canonicalTransaction.canonical.turns).some(turn =>
6478
+ ACTIVE_TURN_STATUSES.has(turn.status as TurnStatus),
6479
+ )
6480
+ : false;
6481
+ return canonicalActiveTurn || (await readActiveTurn(namespaceDir, id)) !== null;
6482
+ }
6483
+
6484
+ /**
6485
+ * Recovery-safe force eviction for a session whose endpoint is stale. A stale
6486
+ * endpoint has no live broker to close against, but the session's durable
6487
+ * footprint must not be orphaned: removing only the projection rows would leak
6488
+ * the canonical WAL, retained public-delivery claims, and the scheduler/index
6489
+ * registry hints. We instead write a durable deletion (retirement) record and
6490
+ * run the same cleanup the normal reap path uses — drain retained deliveries,
6491
+ * remove the WAL (which also deregisters roster/retained scheduler hints),
6492
+ * remove projections, and emit the reaped event. If any step cannot finish yet
6493
+ * the record stays in `cleanup_pending`, so a subsequent reapSession() for this
6494
+ * id (selected by session_id, not deletion_id) resumes and completes it.
6495
+ */
6496
+ async function forceEvictStaleSession(sessionId: string): Promise<void> {
6497
+ await ensureQuestionStateReady();
6498
+ // Recover the session by id from the canonical WAL — not just the projection.
6499
+ // A crash/partial-write, or a malformed/legacy projection, can leave canonical
6500
+ // state committed while the projection has lost its endpoint_incarnation.
6501
+ // Keying force eviction off the projection alone would then retire only the
6502
+ // projection files and orphan the WAL, retained public-delivery claims,
6503
+ // registry (roster/retained) hints, and scheduler markers indefinitely: after
6504
+ // three stale sweeps the projection disappears and the normal
6505
+ // deletion-intent/completeDeletionCleanup path can no longer select the id.
6506
+ const canonicalTransaction = await readSessionTransaction(questionPaths, sessionId);
6507
+ if (!canonicalTransaction) {
6508
+ // Canonical state confirmed absent (this also covers the malformed/absent
6509
+ // projection case): the only durable footprint left is the projection, so
6510
+ // best-effort projection removal is all that remains.
6511
+ await removeReapedProjection(sessionId, [], []);
6512
+ return;
6513
+ }
6514
+ // Endpoint authority for the durable retirement comes from the canonical WAL,
6515
+ // never the projection: removeSessionTransaction fences the delete on
6516
+ // `canonicalTransaction.endpoint.incarnation`, so a present-but-stale
6517
+ // projection incarnation would make cleanup fail with endpoint_stale and
6518
+ // orphan the WAL, retained deliveries, and registry state. Using the WAL's
6519
+ // own incarnation guarantees the fence matches.
6520
+ const persistedIncarnation = optionalString(canonicalTransaction.endpoint?.incarnation);
6521
+ if (!persistedIncarnation) {
6522
+ // The WAL exists but never observed an endpoint incarnation, so there is
6523
+ // no authority to fence a durable retirement against. Leave the state for
6524
+ // the normal reap path rather than orphaning the WAL with a
6525
+ // projection-only delete.
6526
+ return;
6527
+ }
6528
+ const deletionId = `force-evict:${sessionId}:${persistedIncarnation}`;
6529
+ const deletionKey = createHash("sha256").update(deletionId).digest("hex");
6530
+ const now = new Date().toISOString();
6531
+ const entry: NamespaceDeletionEntryV1 = {
6532
+ deletion_id: deletionId,
6533
+ session_id: sessionId,
6534
+ endpoint_incarnation: persistedIncarnation,
6535
+ operation_id: deletionId,
6536
+ key_digest: deletionKey,
6537
+ request_digest: deletionKey,
6538
+ close_key: deletionId,
6539
+ phase: "cleanup_pending",
6540
+ cleanup: {
6541
+ wal: false,
6542
+ turns: false,
6543
+ reports: false,
6544
+ session: false,
6545
+ events: false,
6546
+ turn_ids: canonicalTransaction ? Object.keys(canonicalTransaction.canonical.turns) : [],
6547
+ report_ids: canonicalTransaction ? Object.keys(canonicalTransaction.canonical.reports) : [],
6548
+ },
6549
+ authority_digest: deletionKey,
6550
+ created_at: now,
6551
+ updated_at: now,
6552
+ };
6553
+ await recordDeletionIntent(questionPaths, entry);
6554
+ await completeDeletionCleanup(entry, "idle_reaper_force_evict", true);
6555
+ }
6556
+
6214
6557
  const sessionReaper: SessionReaper = createSessionReaper(
6215
6558
  {
6216
6559
  listSessions: async (): Promise<ReapableSession[]> => {
@@ -6261,6 +6604,23 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6261
6604
  const result = await reapSession(sessionId, { reason: "idle_reaper" });
6262
6605
  if (!result.ok) throw new Error(result.reason ?? "session_reap_failed");
6263
6606
  },
6607
+ markSessionDead: async (sessionId: string): Promise<void> => {
6608
+ // Force-evict a session that cannot be reaped normally (endpoint_stale
6609
+ // repeated MAX_REAP_FAILURES times). The candidate was selected before
6610
+ // this transition acquired its per-session lock, so revalidate the
6611
+ // durable authority under the lock: a turn may have started, or the
6612
+ // endpoint may have recovered, since listSessions() ran.
6613
+ await withSessionTransition(sessionId, async () => {
6614
+ if (await sessionHasActiveTurn(sessionId)) return;
6615
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
6616
+ // Prove endpoint staleness before any destructive eviction, even when the
6617
+ // projection is absent: isSessionEndpointStale falls back to the canonical
6618
+ // WAL authority to confirm broker absence/mismatch, so a missing or
6619
+ // partially-written projection can no longer force-evict a live broker.
6620
+ if (!(await isSessionEndpointStale(session, sessionId))) return;
6621
+ await forceEvictStaleSession(sessionId);
6622
+ });
6623
+ },
6264
6624
  now: () => Date.now(),
6265
6625
  },
6266
6626
  { idleTtlMs: config.sessionIdleTtlMs, sweepIntervalMs: config.sessionSweepIntervalMs },
@@ -7924,7 +8284,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7924
8284
  };
7925
8285
  await reconcileSessionRuntime(canonicalSessionId, { observeQuestions: false });
7926
8286
  try {
7927
- let indexedSession = (await listSessions(cwd)).find(
8287
+ const brokerWorkspace = optionalString(session.broker_workspace) ?? cwd;
8288
+ let indexedSession = (await listSessions(brokerWorkspace)).find(
7928
8289
  candidate => brokerSessionId(candidate) === canonicalSessionId,
7929
8290
  );
7930
8291
  // Windows broker locators may differ in drive-letter casing or separator
@@ -7932,7 +8293,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7932
8293
  // the coordinator path. The scoped listing request is still authoritative;
7933
8294
  // only its local path filter is relaxed for the exact requested session.
7934
8295
  if (!indexedSession && platform === "win32") {
7935
- const listing = await paginatedBrokerSessionList(cwd, { cwd });
8296
+ const listing = await paginatedBrokerSessionList(brokerWorkspace, { cwd: brokerWorkspace });
7936
8297
  indexedSession = jsonRecords(Array.isArray(listing.sessions) ? listing.sessions : []).find(
7937
8298
  candidate => brokerSessionId(candidate) === canonicalSessionId,
7938
8299
  );
@@ -7987,8 +8348,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7987
8348
  }
7988
8349
  if (name === "gjc_coordinator_list_questions") return await listQuestions(args);
7989
8350
  if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
7990
- if (name === "gjc_coordinator_read_artifact")
8351
+ if (name === "gjc_coordinator_read_artifact") {
8352
+ // Advertised capability (tools/list) and refusal must agree on the resolved
8353
+ // platform. This can only add a refusal: safeOpenCoordinatorArtifact keeps
8354
+ // the host-real capability and /proc/self/fd identity checks.
8355
+ if (!coordinatorArtifactCapability(platform).available)
8356
+ return publicError(new Error("artifact_unavailable"));
7991
8357
  return await readCoordinatorArtifact(config, { path: args.path });
8358
+ }
7992
8359
  if (name === "gjc_coordinator_read_coordination_status") {
7993
8360
  const scopedSessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
7994
8361
  if (scopedSessionId) await ensureQuestionTransaction(scopedSessionId);
@@ -9041,6 +9408,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9041
9408
  session = normalizeSession({
9042
9409
  session_id: sessionId,
9043
9410
  cwd: sessionCwd,
9411
+ ephemeral: true,
9044
9412
  ...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
9045
9413
  ...(modelResolution.model ? { model: modelResolution.model } : {}),
9046
9414
  broker_workspace: binding.workspace,
@@ -9758,10 +10126,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9758
10126
 
9759
10127
  const admission = await reconcileSessionRuntime(sessionId, { observeQuestions: true });
9760
10128
  const admissionToken = admission.waiting_token;
9761
- const liveAdmissionRequired =
9762
- admission.session_state?.source === "agent_session_event" &&
9763
- admission.session_state.live === true &&
9764
- admission.session_state.state === "needs_user_input";
10129
+ const agentRuntimeAdmissionRequired =
10130
+ admission.session_state?.source === "agent_session_event" && admission.session_state.live === true;
10131
+ const liveAdmissionRequired = admission.session_state?.state === "needs_user_input";
9765
10132
  if (liveAdmissionRequired && !admissionToken)
9766
10133
  return {
9767
10134
  ok: false,
@@ -9778,6 +10145,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9778
10145
  };
9779
10146
  let translated: unknown;
9780
10147
  let session: Record<string, unknown> | null = null;
10148
+ let liveInFlightAnswerAdmitted = false;
9781
10149
  const claimed = await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
9782
10150
  const liveState = await readSessionState(namespaceDir, sessionId);
9783
10151
  const question = transaction.canonical.questions[questionId];
@@ -9858,10 +10226,30 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9858
10226
  authority?.observation.kind === "valid"
9859
10227
  ? authority.observation.first_provenance.runtime_turn_id
9860
10228
  : null;
10229
+ const liveInFlightAskOwned = Boolean(
10230
+ liveState?.schema_version === 1 &&
10231
+ liveState.session_id === sessionId &&
10232
+ liveState?.source === "agent_session_event" &&
10233
+ liveState.live === true &&
10234
+ liveState.state === "running" &&
10235
+ liveState.current_turn_id === turnId &&
10236
+ Number.isFinite(Date.parse(liveState.updated_at)) &&
10237
+ hasExactRuntimeWriterIdentity(liveState, transaction.canonical.session.broker) &&
10238
+ canonicalTurn &&
10239
+ ACTIVE_TURN_STATUSES.has(canonicalTurn.status as TurnStatus) &&
10240
+ hasAcceptedRuntimeReceipt(canonicalTurn) &&
10241
+ authority?.outcome?.state === "pending" &&
10242
+ authority.outcome.turn_id === turnId &&
10243
+ authority.observation.kind === "valid" &&
10244
+ runtimeTurnId === canonicalTurn.delivery.runtime_turn_id,
10245
+ );
9861
10246
  const liveWaitingOwned =
9862
- liveState?.source !== "agent_session_event" ||
9863
- liveState.live !== true ||
9864
- (liveState.state === "needs_user_input" && liveState.current_turn_id === turnId);
10247
+ liveState?.source !== "agent_session_event"
10248
+ ? true
10249
+ : liveState.live === true &&
10250
+ ((liveState.state === "needs_user_input" && liveState.current_turn_id === turnId) ||
10251
+ liveInFlightAskOwned);
10252
+ liveInFlightAnswerAdmitted = liveInFlightAskOwned;
9865
10253
  const provenanceMatches =
9866
10254
  admissionToken === null ||
9867
10255
  (Boolean(canonicalTurn?.runtime_provenance) &&
@@ -9873,7 +10261,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9873
10261
  !liveWaitingOwned ||
9874
10262
  (liveState?.source === "agent_session_event" &&
9875
10263
  liveState.live === true &&
9876
- canonicalTurn?.status !== "waiting_for_answer") ||
10264
+ canonicalTurn?.status !== "waiting_for_answer" &&
10265
+ !liveInFlightAskOwned) ||
9877
10266
  !authority ||
9878
10267
  authority.observation.kind !== "valid" ||
9879
10268
  !canonicalTurn ||
@@ -9985,17 +10374,103 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9985
10374
  const latestAdmission = await reconcileSessionRuntime(sessionId, { observeQuestions: true });
9986
10375
  const latestState = latestAdmission.session_state;
9987
10376
  const latestToken = latestAdmission.waiting_token;
9988
- const latestLiveRequired =
9989
- latestState?.source === "agent_session_event" &&
9990
- latestState.live === true &&
9991
- latestState.state === "needs_user_input";
9992
- if (
9993
- latestAdmission.terminal ||
9994
- (latestLiveRequired && (!latestToken || latestToken.coordinator_turn_id !== turnId)) ||
10377
+ const latestQuestions = await reconcileQuestions(sessionId);
10378
+ const latestSession = asRecord(await readJsonFile(sessionFile(sessionId)));
10379
+ const latestSnapshot = latestSession ? await readCompleteQ12Snapshot(latestSession) : null;
10380
+ let latestFreshGate: (WorkflowGateQueryRecord & WorkflowGate) | undefined;
10381
+ if (latestSnapshot?.complete === true) {
10382
+ latestFreshGate = (latestSnapshot.items as unknown[]).find(row => {
10383
+ if (!row || typeof row !== "object") return false;
10384
+ const record = row as Record<string, unknown>;
10385
+ return record.tag === "pending" && record.gate_id === gateId;
10386
+ }) as (WorkflowGateQueryRecord & WorkflowGate) | undefined;
10387
+ }
10388
+ const latestQ12Proof =
10389
+ latestQuestions.reconciliation.complete &&
10390
+ latestSnapshot?.complete === true &&
10391
+ latestFreshGate !== undefined &&
10392
+ (await withSessionTransaction(questionPaths, sessionId, async transaction => {
10393
+ const canonicalQuestion = transaction.canonical.questions[questionId];
10394
+ const canonicalTurn = transaction.canonical.turns[turnId];
10395
+ const authority = canonicalQuestion
10396
+ ? transaction.canonical.gate_authorities[canonicalQuestion.authority_id]
10397
+ : undefined;
10398
+ if (!canonicalQuestion || !canonicalTurn || !authority || !latestFreshGate) return false;
10399
+ const freshProvenance = runtimeProvenanceToken(
10400
+ sessionId,
10401
+ transaction,
10402
+ canonicalTurn,
10403
+ latestFreshGate,
10404
+ );
10405
+ return Boolean(
10406
+ canonicalQuestion.session_id === sessionId &&
10407
+ canonicalQuestion.turn_id === turnId &&
10408
+ (canonicalQuestion.status === "pending" || canonicalQuestion.status === "resolving") &&
10409
+ ACTIVE_TURN_STATUSES.has(canonicalTurn.status as TurnStatus) &&
10410
+ hasAcceptedRuntimeReceipt(canonicalTurn) &&
10411
+ latestFreshGate.gate_id === authority.authority.gate_id &&
10412
+ latestFreshGate.runtime_turn_id === canonicalTurn.delivery.runtime_turn_id &&
10413
+ authority.outcome.state === "pending" &&
10414
+ authority.outcome.turn_id === turnId &&
10415
+ authority.outcome.question_id === questionId &&
10416
+ authority.observation.kind === "valid" &&
10417
+ canonicalTurn.runtime_provenance &&
10418
+ canonicalJson(authority.observation.first_provenance) ===
10419
+ canonicalJson(freshProvenance) &&
10420
+ canonicalJson(canonicalTurn.runtime_provenance) === canonicalJson(freshProvenance) &&
10421
+ !canonicalTurn.terminal_fence,
10422
+ );
10423
+ }));
10424
+ const latestLiveInFlightAsk =
10425
+ !latestAdmission.terminal &&
10426
+ latestAdmission.active_turn_id === turnId &&
10427
+ latestQ12Proof &&
10428
+ (await withSessionTransaction(questionPaths, sessionId, async transaction => {
10429
+ const runtimeState = await readSessionState(namespaceDir, sessionId);
10430
+ const canonicalQuestion = transaction.canonical.questions[questionId];
10431
+ const canonicalTurn = transaction.canonical.turns[turnId];
10432
+ const authority = canonicalQuestion
10433
+ ? transaction.canonical.gate_authorities[canonicalQuestion.authority_id]
10434
+ : undefined;
10435
+ const authorityRuntimeTurnId =
10436
+ authority?.observation.kind === "valid"
10437
+ ? authority.observation.first_provenance.runtime_turn_id
10438
+ : null;
10439
+ return Boolean(
10440
+ runtimeState?.schema_version === 1 &&
10441
+ runtimeState.session_id === sessionId &&
10442
+ runtimeState.source === "agent_session_event" &&
10443
+ runtimeState.live === true &&
10444
+ runtimeState.state === "running" &&
10445
+ runtimeState.current_turn_id === turnId &&
10446
+ Number.isFinite(Date.parse(runtimeState.updated_at)) &&
10447
+ hasExactRuntimeWriterIdentity(runtimeState, transaction.canonical.session.broker) &&
10448
+ canonicalTurn &&
10449
+ ACTIVE_TURN_STATUSES.has(canonicalTurn.status as TurnStatus) &&
10450
+ hasAcceptedRuntimeReceipt(canonicalTurn) &&
10451
+ canonicalQuestion &&
10452
+ (canonicalQuestion.status === "pending" || canonicalQuestion.status === "resolving") &&
10453
+ authority?.outcome.state === "pending" &&
10454
+ authority.outcome.turn_id === turnId &&
10455
+ authority.observation.kind === "valid" &&
10456
+ canonicalTurn.delivery.runtime_turn_id === authorityRuntimeTurnId &&
10457
+ canonicalTurn.runtime_provenance &&
10458
+ canonicalJson(canonicalTurn.runtime_provenance) ===
10459
+ canonicalJson(authority.observation.first_provenance) &&
10460
+ !canonicalTurn.terminal_fence,
10461
+ );
10462
+ }));
10463
+ const latestAgentRuntimeAdmitted =
10464
+ !agentRuntimeAdmissionRequired ||
9995
10465
  (latestState?.source === "agent_session_event" &&
9996
10466
  latestState.live === true &&
9997
- (latestState.state !== "needs_user_input" || latestState.current_turn_id !== turnId))
9998
- ) {
10467
+ latestState.current_turn_id === turnId &&
10468
+ ((liveInFlightAnswerAdmitted && latestState.state === "running" && latestLiveInFlightAsk) ||
10469
+ (!liveInFlightAnswerAdmitted &&
10470
+ latestState.state === "needs_user_input" &&
10471
+ latestToken?.coordinator_turn_id === turnId &&
10472
+ latestQ12Proof)));
10473
+ if (latestAdmission.terminal || !latestQ12Proof || !latestAgentRuntimeAdmitted) {
9999
10474
  // No remote call has occurred. Undo the durable dispatch claim so this
10000
10475
  // outer request remains retryable rather than stranding the question in
10001
10476
  // remote_started/resolving.
@@ -10510,7 +10985,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
10510
10985
  return { jsonrpc: "2.0", id, result: {} };
10511
10986
  }
10512
10987
  if (request.method === "tools/list") {
10513
- return { jsonrpc: "2.0", id, result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) } };
10988
+ return {
10989
+ jsonrpc: "2.0",
10990
+ id,
10991
+ result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(name => toolSchema(name, platform)) },
10992
+ };
10514
10993
  }
10515
10994
  if (request.method === "prompts/list") {
10516
10995
  return { jsonrpc: "2.0", id, result: { prompts: [] } };
@@ -10564,7 +11043,11 @@ export async function handleCoordinatorMcpRequest(
10564
11043
  };
10565
11044
  }
10566
11045
  if (request.method === "tools/list") {
10567
- return { jsonrpc: "2.0", id: request.id ?? null, result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) } };
11046
+ return {
11047
+ jsonrpc: "2.0",
11048
+ id: request.id ?? null,
11049
+ result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(name => toolSchema(name, process.platform)) },
11050
+ };
10568
11051
  }
10569
11052
  if (request.method === "prompts/list") {
10570
11053
  return { jsonrpc: "2.0", id: request.id ?? null, result: { prompts: [] } };