@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
@@ -1,3 +1,4 @@
1
+ import type { NativeBrokerRestartIntent, NativeBrokerRestartIntentIdentity } from "@gajae-code/natives";
1
2
  import { processIncarnation } from "./process-incarnation";
2
3
  import { SDK_STATE_VERSION } from "./state-version";
3
4
  export type BrokerPublicationObservation = "owned" | "absent" | "replaced" | "ambiguous";
@@ -6,12 +7,30 @@ export interface RetainedBrokerDiscovery {
6
7
  observeAsync(): Promise<BrokerPublicationObservation>;
7
8
  heartbeat(heartbeatAt: number): Promise<boolean>;
8
9
  close(): void;
10
+ /** Owner-side `prepared`: exclusively creates the durable restart-intent slot. */
11
+ prepareRestartIntent(intent: NativeBrokerRestartIntent): Promise<boolean>;
12
+ /** Owner-side `prepared` -> `committed`: rewrites the exact slot prepare created. */
13
+ commitRestartIntent(intent: NativeBrokerRestartIntent): Promise<boolean>;
14
+ /** Owner-side cancel of this process's own prepared/committed slot. */
15
+ cancelRestartIntent(): Promise<boolean>;
16
+ /** Successor-side clear of a predecessor's slot this process never prepared. */
17
+ clearForeignRestartIntent(identity: NativeBrokerRestartIntentIdentity): Promise<boolean>;
9
18
  }
10
19
  export declare const BROKER_HEARTBEAT_TTL_MS = 15000;
