@gajae-code/coding-agent 0.13.2 → 0.14.0

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 (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -1,21 +1,22 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
1
+ import { randomBytes } from "node:crypto";
2
+ import * as fsSync from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
- import * as path from "node:path";
4
- import { createInterface } from "node:readline/promises";
5
4
  import { getAgentDir } from "@gajae-code/utils";
6
5
  import { ensureBroker } from "../broker/ensure";
7
6
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
8
- import {
9
- readSdkBrokerDiscovery,
10
- readSdkSessionEndpoint,
11
- SdkClient,
12
- SdkClientError,
13
- SdkDiscoveryError,
14
- type SdkSentRecord,
15
- } from "../client";
16
- import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status.js";
17
- import { validateAdapterControl } from "../protocol/adapter-validation";
7
+ import { SdkClientError } from "../client";
8
+ import { createBrokerSessionLifecycleService } from "../lifecycle/broker-client";
9
+ import type {
10
+ SessionLifecycleMutationRequest,
11
+ SessionLifecycleOperation,
12
+ SessionLifecycleSavedSession,
13
+ SessionLifecycleSavedSessionIdentity,
14
+ } from "../lifecycle/service";
15
+ import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
16
+ import { validateAdapterControl, validateAdapterSecretFields } from "../protocol/adapter-validation";
18
17
  import { adapterDispositionError, findOperation, type OperationKind } from "../protocol/operation-registry";
18
+ import { type SessionAttachment, SessionRouter, SessionRouterError, type SessionRouterFrame } from "../router";
19
+ import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../session-list";
19
20
  import {
20
21
  type SdkCheckpointRecordV1,
21
22
  type SdkRetentionGapV1,
@@ -24,54 +25,79 @@ import {
24
25
  SESSION_ROWS_VERSION,
25
26
  stripSecretFields,
26
27
  toCheckpointRecordV1,
27
- toOfflineSessionRowV1,
28
28
  toRetentionGapV1,
29
29
  toSessionRowV1,
30
30
  toTailItemV1,
31
31
  } from "./rows";
32
32
 
33
- export type SdkSessionCliAction = "list" | "inspect" | "send" | "status" | "tail" | "elevate" | "raw";
33
+ export type SdkSessionCliAction =
34
+ | "list"
35
+ | "inspect"
36
+ | "send"
37
+ | "status"
38
+ | "tail"
39
+ | "raw"
40
+ | "control"
41
+ | "query"
42
+ | "global";
34
43
  export type SdkSessionCliRawKind = "control" | "query" | "global";
35
44
 
36
45
  export interface SdkSessionCliArgs {
37
46
  action?: string;
38
- /** Raw dispatch kind when action is "raw" (control | query | global). */
39
47
  rawAction?: string;
40
48
  sessionId?: string;
41
- /** Caller-chosen operation reference (ULID) for `send`; required target for `status`. */
42
49
  opRef?: string;
43
50
  operation?: string;
44
51
  query?: string;
45
- /** Convenience `send` body: forwarded as the `text` field of turn.prompt. */
46
52
  text?: string;
47
53
  jsonInput?: string;
48
54
  jsonInputFile?: string;
49
55
  jsonInputStdin?: boolean;
50
56
  idempotencyKey?: string;
51
- elevationRequestId?: string;
52
- operationKind?: OperationKind;
53
57
  confirm?: boolean;
54
- /** Raw query continuation cursor, or a saved checkpoint token to resume `tail` from (re-minted per connection). */
55
58
  cursor?: string;
56
- showEndpointCredential?: boolean;
57
- yes?: boolean;
58
- /** `send --wait`: poll turn.prompt_status until terminal or the wait window elapses. */
59
59
  wait?: boolean;
60
- /** Bounded wait for `send --wait`, `status`, and live `tail` follow. */
61
60
  timeoutMs?: number;
62
- /** `tail --strict`: fail closed on retention gaps (exit 1 retention_gap). */
63
61
  strict?: boolean;
64
- /** `tail --until-idle`: exit once the observed event stream reaches a terminal turn state. */
65
62
  untilIdle?: boolean;
66
- /** `tail --all-events`: include every event-ring kind instead of the tail subset. */
67
63
  allEvents?: boolean;
68
64
  repo?: string;
69
65
  agentDir?: string;
70
66
  }
71
67
 
72
68
  type JsonRecord = Record<string, unknown>;
69
+ type LifecycleMutationOperation = Exclude<SessionLifecycleOperation, "session.list">;
70
+ type TailExitReason = "idle" | "close";
71
+ export interface RetainedTranscriptTailReader {
72
+ readonly size: number;
73
+ readRange(start: number, end: number): Promise<Uint8Array>;
74
+ }
75
+
73
76
  const SECRET_FIELD = /(?:secret|token|password|credential|authorization|api[_-]?key)/i;
74
- const MAX_SESSION_LIST_PAGES = 10_000;
77
+ const SDK_SESSION_CLI_LIFECYCLE_ACTOR = { id: "gjc-sdk-session-cli", namespace: "sdk:session-cli" } as const;
78
+ const ROUTER_START_TIMEOUT_MS = 10_000;
79
+ const ROUTER_STOP_TIMEOUT_MS = 5_000;
80
+ const TAIL_STATUS_POLL_MS = 100;
81
+ const TAIL_OFFLINE_MAX_ENTRIES = 200;
82
+ const TAIL_OFFLINE_SCAN_CHUNK_BYTES = 64 * 1024;
83
+ const TAIL_OFFLINE_MAX_SCAN_BYTES = 4 * 1024 * 1024;
84
+ const TAIL_OFFLINE_MAX_SCANNED_LINES = 4_096;
85
+ const TAIL_OFFLINE_MAX_LINE_BYTES = 256 * 1024;
86
+ const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });
87
+
88
+ const TERMINAL_TURN_KINDS = new Set(["turn_end", "agent_end", "agent_failed"]);
89
+ const CLOSE_EVENT_KINDS = new Set(["session_closed", "session_terminated"]);
90
+ const DEFAULT_TAIL_KINDS = new Set([
91
+ "session_ready",
92
+ "session_prepared",
93
+ "session_closed",
94
+ "session_terminated",
95
+ "turn_start",
96
+ "turn_end",
97
+ "agent_start",
98
+ "agent_end",
99
+ "agent_failed",
100
+ ]);
75
101
 
