@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
@@ -26,6 +26,7 @@ import {
26
26
  parseToolActivityToggleCommand,
27
27
  } from "./config-commands";
28
28
  import { agentDirDigest, daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
29
+ import { type DoctorDaemonOccupancy, doctorDaemonOccupancySettled } from "./doctor-daemon-restart";
29
30
  import {
30
31
  acquireDaemonTransitionLock,
31
32
  type DaemonTransitionLock,
@@ -160,6 +161,33 @@ export type EnsureTelegramDaemonDetailedResult = "spawned" | "reloaded" | "attac
160
161
 
161
162
  export type TelegramDaemonOwnershipPhase = "provisional" | "ready" | "retired";
162
163
 
164
+ /** Closed vocabulary for the durable reason an owner stopped serving. */
165
+ export type TelegramDaemonStopCause =
166
+ | "stop"
167
+ | "reload"
168
+ | "signal"
169
+ | "idle_timeout"
170
+ | "ownership_loss"
171
+ | "startup_failed"
172
+ | "unexpected_exception"
173
+ | "postmortem";
174
+
175
+ export function isTelegramDaemonStopCause(value: unknown): value is TelegramDaemonStopCause {
176
+ switch (value) {
177
+ case "stop":
178
+ case "reload":
179
+ case "signal":
180
+ case "idle_timeout":
181
+ case "ownership_loss":
182
+ case "startup_failed":
183
+ case "unexpected_exception":
184
+ case "postmortem":
185
+ return true;
186
+ default:
187
+ return false;
188
+ }
189
+ }
190
+
163
191
  export interface DaemonState {
164
192
  pid: number;
165
193
  /** OS process-start provenance; mandatory for PID-authorized ownership actions. */
@@ -189,6 +217,8 @@ export interface DaemonState {
189
217
  /** Lifecycle-serving compatibility epoch; absent pre-epoch records are epoch 1. */
190
218
  servingEpoch?: number;
191
219
  stoppedAt?: number;
220
+ /** Bounded, secret-free explanation for a stopped owner; absent on legacy/unknown records. */
221
+ stopCause?: TelegramDaemonStopCause;
192
222
  }
193
223
  interface ExactFileStat {
194
224
  dev: bigint;
@@ -1640,6 +1670,7 @@ export function hasSafeDaemonStateShape(state: unknown): state is DaemonState {
1640
1670
  (candidate.generation === undefined ||
1641
1671
  (Number.isSafeInteger(candidate.generation) && (candidate.generation as number) > 0)) &&
1642
1672
  (candidate.stoppedAt === undefined || Number.isSafeInteger(candidate.stoppedAt)) &&
1673
+ (candidate.stopCause === undefined || isTelegramDaemonStopCause(candidate.stopCause)) &&
1643
1674
  (candidate.servingEpoch === undefined ||
1644
1675
  (Number.isSafeInteger(candidate.servingEpoch) && (candidate.servingEpoch as number) > 0)),
1645
1676
  );
@@ -2471,6 +2502,7 @@ export async function renewDaemonHeartbeat(input: {
2471
2502
  ...boundState,
2472
2503
  ownershipPhase: "retired",
2473
2504
  stoppedAt: (input.now ?? Date.now)(),
2505
+ stopCause: boundState.stopCause ?? "startup_failed",
2474
2506
  }).catch(() => undefined);
2475
2507
  if (await transitionLockIsHeldByCaller({ fs: fsImpl, path: paths.steal, lock: transition }))
2476
2508
  await unlinkOwnershipLockExactly(fsImpl, paths.lock, lock);
@@ -2609,10 +2641,14 @@ export async function retireProvisionalDaemonOwnership(input: {
2609
2641
  if (!(await transitionLockIsHeldByCaller({ fs: fsImpl, path: paths.steal, lock: transition }))) return false;
2610
2642
  const lock = await readOwnershipLock(fsImpl, paths.lock);
2611
2643
  if (!ownershipLockMatchesState(lock, state)) return false;
2644
+ if (!(await transitionLockIsHeldByCaller({ fs: fsImpl, path: paths.steal, lock: transition }))) return false;
2645
+ const stoppedAt = state.stoppedAt ?? (input.now ?? Date.now)();
2646
+ const stopCause = state.stopCause ?? (state.stoppedAt === undefined ? "startup_failed" : undefined);
2612
2647
  await writeJsonAtomic(fsImpl, paths.state, {
2613
2648
  ...state,
2614
2649
  ownershipPhase: "retired",
2615
- stoppedAt: (input.now ?? Date.now)(),
2650
+ stoppedAt,
2651
+ ...(stopCause === undefined ? {} : { stopCause }),
2616
2652
  });
2617
2653
  if (!(await transitionLockIsHeldByCaller({ fs: fsImpl, path: paths.steal, lock: transition }))) return false;
2618
2654
  return await unlinkOwnershipLockExactly(fsImpl, paths.lock, lock);
@@ -2859,6 +2895,7 @@ export async function releaseDaemonOwnership(input: {
2859
2895
  pidIncarnation?: (pid: number) => string | undefined;
2860
2896
  fs?: TelegramDaemonFs;
2861
2897
  now?: () => number;
2898
+ stopCause?: TelegramDaemonStopCause;
2862
2899
  }): Promise<void> {
2863
2900
  const fsImpl = input.fs ?? nodeFs;
2864
2901
  const paths = daemonPaths(input.settings.getAgentDir());
@@ -2890,7 +2927,15 @@ export async function releaseDaemonOwnership(input: {
2890
2927
  if (!(await transitionLockIsHeldByCaller({ fs: fsImpl, path: paths.steal, lock: transition }))) return;
2891
2928
  const lock = await readOwnershipLock(fsImpl, paths.lock);
2892
2929
  if (!ownershipLockMatchesState(lock, state)) return;
2893
- await writeJsonAtomic(fsImpl, paths.state, { ...state, stoppedAt: (input.now ?? Date.now)() });
2930
+ const stoppedAt = state.stoppedAt ?? (input.now ?? Date.now)();
2931
+ const stopCause =
2932
+ state.stopCause ??
2933
+ (state.stoppedAt === undefined && isTelegramDaemonStopCause(input.stopCause) ? input.stopCause : undefined);
2934
+ await writeJsonAtomic(fsImpl, paths.state, {
2935
+ ...state,
2936
+ stoppedAt,
2937
+ ...(stopCause === undefined ? {} : { stopCause }),
2938
+ });
2894
2939
  try {
2895
2940
  const { removeTelegramOwnerMarker } = await import("./telegram-daemon-owner-registry");
2896
2941
  await removeTelegramOwnerMarker(fsImpl, input.settings.getAgentDir(), state.acquisitionId ?? state.ownerId);
@@ -2912,9 +2957,9 @@ export async function releaseDaemonOwnership(input: {
2912
2957
  * `ownershipPhase: "ready"` and a fresh-looking lock behind forever, so later
2913
2958
  * readers attached to an owner that had not existed for hours.
2914
2959
  *
2915
- * This writes `stoppedAt` and nothing else. The lock is left in place for the
2916
- * existing reclaim path to adjudicate, because a process on its way out is the
2917
- * least qualified party to decide who owns what next.
2960
+ * This writes `stoppedAt` and a closed-vocabulary `stopCause`. The lock is left
2961
+ * in place for the existing reclaim path to adjudicate, because a process on
2962
+ * its way out is the least qualified party to decide who owns what next.
2918
2963
  *
2919
2964
  * Fenced on full owner identity: if the state no longer names this exact
2920
2965
  * owner, acquisition, pid and incarnation, a successor already took over and
@@ -2950,7 +2995,12 @@ export async function markDaemonOwnerStopped(input: {
2950
2995
  return false;
2951
2996
  const lock = await readOwnershipLock(fsImpl, paths.lock);
2952
2997
  if (!ownershipLockMatchesState(lock, state)) return false;
2953
- await writeJsonAtomic(fsImpl, paths.state, { ...state, stoppedAt: (input.now ?? Date.now)() });
2998
+ const stopCause = state.stopCause ?? "postmortem";
2999
+ await writeJsonAtomic(fsImpl, paths.state, {
3000
+ ...state,
3001
+ stoppedAt: (input.now ?? Date.now)(),
3002
+ stopCause,
3003
+ });
2954
3004
  try {
2955
3005
  const { removeTelegramOwnerMarker } = await import("./telegram-daemon-owner-registry");
2956
3006
  await removeTelegramOwnerMarker(fsImpl, input.settings.getAgentDir(), state.acquisitionId ?? state.ownerId);
@@ -3992,6 +4042,12 @@ export class TelegramEventDispatchState {
3992
4042
  export interface DaemonControlHooks {
3993
4043
  /** Returns true when a stop/reload has been requested for this owner. */
3994
4044
  shouldStop(ownerId: string): Promise<boolean>;
4045
+ /**
4046
+ * Returns the owner-fenced action behind `shouldStop`, when the control
4047
+ * plane can provide one. This is a diagnostic projection, not an alternate
4048
+ * stop authority: callers must still gate it behind `shouldStop`.
4049
+ */
4050
+ requestedAction?(ownerId: string): Promise<"reload" | "stop" | undefined>;
3995
4051
  /** Clear a consumed control request (best-effort). */
3996
4052
  clear?(ownerId: string): Promise<void>;
3997
4053
  }
@@ -4405,10 +4461,18 @@ class TelegramEffectSupervisor {
4405
4461
  return this.#stopping;
4406
4462
  }
4407
4463
 
4464
+ get admitting(): boolean {
4465
+ return this.#admitting;
4466
+ }
4467
+
4408
4468
  closeAdmission(): void {
4409
4469
  this.#admitting = false;
4410
4470
  }
4411
4471
 
4472
+ openAdmission(): void {
4473
+ if (!this.#stopping) this.#admitting = true;
4474
+ }
4475
+
4412
4476
  beginShutdown(): void {
4413
4477
  this.closeAdmission();
4414
4478
  this.#stopping = true;
@@ -4496,6 +4560,10 @@ export class TelegramNotificationDaemon {
4496
4560
  private running = false;
4497
4561
  /** Once set, a concurrent startup await can never restore a running daemon. */
4498
4562
  private stopRequested = false;
4563
+ /** First observed stop cause wins, including an explicit stop. */
4564
+ #stopCause: TelegramDaemonStopCause | undefined;
4565
+ /** Signal cause resolution is asynchronous because control requests are file-backed. */
4566
+ #stopCauseResolution: Promise<void> | undefined;
4499
4567
 
4500
4568
  private readonly fsImpl: TelegramDaemonFs;
4501
4569
  readonly #deliveryAbort = new AbortController();
@@ -4652,12 +4720,47 @@ export class TelegramNotificationDaemon {
4652
4720
  }
4653
4721
  }
4654
4722
 
4723
+ #resolveControlStopCause(fallback: "signal" | "stop"): void {
4724
+ if (this.#stopCause !== undefined || this.#stopCauseResolution !== undefined) return;
4725
+ const requestedAction = this.opts.control?.requestedAction;
4726
+ if (!requestedAction) {
4727
+ this.#stopCause = fallback;
4728
+ return;
4729
+ }
4730
+ // Diagnostics must never hold ownership cleanup behind a stalled file read.
4731
+ const deadline = Promise.withResolvers<void>();
4732
+ const timer = setTimeout(() => {
4733
+ this.#stopCause ??= fallback;
4734
+ deadline.resolve();
4735
+ }, 250);
4736
+ const lookup = Promise.resolve()
4737
+ .then(() => requestedAction.call(this.opts.control, this.opts.ownerId))
4738
+ .then(
4739
+ action => {
4740
+ this.#stopCause ??= action === "reload" || action === "stop" ? action : fallback;
4741
+ },
4742
+ () => {
4743
+ this.#stopCause ??= fallback;
4744
+ },
4745
+ );
4746
+ this.#stopCauseResolution = Promise.race([lookup, deadline.promise]).finally(() => clearTimeout(timer));
4747
+ }
4748
+
4655
4749
  /**
4656
4750
  * Cooperatively stop the daemon: set the stop flag and abort the in-flight
4657
4751
  * long poll so the run loop wakes immediately instead of waiting out the
4658
- * ~25s getUpdates timeout. Safe to call from a signal handler.
4752
+ * ~25s getUpdates timeout. Safe to call from a signal handler. The
4753
+ * no-argument form is the generic finally fallback and never overwrites a
4754
+ * previously observed specific cause.
4659
4755
  */
4660
- requestStop(_reason?: "reload" | "stop" | "signal"): void {
4756
+ requestStop(reason?: TelegramDaemonStopCause): void {
4757
+ if (reason === "signal") {
4758
+ this.#resolveControlStopCause("signal");
4759
+ } else if (reason !== undefined && this.#stopCauseResolution === undefined) {
4760
+ this.#stopCause ??= reason;
4761
+ } else if (this.#stopCauseResolution === undefined) {
4762
+ this.#stopCause ??= "stop";
4763
+ }
4661
4764
  this.stopRequested = true;
4662
4765
  this.effects.closeAdmission();
4663
4766
  this.#deliveryAbort.abort();
@@ -4705,6 +4808,45 @@ export class TelegramNotificationDaemon {
4705
4808
  this.running = false;
4706
4809
  }
4707
4810
 
4811
+ /** Doctor reservation hook: stop admitting new provider work without killing the owner. Synchronous fence; returns a real acknowledgement. */
4812
+ prepareDoctorRestart(): boolean {
4813
+ this.effects.closeAdmission();
4814
+ return !this.effects.admitting;
4815
+ }
4816
+
4817
+ /**
4818
+ * Real occupancy read from the live runtime structures the daemon actually
4819
+ * uses, not synthetic zeros. `inflight` is turns currently executing;
4820
+ * `inbound` is inbound work accepted but not yet resolved (pending /btw
4821
+ * turns, topic-adoption session_create dispatches in flight, and inbound
4822
+ * reaction acks not yet terminal); `outbound` is the real rate-limit pool
4823
+ * backlog plus terminal /btw deliveries still in flight; `cleanup` counts
4824
+ * only genuinely outstanding cleanup — non-completed cleanup receipts plus
4825
+ * live archive/compensation fences — never a completed receipt or an
4826
+ * unrelated rejected-topic notice timer.
4827
+ */
4828
+ doctorOccupancy(): DoctorDaemonOccupancy {
4829
+ const outstandingCleanupReceipts = [...this.#cleanupReceipts.values()].filter(
4830
+ receipt => receipt.state !== "completed",
4831
+ ).length;
4832
+ return {
4833
+ attached: this.sessions.size,
4834
+ inflight: this.busy.size,
4835
+ inbound:
4836
+ this.#pendingBtwTurns.size + this.#adoptionStartingTopics.size + this.dispatchState.inboundReactions.size,
4837
+ outbound: this.pool.pending + this.#btwTerminalDeliveries.size,
4838
+ cleanup: outstandingCleanupReceipts + this.archiveFlights.size + this.compensationFenceRetries.size,
4839
+ };
4840
+ }
4841
+
4842
+ doctorRestartReady(): boolean {
4843
+ return doctorDaemonOccupancySettled(this.doctorOccupancy());
4844
+ }
4845
+
4846
+ cancelDoctorRestart(): void {
4847
+ if (!this.stopRequested) this.effects.openAdmission();
4848
+ }
4849
+
4708
4850
  #lifecycleActor(): SessionLifecycleActor {
4709
4851
  return { id: String(this.opts.chatId), namespace: `telegram:${tokenFingerprint(this.opts.botToken)}` };
4710
4852
  }
@@ -8884,7 +9026,7 @@ export class TelegramNotificationDaemon {
8884
9026
  );
8885
9027
  }
8886
9028
  private submitPool(item: Parameters<RateLimitPool<TelegramQueuePayload>["submit"]>[0]): boolean {
8887
- if (this.stopRequested || this.effects.stopping) return false;
9029
+ if (this.stopRequested || this.effects.stopping || !this.effects.admitting) return false;
8888
9030
  this.pool.submit(item);
8889
9031
  return true;
8890
9032
  }
@@ -9832,7 +9974,7 @@ export class TelegramNotificationDaemon {
9832
9974
  // and the next interval republishes (#4200). Only a proven
9833
9975
  // ownership loss stops the owner.
9834
9976
  if ((await this.renewOwnershipHeartbeat()) === "not_owner") {
9835
- this.runtime.requestStop();
9977
+ this.requestStop("ownership_loss");
9836
9978
  return;
9837
9979
  }
9838
9980
  await this.renewActiveTopicLeases();
@@ -11949,6 +12091,16 @@ export class TelegramNotificationDaemon {
11949
12091
  if (this.validationMode() || !this.#topicRegistryMutationAllowed()) return;
11950
12092
  if ((await this.handleForumTopicCreatedUpdate(update)) !== "not-topic") return;
11951
12093
  if ((await this.handleForumTopicEdited(update)) !== "not-topic") return;
12094
+ // Doctor admission fence: forum-topic bookkeeping above is structural
12095
+ // registry housekeeping for topics that already exist, not new user-facing
12096
+ // work, so it stays live. Everything below this line admits genuinely NEW
12097
+ // inbound work — adoption/session_create, /session_* lifecycle commands,
12098
+ // /btw turns, model-picker taps, and inline session control dispatch — and
12099
+ // MUST be refused while the owner has closed admission for a doctor
12100
+ // restart. Already-active turns and the outbound queue are untouched here:
12101
+ // they drain through their own existing paths (RateLimitPool.drain,
12102
+ // in-flight session turns), never invalidated by this refusal.
12103
+ if (!this.effects.admitting) return;
11952
12104
  // A raw path is accepted only after the explicit direct-entry choice. The exact
11953
12105
  // `/session_create path <dir>` form remains available in any pending topic.
11954
12106
  // Both must precede the general lifecycle handler, which would otherwise create
@@ -12803,7 +12955,10 @@ export class TelegramNotificationDaemon {
12803
12955
  `notifications: ownership heartbeat renewal threw; continuing: ${sanitizeDiagnostic(String(error))}`,
12804
12956
  );
12805
12957
  }
12806
- if (!ownerHeld) break;
12958
+ if (!ownerHeld) {
12959
+ this.requestStop("ownership_loss");
12960
+ break;
12961
+ }
12807
12962
  if (await this.controlStopRequested()) break;
12808
12963
  const idleElapsed = this.runtime.now() - idleSince >= (this.opts.idleTimeoutMs ?? 60_000);
12809
12964
  if (this.sessions.size > 0) {
@@ -12811,6 +12966,7 @@ export class TelegramNotificationDaemon {
12811
12966
  } else if (idleElapsed) {
12812
12967
  // Zero sessions past the idle window: exit so the owner does not run
12813
12968
  // forever. An active session resets the idle window above.
12969
+ this.requestStop("idle_timeout");
12814
12970
  break;
12815
12971
  }
12816
12972
  // Poll getUpdates whenever the daemon owns the token — even with zero
@@ -12836,8 +12992,12 @@ export class TelegramNotificationDaemon {
12836
12992
  if (await this.controlStopRequested()) break;
12837
12993
  await this.runtime.sleep(10);
12838
12994
  }
12995
+ } catch (error) {
12996
+ this.requestStop("unexpected_exception");
12997
+ throw error;
12839
12998
  } finally {
12840
- this.requestStop("stop");
12999
+ this.requestStop();
13000
+ await this.#stopCauseResolution;
12841
13001
  this.running = false;
12842
13002
  await this.#attachmentRouter
12843
13003
  .stop()
@@ -12911,6 +13071,7 @@ export class TelegramNotificationDaemon {
12911
13071
  pidIncarnation: this.opts.pidIncarnation,
12912
13072
  fs: this.fsImpl,
12913
13073
  now: this.opts.now,
13074
+ stopCause: this.#stopCause,
12914
13075
  });
12915
13076
  }
12916
13077
  }
@@ -12922,7 +13083,11 @@ export class TelegramNotificationDaemon {
12922
13083
  if (this.runtime.stopRequested) return true;
12923
13084
  if (!this.opts.control) return false;
12924
13085
  try {
12925
- return await this.opts.control.shouldStop(this.opts.ownerId);
13086
+ const requested = await this.opts.control.shouldStop(this.opts.ownerId);
13087
+ if (!requested) return false;
13088
+ this.#resolveControlStopCause("stop");
13089
+ this.requestStop();
13090
+ return true;
12926
13091
  } catch {
12927
13092
  return false;
12928
13093
  }
@@ -86,6 +86,7 @@ export interface SdkSessionCliArgs {
86
86
  strict?: boolean;
87
87
  untilIdle?: boolean;
88
88
  allEvents?: boolean;
89
+ page?: boolean;
89
90
  repo?: string;
90
91
  scope?: string;
91
92
  limit?: number;
@@ -95,7 +96,7 @@ export interface SdkSessionCliArgs {
95
96
  }
96
97
 
97
98
  type JsonRecord = Record<string, unknown>;
98
- type LifecycleMutationOperation = Exclude<SessionLifecycleOperation, "session.list">;
99
+ type LifecycleMutationOperation = Exclude<SessionLifecycleOperation, "session.lookup" | "session.list">;
99
100
  type TailExitReason = "idle" | "close";
100
101
  export interface RetainedTranscriptTailReader {
101
102
  readonly size: number;
@@ -278,15 +279,27 @@ function reportRouterCleanupFailure(error: unknown): void {
278
279
  process.stderr.write(`SDK session Router cleanup failed: ${message}\n`);
279
280
  }
280
281
 
282
+ /**
283
+ * Sessions a CLI-opened Router is allowed to attach to. Required, and required
284
+ * to be exhaustive: attaching is not free and is not invisible, because every
285
+ * attachment registers this process as a live client on that host and renews
286
+ * the host's abandonment window. An omitted scope used to mean "the whole
287
+ * fleet", so a single-session command silently kept every unrelated host alive.
288
+ *
289
+ * `BROKER_ONLY` is the empty scope, for work the Broker answers over its own
290
+ * client (`session.list`) and which therefore needs no session transport at all.
291
+ */
292
+ const BROKER_ONLY: readonly string[] = [];
293
+
281
294
  async function withRouter<T>(
282
295
  agentDir: string,
296
+ sessionIds: readonly string[],
283
297
  action: (router: SessionRouter) => Promise<T>,
284
298
  onFrame?: (attachment: SessionAttachment, frame: SessionRouterFrame) => void,
285
- sessionIds?: readonly string[],
286
299
  ): Promise<T> {
287
300
  const router = new SessionRouter({
288
301
  agentDir,
289
- ...(sessionIds === undefined ? {} : { sessionIds }),
302
+ sessionIds,
290
303
  ...(onFrame === undefined ? {} : { deps: { onFrame } }),
291
304
  });
292
305
  let result!: T;
@@ -370,6 +383,36 @@ async function paginatedSessionList(
370
383
  }
371
384
  }
372
385
 
386
+ async function singleSessionListPage(
387
+ router: SessionRouter,
388
+ input: JsonRecord = {},
389
+ requestKey = `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.list:${randomBytes(12).toString("hex")}`,
390
+ ): Promise<unknown> {
391
+ try {
392
+ const cursor = typeof input.cursor === "string" ? input.cursor : "initial";
393
+ const response = object(await router.listBrokerSessions(input, `${requestKey}:${cursor}`));
394
+ if (response?.ok === false) {
395
+ const failure = object(response.error);
396
+ throw new SdkClientError(
397
+ typeof failure?.code === "string" ? failure.code : "broker_error",
398
+ typeof failure?.message === "string" ? failure.message : "session.list failed",
399
+ );
400
+ }
401
+ const page = sessionListPageFromResponse(response);
402
+ const continuationCursor = page?.continuationCursor;
403
+ if (
404
+ !page ||
405
+ (continuationCursor !== undefined &&
406
+ (typeof continuationCursor !== "string" || continuationCursor.length === 0))
407
+ )
408
+ throw new SessionListTraversalError("malformed_page");
409
+ return response;
410
+ } catch (error) {
411
+ if (error instanceof SessionListTraversalError) throw new SdkClientError("protocol_error", error.message);
412
+ throw error;
413
+ }
414
+ }
415
+
373
416
  type SessionRows = {
374
417
  indexSeq?: number;
375
418
  warnings: unknown[];
@@ -378,7 +421,12 @@ type SessionRows = {
378
421
 
379
422
  async function sessionRows(agentDir: string, input: JsonRecord = {}): Promise<SessionRows> {
380
423
  await ensureBroker({ agentDir });
381
- return await withRouter(agentDir, async router => {
424
+ // `session.list` is answered by the Broker over its own client, so resolving a
425
+ // row needs no session transport. `runInspect` and `runTail` resolve exactly
426
+ // one session through here, and an unscoped lookup attached every live
427
+ // session in the directory before the scoped operation that follows it ever
428
+ // ran — renewing the abandonment window of hosts the caller never named.
429
+ return await withRouter(agentDir, BROKER_ONLY, async router => {
382
430
  const response = await paginatedSessionList(router, input);
383
431
  const result = resultObject(response) ?? {};
384
432
  let sessions: SdkSessionRowV1[];
@@ -433,9 +481,8 @@ async function probeSearchRows(agentDir: string, result: SdkSearchResultV1): Pro
433
481
  try {
434
482
  const probes = await withRouter(
435
483
  agentDir,
436
- async router => await Promise.all(rows.map(row => searchProbe(row, router))),
437
- undefined,
438
484
  rows.map(row => row.id),
485
+ async router => await Promise.all(rows.map(row => searchProbe(row, router))),
439
486
  );
440
487
  return { ...result, rows: mergeProbedSearchRows(result.rows, probes) };
441
488
  } catch {
@@ -913,7 +960,7 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
913
960
  if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
914
961
  await ensureBroker({ agentDir });
915
962
 
916
- return await withRouter(agentDir, async router => {
963
+ return await withRouter(agentDir, [sessionId], async router => {
917
964
  const response = await requestControl(router, sessionId, "turn.prompt", promptInput, args);
918
965
  const result: JsonRecord = {
919
966
  version: SESSION_ROWS_VERSION,
@@ -945,7 +992,7 @@ async function runStatus(
945
992
  ): Promise<unknown> {
946
993
  assertClientRef(opRef);
947
994
  await ensureBroker({ agentDir });
948
- return await withRouter(agentDir, async router => {
995
+ return await withRouter(agentDir, [sessionId], async router => {
949
996
  const response = await requestQuery(router, sessionId, "turn.result", { kind: "prompt", clientRef: opRef }, args);
950
997
  const status = resultObject(response) ?? {};
951
998
  const raw = typeof status.status === "string" ? status.status : "unknown";
@@ -1508,6 +1555,7 @@ async function runLiveTail(
1508
1555
 
1509
1556
  return await withRouter(
1510
1557
  agentDir,
1558
+ [sessionId],
1511
1559
  async router => {
1512
1560
  const attachment = attachmentFor(router, sessionId);
1513
1561
  const checkpointResponse = await router.request(
@@ -1695,7 +1743,11 @@ async function runRawControl(
1695
1743
  await ensureBroker({ agentDir });
1696
1744
  const operatorRequest = operatorAbortBrokerRequest(sessionId, operation, input, args);
1697
1745
  if (operatorRequest) return await requestBrokerOperatorAbort(agentDir, operatorRequest, args);
1698
- return await withRouter(agentDir, async router => await requestControl(router, sessionId, operation, input, args));
1746
+ return await withRouter(
1747
+ agentDir,
1748
+ [sessionId],
1749
+ async router => await requestControl(router, sessionId, operation, input, args),
1750
+ );
1699
1751
  }
1700
1752
 
1701
1753
  async function runRawQuery(
@@ -1706,7 +1758,11 @@ async function runRawQuery(
1706
1758
  args: SdkSessionCliArgs,
1707
1759
  ): Promise<unknown> {
1708
1760
  await ensureBroker({ agentDir });
1709
- return await withRouter(agentDir, async router => await requestQuery(router, sessionId, operation, input, args));
1761
+ return await withRouter(
1762
+ agentDir,
1763
+ [sessionId],
1764
+ async router => await requestQuery(router, sessionId, operation, input, args),
1765
+ );
1710
1766
  }
1711
1767
 
1712
1768
  function lifecycleMutationRequest(
@@ -1753,9 +1809,39 @@ async function runRawGlobal(
1753
1809
  input: JsonRecord,
1754
1810
  args: SdkSessionCliArgs,
1755
1811
  ): Promise<unknown> {
1812
+ if (args.page && operation !== "session.list")
1813
+ throw new SdkSessionCliError("usage", "--page is only available for raw global session.list.", 2);
1756
1814
  if (operation === "session.list") {
1757
1815
  await ensureBroker({ agentDir });
1758
- return await withRouter(agentDir, async router => await paginatedSessionList(router, input));
1816
+ const pageInput = { ...input };
1817
+ if (args.cursor !== undefined) {
1818
+ if (pageInput.cursor !== undefined && pageInput.cursor !== args.cursor)
1819
+ throw new SdkSessionCliError("usage", "--cursor must match the cursor in --json-input.", 2);
1820
+ pageInput.cursor = args.cursor;
1821
+ }
1822
+ if (args.limit !== undefined) {
1823
+ if (pageInput.limit !== undefined && pageInput.limit !== args.limit)
1824
+ throw new SdkSessionCliError("usage", "--limit must match the limit in --json-input.", 2);
1825
+ pageInput.limit = args.limit;
1826
+ }
1827
+ return await withRouter(
1828
+ agentDir,
1829
+ BROKER_ONLY,
1830
+ async router =>
1831
+ await (args.page ? singleSessionListPage(router, pageInput) : paginatedSessionList(router, pageInput)),
1832
+ );
1833
+ }
1834
+ if (operation === "session.lookup") {
1835
+ if (!args.idempotencyKey)
1836
+ throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for session lookup.", 2);
1837
+ const lifecycle = createBrokerSessionLifecycleService(agentDir);
1838
+ return await lifecycle.lookup({
1839
+ actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
1840
+ capability: "session.lookup",
1841
+ operation: "session.create",
1842
+ requestKey: args.idempotencyKey,
1843
+ target: input,
1844
+ });
1759
1845
  }
1760
1846
  if (!isLifecycleOperation(operation))
1761
1847
  throw new SdkSessionCliError("unknown_operation", `Unknown global operation: ${operation}`, 1);
@@ -1884,7 +1970,9 @@ export async function runSdkSessionCli(
1884
1970
  const secretError = validateAdapterSecretFields(operation, input);
1885
1971
  if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
1886
1972
  if (kind === "global") {
1887
- writeOutput(stripSecretFields(await runRawGlobal(agentDir, operation, input, args)));
1973
+ const result = await runRawGlobal(agentDir, operation, input, args);
1974
+ writeOutput(stripSecretFields(result));
1975
+ if (isRecord(result) && result.ok === false) setExitCode(1);
1888
1976
  return;
1889
1977
  }
1890
1978
  const sessionId = requireValue(args.sessionId, "<sessionId>");
@@ -1,6 +1,6 @@
1
1
  import { logger } from "@gajae-code/utils";
2
2
  import { AUTOROUTING_INACTIVE_WARNING } from "../../config/autorouting-contract";
3
- import { FileLockAcquireError } from "../../config/file-lock";
3
+ import { isFileLockAcquireTimeout } from "../../config/file-lock";
4
4
  import { redactBrokerRuntimeCapabilities, redactObservedRequestContent } from "./control/runtime-gate";
5
5
  import { type EventFrame, SessionEventStream } from "./events";
6
6
  import { isAutoroutingInactive } from "./internal-autorouting-state";
@@ -395,12 +395,7 @@ export class SessionSdkHost {
395
395
  // the durable row in place is fail-closed because peers still fence
396
396
  // authority on process liveness and incarnation, while propagating the
397
397
  // contention error turns ordinary shutdown into an uncaught failure.
398
- if (
399
- !options.allowLockContention ||
400
- !(error instanceof FileLockAcquireError) ||
401
- error.code !== "acquire_timeout"
402
- )
403
- throw error;
398
+ if (!options.allowLockContention || !isFileLockAcquireTimeout(error)) throw error;
404
399
  logger.warn("sdk broker unregister deferred because the session index is busy", {
405
400
  holder: error.holder,
406
401
  lockPath: error.lockPath,