11
20
  export interface BrokerDiscovery {
12
21
  version: typeof SDK_STATE_VERSION;
13
22
  protocolVersion: 3;
14
23
  packageGeneration: string;
24
+ /**
25
+ * The runtime image this broker launches internal SDK processes with. Present
26
+ * whenever the publisher could classify its own runtime, including when that
27
+ * image was already gone at publication time -- absence is the reader's
28
+ * verdict to reach, not a reason to publish nothing. Absent only for records
29
+ * written before this field existed and for a publisher whose runtime
30
+ * evidence could not be classified at all; readers then reuse the broker as
31
+ * before rather than retiring it on unknown evidence.
32
+ */
33
+ runtime?: string;
15
34
  ownerId: string;
16
35
  pid: number;
17
36
  incarnation: string;
@@ -21,6 +40,14 @@ export interface BrokerDiscovery {
21
40
  token: string;
22
41
  startedAt: number;
23
42
  heartbeatAt: number;
43
+ restartRequestId?: string;
44
+ rootDigest?: string;
45
+ }
46
+ export interface BrokerRestartIntent {
47
+ requestId: string;
48
+ lease: string;
49
+ expiresAt: number;
50
+ phase?: "prepared" | "committed" | "cancelled";
24
51
  }
25
52
  export type RedactedBrokerDiscovery = Omit<BrokerDiscovery, "token"> & {
26
53
  token: "[redacted]";
@@ -38,4 +65,15 @@ export declare function publishBrokerDiscovery(agentDir: string, discovery: Brok
38
65
  /** Heartbeat through retained authority, or the explicit Windows compatibility path. */
39
66
  export declare function heartbeatBrokerDiscoveryRetained(publication: RetainedBrokerDiscovery, heartbeatAt: number): Promise<boolean>;
40
67
  export declare function readBrokerDiscovery(agentDir: string, ttlMs?: number): Promise<BrokerDiscovery | null>;
68
+ /**
69
+ * The restart-intent slot lives directly beneath `sdk/`, independent of
70
+ * `sdk/broker.lock/` -- so it durably survives a stale-lock reclaim that
71
+ * renames the incumbent's lock directory aside (`Broker#reclaimStaleLock`).
72
+ */
73
+ export declare const brokerRestartIntentPath: (agentDir: string) => string;
74
+ export interface BrokerRestartIntentWithIdentity extends BrokerRestartIntent {
75
+ /** Existing platform file identity, for a successor's exact-identity clear. */
76
+ identity: NativeBrokerRestartIntentIdentity;
77
+ }
78
+ export declare function readBrokerRestartIntent(agentDir: string): Promise<BrokerRestartIntentWithIdentity | null>;
41
79
  export declare function redactBrokerDiscovery(discovery: BrokerDiscovery): RedactedBrokerDiscovery;
@@ -0,0 +1,79 @@
1
+ import { SdkClient } from "../client/client";
2
+ import type { BrokerRestartOwnerIdentity, BrokerRestartPrepareResult, BrokerRestartResult } from "./broker";
3
+ import { type BrokerDiscovery } from "./discovery";
4
+ export interface DoctorBrokerRestartOptions {
5
+ agentDir: string;
6
+ deadlineMs?: number;
7
+ drain?: boolean;
8
+ requestId?: string;
9
+ }
10
+ /**
11
+ * Concrete typed before/after-effect outcome of a full D8 broker restart
12
+ * attempt. Every refusal names the exact stage and, where the broker itself
13
+ * refused, the broker's own typed error code -- never an opaque thrown
14
+ * message standing in for a decision this adapter could name.
15
+ */
16
+ export type DoctorBrokerRestartOutcome = {
17
+ kind: "restarted";
18
+ requestId: string;
19
+ oldOwner: BrokerRestartOwnerIdentity;
20
+ successor: BrokerDiscovery;
21
+ /** True when an already-committed successor for this exact request was found and reused, not spawned. */
22
+ adopted: boolean;
23
+ } | {
24
+ kind: "owner_unavailable";
25
+ reason: "no_discovery" | "owner_not_confirmed_live";
26
+ } | {
27
+ kind: "prepare_refused";
28
+ code: string;
29
+ message: string;
30
+ } | {
31
+ kind: "commit_refused";
32
+ code: string;
33
+ message: string;
34
+ } | {
35
+ kind: "old_owner_exit_timeout";
36
+ requestId: string;
37
+ oldOwner: BrokerRestartOwnerIdentity;
38
+ } | {
39
+ kind: "successor_refused";
40
+ requestId: string;
41
+ oldOwner: BrokerRestartOwnerIdentity;
42
+ reason: "deadline_elapsed" | "intent_not_committed" | "spawn_failed" | "spawn_exited_before_publication" | "publication_timeout";
43
+ detail?: string;
44
+ };
45
+ export declare function prepareDoctorBrokerRestart(client: SdkClient, discovery: BrokerDiscovery, options: DoctorBrokerRestartOptions & {
46
+ requestId: string;
47
+ }): Promise<BrokerRestartResult>;
48
+ export declare function commitDoctorBrokerRestart(client: SdkClient, discovery: BrokerDiscovery, prepared: BrokerRestartPrepareResult, requestId: string): Promise<BrokerRestartResult>;
49
+ export declare function cancelDoctorBrokerRestart(client: SdkClient, requestId: string): Promise<BrokerRestartResult>;
50
+ /**
51
+ * Owner-side prepare/commit/stop, positive-exit-confirmed cleanup, then
52
+ * exactly one successor launch or adoption. The exact sequence:
53
+ *
54
+ * 1. Read the CURRENT discovery snapshot (never a stale caller-supplied one)
55
+ * and prove its owner is live via a real connected client, not an
56
+ * incarnation string comparison alone.
57
+ * 2. `prepare_restart` against that exact owner tuple/generation; on any
58
+ * prepare refusal, cancel the reservation and return the broker's own
59
+ * typed refusal rather than throwing.
60
+ * 3. `commit_restart` with the exact lease/occupancyEpoch prepare returned.
61
+ * Once committed, the owner is durably retiring: cancellation is no longer
62
+ * safe and this function must never call cancel past this point.
63
+ * 4. Close the client (every path below runs with the client closed).
64
+ * 5. Wait, bounded by the request deadline, for `observeProcessIncarnation`
65
+ * to positively confirm the exact old pid is OS-absent -- never inferred
66
+ * from a changed/missing incarnation read, and never from discovery
67
+ * disappearing (a crashed writer can leave a stale discovery file behind
68
+ * just as easily as a clean exit can remove it).
69
+ * 6. Launch (or adopt an already-published) exactly one successor for this
70
+ * requestId, holding the shared startup exclusion only around the launch
71
+ * decision itself -- never across the successor's own publication wait,
72
+ * which would deadlock this process against its own spawned child.
73
+ *
74
+ * The successor clears the durable committed intent itself, from inside its
75
+ * own `Broker#start()`, only after its own publication has independently
76
+ * proven it owns the root -- never here, and never before that proof exists.
77
+ */
78
+ export declare function restartBrokerForDoctor(options: DoctorBrokerRestartOptions): Promise<DoctorBrokerRestartOutcome>;
79
+ export type { BrokerRestartResult };
@@ -13,6 +13,13 @@ export type IndexedEndpointAuthority = {
13
13
  endpointMtimeMs?: number;
14
14
  endpointFileId?: string;
15
15
  };
16
+ /** Derive the broker-compatible opaque authority for one indexed endpoint generation. */
17
+ export declare function endpointIncarnation(record: {
18
+ endpointGeneration: number;
19
+ endpointMtimeMs?: number;
20
+ pid: number;
21
+ endpointFileId?: string;
22
+ }, sessionId: string): string | undefined;
16
23
  /**
17
24
  * Reads one endpoint from the descriptor opened for its pathname. The opened
18
25
  * object must remain a small, singly-linked regular file with stable identity
@@ -4,8 +4,22 @@ import { type FileLockOptions } from "../../config/file-lock";
4
4
  import { type BrokerDiscovery } from "./discovery";
5
5
  import { type SdkInternalSpawnCommand } from "./runtime";
6
6
  export declare function isBrokerGenerationCompatible(discovery: BrokerDiscovery | null): boolean;
7
+ /**
8
+ * A live incumbent stops being reusable once the runtime image it launches
9
+ * internal processes with is provably gone. A broker outlives its own executable
10
+ * (a package manager replacing the interpreter it was started from, an install
11
+ * directory swapped underneath it): it keeps heartbeating and answering
12
+ * requests, but every `session.create` it accepts is refused at spawn time.
13
+ * Retiring it here repairs the endpoint before a caller reaches session
14
+ * creation. Absent runtime evidence -- a record predating the field, or a
15
+ * publisher that could not classify its own runtime at all -- and evidence this
16
+ * caller merely cannot inspect both keep the incumbent reusable.
17
+ */
18
+ export declare function isBrokerReusable(discovery: BrokerDiscovery | null): Promise<boolean>;
7
19
  export interface EnsureBrokerSettings {
8
20
  agentDir: string;
21
+ /** Only an authorized doctor successor may consume a restart-intent publication. */
22
+ restartRequestId?: string;
9
23
  heartbeatTtlMs?: number;
10
24
  /**
11
25
  * Environment for the spawned detached broker. Defaults to `process.env`; tests
@@ -233,6 +233,8 @@ export declare function probePublishedReadyAuthorityForTest(root: string, id: st
233
233
  export declare function removeOwnedLifecycleArtifactsForTest(root: string, id: string, expected: EffectMarker): Promise<boolean>;
234
234
  /** Test seam for the worktree occupancy boundary. */
235
235
  export declare function worktreeOccupantForTest(sessions: IndexedSession[], worktreePath: string, observe?: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation): string | null;
236
+ /** Test seam for the forced stale-worktree release boundary. */
237
+ export declare function releaseForcedStaleWorktreeForTest(broker: Broker, id: string, expected: IndexedSession): Promise<void>;
236
238
  export declare function canonicalDeleteLocatorPath(pathname: string): string;
237
239
  export interface LifecycleExecutionOutcome {
238
240
  response: BrokerResponse;
@@ -11,6 +11,56 @@ export interface ProcessIncarnationOptions {
11
11
  export declare function parseDarwinProcessIncarnation(info: Uint8Array): string | undefined;
12
12
  /** Whether `value` is a canonical process-incarnation string (`linux:`/`darwin:`/`windows:`). */
13
13
  export declare function isProcessIncarnation(value: unknown): value is string;
14
+ /**
15
+ * Read-only, non-mutating observation of whether a pid currently names a
16
+ * verifiable process incarnation.
17
+ *
18
+ * - `status: "present"` carries the exact kernel-reported identity evidence
19
+ * (the same value `processIncarnation` returns).
20
+ * - `status: "absent"` is positive OS-confirmed death proof (POSIX
21
+ * `kill(pid, 0)` returning `ESRCH`, or Windows `OpenProcess` failing with
22
+ * the exact code the kernel reports for an unresolved client id) — never a
23
+ * permission failure or an ambiguous disappearance.
24
+ * - `status: "unknown"` covers every inconclusive case (invalid pid, missing
25
+ * or unavailable native addon, permission denial, platform limitation).
26
+ * Callers MUST NOT treat this as death proof.
27
+ */
28
+ export type ProcessIncarnationObservation = {
29
+ status: "present";
30
+ incarnation: string;
31
+ } | {
32
+ status: "absent";
33
+ } | {
34
+ status: "unknown";
35
+ reasonCode: string;
36
+ };
37
+ /** Raw shape returned by the native `Process.observe` binding. */
38
+ export type NativeProcessObserveResult = {
39
+ status?: unknown;
40
+ incarnation?: unknown;
41
+ reasonCode?: unknown;
42
+ };
43
+ export interface ObserveProcessIncarnationOptions {
44
+ /**
45
+ * Injectable native observe function, used by tests to exercise error
46
+ * classification (malformed shapes, unavailable addon) without a global
47
+ * mock. Defaults to the real `Process.observe` binding.
48
+ */
49
+ observe?: (pid: number) => NativeProcessObserveResult;
50
+ }
51
+ /**
52
+ * Read-only process-incarnation observation for `pid`, distinguishing a
53
+ * proved-live incarnation from positive OS-confirmed absence from every
54
+ * inconclusive outcome. Never signals, kills, reaps, waits on, or spawns any
55
+ * process — it only reads existing native process-table state via
56
+ * `Process.observe`.
57
+ *
58
+ * `pid` must be a positive safe integer; any other value (including `0`,
59
+ * negative, `NaN`, or non-integer) is reported as `unknown` rather than
60
+ * risking an ambiguous platform-specific interpretation (`0`/negative PIDs
61
+ * address process groups on POSIX, not a single process).
62
+ */
63
+ export declare function observeProcessIncarnation(pid: number, options?: ObserveProcessIncarnationOptions): ProcessIncarnationObservation;
14
64
  /** A PID is reusable; bind it to the strongest OS-provided process start incarnation available. */
15
65
  export declare function processIncarnation(pid: number, options?: ProcessIncarnationOptions): string | undefined;
16
66
  export {};
@@ -26,8 +26,34 @@ export interface SdkInternalRuntimeDescriptorTestOptions {
26
26
  configPath?: string;
27
27
  bunAvailable?: boolean;
28
28
  }
29
+ /**
30
+ * The on-disk image internal SDK spawns from this process execute, or
31
+ * `undefined` only when runtime evidence cannot be classified at all. It comes
32
+ * from the same marker authority a spawn uses, but deliberately not from the
33
+ * source-tree assets a spawn also proves (CLI entrypoint, isolated Bun config,
34
+ * package identity): a failure to read one of those says nothing about the
35
+ * runtime image, and publishing `undefined` for it would blind every reader.
36
+ *
37
+ * An image the classification names but this process can no longer prove is
38
+ * still published. `undefined` means only "no evidence" -- readers keep such a
39
+ * broker reusable, so a broker whose own image vanished before it published
40
+ * must not hide behind it.
41
+ */
42
+ export declare function sdkInternalRuntimeImage(): string | undefined;
43
+ /**
44
+ * Whether a published runtime image is still a regular file on disk. A resident broker keeps
45
+ * answering requests after the executable it was started from is deleted (a
46
+ * Homebrew Bun upgrade removing the Cellar path, an install directory swapped
47
+ * underneath it), yet every internal spawn it then attempts is refused. Callers
48
+ * use this to retire such an incumbent before a session launch reaches it, so it
49
+ * answers `true` on every inconclusive outcome, including a probe that outlives
50
+ * its bound: a false retirement kills a healthy broker.
51
+ */
52
+ export declare function isSdkInternalRuntimeImagePresent(file: string): Promise<boolean>;
29
53
  /** Resolve the production descriptor from the statically imported marker and current Bun runtime evidence. */
30
54
  export declare function resolveSdkInternalSpawnCommand(action: SdkInternalAction): SdkInternalSpawnCommand;
31
55
  /** Test hook: injects runtime evidence without weakening the production marker authority. */
32
56
  export declare function resolveSdkInternalSpawnCommandForTest(action: SdkInternalAction, options: SdkInternalRuntimeDescriptorTestOptions): SdkInternalSpawnCommand;
57
+ /** Test hook: the publication decision under injected evidence, same authority as production. */
58
+ export declare function sdkInternalRuntimeImageForTest(options: SdkInternalRuntimeDescriptorTestOptions): string | undefined;
33
59
  export {};
@@ -77,6 +77,13 @@ export interface SessionIndexEvent {
77
77
  endpointFileId?: string;
78
78
  lifecycleRequestId?: string;
79
79
  terminalUncertain?: boolean;
80
+ /**
81
+ * Distinguishes the terminal-uncertain claim written by a forced stop of a
82
+ * stale-endpoint session from the one written by the fail-closed tail of an
83
+ * ordinary teardown. Only the forced claim releases the session's worktree;
84
+ * see `worktreeOccupant`.
85
+ */
86
+ forcedStaleRelease?: boolean;
80
87
  /** OS process incarnation (C1); absent on legacy v1/v2 events. */
81
88
  hostIncarnation?: string;
82
89
  /** Present on host_heartbeat checkpoints (C2). */
@@ -99,6 +106,8 @@ export interface IndexedSession {
99
106
  indexSeq: number;
100
107
  lifecycleRequestId?: string;
101
108
  terminalUncertain?: boolean;
109
+ /** True only for a terminal-uncertain claim written by a forced stale-worktree release. */
110
+ forcedStaleRelease?: boolean;
102
111
  hostIncarnation?: string;
103
112
  identityProvenance: SessionIdentityProvenance;
104
113
  activity?: SessionActivity;
@@ -192,7 +201,7 @@ export declare class SessionIndex {
192
201
  refresh(): Promise<void>;
193
202
  get indexSeq(): number;
194
203
  append(input: Omit<SessionIndexEvent, "version" | "indexSeq" | "checksum" | "ts"> & Partial<Pick<SessionIndexEvent, "ts">>): Promise<SessionIndexEvent>;
195
- unregisterIfCurrent(expected: IndexedSession): Promise<boolean>;
204
+ unregisterIfCurrent(expected: Pick<SessionIndexEvent, "sessionId" | "locator" | "endpointGeneration" | "pid" | "indexSeq" | "endpointMtimeMs" | "endpointFileId" | "lifecycleRequestId" | "processIncarnation" | "hostIncarnation">): Promise<boolean>;
196
205
  /** Hold the canonical index lock across an authority-sensitive operation. */
197
206
  withLocked<T>(callback: () => Promise<T>): Promise<T>;
198
207
  snapshot(): Promise<void>;
@@ -229,12 +238,12 @@ export declare class SessionIndex {
229
238
  lifecycleRequestId?: string;
230
239
  }): IndexedSession | undefined;
231
240
  /** Returns the latest identity-bound terminal event, if retained. */
232
- findSessionTerminalEvidence(expected: Pick<IndexedSession, "sessionId" | "locator" | "endpointGeneration" | "pid" | "processIncarnation" | "hostIncarnation" | "endpointMtimeMs" | "lifecycleRequestId">): {
241
+ findSessionTerminalEvidence(expected: Pick<IndexedSession, "sessionId" | "locator" | "endpointGeneration" | "pid" | "processIncarnation" | "hostIncarnation" | "endpointMtimeMs" | "endpointFileId" | "lifecycleRequestId">): {
233
242
  type: "host_unregistered" | "session_closed" | "session_deleted";
234
243
  indexSeq: number;
235
244
  } | undefined;
236
245
  /** Returns the latest identity-bound session_closed evidence, even after a tombstone. */
237
- findSessionClosedEvidence(expected: Pick<IndexedSession, "sessionId" | "locator" | "endpointGeneration" | "pid" | "processIncarnation" | "hostIncarnation" | "endpointMtimeMs" | "lifecycleRequestId">): number | undefined;
246
+ findSessionClosedEvidence(expected: Pick<IndexedSession, "sessionId" | "locator" | "endpointGeneration" | "pid" | "processIncarnation" | "hostIncarnation" | "endpointMtimeMs" | "endpointFileId" | "lifecycleRequestId">): number | undefined;
238
247
  /**
239
248
  * Production coalesced heartbeat checkpoint pass (C2): appends one
240
249
  * `host_heartbeat` per session at most once per {@link SESSION_HEARTBEAT_INTERVAL_MS}.
@@ -247,7 +256,7 @@ export declare class SessionIndex {
247
256
  * unknown/not-live (never fresh forever). Returns the number of checkpoints written.
248
257
  */
249
258
  checkpointLiveHeartbeats(now?: number): Promise<number>;
250
- hostUnregisteredAfter(registration: Pick<IndexedSession, "sessionId" | "locator" | "endpointGeneration" | "pid" | "indexSeq" | "lifecycleRequestId" | "hostIncarnation" | "processIncarnation">): {
259
+ hostUnregisteredAfter(registration: Pick<IndexedSession, "sessionId" | "locator" | "endpointGeneration" | "pid" | "indexSeq" | "lifecycleRequestId" | "hostIncarnation" | "processIncarnation" | "endpointFileId">): {
251
260
  indexSeq: number;
252
261
  lifecycleRequestId?: string;
253
262
  } | undefined;
@@ -24,5 +24,19 @@ export declare function observeProcess(pid: number, expectedIncarnation: string
24
24
  * `uncertain` counts as occupied: refusing a launch is recoverable by picking
25
25
  * another worktree name, whereas two live sessions sharing a checkout corrupts
26
26
  * work already done.
27
+ *
28
+ * A row carrying BOTH `terminalUncertain` and `forcedStaleRelease` is the one
29
+ * exception. That pair is written only by the forced stop of a stale-endpoint
30
+ * session: teardown was requested explicitly, so parking its worktree until an
31
+ * OS probe happens to prove exit would leave the checkout locked indefinitely
32
+ * (#5581) exactly when the probe returns `uncertain`. Such a row is released
33
+ * regardless of the process observation.
34
+ *
35
+ * `terminalUncertain` on its own is not enough. The fail-closed tail of a
36
+ * signal-escalated teardown sets it after SIGKILL without ever proving the child
37
+ * gone, so that process may still be running in the checkout; releasing on the
38
+ * flag alone would let a second session into the same worktree. Those rows stay
39
+ * under the strict "only definitive exit releases" rule like every other
40
+ * ordinary retained row.
27
41
  */
28
42
  export declare function worktreeOccupant(sessions: readonly IndexedSession[], worktreePath: string, observe?: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation): string | null;
@@ -7,6 +7,22 @@ export interface ChatDaemonRuntimeHandle {
7
7
  transportHealthy?(): boolean;
8
8
  /** Executes operator commands that must run inside the owning daemon. */
9
9
  bindExistingRoot?(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
10
+ prepareDoctorRestart?(): void;
11
+ cancelDoctorRestart?(): void;
12
+ doctorOccupancy?(): {
13
+ attached: number;
14
+ inflight: number;
15
+ inbound: number;
16
+ outbound: number;
17
+ cleanup: number;
18
+ };
19
+ doctorRestartReady?(): boolean;
20
+ doctorRestartStatus?(identity: {
21
+ owner: "discord" | "slack";
22
+ ownerId: string;
23
+ generation: number;
24
+ incarnation: string;
25
+ }, requestId: string, leaseExpiresAt: number): unknown;
10
26
  }
11
27
  export interface RunChatDaemonInternalDeps {
12
28
  processPid?: number;
@@ -1,5 +1,6 @@
1
1
  import type { Settings } from "../../config/settings";
2
2
  import type { BuiltInDaemonController, DaemonOperationOptions, DaemonOperationResult, DaemonRuntimeInfo, DaemonStatus } from "../../daemon/control-types";
3
+ import { type DoctorDaemonControlRequest } from "./doctor-daemon-restart";
3
4
  export type ChatDaemonKind = "discord" | "slack";
4
5
  export type ChatDaemonAction = "stop" | "reload";
5
6
  /**
@@ -122,6 +123,14 @@ export interface ChatDaemonState {
122
123
  transportHealthy: boolean;
123
124
  generation: number;
124
125
  stoppedAt?: number;
126
+ /**
127
+ * Digest of the canonical (symlink-resolved) agent root this record was
128
+ * published for. Legacy records omit it and remain non-authorizing for the
129
+ * D9 cross-bind check: a state record without a matching rootDigest can
130
+ * never be treated as this root's live owner for that check, even though it
131
+ * is still valid for every other pre-existing owner check in this module.
132
+ */
133
+ rootDigest?: string;
125
134
  }
126
135
  /**
127
136
  * State files are untrusted persisted input. A record must be completely valid
@@ -161,12 +170,51 @@ export interface ChatDaemonControlDeps {
161
170
  sleep?: (ms: number) => Promise<void>;
162
171
  spawnReadyTimeoutMs?: number;
163
172
  }
173
+ /**
174
+ * `version`/`ownerId`/`rootDigest` are the D9 cross-bind receipt delta: a
175
+ * legacy lock predating this generation omits them. Legacy records remain
176
+ * valid, signalable owner locks for every existing check in this module —
177
+ * they are simply non-authorizing for a D9 cross-bind check that requires a
178
+ * matching `ownerId`/`rootDigest` against the sibling state record.
179
+ */
180
+ export interface ChatDaemonOwnerLock {
181
+ version?: 1;
182
+ pid: number;
183
+ incarnation: string;
184
+ createdAt: number;
185
+ ownerId?: string;
186
+ rootDigest?: string;
187
+ }
188
+ export interface ChatDaemonOwnerLockLease {
189
+ content: string;
190
+ dev: bigint;
191
+ ino: bigint;
192
+ size: bigint;
193
+ mtimeNs: bigint;
194
+ nlink: bigint;
195
+ parentDev: bigint;
196
+ parentIno: bigint;
197
+ sha256: string;
198
+ }
199
+ /** 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. */
200
+ export type ChatDaemonOwnerLockLeaseResult = {
201
+ status: "present";
202
+ lease: ChatDaemonOwnerLockLease;
203
+ } | {
204
+ status: "absent";
205
+ } | {
206
+ status: "unreadable";
207
+ };
164
208
  export declare function chatDaemonPaths(agentDir: string, kind: ChatDaemonKind): {
165
209
  dir: string;
166
210
  lock: string;
167
211
  state: string;
168
212
  control: string;
169
213
  };
214
+ export declare function chatDoctorControlRequestPath(agentDir: string, kind: ChatDaemonKind): string;
215
+ export declare function readChatDoctorControlRequest(agentDir: string, kind: ChatDaemonKind): Promise<DoctorDaemonControlRequest | undefined>;
216
+ export declare function writeChatDoctorControlRequest(agentDir: string, kind: ChatDaemonKind, request: DoctorDaemonControlRequest): Promise<void>;
217
+ export declare function clearChatDoctorControlRequest(agentDir: string, kind: ChatDaemonKind, requestId?: string): Promise<void>;
170
218
  /**
171
219
  * Configuration fingerprint that identifies which settings a daemon owner was
172
220
  * started for. `undefined` means the current settings cannot configure that
@@ -223,7 +271,7 @@ export declare class ChatDaemonController implements BuiltInDaemonController {
223
271
  export type EnsureChatDaemonResult = "disabled" | "owner_spawned" | "attached";
224
272
  export declare function ensureDiscordDaemon(settings: Settings, deps?: ChatDaemonControlDeps): Promise<EnsureChatDaemonResult>;
225
273
  export declare function ensureSlackDaemon(settings: Settings, deps?: ChatDaemonControlDeps): Promise<EnsureChatDaemonResult>;
226
- export declare function acquireChatDaemonOwnership(input: {
274
+ export interface AcquireChatDaemonOwnershipInput {
227
275
  agentDir: string;
228
276
  kind: ChatDaemonKind;
229
277
  ownerId: string;
@@ -232,8 +280,40 @@ export declare function acquireChatDaemonOwnership(input: {
232
280
  incarnation?: string;
233
281
  pidAlive?: (pid: number) => boolean;
234
282
  pidIncarnation?: (pid: number) => string | undefined;
235
- }): Promise<boolean>;
236
- export declare function renewChatDaemonHeartbeat(input: {
283
+ }
284
+ /**
285
+ * Publishes this process as the `(agentDir, kind)` chat daemon owner
286
+ * (owner.lock + state.json). Runs behind the shared cross-process startup
287
+ * exclusion so it can never race a doctor startup/maintenance repair that
288
+ * holds the same guard (e.g. detaching a stale owner-lock quarantine) — the
289
+ * guard is acquired here and released before returning; the actual
290
+ * lock-creation/state-publication logic lives in the unlocked
291
+ * {@link acquireChatDaemonOwnershipLocked} so the guard is never held across
292
+ * a nested reacquisition of itself.
293
+ */
294
+ export declare function acquireChatDaemonOwnership(input: AcquireChatDaemonOwnershipInput): Promise<boolean>;
295
+ /**
296
+ * Bounded, no-follow, non-blocking owner-lock lease capture. Opens with the
297
+ * same `O_NOFOLLOW|O_NONBLOCK` posture as the canonical lock-info reader in
298
+ * config/file-lock.ts, reads at most {@link CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES}
299
+ * bytes through the open descriptor (never an unbounded `readFile`), and
300
+ * revalidates identity/parent-directory metadata before and after the read so a
301
+ * TOCTOU replacement during the read is detected rather than silently trusted.
302
+ *
303
+ * Only `ENOENT` on the initial open means the lock is currently absent.
304
+ * SYMLINK, FIFO/special-file, oversize, and any other I/O failure are all
305
+ * `"unreadable"` — distinct from absence, and callers must not treat them as
306
+ * "no lock exists".
307
+ */
308
+ export declare function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemonOwnerLockLeaseResult>;
309
+ /**
310
+ * Strict: `pid`/`incarnation`/`createdAt` are always required. `version`,
311
+ * `ownerId`, and `rootDigest` are individually optional (legacy locks omit
312
+ * them), but any one that IS present must be well-formed — a malformed
313
+ * optional field is never silently ignored.
314
+ */
315
+ export declare function isChatDaemonOwnerLock(value: unknown): value is ChatDaemonOwnerLock;
316
+ export interface RenewChatDaemonHeartbeatInput {
237
317
  agentDir: string;
238
318
  kind: ChatDaemonKind;
239
319
  ownerId: string;
@@ -242,8 +322,16 @@ export declare function renewChatDaemonHeartbeat(input: {
242
322
  transportHealthy: boolean;
243
323
  pidAlive?: (pid: number) => boolean;
244
324
  pidIncarnation?: (pid: number) => string | undefined;
245
- }): Promise<boolean>;
246
- export declare function releaseChatDaemonOwnership(input: {
325
+ }
326
+ /**
327
+ * Renews the exact-owner heartbeat for `(agentDir, kind)`. Runs behind the
328
+ * shared cross-process startup exclusion so a heartbeat can never publish in
329
+ * the middle of a doctor startup/maintenance repair holding the same guard;
330
+ * the actual re-check-and-write logic lives in the unlocked
331
+ * {@link renewChatDaemonHeartbeatLocked}.
332
+ */
333
+ export declare function renewChatDaemonHeartbeat(input: RenewChatDaemonHeartbeatInput): Promise<boolean>;
334
+ export interface ReleaseChatDaemonOwnershipInput {
247
335
  agentDir: string;
248
336
  kind: ChatDaemonKind;
249
337
  ownerId: string;
@@ -251,4 +339,11 @@ export declare function releaseChatDaemonOwnership(input: {
251
339
  incarnation: string;
252
340
  pidAlive?: (pid: number) => boolean;
253
341
  pidIncarnation?: (pid: number) => string | undefined;
254
- }): Promise<void>;
342
+ }
343
+ /**
344
+ * Releases this process's `(agentDir, kind)` chat daemon ownership. Runs
345
+ * behind the shared cross-process startup exclusion for the same reason as
346
+ * {@link acquireChatDaemonOwnership}; the actual re-check-and-unpublish logic
347
+ * lives in the unlocked {@link releaseChatDaemonOwnershipLocked}.
348
+ */
349
+ export declare function releaseChatDaemonOwnership(input: ReleaseChatDaemonOwnershipInput): Promise<void>;
@@ -2,6 +2,7 @@ import type { SessionRouterDeps } from "../router";
2
2
  import type { ChatDaemonCommandBindInput, ChatDaemonCommandOutcome } from "./chat-daemon-command-channel";
3
3
  import type { ChatDaemonKind } from "./chat-daemon-control";
4
4
  import type { DiscordProvider } from "./discord-provider";
5
+ import { type DoctorDaemonIdentity, type DoctorDaemonOccupancy, type DoctorDaemonStatus } from "./doctor-daemon-restart";
5
6
  import { type SlackProviderClient } from "./slack-provider";
6
7
  export interface ChatDaemonRuntimeConfig {
7
8
  identity: string;
@@ -63,6 +64,21 @@ export declare class ChatDaemonRuntime {
63
64
  }, deps?: ChatDaemonRuntimeDeps);
64
65
  start(): Promise<void>;
65
66
  transportHealthy(): boolean;
67
+ /** Doctor reservation hook: stop admitting new `/sdk` command dispatch. Synchronous fence. */
68
+ prepareDoctorRestart(): boolean;
69
+ cancelDoctorRestart(): void;
70
+ /**
71
+ * Real runtime occupancy, not synthetic placeholders. `attached` is live
72
+ * session attachments; `inflight` is attachment (re)publication barriers
73
+ * currently resolving; `inbound` is admitted `/sdk` command dispatches
74
+ * (including operator `bindExistingRoot` binds) actually executing;
75
+ * `outbound` is real Discord/Slack notify/resume provider deliveries in
76
+ * flight; `cleanup` is provider close/retire work that has not yet
77
+ * settled.
78
+ */
79
+ doctorOccupancy(): DoctorDaemonOccupancy;
80
+ doctorRestartStatus(identity: DoctorDaemonIdentity, requestId: string, leaseExpiresAt: number): DoctorDaemonStatus;
81
+ doctorRestartReady(): boolean;
66
82
  /** Reconcile indexed attachments and optionally wait for their replay tails. */
67
83
  reconcile(options?: {
68
84
  waitForReplay?: boolean;
@@ -0,0 +1,43 @@
1
+ export type DaemonStartupExclusionOwner = "telegram" | "discord" | "slack";
2
+ export interface DaemonStartupExclusionOptions {
3
+ /** Abort acquisition early; never releases a lease this call did not itself acquire. */
4
+ signal?: AbortSignal;
5
+ /** Upper bound on total wait while contended. Defaults to {@link DAEMON_STARTUP_EXCLUSION_DEFAULT_TIMEOUT_MS}. */
6
+ timeoutMs?: number;
7
+ }
8
+ /** Matches the production wall-time bound already established for other durable daemon locks. */
9
+ export declare const DAEMON_STARTUP_EXCLUSION_DEFAULT_TIMEOUT_MS = 5000;
10
+ /**
11
+ * The canonical per-service guard path. Lives inside the SAME per-service
12
+ * directory as the service's ordinary owner/state artifacts (so it inherits
13
+ * that directory's existing parent-permission posture) but under a filename
14
+ * reserved exclusively for this guard — never a name any ownership,
15
+ * publication, or transition-marker code path reads or writes.
16
+ *
17
+ * This function only computes a path; it performs no filesystem I/O and
18
+ * creates nothing. (`withFileLock` itself creates the missing parent
19
+ * directory chain, mode 0o700, purely as lock-acquisition scaffolding — an
20
+ * empty directory carries no service state.)
21
+ */
22
+ export declare function daemonStartupExclusionPath(agentDir: string, owner: DaemonStartupExclusionOwner): string;
23
+ /**
24
+ * Run `operation` while holding the cross-process startup/maintenance
25
+ * exclusion for `(agentDir, owner)`. Bounded: acquisition retries back off
26
+ * for at most `timeoutMs` (default {@link DAEMON_STARTUP_EXCLUSION_DEFAULT_TIMEOUT_MS})
27
+ * and additionally honors `signal` if supplied — whichever bound is hit first
28
+ * rejects the acquisition. A rejected/timed-out/aborted acquisition never held
29
+ * a lease and therefore never releases one; it cannot touch a foreign owner's
30
+ * lock. On success, the lease this call acquired is released exactly once,
31
+ * through the same identity-bound release primitive `withFileLock` uses for
32
+ * every other durable lock in this codebase — never a recursive directory
33
+ * removal, and never a process signal.
34
+ *
35
+ * This primitive itself never reads, writes, or removes any owner/state/lock
36
+ * file belonging to the service; it only fences concurrent callers of this
37
+ * same function (and, by construction, is never consulted by ordinary
38
+ * ownership-acquisition or publication code, which uses its own distinct
39
+ * lock file).
40
+ */
41
+ export declare function withDaemonStartupExclusion<T>(agentDir: string, owner: DaemonStartupExclusionOwner, operation: () => Promise<T>, options?: DaemonStartupExclusionOptions): Promise<T>;
42
+ /** Retain the same exclusion across an existing acquire/release lifecycle. */
43
+ export declare function acquireDaemonStartupExclusion(agentDir: string, owner: DaemonStartupExclusionOwner, options?: DaemonStartupExclusionOptions): Promise<() => Promise<void>>;