@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
@@ -14,6 +14,7 @@ function nativeChatDaemon(): NativeChatDaemonBindings {
14
14
  return nativeChatDaemonBindings;
15
15
  }
16
16
 
17
+ import { isEnoent } from "@gajae-code/utils/fs-error";
17
18
  import type { Settings } from "../../config/settings";
18
19
  import type {
19
20
  BuiltInDaemonController,
@@ -25,7 +26,10 @@ import type {
25
26
  } from "../../daemon/control-types";
26
27
  import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
27
28
  import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
29
+ import { CHAT_DAEMON_DIRECTORY, CHAT_DAEMON_FILES, canonicalServiceRootDigest } from "../service-artifact-paths";
28
30
  import { getNotificationConfig, isDiscordComplete, isProviderEffectivelyEnabled, isSlackComplete } from "./config";
31
+ import { withDaemonStartupExclusion } from "./daemon-startup-exclusion";
32
+ import { type DoctorDaemonControlRequest, isDoctorDaemonControlRequest } from "./doctor-daemon-restart";
29
33
 
30
34
  export type ChatDaemonKind = "discord" | "slack";
31
35
  export type ChatDaemonAction = "stop" | "reload";
@@ -137,8 +141,8 @@ export type ChatDaemonAction = "stop" | "reload";
137
141
  * crashes, recovery, and attachment retirement cannot replay ambiguous work.
138
142
  */
139
143
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
140
- discord: 73,
141
- slack: 80,
144
+ discord: 76,
145
+ slack: 83,
142
146
  };
143
147
 
144
148
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -157,6 +161,14 @@ export interface ChatDaemonState {
157
161
  transportHealthy: boolean;
158
162
  generation: number;
159
163
  stoppedAt?: number;
164
+ /**
165
+ * Digest of the canonical (symlink-resolved) agent root this record was
166
+ * published for. Legacy records omit it and remain non-authorizing for the
167
+ * D9 cross-bind check: a state record without a matching rootDigest can
168
+ * never be treated as this root's live owner for that check, even though it
169
+ * is still valid for every other pre-existing owner check in this module.
170
+ */
171
+ rootDigest?: string;
160
172
  }
161
173
 
162
174
  /**
@@ -326,13 +338,23 @@ const DEFAULT_KILL_TIMEOUT_MS = 3_000;
326
338
  /** Covers Discord READY plus its first 5-second heartbeat; tests inject a smaller timeout. */
327
339
  const DEFAULT_SPAWN_READY_TIMEOUT_MS = 8_000;
328
340
 