76
102
  class SdkSessionCliError extends Error {
77
103
  constructor(
@@ -84,17 +110,42 @@ class SdkSessionCliError extends Error {
84
110
  }
85
111
  }
86
112
 
113
+ class RetainedTranscriptTailError extends Error {
114
+ constructor(
115
+ readonly reason: "unavailable" | "corrupt" | "line_limit" | "scan_limit" | "changed",
116
+ message: string,
117
+ ) {
118
+ super(message);
119
+ }
120
+ }
121
+
87
122
  function writeJson(value: unknown): void {
88
123
  process.stdout.write(`${JSON.stringify(value)}\n`);
89
124
  }
90
125
 
126
+ function isRecord(value: unknown): value is JsonRecord {
127
+ return value !== null && typeof value === "object" && !Array.isArray(value);
128
+ }
129
+
130
+ function object(value: unknown): JsonRecord | undefined {
131
+ return isRecord(value) ? value : undefined;
132
+ }
133
+
134
+ function arrayOf(value: unknown): unknown[] {
135
+ return Array.isArray(value) ? value : [];
136
+ }
137
+
138
+ function resultObject(response: unknown): JsonRecord | undefined {
139
+ const record = object(response);
140
+ return object(record?.result);
141
+ }
142
+
91
143
  function parseInput(raw: string | undefined, source: string): JsonRecord {
92
144
  if (raw === undefined) return {};
93
145
  try {
94
146
  const value: unknown = JSON.parse(raw);
95
- if (!value || typeof value !== "object" || Array.isArray(value))
96
- throw new SdkSessionCliError("invalid_input", `${source} must be a JSON object.`, 2);
97
- return value as JsonRecord;
147
+ if (!isRecord(value)) throw new SdkSessionCliError("invalid_input", `${source} must be a JSON object.`, 2);
148
+ return value;
98
149
  } catch (error) {
99
150
  if (error instanceof SdkSessionCliError) throw error;
100
151
  throw new SdkSessionCliError("invalid_json", `${source} must contain valid JSON.`, 2);
@@ -103,14 +154,10 @@ function parseInput(raw: string | undefined, source: string): JsonRecord {
103
154
 
104
155
  function containsSecretField(value: unknown): boolean {
105
156
  if (Array.isArray(value)) return value.some(containsSecretField);
106
- if (!value || typeof value !== "object") return false;
157
+ if (!isRecord(value)) return false;
107
158
  return Object.entries(value).some(([key, nested]) => SECRET_FIELD.test(key) || containsSecretField(nested));
108
159
  }
109
160
 
110
- function object(value: unknown): JsonRecord | undefined {
111
- return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : undefined;
112
- }
113
-
114
161
  async function inputFromArgs(args: SdkSessionCliArgs): Promise<JsonRecord> {
115
162
  const sources = [
116
163
  args.jsonInput !== undefined,
@@ -155,12 +202,13 @@ function isEndpointOperation(operation: string): boolean {
155
202
  return operation === "session.get_endpoint";
156
203
  }
157
204
 
158
- function isCredentialBearingOperation(operation: string): boolean {
205
+ function isLifecycleOperation(operation: string): operation is LifecycleMutationOperation {
159
206
  return (
160
- isEndpointOperation(operation) ||
161
207
  operation === "session.create" ||
162
208
  operation === "session.fork" ||
163
- operation === "session.resume"
209
+ operation === "session.resume" ||
210
+ operation === "session.close" ||
211
+ operation === "session.delete"
164
212
  );
165
213
  }
166
214
 
@@ -171,156 +219,164 @@ function cliOperationError(kind: OperationKind, operation: string): { code: stri
171
219
  if (row?.adapterDispositions.daemonCli === "prohibited")
172
220
  return {
173
221
  code: error.code,
174
- message: `${operation} is unavailable through the ordinary CLI; provider mode is out of scope this phase.`,
222
+ message: `${operation} is unavailable through the SDK session CLI.`,
175
223
  };
176
224
  return error;
177
225
  }
178
226
 
179
- function isLifecycleOperation(operation: string): boolean {
180
- return (
181
- operation === "session.create" ||
182
- operation === "session.fork" ||
183
- operation === "session.resume" ||
184
- operation === "session.close" ||
185
- operation === "session.delete"
186
- );
187
- }
188
-
189
- async function confirmEndpointCredentialOutput(): Promise<boolean> {
190
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
227
+ async function bounded<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
228
+ let timer: NodeJS.Timeout | undefined;
229
+ const timeout = Promise.withResolvers<never>();
191
230
  try {
192
- return (await prompt.question("Print the endpoint credential to stdout? [y/N] ")).trim().toLowerCase() === "y";
231
+ timer = setTimeout(() => timeout.reject(new SdkClientError("timeout", message)), timeoutMs);
232
+ return await Promise.race([promise, timeout.promise]);
193
233
  } finally {
194
- prompt.close();
234
+ if (timer) clearTimeout(timer);
195
235
  }
196
236
  }
197
237
 
198
- async function connectBroker(agentDir: string): Promise<SdkClient> {
199
- await ensureBroker({ agentDir });
200
- const discovery = await readSdkBrokerDiscovery(agentDir);
201
- if (!discovery) throw new SdkSessionCliError("broker_unavailable", "SDK broker discovery is unavailable.", 1);
202
- return await SdkClient.connect(discovery.url, discovery.token);
238
+ function reportRouterCleanupFailure(error: unknown): void {
239
+ const message = error instanceof Error ? error.message : String(error);
240
+ process.stderr.write(`SDK session Router cleanup failed: ${message}\n`);
203
241
  }
204
242
 
205
- async function connectExistingBroker(agentDir: string): Promise<SdkClient> {
206
- const discovery = await readSdkBrokerDiscovery(agentDir);
207
- if (!discovery) throw new SdkSessionCliError("broker_unavailable", "SDK broker discovery is unavailable.", 1);
208
- return await SdkClient.connect(discovery.url, discovery.token);
243
+ async function withRouter<T>(
244
+ agentDir: string,
245
+ action: (router: SessionRouter) => Promise<T>,
246
+ onFrame?: (attachment: SessionAttachment, frame: SessionRouterFrame) => void,
247
+ ): Promise<T> {
248
+ const router = new SessionRouter({
249
+ agentDir,
250
+ ...(onFrame === undefined ? {} : { deps: { onFrame } }),
251
+ });
252
+ let result!: T;
253
+ let actionFailed = false;
254
+ let actionError: unknown;
255
+ try {
256
+ await bounded(router.start(), ROUTER_START_TIMEOUT_MS, "SDK session Router startup timed out.");
257
+ result = await action(router);
258
+ } catch (error) {
259
+ actionFailed = true;
260
+ actionError = error;
261
+ }
262
+ try {
263
+ await bounded(router.stop(), ROUTER_STOP_TIMEOUT_MS, "SDK session Router shutdown timed out.");
264
+ } catch (error) {
265
+ reportRouterCleanupFailure(error);
266
+ }
267
+ if (actionFailed) throw actionError;
268
+ return result;
209
269
  }
210
270
 
211
- function brokerAbsent(error: unknown): boolean {
212
- if (error instanceof SdkSessionCliError) return error.code === "broker_unavailable";
213
- const details = error instanceof SdkClientError ? error.details : error;
214
- const code = (details as { code?: unknown } | undefined)?.code;
215
- return (
216
- code === "ENOENT" ||
217
- code === "ECONNREFUSED" ||
218
- /(?:ENOENT|ECONNREFUSED|connection refused)/i.test(error instanceof Error ? error.message : "")
271
+ function attachmentFor(router: SessionRouter, sessionId: string): SessionAttachment {
272
+ const attachment = router.attachment(sessionId);
273
+ if (!attachment)
274
+ throw new SdkSessionCliError(
275
+ "session_unavailable",
276
+ `SDK session ${sessionId} is unavailable through the session Router.`,
277
+ 1,
278
+ );
279
+ return attachment;
280
+ }
281
+
282
+ function throwResponseFailure(response: unknown): void {
283
+ const record = object(response);
284
+ if (record?.ok !== false) return;
285
+ const failure = object(record.error);
286
+ throw new SdkSessionCliError(
287
+ typeof failure?.code === "string" ? failure.code : "unavailable",
288
+ typeof failure?.message === "string" ? failure.message : "SDK request failed.",
289
+ 1,
290
+ failure,
219
291
  );
220
292
  }
221
293
 
222
- async function paginatedSessionList(client: SdkClient, input: JsonRecord = {}): Promise<unknown> {
223
- const aggregate: JsonRecord = {};
224
- const sessions: unknown[] = [];
225
- let firstResponse: JsonRecord | undefined;
226
- let cursor: string | undefined;
227
- for (let pageCount = 0; pageCount < MAX_SESSION_LIST_PAGES; pageCount++) {
228
- const response = object(
229
- await client.global("session.list", { ...input, ...(cursor === undefined ? {} : { cursor }) }),
294
+ async function paginatedSessionList(
295
+ router: SessionRouter,
296
+ input: JsonRecord = {},
297
+ requestKey = `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.list`,
298
+ ): Promise<unknown> {
299
+ try {
300
+ const pages = await traverseSessionList(
301
+ input,
302
+ async pageInput => {
303
+ const response = object(await router.listBrokerSessions(pageInput, requestKey));
304
+ if (response?.ok === false) {
305
+ const failure = object(response.error);
306
+ throw new SdkClientError(
307
+ typeof failure?.code === "string" ? failure.code : "broker_error",
308
+ typeof failure?.message === "string" ? failure.message : "session.list failed",
309
+ );
310
+ }
311
+ return response;
312
+ },
313
+ response => sessionListPageFromResponse(response),
230
314
  );
231
- firstResponse ??= response;
232
- if (response?.ok === false) {
233
- const failure = object(response.error);
234
- throw new SdkClientError(
235
- typeof failure?.code === "string" ? failure.code : "broker_error",
236
- typeof failure?.message === "string" ? failure.message : "session.list failed",
237
- );
238
- }
239
- const listing = object(response?.result) ?? response;
240
- if (listing) {
241
- for (const [key, value] of Object.entries(listing)) {
315
+ const aggregate: JsonRecord = {};
316
+ const sessions: unknown[] = [];
317
+ for (const { page } of pages) {
318
+ for (const [key, value] of Object.entries(page)) {
242
319
  if (key !== "sessions" && key !== "continuationCursor") aggregate[key] = value;
243
320
  }
244
- if (Array.isArray(listing.sessions)) sessions.push(...listing.sessions);
245
- const nextCursor =
246
- typeof listing.continuationCursor === "string" && listing.continuationCursor.length > 0
247
- ? listing.continuationCursor
248
- : undefined;
249
- if (nextCursor) {
250
- cursor = nextCursor;
251
- continue;
252
- }
321
+ sessions.push(...page.sessions);
253
322
  }
254
323
  const result = { ...aggregate, sessions };
255
- return firstResponse && "result" in firstResponse ? { ...firstResponse, result } : result;
256
- }
257
- throw new SdkClientError("protocol_error", "session.list exceeded the page budget.");
258
- }
259
-
260
- /**
261
- * Broker-bound session endpoint resolution (C10): the broker validates the
262
- * indexed session against its durable endpoint record and returns the
263
- * credential, which the CLI uses to connect and never renders. Both clients
264
- * (broker + session) are returned so callers close exactly what they opened.
265
- */
266
- async function connectSessionViaBroker(
267
- agentDir: string,
268
- sessionId: string,
269
- ): Promise<{ broker: SdkClient; session: SdkClient; endpoint: unknown }> {
270
- const broker = await connectBroker(agentDir);
271
- try {
272
- const response = await broker.global("session.get_endpoint", { sessionId });
273
- const endpoint = (response as { result?: unknown } | undefined)?.result;
274
- const url = (endpoint as { url?: unknown } | undefined)?.url;
275
- const token = (endpoint as { token?: unknown } | undefined)?.token;
276
- if (typeof url !== "string" || !url || typeof token !== "string")
277
- throw new SdkSessionCliError(
278
- "session_unavailable",
279
- `SDK endpoint for session ${sessionId} is unavailable.`,
280
- 1,
281
- );
282
- const session = await SdkClient.connect(url, token);
283
- return { broker, session, endpoint };
324
+ const firstResponse = pages[0]?.response;
325
+ return firstResponse && Object.hasOwn(firstResponse, "result") ? { ...firstResponse, result } : result;
284
326
  } catch (error) {
285
- await broker.close();
327
+ if (error instanceof SessionListTraversalError) throw new SdkClientError("protocol_error", error.message);
286
328
  throw error;
287
329
  }
288
330
  }
289
331
 
290
- function resultObject(response: unknown): Record<string, unknown> | undefined {
291
- const result = (response as { result?: unknown } | undefined)?.result;
292
- return result && typeof result === "object" && !Array.isArray(result)
293
- ? (result as Record<string, unknown>)
294
- : undefined;
332
+ type SessionRows = {
333
+ indexSeq?: number;
334
+ warnings: unknown[];
335
+ sessions: SdkSessionRowV1[];
336
+ };
337
+
338
+ async function sessionRows(agentDir: string): Promise<SessionRows> {
339
+ await ensureBroker({ agentDir });
340
+ return await withRouter(agentDir, async router => {
341
+ const response = await paginatedSessionList(router);
342
+ const result = resultObject(response) ?? {};
343
+ let sessions: SdkSessionRowV1[];
344
+ try {
345
+ sessions = arrayOf(result.sessions).map(toSessionRowV1);
346
+ } catch {
347
+ throw new SdkClientError("protocol_error", "session.list returned a malformed session row.");
348
+ }
349
+ return {
350
+ ...(typeof result.indexSeq === "number" ? { indexSeq: result.indexSeq } : {}),
351
+ warnings: arrayOf(result.warnings),
352
+ sessions,
353
+ };
354
+ });
295
355
  }
296
356
 
297
- function arrayOf(value: unknown): unknown[] {
298
- return Array.isArray(value) ? value : [];
357
+ async function runList(agentDir: string): Promise<unknown> {
358
+ const listing = await sessionRows(agentDir);
359
+ return {
360
+ ok: true,
361
+ result: {
362
+ version: SESSION_ROWS_VERSION,
363
+ source: "broker",
364
+ ...(listing.indexSeq === undefined ? {} : { indexSeq: listing.indexSeq }),
365
+ sessions: listing.sessions,
366
+ warnings: listing.warnings,
367
+ },
368
+ };
299
369
  }
300
370
 
301
- const TERMINAL_TURN_KINDS = new Set(["turn_end", "agent_end", "agent_failed"]);
302
- const CLOSE_EVENT_KINDS = new Set(["session_closed", "session_terminated"]);
303
- /** Default tail emission subset; `--all-events` widens to every ring kind. */
304
- const DEFAULT_TAIL_KINDS = new Set([
305
- "session_ready",
306
- "session_prepared",
307
- "session_closed",
308
- "session_terminated",
309
- "turn_start",
310
- "turn_end",
311
- "agent_start",
312
- "agent_end",
313
- "agent_failed",
314
- ]);
315
- const TAIL_OFFLINE_MAX_ENTRIES = 200;
316
- const TAIL_STATUS_POLL_MS = 100;
371
+ async function runInspect(agentDir: string, sessionId: string): Promise<unknown> {
372
+ const listing = await sessionRows(agentDir);
373
+ const session = listing.sessions.find(candidate => candidate.sessionId === sessionId);
374
+ if (!session)
375
+ throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
376
+ return { ok: true, result: { version: SESSION_ROWS_VERSION, source: "broker", session } };
377
+ }
317
378
 
318
- /**
319
- * Time-ordered operation reference (ULID): 48-bit millisecond timestamp plus
320
- * 80 bits of randomness, Crockford base32, lowercase. The ref is used as the
321
- * `turn.prompt` clientRef so `send`/`status` correlate without a broker round
322
- * trip, and matches the plan's durable op-ref ledger shape (C7).
323
- */
379
+ /** Creates a lowercase ULID operation reference for prompt reconciliation. */
324
380
  export function createOperationRef(now: number = Date.now()): string {
325
381
  const crockford = "0123456789abcdefghjkmnpqrstvwxyz";
326
382
  let random = 0n;
@@ -332,8 +388,7 @@ export function createOperationRef(now: number = Date.now()): string {
332
388
  }
333
389
 
334
390
  function clientRefFromInput(input: JsonRecord): string | undefined {
335
- const raw = typeof input.clientRef === "string" ? input.clientRef.trim() : undefined;
336
- return raw;
391
+ return typeof input.clientRef === "string" ? input.clientRef.trim() : undefined;
337
392
  }
338
393
 
339
394
  function assertClientRef(clientRef: string): void {
@@ -345,122 +400,131 @@ function assertClientRef(clientRef: string): void {
345
400
  );
346
401
  }
347
402
 
348
- function isRecord(value: unknown): value is Record<string, unknown> {
349
- return typeof value === "object" && value !== null && !Array.isArray(value);
350
- }
351
-
352
- /** Renders the broker `session.list` result as a versioned, credential-free DTO. */
353
- async function runList(agentDir: string): Promise<unknown> {
354
- const client = await connectBroker(agentDir);
355
- try {
356
- const response = await paginatedSessionList(client);
357
- const result = resultObject(response) ?? {};
358
- const sessions = arrayOf(result.sessions).map(toSessionRowV1);
359
- return {
360
- ok: true,
361
- result: {
362
- version: SESSION_ROWS_VERSION,
363
- source: "broker",
364
- indexSeq: typeof result.indexSeq === "number" ? result.indexSeq : undefined,
365
- sessions,
366
- warnings: arrayOf(result.warnings),
367
- },
368
- };
369
- } finally {
370
- await client.close();
371
- }
403
+ async function requestControl(
404
+ router: SessionRouter,
405
+ sessionId: string,
406
+ operation: string,
407
+ input: JsonRecord,
408
+ args: SdkSessionCliArgs,
409
+ ): Promise<JsonRecord> {
410
+ const attachment = attachmentFor(router, sessionId);
411
+ const response = await router.request(
412
+ sessionId,
413
+ { type: "control_request", operation, input, confirm: args.confirm === true },
414
+ attachment.generation,
415
+ attachment,
416
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
417
+ );
418
+ throwResponseFailure(response);
419
+ return response;
372
420
  }
373
421
 
374
- async function runInspect(repo: string, agentDir: string, sessionId: string): Promise<unknown> {
375
- try {
376
- const client = await connectExistingBroker(agentDir);
377
- try {
378
- const response = await paginatedSessionList(client);
379
- const sessions = arrayOf(resultObject(response)?.sessions).map(toSessionRowV1);
380
- const row = sessions.find(candidate => candidate.sessionId === sessionId);
381
- if (!row)
382
- throw new SdkSessionCliError(
383
- "session_unavailable",
384
- `Session ${sessionId} is not indexed by the broker.`,
385
- 1,
386
- );
387
- return { ok: true, result: { version: SESSION_ROWS_VERSION, source: "broker", session: row } };
388
- } finally {
389
- await client.close();
390
- }
391
- } catch (error) {
392
- // Broker absent: offline inspect-only (principle 1) from the local
393
- // endpoint discovery record, still projected without credentials.
394
- if (!brokerAbsent(error)) throw error;
395
- const endpoint = await readSdkSessionEndpoint(repo, sessionId);
396
- if (!endpoint)
397
- throw new SdkSessionCliError(
398
- "session_unavailable",
399
- `SDK endpoint for session ${sessionId} is unavailable.`,
400
- 1,
401
- );
402
- return {
403
- ok: true,
404
- result: { version: SESSION_ROWS_VERSION, source: "offline", session: toOfflineSessionRowV1(endpoint) },
405
- };
406
- }
422
+ async function requestQuery(
423
+ router: SessionRouter,
424
+ sessionId: string,
425
+ query: string,
426
+ input: JsonRecord,
427
+ args: SdkSessionCliArgs,
428
+ ): Promise<JsonRecord> {
429
+ const attachment = attachmentFor(router, sessionId);
430
+ const response = await router.request(
431
+ sessionId,
432
+ {
433
+ type: "query_request",
434
+ query,
435
+ input,
436
+ ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
437
+ },
438
+ attachment.generation,
439
+ attachment,
440
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
441
+ );
442
+ throwResponseFailure(response);
443
+ return response;
407
444
  }
408
445
 
409
- async function waitForTerminalStatus(
410
- session: SdkClient,
446
+ /** Exported for tests: each status poll must stay inside the caller's wait window. */
447
+ export async function waitForTerminalStatus(
448
+ router: SessionRouter,
449
+ sessionId: string,
411
450
  clientRef: string,
412
451
  timeoutMs: number | undefined,
413
452
  ): Promise<{ terminal: boolean; status: string; detail: unknown }> {
414
453
  const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
454
+ let status = "unknown";
455
+ let detail: unknown = {};
415
456
  for (;;) {
416
- const response = await session.query("turn.prompt_status", { clientRef });
417
- const result = resultObject(response);
418
- const status = typeof result?.status === "string" ? result.status : "unknown";
457
+ // Each poll is bounded by what is left of the caller's wait window, twice over.
458
+ // The request budget bounds the reply, and `bounded` bounds the whole poll: a
459
+ // disconnected transport reconnects before the reply timer is even installed,
460
+ // so without the outer bound the router's independent reconnect budget could
461
+ // still dispatch the query after the caller's window closed.
462
+ const remainingMs = deadline === undefined ? undefined : Math.max(1, deadline - Date.now());
463
+ const poll = requestQuery(
464
+ router,
465
+ sessionId,
466
+ "turn.result",
467
+ { kind: "prompt", clientRef },
468
+ remainingMs === undefined ? {} : { timeoutMs: remainingMs },
469
+ );
470
+ let response: JsonRecord;
471
+ try {
472
+ response =
473
+ remainingMs === undefined
474
+ ? await poll
475
+ : await bounded(poll, remainingMs, `Prompt ${clientRef} status poll exceeded the wait window.`);
476
+ } catch (error) {
477
+ // An elapsed wait window is the documented `wait_timeout` outcome with the
478
+ // last observed status, not a transport failure. Anything else — and any
479
+ // failure with window left — is a real error the caller must see.
480
+ if (deadline !== undefined && Date.now() >= deadline && isWaitWindowFailure(error))
481
+ return { terminal: false, status, detail };
482
+ throw error;
483
+ }
484
+ const result = resultObject(response) ?? {};
485
+ status = typeof result.status === "string" ? result.status : "unknown";
486
+ detail = result;
419
487
  if (status === "terminal_ok" || status === "failed") return { terminal: true, status, detail: result };
420
- if (deadline !== undefined && Date.now() >= deadline) return { terminal: false, status, detail: undefined };
488
+ if (deadline !== undefined && Date.now() >= deadline) return { terminal: false, status, detail: result };
421
489
  await Bun.sleep(TAIL_STATUS_POLL_MS);
422
490
  }
423
491
  }
424
492
 
493
+ /**
494
+ * True for the failures a poll produces when the wait window itself ends it:
495
+ * the outer bound, the request reply timer, and the uncertain send the transport
496
+ * reports when a request that was already on the wire is abandoned.
497
+ */
498
+ function isWaitWindowFailure(error: unknown): boolean {
499
+ if (error instanceof SdkClientError) return error.code === "timeout" || error.code === "uncertain_after_send";
500
+ return error instanceof SdkSessionCliError && error.code === "timeout";
501
+ }
502
+
425
503
  async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliArgs): Promise<unknown> {
426
- const generatedRef = createOperationRef();
427
504
  const input = await inputFromArgs(args);
428
505
  if (args.text !== undefined && Object.keys(input).length > 0)
429
506
  throw new SdkSessionCliError("usage", "Use either --text or one JSON input source for the prompt, not both.", 2);
430
- const promptInput: JsonRecord = args.text !== undefined ? { text: args.text } : { ...input };
507
+ const promptInput: JsonRecord = args.text === undefined ? { ...input } : { text: args.text };
431
508
  const inputRef = clientRefFromInput(promptInput);
432
- const clientRef = inputRef ?? args.opRef?.trim() ?? generatedRef;
509
+ const clientRef = inputRef ?? args.opRef?.trim() ?? createOperationRef();
433
510
  if (args.opRef !== undefined && inputRef !== undefined && inputRef !== args.opRef.trim())
434
511
  throw new SdkSessionCliError("usage", "--op-ref must match the clientRef in the JSON input.", 2);
435
512
  assertClientRef(clientRef);
436
513
  if (inputRef === undefined) promptInput.clientRef = clientRef;
437
514
  const invalid = validateAdapterControl("turn.prompt", promptInput);
438
515
  if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
439
- const { broker, session } = await connectSessionViaBroker(agentDir, sessionId);
440
- try {
441
- const response = await broker.global("session.control", {
442
- sessionId,
443
- operation: "turn.prompt",
444
- input: promptInput,
445
- confirm: args.confirm === true,
446
- });
447
- if (isRecord(response) && response.ok === false) {
448
- const responseError = isRecord(response.error) ? response.error : {};
449
- throw new SdkSessionCliError(
450
- typeof responseError.code === "string" ? responseError.code : "unavailable",
451
- typeof responseError.message === "string" ? responseError.message : "Prompt dispatch failed.",
452
- 1,
453
- );
454
- }
455
- const receipt = resultObject(response) ?? response;
516
+ await ensureBroker({ agentDir });
517
+
518
+ return await withRouter(agentDir, async router => {
519
+ const response = await requestControl(router, sessionId, "turn.prompt", promptInput, args);
456
520
  const result: JsonRecord = {
457
521
  version: SESSION_ROWS_VERSION,
458
522
  operationRef: clientRef,
459
523
  status: "accepted",
460
- receipt,
524
+ receipt: resultObject(response) ?? response,
461
525
  };
462
526
  if (args.wait === true) {
463
- const outcome = await waitForTerminalStatus(session, clientRef, args.timeoutMs ?? 30_000);
527
+ const outcome = await waitForTerminalStatus(router, sessionId, clientRef, args.timeoutMs ?? 30_000);
464
528
  if (!outcome.terminal)
465
529
  throw new SdkSessionCliError(
466
530
  "wait_timeout",
@@ -471,34 +535,32 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
471
535
  result.status = outcome.status;
472
536
  result.statusDetail = outcome.detail;
473
537
  }
474
- return { ok: true, result: stripSecretFields(result) };
475
- } finally {
476
- await session.close();
477
- await broker.close();
478
- }
538
+ return { ok: true, result };
539
+ });
479
540
  }
480
541
 
481
- async function runStatus(agentDir: string, sessionId: string, opRef: string): Promise<unknown> {
542
+ async function runStatus(
543
+ agentDir: string,
544
+ sessionId: string,
545
+ opRef: string,
546
+ args: SdkSessionCliArgs,
547
+ ): Promise<unknown> {
482
548
  assertClientRef(opRef);
483
- const { broker, session } = await connectSessionViaBroker(agentDir, sessionId);
484
- try {
485
- const response = await session.query("turn.prompt_status", { clientRef: opRef });
486
- const status = resultObject(response);
487
- const record = status ?? (isRecord(response) ? response : {});
488
- const raw = typeof record.status === "string" ? record.status : "unknown";
549
+ await ensureBroker({ agentDir });
550
+ return await withRouter(agentDir, async router => {
551
+ const response = await requestQuery(router, sessionId, "turn.result", { kind: "prompt", clientRef: opRef }, args);
552
+ const status = resultObject(response) ?? {};
553
+ const raw = typeof status.status === "string" ? status.status : "unknown";
489
554
  return {
490
555
  ok: true,
491
- result: stripSecretFields({
556
+ result: {
492
557
  version: SESSION_ROWS_VERSION,
493
558
  operationRef: opRef,
494
- status: record,
559
+ status,
495
560
  summary: { completed: raw === "terminal_ok" || raw === "failed" },
496
- }),
561
+ },
497
562
  };
498
- } finally {
499
- await session.close();
500
- await broker.close();
501
- }
563
+ });
502
564
  }
503
565
 
504
566
  type CheckpointExtraction = {
@@ -513,9 +575,6 @@ function extractCheckpoint(response: unknown): CheckpointExtraction {
513
575
  const gap = toRetentionGapV1(result.gap);
514
576
  if (gap !== undefined) return { gap };
515
577
  const record = toCheckpointRecordV1(result.checkpoint ?? result);
516
- // The Q30 result's `cursor`/`checkpointToken` is the signed checkpoint
517
- // cursor: `cursor` is the legacy alias kept for wire compatibility, and the
518
- // signed token is the resume artifact (`--cursor`) consumed by Q01.
519
578
  const cursor =
520
579
  typeof result.cursor === "string" && result.cursor
521
580
  ? result.cursor
@@ -526,8 +585,9 @@ function extractCheckpoint(response: unknown): CheckpointExtraction {
526
585
  }
527
586
 
528
587
  function extractTranscriptPage(response: unknown): { items: unknown[]; complete: boolean; cursor?: string } {
529
- const page = (response as { page?: unknown } | undefined)?.page;
530
- if (!isRecord(page)) return { items: arrayOf(resultObject(response)?.items), complete: true };
588
+ const record = object(response);
589
+ const page = object(record?.page) ?? object(resultObject(response)?.page);
590
+ if (!page) return { items: arrayOf(resultObject(response)?.items), complete: true };
531
591
  return {
532
592
  items: arrayOf(page.items),
533
593
  complete: page.complete === true,
@@ -535,20 +595,9 @@ function extractTranscriptPage(response: unknown): { items: unknown[]; complete:
535
595
  };
536
596
  }
537
597
 
538
- function eventKindOf(frame: Record<string, unknown>): string | undefined {
539
- if (typeof frame.kind === "string" && frame.kind) return frame.kind;
540
- if (typeof frame.name === "string" && frame.name) return frame.name;
541
- return undefined;
542
- }
543
-
544
598
  function tailItemKey(item: SdkTailItemV1): string {
545
- // Ring events carry a synchronous generation/seq identity; keying on it (not
546
- // kind+optional id) keeps repeated same-kind no-id events distinct and
547
- // dedupes the replay/live overlap exactly once.
548
599
  if (item.generation !== undefined && item.seq !== undefined)
549
600
  return `${item.kind}\u0000${item.generation}\u0000${item.seq}`;
550
- // Retained transcript entries have stable ids (or none); never silently drop
551
- // distinct identity-less items, so fall back to the payload itself.
552
601
  if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
553
602
  return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
554
603
  }
@@ -569,7 +618,7 @@ function mergeTailItems(
569
618
 
570
619
  function eventGapToRetentionGap(
571
620
  value: unknown,
572
- frame: Record<string, unknown>,
621
+ frame: JsonRecord,
573
622
  record: SdkCheckpointRecordV1 | undefined,
574
623
  ): SdkRetentionGapV1 | undefined {
575
624
  const existing = toRetentionGapV1(value);
@@ -592,325 +641,399 @@ function eventGapToRetentionGap(
592
641
  return undefined;
593
642
  }
594
643
 
595
- async function requestEventReplay(
596
- session: SdkClient,
597
- record: SdkCheckpointRecordV1 | undefined,
598
- timeoutMs: number,
599
- ): Promise<{ events: SdkTailItemV1[]; gap?: SdkRetentionGapV1; closed: boolean }> {
600
- const id = randomUUID();
601
- const { promise, resolve, reject } = Promise.withResolvers<{
602
- events: SdkTailItemV1[];
603
- gap?: SdkRetentionGapV1;
604
- closed: boolean;
605
- }>();
606
- const bufferedLiveEvents: SdkTailItemV1[] = [];
607
- const off = session.onFrame(frame => {
608
- if (frame.type === "event") {
609
- bufferedLiveEvents.push(toTailItemV1(frame, { kind: eventKindOf(frame) ?? "event" }));
610
- return;
611
- }
612
- if (frame.type !== "event_replay_result" || frame.id !== id) return;
613
- const replayFrame = frame as Record<string, unknown>;
614
- const events = [
615
- ...arrayOf(replayFrame.events).map(event => toTailItemV1(event, { kind: "event" })),
616
- ...bufferedLiveEvents,
617
- ];
618
- const gap = eventGapToRetentionGap(replayFrame.gap, replayFrame, record);
619
- resolve({ events, ...(gap !== undefined ? { gap } : {}), closed: false });
620
- });
621
- const offFailed = session.onReconnectFailed(() => resolve({ events: [], closed: true }));
622
- const timer = setTimeout(
623
- () => reject(new SdkSessionCliError("tail_timeout", "Timed out waiting for the event replay response.", 1)),
624
- timeoutMs,
644
+ function publicationSequence(publicationId: string): number | undefined {
645
+ const parts = publicationId.split(":");
646
+ const value = Number(parts.at(-1));
647
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
648
+ }
649
+
650
+ function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | undefined {
651
+ if (frame.publicationId === undefined || frame.name === undefined) return undefined;
652
+ const seq = publicationSequence(frame.publicationId);
653
+ return toTailItemV1(
654
+ {
655
+ kind: frame.name,
656
+ ...(frame.generation === undefined ? {} : { generation: frame.generation }),
657
+ ...(seq === undefined ? {} : { seq }),
658
+ payload: frame.body,
659
+ },
660
+ {
661
+ kind: frame.name,
662
+ ...(frame.generation === undefined ? {} : { generation: frame.generation }),
663
+ ...(seq === undefined ? {} : { seq }),
664
+ },
625
665
  );
626
- try {
627
- session.send({
628
- type: "event_replay",
629
- id,
630
- // Resume strictly after the checkpoint sequence: the checkpoint event
631
- // was already observed, so re-emitting it would let a terminal
632
- // checkpoint event satisfy --until-idle without any new turn.
633
- ...(record !== undefined ? { sinceGeneration: record.generation, sinceSeq: record.seq } : {}),
634
- });
635
- return await promise;
636
- } finally {
637
- clearTimeout(timer);
638
- off();
639
- offFailed();
640
- }
641
666
  }
642
667
 
643
- async function followLiveEvents(
644
- session: SdkClient,
645
- items: SdkTailItemV1[],
646
- seen: Set<string>,
647
- args: SdkSessionCliArgs,
648
- include: (kind: string) => boolean,
649
- timeoutMs: number,
650
- ): Promise<{ reason: "idle" | "close" | "timeout" | "reconnect" | "reconnect_failed" }> {
651
- const { promise, resolve } = Promise.withResolvers<{
652
- reason: "idle" | "close" | "timeout" | "reconnect" | "reconnect_failed";
653
- }>();
654
- const off = session.onFrame(frame => {
655
- if (!isRecord(frame) || frame.type !== "event") return;
656
- const kind = eventKindOf(frame);
657
- if (kind === undefined) return;
658
- const item = toTailItemV1(frame, { kind });
659
- const key = tailItemKey(item);
660
- if (!seen.has(key)) {
661
- seen.add(key);
662
- if (include(kind)) items.push(item);
663
- }
664
- if (TERMINAL_TURN_KINDS.has(kind) && args.untilIdle === true) resolve({ reason: "idle" });
665
- if (CLOSE_EVENT_KINDS.has(kind)) resolve({ reason: "close" });
666
- });
667
- const offClosed = session.onReconnectFailed(() => resolve({ reason: "reconnect_failed" }));
668
- const offReconnect = session.onReconnect(() => resolve({ reason: "reconnect" }));
669
- const timer = setTimeout(() => resolve({ reason: "timeout" }), timeoutMs);
668
+ function retainedTranscriptUnavailable(): RetainedTranscriptTailError {
669
+ return new RetainedTranscriptTailError("unavailable", "Retained transcript history is unavailable.");
670
+ }
671
+
672
+ function retainedTranscriptOpenFlags(): number {
673
+ const noFollow = process.platform === "win32" ? 0 : fsSync.constants.O_NOFOLLOW;
674
+ if (process.platform !== "win32" && !noFollow) throw retainedTranscriptUnavailable();
675
+ return (
676
+ fsSync.constants.O_RDONLY | noFollow | (process.platform === "win32" ? 0 : (fsSync.constants.O_NONBLOCK ?? 0))
677
+ );
678
+ }
679
+
680
+ function retainedTranscriptIdentityMismatch(): RetainedTranscriptTailError {
681
+ return new RetainedTranscriptTailError(
682
+ "changed",
683
+ "Retained transcript history no longer matches the Broker-selected identity; refusing to replay it.",
684
+ );
685
+ }
686
+
687
+ function matchesRetainedTranscriptIdentity(
688
+ identity: SessionLifecycleSavedSessionIdentity,
689
+ descriptor: fsSync.BigIntStats,
690
+ ): boolean {
670
691
  try {
671
- const result = await promise;
672
- return result;
673
- } finally {
674
- clearTimeout(timer);
675
- off();
676
- offClosed();
677
- offReconnect();
692
+ return (
693
+ descriptor.isFile() &&
694
+ descriptor.dev === BigInt(identity.dev) &&
695
+ descriptor.ino === BigInt(identity.ino) &&
696
+ descriptor.nlink === BigInt(identity.nlink) &&
697
+ descriptor.size === BigInt(identity.size) &&
698
+ descriptor.mtimeMs === BigInt(identity.mtimeMs) &&
699
+ descriptor.mtimeNs === BigInt(identity.mtimeNs) &&
700
+ descriptor.ctimeNs === BigInt(identity.ctimeNs)
701
+ );
702
+ } catch {
703
+ return false;
678
704
  }
679
705
  }
680
706
 
681
- /** Read-only retained-history replay for a stopped/terminal session (offline, no endpoint). */
682
- async function offlineTailReplay(
683
- repo: string,
684
- agentDir: string,
685
- sessionId: string,
686
- row: SdkSessionRowV1,
687
- ): Promise<unknown> {
688
- const broker = await connectBroker(agentDir);
689
- try {
690
- const response = await broker.global("session.list", { resolveSessionId: sessionId, cwd: repo });
691
- const savedSession = resultObject(response)?.savedSession;
692
- const savedPath = isRecord(savedSession) && typeof savedSession.path === "string" ? savedSession.path : undefined;
693
- if (!savedPath) {
694
- throw new SdkSessionCliError(
695
- "session_unavailable",
696
- `Session ${sessionId} is stopped and has no retained transcript replay.`,
697
- 1,
707
+ function retainedTranscriptCorrupt(): RetainedTranscriptTailError {
708
+ return new RetainedTranscriptTailError(
709
+ "corrupt",
710
+ "Retained transcript history contains unparseable entries; refusing to replay corrupted history.",
711
+ );
712
+ }
713
+
714
+ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailReader): Promise<unknown[]> {
715
+ if (!Number.isSafeInteger(reader.size) || reader.size < 0) throw retainedTranscriptUnavailable();
716
+ const entries: unknown[] = [];
717
+ let position = reader.size;
718
+ let scannedBytes = 0;
719
+ let scannedLines = 0;
720
+ let trailingFragment = new Uint8Array();
721
+ while (position > 0 && entries.length < TAIL_OFFLINE_MAX_ENTRIES) {
722
+ const remainingBytes = TAIL_OFFLINE_MAX_SCAN_BYTES - scannedBytes;
723
+ if (remainingBytes <= 0)
724
+ throw new RetainedTranscriptTailError(
725
+ "scan_limit",
726
+ "Retained transcript history exceeds the bounded tail replay limit.",
698
727
  );
728
+ const length = Math.min(TAIL_OFFLINE_SCAN_CHUNK_BYTES, position, remainingBytes);
729
+ const start = position - length;
730
+ let chunk: Uint8Array;
731
+ try {
732
+ chunk = await reader.readRange(start, position);
733
+ } catch {
734
+ throw retainedTranscriptUnavailable();
735
+ }
736
+ if (chunk.byteLength !== length) throw retainedTranscriptUnavailable();
737
+ scannedBytes += chunk.byteLength;
738
+ const combined = new Uint8Array(chunk.byteLength + trailingFragment.byteLength);
739
+ combined.set(chunk);
740
+ combined.set(trailingFragment, chunk.byteLength);
741
+
742
+ let complete = combined;
743
+ let partial = new Uint8Array();
744
+ if (start > 0) {
745
+ const firstNewline = combined.indexOf(0x0a);
746
+ if (firstNewline === -1) {
747
+ if (combined.byteLength > TAIL_OFFLINE_MAX_LINE_BYTES)
748
+ throw new RetainedTranscriptTailError(
749
+ "line_limit",
750
+ "Retained transcript history exceeds the bounded tail replay limit.",
751
+ );
752
+ trailingFragment = combined;
753
+ position = start;
754
+ continue;
755
+ }
756
+ partial = combined.subarray(0, firstNewline);
757
+ complete = combined.subarray(firstNewline + 1);
699
758
  }
700
- const text = await fs.readFile(savedPath, "utf8");
701
- const parsedEntries: unknown[] = [];
702
- const malformed: number[] = [];
703
- const lines = text.split("\n");
704
- for (let index = 0; index < lines.length; index++) {
705
- const line = lines[index]!;
706
- if (!line) continue;
759
+
760
+ let lineEnd = complete.byteLength;
761
+ while (lineEnd > 0 && entries.length < TAIL_OFFLINE_MAX_ENTRIES) {
762
+ const newline = complete.lastIndexOf(0x0a, lineEnd - 1);
763
+ const lineStart = newline < 0 ? 0 : newline + 1;
764
+ const line = complete.subarray(lineStart, lineEnd);
765
+ lineEnd = newline < 0 ? 0 : newline;
766
+ scannedLines++;
767
+ if (scannedLines > TAIL_OFFLINE_MAX_SCANNED_LINES)
768
+ throw new RetainedTranscriptTailError(
769
+ "scan_limit",
770
+ "Retained transcript history exceeds the bounded tail replay limit.",
771
+ );
772
+ if (line.byteLength === 0) continue;
773
+ if (line.byteLength > TAIL_OFFLINE_MAX_LINE_BYTES)
774
+ throw new RetainedTranscriptTailError(
775
+ "line_limit",
776
+ "Retained transcript history exceeds the bounded tail replay limit.",
777
+ );
707
778
  try {
708
- parsedEntries.push(JSON.parse(line));
779
+ entries.push(JSON.parse(transcriptDecoder.decode(line)));
709
780
  } catch {
710
- malformed.push(index);
781
+ throw retainedTranscriptCorrupt();
711
782
  }
712
783
  }
713
- // Retained-history corruption is never silently skipped: a torn or
714
- // unparseable line means the retained replay is not trustworthy, so tail
715
- // fails closed with a typed retention error instead of reporting success.
716
- if (malformed.length > 0)
717
- throw new SdkSessionCliError(
718
- "retention_gap",
719
- "Retained transcript history contains unparseable entries; refusing to replay corrupted history.",
720
- 1,
721
- { code: "retention_gap", lines: malformed },
784
+ if (entries.length === TAIL_OFFLINE_MAX_ENTRIES) break;
785
+ if (start > 0) {
786
+ if (partial.byteLength > TAIL_OFFLINE_MAX_LINE_BYTES)
787
+ throw new RetainedTranscriptTailError(
788
+ "line_limit",
789
+ "Retained transcript history exceeds the bounded tail replay limit.",
790
+ );
791
+ trailingFragment = partial;
792
+ }
793
+ position = start;
794
+ }
795
+ return entries.reverse();
796
+ }
797
+
798
+ async function readRetainedTranscriptTail(savedSession: SessionLifecycleSavedSession): Promise<unknown[]> {
799
+ let descriptor: fs.FileHandle | undefined;
800
+ try {
801
+ descriptor = await fs.open(savedSession.path, retainedTranscriptOpenFlags());
802
+ const before = await descriptor.stat({ bigint: true });
803
+ if (!matchesRetainedTranscriptIdentity(savedSession.identity, before)) throw retainedTranscriptIdentityMismatch();
804
+ if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) throw retainedTranscriptUnavailable();
805
+ // Bind every range read to the opened descriptor so a later pathname replacement
806
+ // cannot change the retained history selected by the lifecycle lookup.
807
+ const file = Bun.file(descriptor.fd);
808
+ const entries = await scanRetainedTranscriptTail({
809
+ size: Number(before.size),
810
+ readRange: async (start, end) => new Uint8Array(await file.slice(start, end).arrayBuffer()),
811
+ });
812
+ const after = await descriptor.stat({ bigint: true });
813
+ if (
814
+ !matchesRetainedTranscriptIdentity(savedSession.identity, after) ||
815
+ before.dev !== after.dev ||
816
+ before.ino !== after.ino ||
817
+ before.size !== after.size ||
818
+ before.mtimeNs !== after.mtimeNs ||
819
+ before.ctimeNs !== after.ctimeNs
820
+ )
821
+ throw new RetainedTranscriptTailError(
822
+ "changed",
823
+ "Retained transcript history changed while reading; refusing to replay it.",
722
824
  );
723
- const entries = parsedEntries.slice(-TAIL_OFFLINE_MAX_ENTRIES);
724
- const items = entries.map((entry, index) => toTailItemV1(entry, { kind: "transcript", seq: index }));
725
- return {
726
- ok: true,
727
- result: {
728
- version: SESSION_ROWS_VERSION,
729
- source: "offline",
730
- session: row,
731
- items,
732
- terminal: true,
733
- },
734
- };
825
+ return entries;
826
+ } catch (error) {
827
+ if (error instanceof RetainedTranscriptTailError) throw error;
828
+ throw retainedTranscriptUnavailable();
735
829
  } finally {
736
- await broker.close();
830
+ if (descriptor !== undefined) await descriptor.close().catch(() => {});
737
831
  }
738
832
  }
739
833
 
740
- export async function runTail(
834
+ async function offlineTailReplay(
741
835
  repo: string,
742
836
  agentDir: string,
743
837
  sessionId: string,
744
- args: SdkSessionCliArgs,
838
+ row: SdkSessionRowV1,
745
839
  ): Promise<unknown> {
746
- const broker = await connectBroker(agentDir);
747
- let row: SdkSessionRowV1 | undefined;
840
+ const lifecycle = createBrokerSessionLifecycleService(agentDir);
841
+ const outcome = await lifecycle.list({
842
+ actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
843
+ capability: "session.list",
844
+ target: { cwd: repo, resolveSessionId: sessionId },
845
+ });
846
+ if (!outcome.ok)
847
+ throw new SdkSessionCliError(outcome.error.code, outcome.error.message, 1, { certainty: outcome.certainty });
848
+ const savedSession = outcome.result.savedSession;
849
+ if (!savedSession)
850
+ throw new SdkSessionCliError(
851
+ "session_unavailable",
852
+ `Session ${sessionId} is stopped and has no retained transcript replay.`,
853
+ 1,
854
+ );
855
+ let entries: unknown[];
748
856
  try {
749
- const response = await paginatedSessionList(broker);
750
- row = arrayOf(resultObject(response)?.sessions ?? undefined)
751
- .map(toSessionRowV1)
752
- .find(candidate => candidate.sessionId === sessionId);
753
- } finally {
754
- await broker.close();
857
+ entries = await readRetainedTranscriptTail(savedSession);
858
+ } catch (error) {
859
+ const retained = error instanceof RetainedTranscriptTailError ? error : retainedTranscriptUnavailable();
860
+ throw new SdkSessionCliError("retention_gap", retained.message, 1, {
861
+ code: "retention_gap",
862
+ reason: retained.reason,
863
+ });
755
864
  }
756
- if (!row)
757
- throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
758
- if (row.deleted)
759
- throw new SdkSessionCliError("session_deleted", `Session ${sessionId} was deleted and has no tail.`, 1);
865
+ return {
866
+ ok: true,
867
+ result: {
868
+ version: SESSION_ROWS_VERSION,
869
+ source: "offline",
870
+ session: row,
871
+ items: entries.map((entry, index) => toTailItemV1(entry, { kind: "transcript", seq: index })),
872
+ terminal: true,
873
+ },
874
+ };
875
+ }
760
876
 
877
+ async function runLiveTail(
878
+ agentDir: string,
879
+ sessionId: string,
880
+ row: SdkSessionRowV1,
881
+ args: SdkSessionCliArgs,
882
+ ): Promise<unknown> {
761
883
  const include = (kind: string): boolean =>
762
884
  kind === "transcript" || args.allEvents === true || DEFAULT_TAIL_KINDS.has(kind);
763
885
  const items: SdkTailItemV1[] = [];
764
886
  const seen = new Set<string>();
765
887
  let checkpoint: SdkCheckpointRecordV1 | undefined;
766
888
  let gap: SdkRetentionGapV1 | undefined;
767
- const timeoutMs = args.timeoutMs ?? 10_000;
889
+ let liveReason: TailExitReason | undefined;
890
+ let resolveLive: ((reason: TailExitReason) => void) | undefined;
768
891
 
769
- let brokerClient: SdkClient | undefined;
770
- let session: SdkClient | undefined;
771
- try {
772
- try {
773
- const connected = await connectSessionViaBroker(agentDir, sessionId);
774
- brokerClient = connected.broker;
775
- session = connected.session;
776
- } catch (error) {
777
- if (row.live === false && brokerAbsent(error) === false)
778
- return await offlineTailReplay(repo, agentDir, sessionId, row);
779
- throw error;
892
+ const recordLiveFrame = (attachment: SessionAttachment, frame: SessionRouterFrame): void => {
893
+ if (attachment.sessionId !== sessionId) return;
894
+ const item = tailItemFromRouterFrame(frame);
895
+ if (!item) return;
896
+ mergeTailItems(items, seen, [item], include);
897
+ if (args.untilIdle === true && TERMINAL_TURN_KINDS.has(item.kind)) {
898
+ liveReason = "idle";
899
+ resolveLive?.("idle");
780
900
  }
781
-
782
- let cursor: string | undefined;
783
- // Q30 exchanges a still-valid signed checkpoint claim into a fresh
784
- // connection-owned pin; direct cross-connection cursor consumption remains rejected.
785
- const checkpointResponse = await session.query(
786
- "session.checkpoint",
787
- args.cursor === undefined ? {} : { checkpointToken: args.cursor },
788
- );
789
- const extraction = extractCheckpoint(checkpointResponse);
790
- checkpoint = extraction.record;
791
- gap = extraction.gap;
792
- cursor = extraction.cursor;
793
- if (gap !== undefined && args.strict === true)
794
- throw new SdkSessionCliError(
795
- "retention_gap",
796
- "Retained history is missing entries before the checkpoint (strict mode).",
797
- 1,
798
- gap,
799
- );
800
-
801
- // Continue paginating the retained transcript from the checkpoint cursor
802
- // (the resume page above may itself be incomplete).
803
- while (cursor !== undefined) {
804
- const page = extractTranscriptPage(await session.query("transcript.list", {}, cursor));
805
- mergeTailItems(
806
- items,
807
- seen,
808
- page.items.map(item => toTailItemV1(item, { kind: "transcript" })),
809
- include,
810
- );
811
- if (page.complete || !page.cursor) break;
812
- cursor = page.cursor;
901
+ if (CLOSE_EVENT_KINDS.has(item.kind)) {
902
+ liveReason = "close";
903
+ resolveLive?.("close");
813
904
  }
905
+ };
814
906
 
815
- // Stopped/terminal sessions replay retained history and exit; live
816
- // sessions follow the event ring from the checkpoint position. A
817
- // lifecycle-terminal row is treated as terminal for exit purposes even
818
- // when its process is still alive (uncertain stop), so `tail` never
819
- // hangs on a session whose lifecycle already ended.
820
- if (row.live === false || row.terminalUncertain === true) {
821
- return {
822
- ok: true,
823
- result: {
824
- version: SESSION_ROWS_VERSION,
825
- source: "session",
826
- session: row,
827
- ...(checkpoint !== undefined ? { checkpoint } : {}),
828
- ...(gap !== undefined ? { gap } : {}),
829
- items,
830
- terminal: true,
907
+ return await withRouter(
908
+ agentDir,
909
+ async router => {
910
+ const attachment = attachmentFor(router, sessionId);
911
+ const checkpointResponse = await router.request(
912
+ sessionId,
913
+ {
914
+ type: "query_request",
915
+ query: "session.checkpoint",
916
+ input: args.cursor === undefined ? {} : { checkpointToken: args.cursor },
831
917
  },
832
- };
833
- }
834
-
835
- let outcome: { reason: "idle" | "close" | "timeout" | "reconnect" | "reconnect_failed" } = { reason: "close" };
836
- for (let reconnectAttempt = 0; reconnectAttempt <= 3; reconnectAttempt++) {
837
- const replay = await requestEventReplay(session, checkpoint, timeoutMs);
838
- if (replay.closed)
918
+ attachment.generation,
919
+ attachment,
920
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
921
+ );
922
+ throwResponseFailure(checkpointResponse);
923
+ const extraction = extractCheckpoint(checkpointResponse);
924
+ checkpoint = extraction.record;
925
+ gap = extraction.gap;
926
+ if (gap !== undefined && args.strict === true)
839
927
  throw new SdkSessionCliError(
840
- "reconnect_failed",
841
- "Tail connection exhausted reconnect attempts before replay continuity was restored.",
928
+ "retention_gap",
929
+ "Retained history is missing entries before the checkpoint (strict mode).",
842
930
  1,
931
+ gap,
932
+ );
933
+
934
+ let cursor = extraction.cursor;
935
+ while (cursor !== undefined) {
936
+ const response = await router.request(
937
+ sessionId,
938
+ { type: "query_request", query: "transcript.list", input: {}, cursor },
939
+ attachment.generation,
940
+ attachment,
941
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
843
942
  );
844
- if (replay.gap !== undefined) {
845
- gap = replay.gap;
943
+ throwResponseFailure(response);
944
+ const page = extractTranscriptPage(response);
945
+ mergeTailItems(
946
+ items,
947
+ seen,
948
+ page.items.map(item => toTailItemV1(item, { kind: "transcript" })),
949
+ include,
950
+ );
951
+ if (page.complete || page.cursor === undefined) break;
952
+ cursor = page.cursor;
953
+ }
954
+
955
+ const replayResponse = await router.request(
956
+ sessionId,
957
+ {
958
+ type: "event_replay",
959
+ ...(checkpoint === undefined
960
+ ? {}
961
+ : { sinceGeneration: checkpoint.generation, sinceSeq: checkpoint.seq }),
962
+ },
963
+ attachment.generation,
964
+ attachment,
965
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
966
+ );
967
+ throwResponseFailure(replayResponse);
968
+ const replay = object(replayResponse) ?? {};
969
+ const replayGap = eventGapToRetentionGap(replay.gap, replay, checkpoint);
970
+ if (replayGap !== undefined) {
971
+ gap = replayGap;
846
972
  if (args.strict === true)
847
973
  throw new SdkSessionCliError(
848
974
  "retention_gap",
849
975
  "The event ring dropped entries before the checkpoint (strict mode).",
850
976
  1,
851
- replay.gap,
977
+ replayGap,
852
978
  );
853
979
  }
854
- mergeTailItems(items, seen, replay.events, include);
855
- const terminal = replay.events.some(item => TERMINAL_TURN_KINDS.has(item.kind)) && args.untilIdle === true;
856
- if (args.untilIdle === true && terminal) {
857
- return {
858
- ok: true,
859
- result: {
860
- version: SESSION_ROWS_VERSION,
861
- source: "session",
862
- session: row,
863
- ...(checkpoint !== undefined ? { checkpoint } : {}),
864
- items,
865
- terminal: true,
866
- },
867
- };
868
- }
869
- outcome = await followLiveEvents(session, items, seen, args, include, timeoutMs);
870
- if (outcome.reason !== "reconnect") break;
871
- if (reconnectAttempt === 3)
872
- throw new SdkSessionCliError(
873
- "reconnect_failed",
874
- "Tail could not restore replay continuity after reconnect.",
875
- 1,
980
+ const replayItems = arrayOf(replay.events).map(event => toTailItemV1(event, { kind: "event" }));
981
+ mergeTailItems(items, seen, replayItems, include);
982
+ if (args.untilIdle === true && replayItems.some(item => TERMINAL_TURN_KINDS.has(item.kind)))
983
+ liveReason = "idle";
984
+ if (liveReason === undefined) {
985
+ const completion = Promise.withResolvers<TailExitReason>();
986
+ resolveLive = completion.resolve;
987
+ const timeoutMs = args.timeoutMs ?? 10_000;
988
+ const timer = setTimeout(
989
+ () =>
990
+ completion.reject(
991
+ new SdkSessionCliError(
992
+ "tail_timeout",
993
+ "Tail did not reach an exit condition within the wait window.",
994
+ 1,
995
+ { sessionId, timeoutMs },
996
+ ),
997
+ ),
998
+ timeoutMs,
876
999
  );
877
- const lastEvent = [...items].reverse().find(item => item.generation !== undefined && item.seq !== undefined);
878
- if (lastEvent?.generation !== undefined && lastEvent.seq !== undefined)
879
- checkpoint = {
880
- revision: checkpoint?.revision ?? 0,
881
- generation: lastEvent.generation,
882
- seq: lastEvent.seq,
883
- };
884
- }
885
- if (outcome.reason === "reconnect_failed")
886
- throw new SdkSessionCliError(
887
- "reconnect_failed",
888
- "Tail connection exhausted reconnect attempts before continuity was restored.",
889
- 1,
890
- );
891
- if (outcome.reason === "timeout")
892
- throw new SdkSessionCliError(
893
- "tail_timeout",
894
- `Tail did not reach an exit condition within the wait window.`,
895
- 1,
896
- { sessionId, timeoutMs },
897
- );
898
- return {
899
- ok: true,
900
- result: {
901
- version: SESSION_ROWS_VERSION,
902
- source: "session",
903
- session: row,
904
- ...(checkpoint !== undefined ? { checkpoint } : {}),
905
- ...(gap !== undefined ? { gap } : {}),
906
- items,
907
- terminal: outcome.reason === "idle" || outcome.reason === "close",
908
- },
909
- };
910
- } finally {
911
- if (session !== undefined) await session.close();
912
- if (brokerClient !== undefined) await brokerClient.close();
913
- }
1000
+ try {
1001
+ liveReason = await completion.promise;
1002
+ } finally {
1003
+ clearTimeout(timer);
1004
+ resolveLive = undefined;
1005
+ }
1006
+ }
1007
+ return {
1008
+ ok: true,
1009
+ result: {
1010
+ version: SESSION_ROWS_VERSION,
1011
+ source: "session",
1012
+ session: row,
1013
+ ...(checkpoint === undefined ? {} : { checkpoint }),
1014
+ ...(gap === undefined ? {} : { gap }),
1015
+ items,
1016
+ terminal: liveReason === "idle" || liveReason === "close",
1017
+ },
1018
+ };
1019
+ },
1020
+ recordLiveFrame,
1021
+ );
1022
+ }
1023
+
1024
+ export async function runTail(
1025
+ repo: string,
1026
+ agentDir: string,
1027
+ sessionId: string,
1028
+ args: SdkSessionCliArgs,
1029
+ ): Promise<unknown> {
1030
+ const row = (await sessionRows(agentDir)).sessions.find(candidate => candidate.sessionId === sessionId);
1031
+ if (!row)
1032
+ throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
1033
+ if (row.deleted)
1034
+ throw new SdkSessionCliError("session_deleted", `Session ${sessionId} was deleted and has no tail.`, 1);
1035
+ if (!row.live || row.terminalUncertain === true) return await offlineTailReplay(repo, agentDir, sessionId, row);
1036
+ return await runLiveTail(agentDir, sessionId, row, args);
914
1037
  }
915
1038
 
916
1039
  async function runRawControl(
@@ -922,18 +1045,8 @@ async function runRawControl(
922
1045
  ): Promise<unknown> {
923
1046
  const invalid = validateAdapterControl(operation, input);
924
1047
  if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
925
- const broker = await connectBroker(agentDir);
926
- try {
927
- return await broker.global("session.control", {
928
- sessionId,
929
- operation,
930
- input,
931
- confirm: args.confirm === true,
932
- ...(args.elevationRequestId === undefined ? {} : { elevationRequestId: args.elevationRequestId }),
933
- });
934
- } finally {
935
- await broker.close();
936
- }
1048
+ await ensureBroker({ agentDir });
1049
+ return await withRouter(agentDir, async router => await requestControl(router, sessionId, operation, input, args));
937
1050
  }
938
1051
 
939
1052
  async function runRawQuery(
@@ -943,13 +1056,41 @@ async function runRawQuery(
943
1056
  input: JsonRecord,
944
1057
  args: SdkSessionCliArgs,
945
1058
  ): Promise<unknown> {
946
- const { broker, session } = await connectSessionViaBroker(agentDir, sessionId);
947
- try {
948
- return await session.query(operation, input, args.cursor);
949
- } finally {
950
- await session.close();
951
- await broker.close();
1059
+ await ensureBroker({ agentDir });
1060
+ return await withRouter(agentDir, async router => await requestQuery(router, sessionId, operation, input, args));
1061
+ }
1062
+
1063
+ function lifecycleMutationRequest(
1064
+ operation: LifecycleMutationOperation,
1065
+ input: JsonRecord,
1066
+ requestKey: string,
1067
+ timeoutMs: number | undefined,
1068
+ ): SessionLifecycleMutationRequest {
1069
+ const base = {
1070
+ actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
1071
+ requestKey,
1072
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
1073
+ };
1074
+ if (operation === "session.create" || operation === "session.fork") {
1075
+ if (typeof input.cwd !== "string" || input.cwd.length === 0)
1076
+ throw new SdkSessionCliError("invalid_input", `${operation} requires a string cwd in the input payload.`, 2);
1077
+ const target = input as JsonRecord & { cwd: string };
1078
+ return operation === "session.create"
1079
+ ? { ...base, operation, capability: operation, target }
1080
+ : { ...base, operation, capability: operation, target };
952
1081
  }
1082
+ if (typeof input.sessionId !== "string" || input.sessionId.length === 0)
1083
+ throw new SdkSessionCliError(
1084
+ "invalid_input",
1085
+ `${operation} requires a string sessionId in the input payload.`,
1086
+ 2,
1087
+ );
1088
+ const target = input as JsonRecord & { sessionId: string };
1089
+ return operation === "session.resume"
1090
+ ? { ...base, operation, capability: operation, target }
1091
+ : operation === "session.close"
1092
+ ? { ...base, operation, capability: operation, target }
1093
+ : { ...base, operation, capability: "session.delete", target };
953
1094
  }
954
1095
 
955
1096
  async function runRawGlobal(
@@ -958,81 +1099,30 @@ async function runRawGlobal(
958
1099
  input: JsonRecord,
959
1100
  args: SdkSessionCliArgs,
960
1101
  ): Promise<unknown> {
961
- const idempotencyKey = args.idempotencyKey;
962
- if (isLifecycleOperation(operation) && !idempotencyKey)
963
- throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
964
- const client = await connectBroker(agentDir);
965
- try {
966
- try {
967
- const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
968
- const response = await client.global(operation, input, {
969
- idempotencyKey,
970
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
971
- ...(args.elevationRequestId === undefined ? {} : { elevationRequestId: args.elevationRequestId }),
972
- });
973
- return args.showEndpointCredential ? response : stripSecretFields(response);
974
- } catch (error) {
975
- if (
976
- isLifecycleOperation(operation) &&
977
- error instanceof SdkClientError &&
978
- error.code === "uncertain_after_send" &&
979
- error.details &&
980
- typeof error.details === "object"
981
- )
982
- return stripSecretFields(await client.lookupLifecycle(error.details as SdkSentRecord));
983
- throw error;
984
- }
985
- } finally {
986
- await client.close();
1102
+ if (operation === "session.list") {
1103
+ await ensureBroker({ agentDir });
1104
+ return await withRouter(agentDir, async router => await paginatedSessionList(router, input));
987
1105
  }
1106
+ if (!isLifecycleOperation(operation))
1107
+ throw new SdkSessionCliError("unknown_operation", `Unknown global operation: ${operation}`, 1);
1108
+ if (!args.idempotencyKey)
1109
+ throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
1110
+ const lifecycle = createBrokerSessionLifecycleService(agentDir);
1111
+ const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
1112
+ const response = await lifecycle.execute(lifecycleMutationRequest(operation, input, args.idempotencyKey, timeoutMs));
1113
+ throwResponseFailure(response);
1114
+ return response;
988
1115
  }
989
1116
 
990
- async function runElevate(agentDir: string, sessionId: string, args: SdkSessionCliArgs): Promise<unknown> {
991
- if (args.confirm !== true || !process.stdin.isTTY)
992
- throw new SdkSessionCliError(
993
- "operator_confirmation_required",
994
- "Elevation approval requires an attended TTY and --confirm.",
995
- 1,
996
- );
997
- const kind = args.operationKind;
998
- if (kind !== "control" && kind !== "global")
999
- throw new SdkSessionCliError("usage", "elevate requires --kind control|global.", 2);
1000
- const operation = requireValue(args.operation, "--op");
1001
- const input = await inputFromArgs(args);
1002
- const broker = await connectBroker(agentDir);
1003
- try {
1004
- const issued = await broker.global("elevation.issue", {
1005
- sessionId,
1006
- operation: { kind, sdkId: operation },
1007
- input,
1008
- requester: { source: "broker_connection", connectionId: `operator-cli:${process.pid}` },
1009
- });
1010
- if (isRecord(issued) && issued.ok === false) return issued;
1011
- const result = resultObject(issued);
1012
- const elevationRequestId = typeof result?.elevationRequestId === "string" ? result.elevationRequestId : undefined;
1013
- const presentedDigest = typeof result?.requestDigest === "string" ? result.requestDigest : undefined;
1014
- if (!elevationRequestId || !presentedDigest)
1015
- throw new SdkSessionCliError("unavailable", "Broker returned a malformed elevation request.", 1);
1016
- const directory = path.join(agentDir, "sdk", "elevation", "operator");
1017
- await fs.mkdir(directory, { recursive: true, mode: 0o700 });
1018
- const target = path.join(directory, `${elevationRequestId}.json`);
1019
- const temporary = `${target}.${randomUUID()}.tmp`;
1020
- await Bun.write(
1021
- temporary,
1022
- `${JSON.stringify({ version: 1, elevationRequestId, answer: "approve", presentedDigest, createdAt: Date.now() })}\n`,
1023
- );
1024
- await fs.chmod(temporary, 0o600);
1025
- await fs.rename(temporary, target);
1026
- return { ok: true, result: { elevationRequestId, requestDigest: presentedDigest, state: "approval_submitted" } };
1027
- } finally {
1028
- await broker.close();
1029
- }
1117
+ function rawKind(action: string, args: SdkSessionCliArgs): SdkSessionCliRawKind | undefined {
1118
+ if (action === "raw")
1119
+ return args.rawAction === "control" || args.rawAction === "query" || args.rawAction === "global"
1120
+ ? args.rawAction
1121
+ : undefined;
1122
+ return action === "control" || action === "query" || action === "global" ? action : undefined;
1030
1123
  }
1031
1124
 
1032
- /**
1033
- * Runs the `gjc sdk session` command family: semantic verbs (list, inspect,
1034
- * send, status, tail) plus the explicit raw control/query/global hatch.
1035
- */
1125
+ /** Runs the broker-bound `gjc sdk session` command family without exposing endpoint credentials. */
1036
1126
  export async function runSdkSessionCli(
1037
1127
  args: SdkSessionCliArgs,
1038
1128
  writeOutput: (value: unknown) => void = writeJson,
@@ -1048,99 +1138,85 @@ export async function runSdkSessionCli(
1048
1138
  action !== "send" &&
1049
1139
  action !== "status" &&
1050
1140
  action !== "tail" &&
1051
- action !== "elevate" &&
1052
- action !== "raw"
1141
+ action !== "raw" &&
1142
+ action !== "control" &&
1143
+ action !== "query" &&
1144
+ action !== "global"
1053
1145
  )
1054
1146
  throw new SdkSessionCliError(
1055
1147
  "usage",
1056
- "Expected one of: list, inspect, send, status, tail, elevate, raw (control|query|global).",
1148
+ "Expected one of: list, inspect, send, status, tail, raw (control|query|global).",
1057
1149
  2,
1058
1150
  );
1059
- const repo = args.repo ?? process.cwd();
1060
1151
  const agentDir = args.agentDir ?? getAgentDir();
1061
-
1062
1152
  if (action === "list") {
1063
- writeOutput(await runList(agentDir));
1153
+ writeOutput(stripSecretFields(await runList(agentDir)));
1064
1154
  return;
1065
1155
  }
1066
- const sessionId =
1067
- action === "raw" && args.rawAction === "global" ? undefined : requireValue(args.sessionId, "<sessionId>");
1068
1156
  if (action === "inspect") {
1069
- writeOutput(await runInspect(repo, agentDir, requireValue(sessionId, "<sessionId>")));
1157
+ writeOutput(stripSecretFields(await runInspect(agentDir, requireValue(args.sessionId, "<sessionId>"))));
1070
1158
  return;
1071
1159
  }
1072
1160
  if (action === "send") {
1073
- writeOutput(await runSend(agentDir, requireValue(sessionId, "<sessionId>"), args));
1161
+ writeOutput(stripSecretFields(await runSend(agentDir, requireValue(args.sessionId, "<sessionId>"), args)));
1074
1162
  return;
1075
1163
  }
1076
1164
  if (action === "status") {
1077
1165
  writeOutput(
1078
- await runStatus(agentDir, requireValue(sessionId, "<sessionId>"), requireValue(args.opRef, "<opRef>")),
1166
+ stripSecretFields(
1167
+ await runStatus(
1168
+ agentDir,
1169
+ requireValue(args.sessionId, "<sessionId>"),
1170
+ requireValue(args.opRef, "<opRef>"),
1171
+ args,
1172
+ ),
1173
+ ),
1079
1174
  );
1080
1175
  return;
1081
1176
  }
1082
1177
  if (action === "tail") {
1083
- writeOutput(await runTail(repo, agentDir, requireValue(sessionId, "<sessionId>"), args));
1084
- return;
1085
- }
1086
- if (action === "elevate") {
1087
- writeOutput(await runElevate(agentDir, requireValue(sessionId, "<sessionId>"), args));
1178
+ writeOutput(
1179
+ stripSecretFields(
1180
+ await runTail(args.repo ?? process.cwd(), agentDir, requireValue(args.sessionId, "<sessionId>"), args),
1181
+ ),
1182
+ );
1088
1183
  return;
1089
1184
  }
1090
- const rawAction = args.rawAction;
1091
- if (rawAction !== "control" && rawAction !== "query" && rawAction !== "global")
1092
- throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
1093
- const operation =
1094
- rawAction === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
1095
- const kind: OperationKind = rawAction === "query" ? "query" : rawAction === "global" ? "global" : "control";
1185
+
1186
+ const kind = rawKind(requireValue(action, "<verb>"), args);
1187
+ if (!kind) throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
1188
+ const operation = kind === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
1096
1189
  const dispositionError = cliOperationError(kind, operation);
1097
1190
  if (dispositionError) throw new SdkSessionCliError(dispositionError.code, dispositionError.message, 1);
1098
- if (isEndpointOperation(operation) && !args.showEndpointCredential)
1191
+ if (isEndpointOperation(operation))
1099
1192
  throw new SdkSessionCliError(
1100
1193
  "endpoint_credential_forbidden",
1101
- "session.get_endpoint requires --show-endpoint-credential.",
1102
- 1,
1103
- );
1104
- if (
1105
- isCredentialBearingOperation(operation) &&
1106
- args.showEndpointCredential &&
1107
- process.stdout.isTTY &&
1108
- !args.yes &&
1109
- !(await confirmEndpointCredentialOutput())
1110
- )
1111
- throw new SdkSessionCliError(
1112
- "endpoint_credential_confirmation_required",
1113
- "Endpoint credential output was not confirmed.",
1194
+ "session.get_endpoint is not available through the SDK session CLI.",
1114
1195
  1,
1115
1196
  );
1116
1197
  const input = await inputFromArgs(args);
1117
- if (rawAction === "global") {
1118
- writeOutput(await runRawGlobal(agentDir, operation, input, args));
1119
- return;
1120
- }
1121
- if (rawAction === "control") {
1122
- writeOutput(await runRawControl(agentDir, requireValue(sessionId, "<sessionId>"), operation, input, args));
1198
+ const secretError = validateAdapterSecretFields(operation, input);
1199
+ if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
1200
+ if (kind === "global") {
1201
+ writeOutput(stripSecretFields(await runRawGlobal(agentDir, operation, input, args)));
1123
1202
  return;
1124
1203
  }
1125
- writeOutput(await runRawQuery(agentDir, requireValue(sessionId, "<sessionId>"), operation, input, args));
1204
+ const sessionId = requireValue(args.sessionId, "<sessionId>");
1205
+ writeOutput(
1206
+ stripSecretFields(
1207
+ kind === "control"
1208
+ ? await runRawControl(agentDir, sessionId, operation, input, args)
1209
+ : await runRawQuery(agentDir, sessionId, operation, input, args),
1210
+ ),
1211
+ );
1126
1212
  } catch (error) {
1127
- if (brokerAbsent(error)) {
1128
- writeOutput({ ok: false, error: { code: "broker_unavailable", message: "SDK broker is unavailable." } });
1129
- setExitCode(1);
1130
- return;
1131
- }
1132
1213
  const cliError =
1133
1214
  error instanceof SdkSessionCliError
1134
1215
  ? error
1135
- : error instanceof SdkClientError
1136
- ? new SdkSessionCliError(
1137
- error.code,
1138
- error.message,
1139
- 1,
1140
- (error.details as { details?: unknown } | undefined)?.details,
1141
- )
1142
- : error instanceof SdkDiscoveryError
1143
- ? new SdkSessionCliError(error.code, error.message, 1)
1216
+ : error instanceof SessionRouterError
1217
+ ? new SdkSessionCliError(error.phase, error.message, 1)
1218
+ : error instanceof SdkClientError
1219
+ ? new SdkSessionCliError(error.code, error.message, 1, error.details)
1144
1220
  : new SdkSessionCliError(
1145
1221
  "operation_failed",
1146
1222
  error instanceof Error ? error.message : "SDK operation failed.",
@@ -1151,7 +1227,7 @@ export async function runSdkSessionCli(
1151
1227
  error: {
1152
1228
  code: cliError.code,
1153
1229
  message: cliError.message,
1154
- ...(cliError.details ? { details: stripSecretFields(cliError.details) } : {}),
1230
+ ...(cliError.details === undefined ? {} : { details: stripSecretFields(cliError.details) }),
1155
1231
  },
1156
1232
  });
1157
1233
  setExitCode(cliError.exitCode);