@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
@@ -5,7 +5,13 @@ import { createHash, randomUUID } from "node:crypto";
5
5
  * Preserves Q26 admit/first-terminal/capacity semantics; indexes and caps are per-kind.
6
6
  */
7
7
 
8
- import { sanitizePromptFailure } from "../prompt-failure";
8
+ import {
9
+ failedPromptOutcome,
10
+ failureEvidence,
11
+ type PromptFailureEvidence,
12
+ rephaseFailedOutcome,
13
+ sanitizePromptFailure,
14
+ } from "../prompt-failure";
9
15
  import type {
10
16
  PromptReconciliationStatus,
11
17
  SdkPromptFailureCode,
@@ -55,7 +61,10 @@ const STOP_REASONS: readonly SdkPromptStopReason[] = [
55
61
  ];
56
62
  const FAILURE_CODES: readonly SdkPromptFailureCode[] = ["prompt_failed", "prompt_deadline_exceeded"];
57
63
 
58
- const normalizeTerminalOutcome = (outcome: unknown): SdkPromptTerminalOutcome | undefined => {
64
+ const normalizeTerminalOutcome = (
65
+ outcome: unknown,
66
+ evidence: PromptFailureEvidence = {},
67
+ ): SdkPromptTerminalOutcome | undefined => {
59
68
  if (outcome === null || typeof outcome !== "object") return undefined;
60
69
  const candidate = outcome as Record<string, unknown>;
61
70
  if (
@@ -73,23 +82,32 @@ const normalizeTerminalOutcome = (outcome: unknown): SdkPromptTerminalOutcome |
73
82
  FAILURE_CODES.includes(candidate.code as SdkPromptFailureCode) &&
74
83
  typeof candidate.message === "string" &&
75
84
  ["agent_failed", "deadline"].includes(candidate.provenance as string)
76
- )
77
- return {
78
- kind: "failed",
85
+ ) {
86
+ return failedPromptOutcome({
79
87
  code: candidate.code as SdkPromptFailureCode,
80
- message:
81
- candidate.code === "prompt_deadline_exceeded" ? "Prompt deadline exceeded." : "Prompt submission failed.",
82
88
  provenance: candidate.provenance as "agent_failed" | "deadline",
83
- };
89
+ ...(typeof candidate.providerCode === "string" ? { providerCode: candidate.providerCode } : {}),
90
+ evidence,
91
+ });
92
+ }
84
93
  return undefined;
85
94
  };
86
95
 
87
- const failedOutcomeForError = (error: { code: string; message: string }): SdkPromptTerminalOutcome => ({
88
- kind: "failed",
89
- code: "prompt_failed",
90
- message: error.message,
91
- provenance: "agent_failed",
92
- });
96
+ const failedOutcomeForError = (
97
+ error: { code: string; message: string },
98
+ evidence: PromptFailureEvidence = {},
99
+ providerCodeOverride?: string,
100
+ ): SdkPromptTerminalOutcome =>
101
+ failedPromptOutcome({
102
+ code: "prompt_failed",
103
+ provenance: "agent_failed",
104
+ ...(providerCodeOverride !== undefined
105
+ ? { providerCode: providerCodeOverride }
106
+ : error.code !== "prompt_failed"
107
+ ? { providerCode: error.code }
108
+ : {}),
109
+ evidence,
110
+ });
93
111
 
94
112
  const isDeadlineOutcome = (outcome: SdkPromptTerminalOutcome | undefined): boolean =>
95
113
  outcome?.kind === "failed" && outcome.provenance === "deadline";
@@ -440,7 +458,10 @@ export function createKindAwareReconciliation(
440
458
  record.receiptState,
441
459
  reportableTurnResultContent(content) ? "present" : undefined,
442
460
  );
443
- const incomingOutcome = normalizeTerminalOutcome(frame.outcome);
461
+ const incomingOutcome = normalizeTerminalOutcome(
462
+ frame.outcome,
463
+ failureEvidence(record, frame.hasActivity),
464
+ );
444
465
  const providerErrorRecord = record.error;
445
466
  const providerError =
446
467
  providerErrorRecord !== undefined && providerErrorRecord.code !== "prompt_deadline_exceeded";
@@ -460,7 +481,7 @@ export function createKindAwareReconciliation(
460
481
  ? providerErrorRecord
461
482
  : { code: providerOutcome.code, message: providerOutcome.message };
462
483
  } else if (providerError) {
463
- outcome = failedOutcomeForError(providerErrorRecord);
484
+ outcome = failedOutcomeForError(providerErrorRecord, failureEvidence(record, frame.hasActivity));
464
485
  error = providerErrorRecord;
465
486
  } else if (
466
487
  incomingOutcome?.kind === "stopped" ||
@@ -479,7 +500,7 @@ export function createKindAwareReconciliation(
479
500
  ? providerErrorRecord
480
501
  : { code: providerOutcome.code, message: providerOutcome.message };
481
502
  } else if (record.status === "failed" && providerError) {
482
- outcome = failedOutcomeForError(providerErrorRecord);
503
+ outcome = failedOutcomeForError(providerErrorRecord, failureEvidence(record, frame.hasActivity));
483
504
  error = providerErrorRecord;
484
505
  } else if (
485
506
  record.status === "terminal_ok" &&
@@ -523,7 +544,7 @@ export function createKindAwareReconciliation(
523
544
  const failure = sanitizePromptFailure(frame.error);
524
545
  if (record.error?.code === "prompt_deadline_exceeded") {
525
546
  record.error = failure;
526
- record.outcome = failedOutcomeForError(failure);
547
+ record.outcome = failedOutcomeForError(failure, failureEvidence(record, frame.hasActivity));
527
548
  record.status = "failed";
528
549
  return { value: undefined, changed: true };
529
550
  }
@@ -557,7 +578,10 @@ export function createKindAwareReconciliation(
557
578
  return { value: undefined, changed: true };
558
579
  }
559
580
  const pendingOutcome = record.pendingOutcome;
560
- const frameOutcome = normalizeTerminalOutcome(frame.type === "agent_end" ? frame.outcome : undefined);
581
+ const frameOutcome = normalizeTerminalOutcome(
582
+ frame.type === "agent_end" ? frame.outcome : undefined,
583
+ failureEvidence(record, frame.hasActivity),
584
+ );
561
585
  delete record.deadlineRecoveryPending;
562
586
  delete record.deadlineMaxAt;
563
587
  record.terminalAt = now();
@@ -572,7 +596,7 @@ export function createKindAwareReconciliation(
572
596
  if (frameOutcome?.kind === "failed" && frameOutcome.provenance === "agent_failed")
573
597
  terminalOutcome = frameOutcome;
574
598
  else if (providerError && (pendingOutcome.kind !== "failed" || isDeadlineOutcome(pendingOutcome)))
575
- terminalOutcome = failedOutcomeForError(providerErrorRecord);
599
+ terminalOutcome = failedOutcomeForError(providerErrorRecord, failureEvidence(record, frame.hasActivity));
576
600
  else if (isDeadlineOutcome(pendingOutcome) && frameOutcome?.kind === "stopped")
577
601
  terminalOutcome = frameOutcome;
578
602
  record.outcome = terminalOutcome;
@@ -598,7 +622,7 @@ export function createKindAwareReconciliation(
598
622
  ? frameOutcome
599
623
  : providerError &&
600
624
  (frameOutcome === undefined || isDeadlineOutcome(frameOutcome) || frameOutcome.kind === "stopped")
601
- ? failedOutcomeForError(providerErrorRecord)
625
+ ? failedOutcomeForError(providerErrorRecord, failureEvidence(record, frame.hasActivity))
602
626
  : frameOutcome;
603
627
  if (terminalOutcome !== undefined) {
604
628
  record.outcome = terminalOutcome;
@@ -618,10 +642,10 @@ export function createKindAwareReconciliation(
618
642
  ) {
619
643
  record.status = "failed";
620
644
  record.error = EMPTY_PROMPT_FAILURE;
621
- record.outcome = failedOutcomeForError(EMPTY_PROMPT_FAILURE);
645
+ record.outcome = failedOutcomeForError(EMPTY_PROMPT_FAILURE, failureEvidence(record, frame.hasActivity));
622
646
  } else if (record.error !== undefined) {
623
647
  record.status = "failed";
624
- record.outcome = failedOutcomeForError(record.error);
648
+ record.outcome = failedOutcomeForError(record.error, failureEvidence(record, frame.hasActivity));
625
649
  } else record.status = "terminal_ok";
626
650
  record.receiptState = reduceReceiptState(
627
651
  record.receiptState,
@@ -645,9 +669,10 @@ export function createKindAwareReconciliation(
645
669
  if (!record || record.terminalAt !== undefined || record.kind !== kind)
646
670
  return { value: normalized, changed: false };
647
671
  if (record.pendingOutcome !== undefined) return { value: record.pendingOutcome, changed: false };
648
- record.pendingOutcome = normalized;
672
+ const decorated = rephaseFailedOutcome(normalized, failureEvidence(record));
673
+ record.pendingOutcome = decorated;
649
674
  record.pendingReceiptState = receiptState;
650
- return { value: normalized, changed: true };
675
+ return { value: decorated, changed: true };
651
676
  });
652
677
 
653
678
  const finalizeOutcome = async (
@@ -673,25 +698,28 @@ export function createKindAwareReconciliation(
673
698
  const record = candidate.get(keyOf(kind, correlation));
674
699
  if (!record || record.terminalAt !== undefined || record.kind !== kind)
675
700
  return { value: undefined, changed: false };
676
- const requestedOutcome = normalizeTerminalOutcome(outcome) ?? record.pendingOutcome;
701
+ const requestedOutcome = normalizeTerminalOutcome(outcome, failureEvidence(record)) ?? record.pendingOutcome;
677
702
  const providerErrorRecord = record.error;
678
703
  const providerError =
679
704
  providerErrorRecord !== undefined && providerErrorRecord.code !== "prompt_deadline_exceeded";
705
+ const requestedProviderCode = requestedOutcome?.kind === "failed" ? requestedOutcome.providerCode : undefined;
680
706
  const finalOutcome = providerError
681
- ? failedOutcomeForError(providerErrorRecord)
707
+ ? failedOutcomeForError(providerErrorRecord, failureEvidence(record), requestedProviderCode)
682
708
  : requestedOutcome?.kind === "stopped"
683
709
  ? requestedOutcome
684
710
  : providerError &&
685
711
  (requestedOutcome === undefined ||
686
712
  (requestedOutcome.kind === "failed" && isDeadlineOutcome(requestedOutcome)))
687
- ? failedOutcomeForError(providerErrorRecord)
713
+ ? failedOutcomeForError(providerErrorRecord, failureEvidence(record), requestedProviderCode)
688
714
  : requestedOutcome;
689
715
  record.terminalAt = now();
690
716
  if (finalOutcome?.kind === "failed") {
691
717
  record.status = "failed";
692
718
  if (recordError !== undefined && !(providerError && recordError.code === "prompt_deadline_exceeded"))
693
- record.error = recordError;
694
- else if (!providerError) record.error = { code: finalOutcome.code, message: finalOutcome.message };
719
+ record.error = { code: recordError.code, message: finalOutcome.message };
720
+ else if (providerError && providerErrorRecord !== undefined)
721
+ record.error = { code: providerErrorRecord.code, message: finalOutcome.message };
722
+ else record.error = { code: finalOutcome.code, message: finalOutcome.message };
695
723
  } else if (
696
724
  kind === "prompt" &&
697
725
  ((finalOutcome === undefined && !sanitizedContent?.text.trim()) ||
@@ -699,7 +727,7 @@ export function createKindAwareReconciliation(
699
727
  ) {
700
728
  record.status = "failed";
701
729
  record.error = EMPTY_PROMPT_FAILURE;
702
- record.outcome = failedOutcomeForError(EMPTY_PROMPT_FAILURE);
730
+ record.outcome = failedOutcomeForError(EMPTY_PROMPT_FAILURE, failureEvidence(record));
703
731
  } else if (finalOutcome?.kind === "stopped") {
704
732
  // A stopped terminal is authoritative only when no provider failure
705
733
  // was recorded before finalization.
@@ -837,16 +865,23 @@ export function createKindAwareReconciliation(
837
865
  loaded.map(record => {
838
866
  const safeOutcome = (
839
867
  outcome: SdkPromptTerminalOutcome | undefined,
840
- ): SdkPromptTerminalOutcome | undefined =>
841
- outcome?.kind === "failed"
842
- ? {
843
- ...outcome,
844
- message:
845
- outcome.code === "prompt_deadline_exceeded"
846
- ? "Prompt deadline exceeded."
847
- : "Prompt submission failed.",
848
- }
849
- : outcome;
868
+ ): SdkPromptTerminalOutcome | undefined => {
869
+ if (outcome?.kind !== "failed") return outcome;
870
+ const rephased = rephaseFailedOutcome(
871
+ outcome,
872
+ record.startedAt !== undefined ? { startedAt: record.startedAt } : {},
873
+ );
874
+ // Legacy durable rows carry a generic message and no phase/category;
875
+ // rewriting them to the current contract is a durable-state sanitization.
876
+ if (
877
+ rephased.kind === "failed" &&
878
+ (outcome.phase !== rephased.phase ||
879
+ outcome.category !== rephased.category ||
880
+ outcome.message !== rephased.message)
881
+ )
882
+ sanitizedDurableRecord = true;
883
+ return rephased;
884
+ };
850
885
  let hydrated: DurableReconciliationRecord;
851
886
  if (record.kind === "steer") {
852
887
  const safeError = record.error === undefined ? undefined : sanitizePromptFailure(record.error);
@@ -52,7 +52,13 @@ import { DiscordLiveProvider } from "./discord-live-provider";
52
52
  import type { DiscordDiagnosticProvider } from "./discord-provider";
53
53
  import { SlackLiveProvider } from "./slack-live-provider";
54
54
  import type { SlackDiagnosticProvider } from "./slack-provider";
55
- import { type OwnerFreshnessSnapshot, readOwnerFreshnessSnapshot, type TelegramDaemonFs } from "./telegram-daemon";
55
+ import {
56
+ isTelegramDaemonStopCause,
57
+ type OwnerFreshnessSnapshot,
58
+ readOwnerFreshnessSnapshot,
59
+ type TelegramDaemonFs,
60
+ type TelegramDaemonStopCause,
61
+ } from "./telegram-daemon";
56
62
  import { DAEMON_GENERATION } from "./telegram-daemon-contract";
57
63
 
58
64
  const DEFAULT_API_BASE = "https://api.telegram.org";
@@ -547,6 +553,7 @@ interface NormalizedDaemonState {
547
553
  startedAt: number | undefined;
548
554
  heartbeatAt: number | undefined;
549
555
  stoppedAt: number | undefined;
556
+ stopCause: TelegramDaemonStopCause | undefined;
550
557
  roots: string[] | undefined;
551
558
  generation: number | undefined;
552
559
  generationStatus: "missing" | "valid" | "invalid";
@@ -594,6 +601,7 @@ function parseDaemonState(raw: string): NormalizedDaemonState | undefined {
594
601
  startedAt: finiteNonNegativeNumber(rec.startedAt),
595
602
  heartbeatAt: finiteNonNegativeNumber(rec.heartbeatAt),
596
603
  stoppedAt: finiteNonNegativeNumber(rec.stoppedAt),
604
+ stopCause: isTelegramDaemonStopCause(rec.stopCause) ? rec.stopCause : undefined,
597
605
  roots: stringArray(rec.roots),
598
606
  generation,
599
607
  generationStatus,
@@ -677,6 +685,8 @@ export interface DaemonHealth {
677
685
  stopped: boolean;
678
686
  heartbeatAt: number | undefined;
679
687
  heartbeatAgeMs: number | undefined;
688
+ /** Bounded persisted stop cause; absent means this record predates diagnostics or is unknown. */
689
+ stopCause?: TelegramDaemonStopCause;
680
690
 
681
691
  /**
682
692
  * Session endpoints the live owner reported an OPEN socket to in its latest
@@ -928,6 +938,7 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
928
938
  stopped: state?.stoppedAt !== undefined,
929
939
  heartbeatAt,
930
940
  heartbeatAgeMs: heartbeatAt === undefined ? undefined : displayHeartbeatAgeMs(now, heartbeatAt),
941
+ ...(state?.stopCause === undefined ? {} : { stopCause: state.stopCause }),
931
942
  attachedEndpoints: snapshot.attachedEndpoints,
932
943
  generation: state?.generation,
933
944
  currentGeneration: DAEMON_GENERATION,
@@ -948,19 +959,26 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
948
959
  } catch {
949
960
  lockPresent = false;
950
961
  }
962
+ const stopCause = daemon.stopCause ?? "unknown";
951
963
  if (lockPresent) {
952
964
  checks.push({
953
965
  name: "daemon",
954
966
  level: "warn",
955
- detail: `daemon owner pid ${daemon.pid} is not alive; run recovery to clear the stale lock`,
967
+ detail: `daemon owner pid ${daemon.pid} is not alive; run recovery to clear the stale lock (stop cause: ${stopCause})`,
956
968
  });
957
969
  } else {
958
970
  checks.push({
959
971
  name: "daemon",
960
972
  level: "warn",
961
- detail: `daemon owner pid ${daemon.pid} is not alive; no lock present (daemon exited cleanly)`,
973
+ detail: `daemon owner pid ${daemon.pid} is not alive; no lock present (stop cause: ${stopCause})`,
962
974
  });
963
975
  }
976
+ } else if (daemon.stopped) {
977
+ checks.push({
978
+ name: "daemon",
979
+ level: "warn",
980
+ detail: `daemon is stopped (stop cause: ${daemon.stopCause ?? "unknown"})`,
981
+ });
964
982
  } else if (!daemon.heartbeatFresh) {
965
983
  checks.push({ name: "daemon", level: "warn", detail: `daemon pid ${daemon.pid} heartbeat is stale` });
966
984
  } else if (telegramConfigured && !daemon.identityMatches) {
@@ -12,6 +12,7 @@
12
12
  import { createHash, randomUUID } from "node:crypto";
13
13
  import * as fs from "node:fs/promises";
14
14
  import * as path from "node:path";
15
+ import { failedPromptOutcome } from "../prompt-failure";
15
16
  import type { PromptReconciliationStatus, SdkPromptTerminalOutcome } from "../prompt-status";
16
17
  import type { ReceiptState } from "../receipt-state";
17
18
  import { reportableTurnResultContent, TURN_RESULT_CONTENT_MAX_BYTES, type TurnResultContent } from "../turn-result";
@@ -559,12 +560,16 @@ export function settleProcessRestart(
559
560
  ): DurableReconciliationRecord[] {
560
561
  const isDeadlineOutcome = (outcome: SdkPromptTerminalOutcome | undefined): boolean =>
561
562
  outcome?.kind === "failed" && outcome.provenance === "deadline";
562
- const providerFailureOutcome = (error: { code: string; message: string }): SdkPromptTerminalOutcome => ({
563
- kind: "failed",
564
- code: "prompt_failed",
565
- message: error.message,
566
- provenance: "agent_failed",
567
- });
563
+ const providerFailureOutcome = (
564
+ error: { code: string; message: string },
565
+ record: { startedAt?: number },
566
+ ): SdkPromptTerminalOutcome =>
567
+ failedPromptOutcome({
568
+ code: "prompt_failed",
569
+ provenance: "agent_failed",
570
+ ...(error.code !== "prompt_failed" ? { providerCode: error.code } : {}),
571
+ evidence: record.startedAt !== undefined ? { startedAt: record.startedAt } : {},
572
+ });
568
573
  return records.map(record => {
569
574
  if (record.kind === "steer") {
570
575
  if (record.status !== "dispatching") return record;
@@ -592,13 +597,14 @@ export function settleProcessRestart(
592
597
  const outcome: SdkPromptTerminalOutcome =
593
598
  providerError &&
594
599
  (pendingOutcome === undefined || isDeadlineOutcome(pendingOutcome) || pendingOutcome.kind === "stopped")
595
- ? providerFailureOutcome(providerErrorRecord)
596
- : (pendingOutcome ?? {
597
- kind: "failed",
600
+ ? providerFailureOutcome(providerErrorRecord, record)
601
+ : (pendingOutcome ??
602
+ failedPromptOutcome({
598
603
  code: "prompt_failed",
599
- message: "Prompt did not complete before process restart.",
600
604
  provenance: "agent_failed",
601
- });
605
+ providerCode: "process_restart_uncertain",
606
+ evidence: record.startedAt !== undefined ? { startedAt: record.startedAt } : {},
607
+ }));
602
608
  const preservedError =
603
609
  providerError && preserveProviderError
604
610
  ? providerErrorRecord
@@ -13,6 +13,7 @@ import {
13
13
  parseNotificationSettingsSnapshot,
14
14
  } from "./config";
15
15
  import { daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
16
+ import { doctorDaemonIdentityMatches, doctorDaemonOccupancySettled } from "./doctor-daemon-restart";
16
17
  import { type NotificationDebrisSweepReport, sweepNotificationDebris } from "./notification-service";
17
18
  import {
18
19
  type DaemonState,
@@ -26,11 +27,26 @@ import {
26
27
  type TelegramDaemonOptions,
27
28
  TelegramNotificationDaemon,
28
29
  } from "./telegram-daemon";
29
- import { clearTelegramControlRequest, readTelegramControlRequest } from "./telegram-daemon-control";
30
+ import {
31
+ clearTelegramControlRequest,
32
+ clearTelegramDoctorControlRequest,
33
+ readTelegramControlRequest,
34
+ readTelegramDoctorControlRequest,
35
+ } from "./telegram-daemon-control";
30
36
 
31
37
  type TelegramDaemonRunner = {
32
38
  run(): Promise<void>;
33
39
  requestStop(reason?: "reload" | "signal" | "stop"): void;
40
+ prepareDoctorRestart?(): void;
41
+ cancelDoctorRestart?(): void;
42
+ doctorOccupancy?(): {
43
+ attached: number;
44
+ inflight: number;
45
+ inbound: number;
46
+ outbound: number;
47
+ cleanup: number;
48
+ };
49
+ doctorRestartReady?(): boolean;
34
50
  };
35
51
 
36
52
  type TelegramDaemonConstructor = new (opts: TelegramDaemonOptions) => TelegramDaemonRunner;
@@ -254,16 +270,23 @@ function ownerProcessIsAlive(ownerId: string, deps: RunDaemonInternalDeps): bool
254
270
 
255
271
  /** Creates owner-fenced daemon control hooks for the CLI lifecycle boundary. */
256
272
  export function createDaemonControlHooks(settings: Settings) {
273
+ const readOwnedRequest = async (owner: string) => {
274
+ const req = await readTelegramControlRequest(settings);
275
+ return req && (!req.ownerId || req.ownerId === owner) ? req : undefined;
276
+ };
257
277
  return {
258
278
  shouldStop: async (owner: string) => {
259
- const req = await readTelegramControlRequest(settings);
260
- return Boolean(req && (!req.ownerId || req.ownerId === owner));
279
+ return (await readOwnedRequest(owner)) !== undefined;
280
+ },
281
+ requestedAction: async (owner: string): Promise<"reload" | "stop" | undefined> => {
282
+ const action = (await readOwnedRequest(owner))?.action;
283
+ return action === "reload" || action === "stop" ? action : undefined;
261
284
  },
262
285
  clear: async (owner: string) => {
263
- const req = await readTelegramControlRequest(settings);
286
+ const req = await readOwnedRequest(owner);
264
287
  // Only clear a request that targets this daemon owner, so an exiting
265
288
  // daemon never erases a newer request meant for a different owner.
266
- if (req && (!req.ownerId || req.ownerId === owner)) await clearTelegramControlRequest(settings, req.requestId);
289
+ if (req) await clearTelegramControlRequest(settings, req.requestId);
267
290
  },
268
291
  };
269
292
  }
@@ -409,6 +432,22 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
409
432
  requireTelegramTopicEligibility: true,
410
433
  orphanReap,
411
434
  });
435
+ const doctorOwnerIdentity = async (): Promise<
436
+ | {
437
+ owner: "telegram";
438
+ ownerId: string;
439
+ generation: number;
440
+ incarnation: string;
441
+ }
442
+ | undefined
443
+ > => {
444
+ const current = await readState(settings as Settings);
445
+ if (!current || !hasSafeDaemonStateShape(current) || current.ownerId !== ownerId) return undefined;
446
+ const expectedIncarnation = (deps.pidIncarnation ?? processIncarnation)(deps.processPid ?? process.pid);
447
+ if (!expectedIncarnation || current.incarnation !== expectedIncarnation || current.generation === undefined)
448
+ return undefined;
449
+ return { owner: "telegram", ownerId, generation: current.generation, incarnation: current.incarnation };
450
+ };
412
451
  // Signals are a process concern: install them at the daemon-internal boundary,
413
452
  // not inside the embeddable daemon class. SIGTERM is the reload wakeup path.
414
453
  const onSignal = (): void => daemon.requestStop("signal");
@@ -418,6 +457,8 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
418
457
  let watchdogActive = true;
419
458
  let watchdogTickInFlight = false;
420
459
  let stopRequested = false;
460
+ let doctorPreparedRequestId: string | undefined;
461
+ let doctorPoll: Timer | undefined;
421
462
  let lastHeartbeatAt: number | undefined;
422
463
  let stalledSince: number | undefined;
423
464
  const watchdogTick = async (): Promise<void> => {
@@ -526,8 +567,53 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
526
567
  await recordOwnerStopped();
527
568
  });
528
569
  try {
570
+ const doctorTick = async (): Promise<void> => {
571
+ const request = await readTelegramDoctorControlRequest(settings as Settings);
572
+ if (!request) return;
573
+ const identity = await doctorOwnerIdentity();
574
+ // The shared predicate is the single definition of "this request addresses
575
+ // exactly me"; spelling the four fields out here would let this owner and the
576
+ // doctor client drift apart silently.
577
+ if (
578
+ !identity ||
579
+ !doctorDaemonIdentityMatches(request, identity) ||
580
+ (request.action !== "prepare" &&
581
+ request.action !== "commit" &&
582
+ request.action !== "cancel" &&
583
+ request.action !== "status")
584
+ )
585
+ return;
586
+ if (request.leaseExpiresAt !== undefined && request.leaseExpiresAt <= (deps.now ?? Date.now)()) {
587
+ // Expiry never reopens admission implicitly; an authorized
588
+ // coordinator must issue cancel after recovering the lease.
589
+ return;
590
+ }
591
+ if (request.action === "prepare") {
592
+ daemon.prepareDoctorRestart?.();
593
+ doctorPreparedRequestId = request.requestId;
594
+ return;
595
+ }
596
+ if (request.action === "cancel") {
597
+ if (doctorPreparedRequestId === request.requestId) daemon.cancelDoctorRestart?.();
598
+ await clearTelegramDoctorControlRequest(settings as Settings, request.requestId);
599
+ doctorPreparedRequestId = undefined;
600
+ return;
601
+ }
602
+ if (request.action === "commit" && doctorPreparedRequestId === request.requestId) {
603
+ const occupancy = daemon.doctorOccupancy?.();
604
+ if (!occupancy || !doctorDaemonOccupancySettled(occupancy)) return;
605
+ if (!daemon.doctorRestartReady?.()) return;
606
+ // A committed intent is durable across this owner's exit. The
607
+ // successor/doctor clears it only after proving old death, cleanup,
608
+ // and successor publication.
609
+ daemon.requestStop("reload");
610
+ doctorPreparedRequestId = undefined;
611
+ }
612
+ };
613
+ doctorPoll = schedule(() => void doctorTick(), 50);
529
614
  await daemon.run();
530
615
  } finally {
616
+ if (doctorPoll !== undefined) unschedule(doctorPoll);
531
617
  watchdogActive = false;
532
618
  unschedule(watchdog);
533
619
  process.off("SIGTERM", onSignal);
@@ -291,7 +291,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
291
291
  * killing a session's mirroring after one transient rejection and must be
292
292
  * replaced across this upgrade.
293
293
  */
294
- export const DAEMON_GENERATION = 181;
294
+ export const DAEMON_GENERATION = 187;
295
295
 
296
296
  /**
297
297
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
@@ -27,6 +27,7 @@ import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
27
27
  import { isProcessIncarnation } from "../broker/process-incarnation";
28
28
 
29
29
  import { getNotificationConfig, isTelegramComplete, tokenFingerprint } from "./config";
30
+ import { type DoctorDaemonControlRequest, isDoctorDaemonControlRequest } from "./doctor-daemon-restart";
30
31
  import {
31
32
  type AttestedLegacyDaemonOwner,
32
33
  confirmTelegramDaemonSpawn,
@@ -36,6 +37,7 @@ import {
36
37
  isCurrentCompatibleOwner,
37
38
  isFreshLiveOwner,
38
39
  isSignalableMatchingOwner,
40
+ isTelegramDaemonStopCause,
39
41
  readAttestedLegacyDaemonOwner,
40
42
  readDaemonState,
41
43
  readOwnerFreshnessSnapshot,
@@ -65,6 +67,48 @@ export function telegramControlRequestPath(agentDir: string): string {
65
67
  return path.join(daemonPaths(agentDir).dir, "telegram-daemon.control.json");
66
68
  }
67
69
 
70
+ export function telegramDoctorControlRequestPath(agentDir: string): string {
71
+ return path.join(daemonPaths(agentDir).dir, "doctor-restart.control.json");
72
+ }
73
+
74
+ export async function readTelegramDoctorControlRequest(
75
+ settings: Settings,
76
+ fsImpl: TelegramDaemonFs = nodeFs,
77
+ ): Promise<DoctorDaemonControlRequest | undefined> {
78
+ try {
79
+ const parsed = JSON.parse(
80
+ await fsImpl.readFile(telegramDoctorControlRequestPath(settings.getAgentDir()), "utf8"),
81
+ ) as unknown;
82
+ return isDoctorDaemonControlRequest(parsed) && parsed.owner === "telegram" ? parsed : undefined;
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ }
87
+
88
+ export async function writeTelegramDoctorControlRequest(
89
+ settings: Settings,
90
+ request: DoctorDaemonControlRequest,
91
+ fsImpl: TelegramDaemonFs = nodeFs,
92
+ ): Promise<void> {
93
+ if (request.owner !== "telegram") throw new Error("telegram doctor request owner mismatch");
94
+ const dir = daemonPaths(settings.getAgentDir()).dir;
95
+ await fsImpl.mkdir(dir, { recursive: true, mode: 0o700 });
96
+ const file = telegramDoctorControlRequestPath(settings.getAgentDir());
97
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
98
+ await fsImpl.writeFile(tmp, `${JSON.stringify(request)}\n`, { mode: 0o600 });
99
+ await fsImpl.rename(tmp, file);
100
+ }
101
+
102
+ export async function clearTelegramDoctorControlRequest(
103
+ settings: Settings,
104
+ requestId?: string,
105
+ fsImpl: TelegramDaemonFs = nodeFs,
106
+ ): Promise<void> {
107
+ const file = telegramDoctorControlRequestPath(settings.getAgentDir());
108
+ if (requestId && (await readTelegramDoctorControlRequest(settings, fsImpl))?.requestId !== requestId) return;
109
+ await fsImpl.unlink(file).catch(() => undefined);
110
+ }
111
+
68
112
  export async function readTelegramControlRequest(
69
113
  settings: Settings,
70
114
  fsImpl: TelegramDaemonFs = nodeFs,
@@ -236,6 +280,8 @@ export class TelegramDaemonController implements BuiltInDaemonController {
236
280
  })
237
281
  ? "running"
238
282
  : "stale";
283
+ const stopCause = isTelegramDaemonStopCause(state?.stopCause) ? state.stopCause : undefined;
284
+ const detail = health === "stopped" && state ? `stop cause: ${stopCause ?? "unknown"}` : undefined;
239
285
  return {
240
286
  kind: this.kind,
241
287
  configured: true,
@@ -244,6 +290,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
244
290
  ownerId: state?.ownerId,
245
291
  startedAt: state?.startedAt,
246
292
  heartbeatAt: snapshot.effectiveHeartbeatAt,
293
+ ...(detail === undefined ? {} : { detail }),
247
294
  runtime,
248
295
  };
249
296
  }
@@ -15,7 +15,7 @@ export interface TelegramOwnerMarker {
15
15
  startedAt: number;
16
16
  }
17
17
 
18
- function validMarker(value: unknown): value is TelegramOwnerMarker {
18
+ export function validMarker(value: unknown): value is TelegramOwnerMarker {
19
19
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
20
20
  const v = value as Record<string, unknown>;
21
21
  return (