329
- interface ChatDaemonOwnerLock {
341
+ /**
342
+ * `version`/`ownerId`/`rootDigest` are the D9 cross-bind receipt delta: a
343
+ * legacy lock predating this generation omits them. Legacy records remain
344
+ * valid, signalable owner locks for every existing check in this module —
345
+ * they are simply non-authorizing for a D9 cross-bind check that requires a
346
+ * matching `ownerId`/`rootDigest` against the sibling state record.
347
+ */
348
+ export interface ChatDaemonOwnerLock {
349
+ version?: 1;
330
350
  pid: number;
331
351
  incarnation: string;
332
352
  createdAt: number;
353
+ ownerId?: string;
354
+ rootDigest?: string;
333
355
  }
334
356
 
335
- interface ChatDaemonOwnerLockLease {
357
+ export interface ChatDaemonOwnerLockLease {
336
358
  content: string;
337
359
  dev: bigint;
338
360
  ino: bigint;
@@ -344,6 +366,19 @@ interface ChatDaemonOwnerLockLease {
344
366
  sha256: string;
345
367
  }
346
368
 
369
+ /** Result of a bounded, no-follow owner-lock lease capture. Only `"absent"` (ENOENT) means the lock currently does not exist; every other failure is `"unreadable"` and must never be treated as absence. */
370
+ export type ChatDaemonOwnerLockLeaseResult =
371
+ | { status: "present"; lease: ChatDaemonOwnerLockLease }
372
+ | { status: "absent" }
373
+ | { status: "unreadable" };
374
+
375
+ const CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES = 64 * 1024;
376
+
377
+ /** Matches the canonical no-follow, non-blocking lock-info open flags used by config/file-lock.ts. */
378
+ const CHAT_DAEMON_OWNER_LOCK_OPEN_FLAGS =
379
+ fs.constants.O_RDONLY |
380
+ (process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
381
+
347
382
  interface ChatDaemonOwnershipProbe {
348
383
  pidAlive(pid: number): boolean;
349
384
  pidIncarnation(pid: number): string | undefined;
@@ -353,15 +388,56 @@ export function chatDaemonPaths(
353
388
  agentDir: string,
354
389
  kind: ChatDaemonKind,
355
390
  ): { dir: string; lock: string; state: string; control: string } {
356
- const dir = path.join(agentDir, "sdk", "daemons", kind);
391
+ const dir = path.join(agentDir, CHAT_DAEMON_DIRECTORY, kind);
357
392
  return {
358
393
  dir,
359
- lock: path.join(dir, "owner.lock"),
360
- state: path.join(dir, "state.json"),
361
- control: path.join(dir, "control.json"),
394
+ lock: path.join(dir, CHAT_DAEMON_FILES.ownerLock),
395
+ state: path.join(dir, CHAT_DAEMON_FILES.state),
396
+ control: path.join(dir, CHAT_DAEMON_FILES.control),
362
397
  };
363
398
  }
364
399
 
400
+ export function chatDoctorControlRequestPath(agentDir: string, kind: ChatDaemonKind): string {
401
+ return path.join(chatDaemonPaths(agentDir, kind).dir, "doctor-restart.control.json");
402
+ }
403
+
404
+ export async function readChatDoctorControlRequest(
405
+ agentDir: string,
406
+ kind: ChatDaemonKind,
407
+ ): Promise<DoctorDaemonControlRequest | undefined> {
408
+ try {
409
+ const parsed = JSON.parse(
410
+ await fs.promises.readFile(chatDoctorControlRequestPath(agentDir, kind), "utf8"),
411
+ ) as unknown;
412
+ return isDoctorDaemonControlRequest(parsed) && parsed.owner === kind ? parsed : undefined;
413
+ } catch {
414
+ return undefined;
415
+ }
416
+ }
417
+
418
+ export async function writeChatDoctorControlRequest(
419
+ agentDir: string,
420
+ kind: ChatDaemonKind,
421
+ request: DoctorDaemonControlRequest,
422
+ ): Promise<void> {
423
+ if (request.owner !== kind) throw new Error(`${kind} doctor request owner mismatch`);
424
+ const file = chatDoctorControlRequestPath(agentDir, kind);
425
+ await fs.promises.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
426
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
427
+ await fs.promises.writeFile(tmp, `${JSON.stringify(request)}\n`, { mode: 0o600 });
428
+ await fs.promises.rename(tmp, file);
429
+ }
430
+
431
+ export async function clearChatDoctorControlRequest(
432
+ agentDir: string,
433
+ kind: ChatDaemonKind,
434
+ requestId?: string,
435
+ ): Promise<void> {
436
+ const file = chatDoctorControlRequestPath(agentDir, kind);
437
+ if (requestId && (await readChatDoctorControlRequest(agentDir, kind))?.requestId !== requestId) return;
438
+ await fs.promises.unlink(file).catch(() => undefined);
439
+ }
440
+
365
441
  /**
366
442
  * Configuration fingerprint that identifies which settings a daemon owner was
367
443
  * started for. `undefined` means the current settings cannot configure that
@@ -951,7 +1027,7 @@ export async function ensureSlackDaemon(
951
1027
  return await ensureChatDaemon("slack", settings, deps);
952
1028
  }
953
1029
 
954
- export async function acquireChatDaemonOwnership(input: {
1030
+ export interface AcquireChatDaemonOwnershipInput {
955
1031
  agentDir: string;
956
1032
  kind: ChatDaemonKind;
957
1033
  ownerId: string;
@@ -960,7 +1036,16 @@ export async function acquireChatDaemonOwnership(input: {
960
1036
  incarnation?: string;
961
1037
  pidAlive?: (pid: number) => boolean;
962
1038
  pidIncarnation?: (pid: number) => string | undefined;
963
- }): Promise<boolean> {
1039
+ }
1040
+
1041
+ /**
1042
+ * Actual publication body. Runs ONLY inside {@link withDaemonStartupExclusion}
1043
+ * (see {@link acquireChatDaemonOwnership}); never call this directly, and
1044
+ * never call it from inside another already-held startup-exclusion guard for
1045
+ * the same `(agentDir, kind)` — that would be a self-deadlocking reacquisition
1046
+ * of a non-reentrant lock.
1047
+ */
1048
+ async function acquireChatDaemonOwnershipLocked(input: AcquireChatDaemonOwnershipInput): Promise<boolean> {
964
1049
  const paths = chatDaemonPaths(input.agentDir, input.kind);
965
1050
  const pid = input.pid ?? process.pid;
966
1051
  const probe: ChatDaemonOwnershipProbe = {
@@ -983,7 +1068,17 @@ export async function acquireChatDaemonOwnership(input: {
983
1068
  )
984
1069
  return false;
985
1070
  }
986
- const owner = { pid, incarnation, createdAt: Date.now() };
1071
+ // Existing root: withDaemonStartupExclusion has already acquired its guard
1072
+ // under this same agentDir, so the directory chain up to it exists.
1073
+ const rootDigest = await canonicalServiceRootDigest(input.agentDir);
1074
+ const owner: ChatDaemonOwnerLock = {
1075
+ version: 1,
1076
+ pid,
1077
+ incarnation,
1078
+ createdAt: Date.now(),
1079
+ ownerId: input.ownerId,
1080
+ rootDigest,
1081
+ };
987
1082
  let lock = await createChatDaemonOwnerLock(paths.lock, owner);
988
1083
  if (!lock) {
989
1084
  if (!(await reclaimChatDaemonOwnerLock(paths.lock, paths.state, probe))) return false;
@@ -1003,11 +1098,26 @@ export async function acquireChatDaemonOwnership(input: {
1003
1098
  heartbeatAt: Date.now(),
1004
1099
  transportHealthy: false,
1005
1100
  generation: chatDaemonGeneration(input.kind),
1101
+ rootDigest,
1006
1102
  } satisfies ChatDaemonState);
1007
1103
  return true;
1008
1104
  });
1009
1105
  }
1010
1106
 
1107
+ /**
1108
+ * Publishes this process as the `(agentDir, kind)` chat daemon owner
1109
+ * (owner.lock + state.json). Runs behind the shared cross-process startup
1110
+ * exclusion so it can never race a doctor startup/maintenance repair that
1111
+ * holds the same guard (e.g. detaching a stale owner-lock quarantine) — the
1112
+ * guard is acquired here and released before returning; the actual
1113
+ * lock-creation/state-publication logic lives in the unlocked
1114
+ * {@link acquireChatDaemonOwnershipLocked} so the guard is never held across
1115
+ * a nested reacquisition of itself.
1116
+ */
1117
+ export async function acquireChatDaemonOwnership(input: AcquireChatDaemonOwnershipInput): Promise<boolean> {
1118
+ return await withDaemonStartupExclusion(input.agentDir, input.kind, () => acquireChatDaemonOwnershipLocked(input));
1119
+ }
1120
+
1011
1121
  async function createChatDaemonOwnerLock(
1012
1122
  lock: string,
1013
1123
  owner: ChatDaemonOwnerLock,
@@ -1029,44 +1139,79 @@ async function createChatDaemonOwnerLock(
1029
1139
  throw error;
1030
1140
  }
1031
1141
  await fs.promises.unlink(temporary);
1032
- return await captureChatDaemonOwnerLockLease(lock);
1142
+ return await captureChatDaemonOwnerLockLeaseOrUndefined(lock);
1033
1143
  } finally {
1034
1144
  await fs.promises.unlink(temporary).catch(() => undefined);
1035
1145
  }
1036
1146
  }
1037
1147
 
1038
- async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemonOwnerLockLease | undefined> {
1148
+ /**
1149
+ * Bounded, no-follow, non-blocking owner-lock lease capture. Opens with the
1150
+ * same `O_NOFOLLOW|O_NONBLOCK` posture as the canonical lock-info reader in
1151
+ * config/file-lock.ts, reads at most {@link CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES}
1152
+ * bytes through the open descriptor (never an unbounded `readFile`), and
1153
+ * revalidates identity/parent-directory metadata before and after the read so a
1154
+ * TOCTOU replacement during the read is detected rather than silently trusted.
1155
+ *
1156
+ * Only `ENOENT` on the initial open means the lock is currently absent.
1157
+ * SYMLINK, FIFO/special-file, oversize, and any other I/O failure are all
1158
+ * `"unreadable"` — distinct from absence, and callers must not treat them as
1159
+ * "no lock exists".
1160
+ */
1161
+ export async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemonOwnerLockLeaseResult> {
1162
+ let handle: fs.promises.FileHandle | undefined;
1039
1163
  try {
1040
- const handle = await fs.promises.open(lock, "r");
1041
1164
  try {
1042
- const before = await handle.stat({ bigint: true });
1043
- const parentBefore = await fs.promises.lstat(path.dirname(lock), { bigint: true });
1044
- if (!before.isFile()) return undefined;
1045
- const content = await handle.readFile({ encoding: "utf8" });
1046
- const after = await handle.stat({ bigint: true });
1047
- const pathname = await fs.promises.lstat(lock, { bigint: true });
1048
- const parentAfter = await fs.promises.lstat(path.dirname(lock), { bigint: true });
1049
- if (
1050
- !after.isFile() ||
1051
- !pathname.isFile() ||
1052
- pathname.isSymbolicLink() ||
1053
- before.nlink !== 1n ||
1054
- pathname.nlink !== 1n ||
1055
- !parentBefore.isDirectory() ||
1056
- parentBefore.isSymbolicLink() ||
1057
- parentBefore.dev !== parentAfter.dev ||
1058
- parentBefore.ino !== parentAfter.ino ||
1059
- before.dev !== after.dev ||
1060
- before.ino !== after.ino ||
1061
- before.size !== after.size ||
1062
- before.mtimeNs !== after.mtimeNs ||
1063
- before.dev !== pathname.dev ||
1064
- before.ino !== pathname.ino ||
1065
- before.size !== pathname.size ||
1066
- before.mtimeNs !== pathname.mtimeNs
1067
- )
1068
- return undefined;
1069
- return {
1165
+ handle = await fs.promises.open(lock, CHAT_DAEMON_OWNER_LOCK_OPEN_FLAGS);
1166
+ } catch (error) {
1167
+ if (isEnoent(error)) return { status: "absent" };
1168
+ return { status: "unreadable" };
1169
+ }
1170
+ const before = await handle.stat({ bigint: true });
1171
+ const parentBefore = await fs.promises.lstat(path.dirname(lock), { bigint: true });
1172
+ if (!before.isFile() || before.size > BigInt(CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES))
1173
+ return { status: "unreadable" };
1174
+ const buffer = Buffer.alloc(CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES);
1175
+ let length = 0;
1176
+ while (length < buffer.length) {
1177
+ const read = await handle.read(buffer, length, buffer.length - length, length);
1178
+ if (read.bytesRead === 0) break;
1179
+ length += read.bytesRead;
1180
+ }
1181
+ const content = buffer.subarray(0, length).toString("utf8");
1182
+ const after = await handle.stat({ bigint: true });
1183
+ let pathname: fs.BigIntStats;
1184
+ let parentAfter: fs.BigIntStats;
1185
+ try {
1186
+ pathname = await fs.promises.lstat(lock, { bigint: true });
1187
+ parentAfter = await fs.promises.lstat(path.dirname(lock), { bigint: true });
1188
+ } catch {
1189
+ return { status: "unreadable" };
1190
+ }
1191
+ if (
1192
+ !after.isFile() ||
1193
+ !pathname.isFile() ||
1194
+ pathname.isSymbolicLink() ||
1195
+ before.nlink !== 1n ||
1196
+ pathname.nlink !== 1n ||
1197
+ !parentBefore.isDirectory() ||
1198
+ parentBefore.isSymbolicLink() ||
1199
+ parentBefore.dev !== parentAfter.dev ||
1200
+ parentBefore.ino !== parentAfter.ino ||
1201
+ before.dev !== after.dev ||
1202
+ before.ino !== after.ino ||
1203
+ before.size !== after.size ||
1204
+ before.mtimeNs !== after.mtimeNs ||
1205
+ before.dev !== pathname.dev ||
1206
+ before.ino !== pathname.ino ||
1207
+ before.size !== pathname.size ||
1208
+ before.mtimeNs !== pathname.mtimeNs ||
1209
+ after.size > BigInt(CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES)
1210
+ )
1211
+ return { status: "unreadable" };
1212
+ return {
1213
+ status: "present",
1214
+ lease: {
1070
1215
  content,
1071
1216
  dev: before.dev,
1072
1217
  ino: before.ino,
@@ -1076,17 +1221,23 @@ async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemon
1076
1221
  parentDev: parentBefore.dev,
1077
1222
  parentIno: parentBefore.ino,
1078
1223
  sha256: crypto.createHash("sha256").update(content).digest("hex"),
1079
- };
1080
- } finally {
1081
- await handle.close();
1082
- }
1224
+ },
1225
+ };
1083
1226
  } catch {
1084
- return undefined;
1227
+ return { status: "unreadable" };
1228
+ } finally {
1229
+ await handle?.close();
1085
1230
  }
1086
1231
  }
1087
1232
 
1233
+ /** Convenience wrapper for existing call sites that only need the lease or `undefined` (absent or unreadable alike). */
1234
+ async function captureChatDaemonOwnerLockLeaseOrUndefined(lock: string): Promise<ChatDaemonOwnerLockLease | undefined> {
1235
+ const result = await captureChatDaemonOwnerLockLease(lock);
1236
+ return result.status === "present" ? result.lease : undefined;
1237
+ }
1238
+
1088
1239
  async function ownsChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockLease): Promise<boolean> {
1089
- const current = await captureChatDaemonOwnerLockLease(lock);
1240
+ const current = await captureChatDaemonOwnerLockLeaseOrUndefined(lock);
1090
1241
  return (
1091
1242
  current?.dev === lease.dev &&
1092
1243
  current.ino === lease.ino &&
@@ -1162,7 +1313,7 @@ async function staleChatDaemonLockLease(
1162
1313
  lock: string,
1163
1314
  probe: ChatDaemonOwnershipProbe,
1164
1315
  ): Promise<ChatDaemonOwnerLockLease | undefined> {
1165
- const lease = await captureChatDaemonOwnerLockLease(lock);
1316
+ const lease = await captureChatDaemonOwnerLockLeaseOrUndefined(lock);
1166
1317
  if (!lease) return undefined;
1167
1318
  let owner: unknown;
1168
1319
  try {
@@ -1197,22 +1348,39 @@ async function canReclaimChatDaemonOwnerLock(
1197
1348
  return await staleChatDaemonLockLease(lock, probe);
1198
1349
  }
1199
1350
 
1200
- function isChatDaemonOwnerLock(value: unknown): value is ChatDaemonOwnerLock {
1201
- return (
1202
- typeof value === "object" &&
1203
- value !== null &&
1204
- typeof (value as ChatDaemonOwnerLock).pid === "number" &&
1205
- Number.isSafeInteger((value as ChatDaemonOwnerLock).pid) &&
1206
- (value as ChatDaemonOwnerLock).pid > 0 &&
1207
- typeof (value as ChatDaemonOwnerLock).incarnation === "string" &&
1208
- typeof (value as ChatDaemonOwnerLock).createdAt === "number"
1209
- );
1351
+ /**
1352
+ * Strict: `pid`/`incarnation`/`createdAt` are always required. `version`,
1353
+ * `ownerId`, and `rootDigest` are individually optional (legacy locks omit
1354
+ * them), but any one that IS present must be well-formed — a malformed
1355
+ * optional field is never silently ignored.
1356
+ */
1357
+ export function isChatDaemonOwnerLock(value: unknown): value is ChatDaemonOwnerLock {
1358
+ if (typeof value !== "object" || value === null) return false;
1359
+ const candidate = value as Record<string, unknown>;
1360
+ if (
1361
+ typeof candidate.pid !== "number" ||
1362
+ !Number.isSafeInteger(candidate.pid) ||
1363
+ candidate.pid <= 0 ||
1364
+ typeof candidate.incarnation !== "string" ||
1365
+ candidate.incarnation.length === 0 ||
1366
+ typeof candidate.createdAt !== "number" ||
1367
+ !Number.isFinite(candidate.createdAt)
1368
+ )
1369
+ return false;
1370
+ if (candidate.version !== undefined && candidate.version !== 1) return false;
1371
+ if (
1372
+ candidate.ownerId !== undefined &&
1373
+ (typeof candidate.ownerId !== "string" || candidate.ownerId.length === 0 || candidate.ownerId.length > 256)
1374
+ )
1375
+ return false;
1376
+ if (candidate.rootDigest !== undefined && !/^[0-9a-f]{64}$/.test(candidate.rootDigest as string)) return false;
1377
+ return true;
1210
1378
  }
1211
1379
 
1212
1380
  function isAlreadyExists(error: unknown): error is NodeJS.ErrnoException {
1213
1381
  return typeof error === "object" && error !== null && (error as NodeJS.ErrnoException).code === "EEXIST";
1214
1382
  }
1215
- export async function renewChatDaemonHeartbeat(input: {
1383
+ export interface RenewChatDaemonHeartbeatInput {
1216
1384
  agentDir: string;
1217
1385
  kind: ChatDaemonKind;
1218
1386
  ownerId: string;
@@ -1221,7 +1389,10 @@ export async function renewChatDaemonHeartbeat(input: {
1221
1389
  transportHealthy: boolean;
1222
1390
  pidAlive?: (pid: number) => boolean;
1223
1391
  pidIncarnation?: (pid: number) => string | undefined;
1224
- }): Promise<boolean> {
1392
+ }
1393
+
1394
+ /** Actual body; runs ONLY inside {@link withDaemonStartupExclusion} (see {@link renewChatDaemonHeartbeat}). */
1395
+ async function renewChatDaemonHeartbeatLocked(input: RenewChatDaemonHeartbeatInput): Promise<boolean> {
1225
1396
  const paths = chatDaemonPaths(input.agentDir, input.kind);
1226
1397
  const pidAlive = input.pidAlive ?? defaultPidAlive;
1227
1398
  const pidIncarnation = input.pidIncarnation ?? defaultPidIncarnation;
@@ -1244,7 +1415,19 @@ export async function renewChatDaemonHeartbeat(input: {
1244
1415
  return true;
1245
1416
  });
1246
1417
  }
1247
- export async function releaseChatDaemonOwnership(input: {
1418
+
1419
+ /**
1420
+ * Renews the exact-owner heartbeat for `(agentDir, kind)`. Runs behind the
1421
+ * shared cross-process startup exclusion so a heartbeat can never publish in
1422
+ * the middle of a doctor startup/maintenance repair holding the same guard;
1423
+ * the actual re-check-and-write logic lives in the unlocked
1424
+ * {@link renewChatDaemonHeartbeatLocked}.
1425
+ */
1426
+ export async function renewChatDaemonHeartbeat(input: RenewChatDaemonHeartbeatInput): Promise<boolean> {
1427
+ return await withDaemonStartupExclusion(input.agentDir, input.kind, () => renewChatDaemonHeartbeatLocked(input));
1428
+ }
1429
+
1430
+ export interface ReleaseChatDaemonOwnershipInput {
1248
1431
  agentDir: string;
1249
1432
  kind: ChatDaemonKind;
1250
1433
  ownerId: string;
@@ -1252,7 +1435,10 @@ export async function releaseChatDaemonOwnership(input: {
1252
1435
  incarnation: string;
1253
1436
  pidAlive?: (pid: number) => boolean;
1254
1437
  pidIncarnation?: (pid: number) => string | undefined;
1255
- }): Promise<void> {
1438
+ }
1439
+
1440
+ /** Actual body; runs ONLY inside {@link withDaemonStartupExclusion} (see {@link releaseChatDaemonOwnership}). */
1441
+ async function releaseChatDaemonOwnershipLocked(input: ReleaseChatDaemonOwnershipInput): Promise<void> {
1256
1442
  const paths = chatDaemonPaths(input.agentDir, input.kind);
1257
1443
  const pidAlive = input.pidAlive ?? defaultPidAlive;
1258
1444
  const pidIncarnation = input.pidIncarnation ?? defaultPidIncarnation;
@@ -1271,7 +1457,7 @@ export async function releaseChatDaemonOwnership(input: {
1271
1457
  )
1272
1458
  return;
1273
1459
  await writeJson(paths.state, { ...state, stoppedAt: Date.now(), transportHealthy: false });
1274
- const lock = await captureChatDaemonOwnerLockLease(paths.lock);
1460
+ const lock = await captureChatDaemonOwnerLockLeaseOrUndefined(paths.lock);
1275
1461
  let owner: unknown;
1276
1462
  try {
1277
1463
  owner = lock && JSON.parse(lock.content);
@@ -1280,3 +1466,13 @@ export async function releaseChatDaemonOwnership(input: {
1280
1466
  unlinkExactChatDaemonOwnerLock(paths.lock, lock);
1281
1467
  });
1282
1468
  }
1469
+
1470
+ /**
1471
+ * Releases this process's `(agentDir, kind)` chat daemon ownership. Runs
1472
+ * behind the shared cross-process startup exclusion for the same reason as
1473
+ * {@link acquireChatDaemonOwnership}; the actual re-check-and-unpublish logic
1474
+ * lives in the unlocked {@link releaseChatDaemonOwnershipLocked}.
1475
+ */
1476
+ export async function releaseChatDaemonOwnership(input: ReleaseChatDaemonOwnershipInput): Promise<void> {
1477
+ await withDaemonStartupExclusion(input.agentDir, input.kind, () => releaseChatDaemonOwnershipLocked(input));
1478
+ }