@gajae-code/coding-agent 0.17.2 → 0.17.4

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 (349) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/dist/types/cli/daemon-cli.d.ts +3 -1
  3. package/dist/types/cli/github-release.d.ts +17 -0
  4. package/dist/types/cli/public-command-entry.d.ts +56 -0
  5. package/dist/types/cli/public-command-errors.d.ts +126 -0
  6. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  7. package/dist/types/cli/public-command-help.d.ts +65 -0
  8. package/dist/types/cli/public-command-registry.d.ts +69 -0
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -0
  11. package/dist/types/cli/skills-cli.d.ts +5 -0
  12. package/dist/types/cli.d.ts +3 -0
  13. package/dist/types/commands/daemon.d.ts +4 -50
  14. package/dist/types/commands/sdk.d.ts +20 -37
  15. package/dist/types/commands/setup.d.ts +3 -0
  16. package/dist/types/commands/skills.d.ts +9 -0
  17. package/dist/types/config/autorouting-tier-map.d.ts +18 -0
  18. package/dist/types/config/file-lock.d.ts +9 -1
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +11 -2
  21. package/dist/types/config/settings-schema.d.ts +11 -0
  22. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  23. package/dist/types/daemon/operator-contract.d.ts +5 -1
  24. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  26. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  28. package/dist/types/extensibility/shared-events.d.ts +3 -1
  29. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  30. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  31. package/dist/types/internal-urls/index.d.ts +1 -0
  32. package/dist/types/internal-urls/parse.d.ts +9 -1
  33. package/dist/types/main.d.ts +22 -0
  34. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  35. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  36. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  37. package/dist/types/modes/components/session-selector.d.ts +5 -2
  38. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  39. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +1 -1
  42. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  43. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  44. package/dist/types/modes/types.d.ts +2 -2
  45. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  46. package/dist/types/runtime-mcp/manager.d.ts +56 -0
  47. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  48. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  49. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  50. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  51. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  52. package/dist/types/sdk/broker/broker.d.ts +13 -2
  53. package/dist/types/sdk/broker/identity.d.ts +3 -1
  54. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  55. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  56. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  57. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  58. package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
  59. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  60. package/dist/types/sdk/bus/index.d.ts +19 -0
  61. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  62. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  63. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  64. package/dist/types/sdk/cli/rows.d.ts +19 -1
  65. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  66. package/dist/types/sdk/client/client.d.ts +2 -0
  67. package/dist/types/sdk/guides/cli.d.ts +3 -4
  68. package/dist/types/sdk/guides/verify.d.ts +2 -0
  69. package/dist/types/sdk/host/host.d.ts +9 -0
  70. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  71. package/dist/types/sdk/host/types.d.ts +10 -0
  72. package/dist/types/sdk/index.d.ts +1 -0
  73. package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
  74. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  75. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  76. package/dist/types/sdk/router/session-router.d.ts +2 -0
  77. package/dist/types/sdk/startup-capability.d.ts +2 -0
  78. package/dist/types/sdk/transport/relay.d.ts +1 -0
  79. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  80. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  81. package/dist/types/session/agent-session.d.ts +168 -21
  82. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  83. package/dist/types/session/fold-coordinator.d.ts +17 -3
  84. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  85. package/dist/types/session/session-manager.d.ts +64 -1
  86. package/dist/types/session/session-work-lease.d.ts +14 -0
  87. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  88. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  89. package/dist/types/tools/bash.d.ts +37 -11
  90. package/dist/types/tools/job.d.ts +1 -1
  91. package/dist/types/tools/steer-fold.d.ts +31 -0
  92. package/dist/types/utils/git.d.ts +28 -2
  93. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  94. package/dist/types/utils/mupdf.d.ts +4 -0
  95. package/dist/types/utils/session-title-generation.d.ts +6 -0
  96. package/dist/types/utils/title-generator.d.ts +11 -2
  97. package/dist/types/web/scrapers/types.d.ts +3 -0
  98. package/package.json +18 -10
  99. package/scripts/build-binary.ts +9 -3
  100. package/scripts/compile-args.ts +0 -9
  101. package/scripts/embed-mupdf.ts +52 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  103. package/scripts/vendor-markit.ts +196 -0
  104. package/src/async/job-manager.ts +22 -1
  105. package/src/cli/daemon-cli.ts +53 -18
  106. package/src/cli/github-release.ts +87 -5
  107. package/src/cli/mcp-cli.ts +38 -4
  108. package/src/cli/public-command-entry.ts +450 -0
  109. package/src/cli/public-command-errors.ts +581 -0
  110. package/src/cli/public-command-evidence.ts +717 -0
  111. package/src/cli/public-command-help.ts +360 -0
  112. package/src/cli/public-command-registry.ts +535 -0
  113. package/src/cli/session-picker.ts +2 -1
  114. package/src/cli/setup-cli.ts +9 -1
  115. package/src/cli/skills-cli.ts +46 -2
  116. package/src/cli.ts +61 -1
  117. package/src/commands/daemon.ts +65 -69
  118. package/src/commands/sdk.ts +175 -117
  119. package/src/commands/setup.ts +2 -0
  120. package/src/commands/skills.ts +11 -0
  121. package/src/config/autorouting-tier-map.ts +14 -0
  122. package/src/config/file-lock.ts +147 -51
  123. package/src/config/model-profile-activation.ts +334 -43
  124. package/src/config/model-registry.ts +183 -25
  125. package/src/config/settings-schema.ts +26 -2
  126. package/src/coordinator-mcp/policy.ts +10 -5
  127. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  128. package/src/coordinator-mcp/question-state.ts +175 -8
  129. package/src/coordinator-mcp/server.ts +507 -81
  130. package/src/coordinator-mcp/session-reaper.ts +6 -4
  131. package/src/crash/record-loader.ts +6 -2
  132. package/src/daemon/operator-contract.ts +26 -12
  133. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  134. package/src/eval/js/context-manager.ts +8 -8
  135. package/src/eval/js/worker-core.ts +9 -4
  136. package/src/eval/js/worker-protocol.ts +2 -0
  137. package/src/extensibility/extensions/runner.ts +52 -4
  138. package/src/extensibility/extensions/types.ts +10 -0
  139. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  140. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  141. package/src/extensibility/shared-events.ts +3 -1
  142. package/src/gjc-runtime/session-state-lock.ts +165 -24
  143. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  144. package/src/internal-urls/docs-index.generated.ts +20 -19
  145. package/src/internal-urls/embedded-protocol.ts +83 -0
  146. package/src/internal-urls/index.ts +1 -0
  147. package/src/internal-urls/parse.ts +21 -1
  148. package/src/internal-urls/router.ts +4 -2
  149. package/src/main.ts +123 -55
  150. package/src/modes/acp/acp-agent.ts +408 -35
  151. package/src/modes/acp/startup-provenance.ts +59 -0
  152. package/src/modes/components/custom-provider-wizard.ts +308 -7
  153. package/src/modes/components/oauth-selector.ts +9 -0
  154. package/src/modes/components/session-selector.ts +47 -4
  155. package/src/modes/components/user-message-selector.ts +57 -15
  156. package/src/modes/controllers/command-controller.ts +58 -32
  157. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  158. package/src/modes/controllers/input-controller.ts +4 -0
  159. package/src/modes/controllers/selector-controller.ts +161 -15
  160. package/src/modes/interactive-mode.ts +21 -7
  161. package/src/modes/runtime-init.ts +1 -0
  162. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  163. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  164. package/src/modes/types.ts +2 -2
  165. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  166. package/src/prompts/system/title-system.md +1 -1
  167. package/src/prompts/tools/bash.md +1 -1
  168. package/src/prompts/tools/job.md +1 -1
  169. package/src/prompts/tools/read.md +2 -0
  170. package/src/runtime/network-prewarm-service.test.ts +36 -1
  171. package/src/runtime/network-prewarm-service.ts +28 -3
  172. package/src/runtime/process-lifecycle.ts +22 -11
  173. package/src/runtime-mcp/config.ts +16 -1
  174. package/src/runtime-mcp/manager.ts +689 -55
  175. package/src/runtime-mcp/pool.test.ts +31 -0
  176. package/src/runtime-mcp/pool.ts +304 -28
  177. package/src/runtime-mcp/redaction.ts +42 -0
  178. package/src/runtime-mcp/startup-policy.ts +7 -0
  179. package/src/runtime-mcp/tool-bridge.ts +71 -0
  180. package/src/runtime-mcp/transports/http.ts +108 -25
  181. package/src/sdk/acp/adapter.ts +27 -8
  182. package/src/sdk/broker/broker.ts +378 -52
  183. package/src/sdk/broker/identity.ts +9 -1
  184. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  185. package/src/sdk/broker/lifecycle.ts +690 -141
  186. package/src/sdk/broker/runtime.ts +1 -1
  187. package/src/sdk/broker/session-index.ts +97 -4
  188. package/src/sdk/broker/spawn-authority.ts +35 -7
  189. package/src/sdk/broker/stderr-drainer.ts +59 -0
  190. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  191. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  192. package/src/sdk/bus/index.ts +690 -80
  193. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  194. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  195. package/src/sdk/bus/telegram-daemon.ts +10 -3
  196. package/src/sdk/cli/master-cli.ts +47 -36
  197. package/src/sdk/cli/rows.ts +30 -7
  198. package/src/sdk/cli/session-cli.ts +687 -184
  199. package/src/sdk/client/client.ts +18 -0
  200. package/src/sdk/guides/catalog.ts +2 -2
  201. package/src/sdk/guides/cli.ts +20 -62
  202. package/src/sdk/guides/verify.ts +14 -0
  203. package/src/sdk/host/host.ts +49 -5
  204. package/src/sdk/host/session-runtime.test.ts +138 -2
  205. package/src/sdk/host/session-runtime.ts +311 -97
  206. package/src/sdk/host/types.ts +12 -1
  207. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  208. package/src/sdk/host/websocket-transport.ts +40 -6
  209. package/src/sdk/index.ts +11 -0
  210. package/src/sdk/prompt-deadline-flush.ts +334 -0
  211. package/src/sdk/prompt-deadline-manager.ts +133 -1
  212. package/src/sdk/prompt-tool-boundary.ts +121 -0
  213. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  214. package/src/sdk/router/session-router.ts +9 -1
  215. package/src/sdk/session.ts +182 -7
  216. package/src/sdk/startup-capability.ts +1 -1
  217. package/src/sdk/transport/relay.ts +71 -6
  218. package/src/sdk/transport/serve-cli.ts +397 -42
  219. package/src/sdk/transport/socket.ts +12 -11
  220. package/src/sdk/transport/stdio.ts +7 -3
  221. package/src/session/agent-session.ts +2437 -400
  222. package/src/session/fallback-chain-controller.ts +6 -7
  223. package/src/session/fold-coordinator.ts +24 -5
  224. package/src/session/internal/managed-session-scope.ts +58 -18
  225. package/src/session/internal/managed-session-storage.ts +304 -11
  226. package/src/session/session-manager.ts +382 -44
  227. package/src/session/session-work-lease.ts +34 -0
  228. package/src/setup/model-onboarding-guidance.ts +1 -1
  229. package/src/setup/provider-onboarding.ts +397 -24
  230. package/src/slash-commands/builtin-registry.ts +258 -14
  231. package/src/task/index.ts +14 -2
  232. package/src/tools/bash.ts +98 -78
  233. package/src/tools/fetch.ts +119 -20
  234. package/src/tools/job.ts +151 -25
  235. package/src/tools/output-meta.ts +15 -2
  236. package/src/tools/path-utils.ts +27 -3
  237. package/src/tools/read.ts +3 -3
  238. package/src/tools/steer-fold.ts +107 -0
  239. package/src/tools/tool-catalog.generated.ts +29 -2
  240. package/src/tools/tool-errors.ts +4 -0
  241. package/src/utils/git.ts +53 -3
  242. package/src/utils/markit.ts +55 -41
  243. package/src/utils/mupdf-assets.d.ts +4 -0
  244. package/src/utils/mupdf-embedded.ts +4 -0
  245. package/src/utils/mupdf.ts +129 -0
  246. package/src/utils/session-title-generation.ts +29 -0
  247. package/src/utils/title-generator.ts +46 -2
  248. package/src/web/scrapers/types.ts +24 -3
  249. package/vendor/markit-ai/LICENSE +21 -0
  250. package/vendor/markit-ai/MANIFEST.json +412 -0
  251. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  252. package/vendor/markit-ai/dist/commands/config.js +139 -0
  253. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  254. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  255. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  256. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  257. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  258. package/vendor/markit-ai/dist/commands/init.js +29 -0
  259. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  260. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  261. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  262. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  263. package/vendor/markit-ai/dist/config.d.ts +26 -0
  264. package/vendor/markit-ai/dist/config.js +42 -0
  265. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  266. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  267. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  268. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  269. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  270. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  271. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  272. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  273. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  274. package/vendor/markit-ai/dist/converters/github.js +148 -0
  275. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  276. package/vendor/markit-ai/dist/converters/html.js +30 -0
  277. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  278. package/vendor/markit-ai/dist/converters/image.js +124 -0
  279. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  280. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  281. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  282. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  283. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  284. package/vendor/markit-ai/dist/converters/json.js +21 -0
  285. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  286. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  287. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  288. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  289. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  290. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  291. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  292. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  293. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  294. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  295. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  296. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  297. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  298. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  299. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  300. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  301. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  302. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  303. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  304. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  305. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  306. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  307. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  308. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  309. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  310. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  312. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  313. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  314. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  315. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  316. package/vendor/markit-ai/dist/index.d.ts +30 -0
  317. package/vendor/markit-ai/dist/index.js +26 -0
  318. package/vendor/markit-ai/dist/main.d.ts +2 -0
  319. package/vendor/markit-ai/dist/main.js +191 -0
  320. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  321. package/vendor/markit-ai/dist/markit.js +267 -0
  322. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  323. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  324. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  325. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  326. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  327. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  328. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  329. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  330. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  331. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  332. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  333. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  334. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  335. package/vendor/markit-ai/dist/providers/index.js +56 -0
  336. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  337. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  338. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  339. package/vendor/markit-ai/dist/providers/types.js +1 -0
  340. package/vendor/markit-ai/dist/types.d.ts +36 -0
  341. package/vendor/markit-ai/dist/types.js +1 -0
  342. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  343. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  344. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  345. package/vendor/markit-ai/dist/utils/output.js +31 -0
  346. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  347. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  348. package/vendor/markit-ai/package.json +74 -0
  349. package/vendor/markit-ai.patch +49 -0
@@ -3,7 +3,9 @@ import * as fsSync from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { replaceTabs, truncateToWidth } from "@gajae-code/tui";
6
- import { getAgentDir } from "@gajae-code/utils";
6
+ import { getAgentDir, logger, sanitizeDisplayLine } from "@gajae-code/utils";
7
+ import { PublicCommandFailure, type PublicEffectProof, type PublicFailureKind } from "../../cli/public-command-errors";
8
+ import type { EvidenceReference } from "../../cli/public-command-evidence";
7
9
  import { repo as resolveGitRepository } from "../../utils/git";
8
10
  import { ensureBroker } from "../broker/ensure";
9
11
  import { resolveSessionLocator } from "../broker/session-index";
@@ -20,6 +22,7 @@ import { createBrokerSessionLifecycleService } from "../lifecycle/broker-client"
20
22
  import type {
21
23
  SessionLifecycleMutationRequest,
22
24
  SessionLifecycleOperation,
25
+ SessionLifecycleResult,
23
26
  SessionLifecycleSavedSession,
24
27
  SessionLifecycleSavedSessionIdentity,
25
28
  SessionLifecycleService,
@@ -58,12 +61,12 @@ export type SdkSessionCliAction =
58
61
  | "send"
59
62
  | "status"
60
63
  | "tail"
64
+ | "close"
61
65
  | "retire"
62
66
  | "global"
63
67
  | "raw"
64
68
  | "control"
65
- | "query"
66
- | "global";
69
+ | "query";
67
70
  export type SdkSessionListScope = "repo" | "cwd" | "worktree" | "all";
68
71
  export type SdkSessionCliRawKind = "control" | "query" | "global";
69
72
 
@@ -86,6 +89,8 @@ export interface SdkSessionCliArgs {
86
89
  strict?: boolean;
87
90
  untilIdle?: boolean;
88
91
  allEvents?: boolean;
92
+ /** tail --cursor: transcript row id the caller has already processed; rows up to and including it are omitted. */
93
+ afterTranscriptId?: string;
89
94
  page?: boolean;
90
95
  repo?: string;
91
96
  scope?: string;
@@ -95,9 +100,14 @@ export interface SdkSessionCliArgs {
95
100
  agentDir?: string;
96
101
  }
97
102
 
103
+ export interface SdkSessionCliDependencies {
104
+ readonly lifecycleService?: SessionLifecycleService;
105
+ }
106
+
98
107
  type JsonRecord = Record<string, unknown>;
99
108
  type LifecycleMutationOperation = Exclude<SessionLifecycleOperation, "session.lookup" | "session.list">;
100
- type TailExitReason = "idle" | "close";
109
+ /** Why a tail call returned: the turn went idle, the session closed, or the caller's wait window closed (non-terminal). */
110
+ type TailExitReason = "idle" | "close" | "timeout";
101
111
  export interface RetainedTranscriptTailReader {
102
112
  readonly size: number;
103
113
  readRange(start: number, end: number): Promise<Uint8Array>;
@@ -117,6 +127,7 @@ const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });
117
127
  const SEARCH_PROBE_TIMEOUT_MS = 2_000;
118
128
  const SEARCH_PROBE_MAX_ROWS = 100;
119
129
  const SEARCH_TEXT_WIDTH = 80;
130
+ export const SDK_JSON_INPUT_FILE_MAX_BYTES = 4 * 1024 * 1024;
120
131
 
121
132
  const TERMINAL_TURN_KINDS = new Set(["turn_end", "agent_end"]);
122
133
  const START_TURN_KINDS = new Set(["turn_start", "agent_start"]);
@@ -144,9 +155,109 @@ class SdkSessionCliError extends Error {
144
155
  }
145
156
  }
146
157
 
158
+ /** Translate only SDK-owned codes and explicitly allowlisted reconciliation fields. */
159
+ export function sdkPublicFailure(code: string, details?: unknown, proof?: PublicEffectProof): PublicCommandFailure {
160
+ const kinds: Record<string, PublicFailureKind> = {
161
+ usage: "usage",
162
+ invalid_input: "usage",
163
+ invalid_json: "invalid_json",
164
+ broker_unavailable: "broker_unavailable",
165
+ session_unavailable: "endpoint_stale",
166
+ endpoint_stale: "endpoint_stale",
167
+ broker_restarting: "broker_restarting",
168
+ unavailable: "unavailable",
169
+ timeout: "timeout",
170
+ tail_timeout: "timeout",
171
+ wait_timeout: "wait_timeout",
172
+ uncertain_after_send: "uncertain_after_send",
173
+ authorization_denied: "authorization_denied",
174
+ unauthorized: "authorization_denied",
175
+ forbidden: "authorization_denied",
176
+ master_context_required: "authorization_denied",
177
+ adapter_operation_prohibited: "authorization_denied",
178
+ endpoint_credential_forbidden: "authorization_denied",
179
+ };
180
+ const references: EvidenceReference[] = [];
181
+ const source = object(details);
182
+ for (const [field, kind] of [
183
+ ["sessionId", "sessionId"],
184
+ ["operationRef", "operationRef"],
185
+ ["clientRef", "operationRef"],
186
+ ["idempotencyKey", "idempotencyKey"],
187
+ ["claimId", "claimId"],
188
+ ["commandId", "commandId"],
189
+ ["turnId", "turnId"],
190
+ ] as const) {
191
+ if (typeof source?.[field] === "string") references.push({ kind, value: source[field] as string });
192
+ }
193
+ return new PublicCommandFailure({
194
+ kind: Object.hasOwn(kinds, code) ? kinds[code]! : "operation_failed",
195
+ proof: code === "wait_timeout" ? "accepted" : proof,
196
+ references,
197
+ });
198
+ }
199
+
200
+ function normalizeSessionFailure(error: unknown, args: SdkSessionCliArgs): PublicCommandFailure {
201
+ if (error instanceof PublicCommandFailure)
202
+ return new PublicCommandFailure({
203
+ ...error.input,
204
+ references: [
205
+ ...(error.input.references ?? []),
206
+ ...(sdkPublicFailure("operation_failed", {
207
+ sessionId: args.sessionId,
208
+ operationRef: args.opRef,
209
+ idempotencyKey: args.idempotencyKey,
210
+ }).input.references ?? []),
211
+ ],
212
+ });
213
+ const context = { sessionId: args.sessionId, operationRef: args.opRef, idempotencyKey: args.idempotencyKey };
214
+ if (error instanceof SdkSessionCliError)
215
+ return sdkPublicFailure(
216
+ error.exitCode === 2 && error.code !== "invalid_json" ? "usage" : error.code,
217
+ { ...context, ...object(error.details) },
218
+ error.exitCode === 2 ? "pre-effect" : undefined,
219
+ );
220
+ if (error instanceof SdkClientError)
221
+ return sdkPublicFailure(
222
+ error.code,
223
+ { ...context, ...object(error.details) },
224
+ object(error.details)?.requestSent === false
225
+ ? "pre-send"
226
+ : object(error.details)?.requestSent === true
227
+ ? "sent"
228
+ : undefined,
229
+ );
230
+ if (error instanceof SessionRouterError)
231
+ return sdkPublicFailure(
232
+ error.phase === "pre_send" ? "endpoint_stale" : "uncertain_after_send",
233
+ context,
234
+ error.phase === "pre_send" ? "pre-send" : "sent",
235
+ );
236
+ return sdkPublicFailure("operation_failed", context);
237
+ }
238
+
239
+ /** Lifecycle terminality/retryability is not, by itself, proof of an effect outcome. */
240
+ export function lifecyclePublicFailure(outcome: Extract<SessionLifecycleResult, { ok: false }>): PublicCommandFailure {
241
+ const failure = sdkPublicFailure(outcome.error.code);
242
+ // The service emits retryable protocol_error only when requestSent is explicitly false.
243
+ const proof: PublicEffectProof =
244
+ outcome.certainty === "retryable" && outcome.error.code === "protocol_error" ? "pre-send" : "unknown";
245
+ return new PublicCommandFailure({
246
+ ...failure.input,
247
+ // Generic lifecycle codes cannot override the authoritative, conservative proof.
248
+ kind:
249
+ failure.input.kind === "usage" || failure.input.kind === "invalid_json"
250
+ ? "operation_failed"
251
+ : failure.input.kind === "wait_timeout"
252
+ ? "timeout"
253
+ : failure.input.kind,
254
+ proof,
255
+ });
256
+ }
257
+
147
258
  class RetainedTranscriptTailError extends Error {
148
259
  constructor(
149
- readonly reason: "unavailable" | "corrupt" | "line_limit" | "scan_limit" | "changed",
260
+ readonly reason: "unavailable" | "corrupt" | "line_limit" | "scan_limit" | "changed" | "boundary_out_of_window",
150
261
  message: string,
151
262
  ) {
152
263
  super(message);
@@ -186,6 +297,128 @@ function parseInput(raw: string | undefined, source: string): JsonRecord {
186
297
  }
187
298
  }
188
299
 
300
+ function matchesSecurePathIdentity(before: fsSync.BigIntStats, after: fsSync.BigIntStats): boolean {
301
+ return (
302
+ before.dev === after.dev &&
303
+ before.ino === after.ino &&
304
+ before.nlink === after.nlink &&
305
+ before.mode === after.mode &&
306
+ before.size === after.size &&
307
+ before.mtimeNs === after.mtimeNs &&
308
+ before.ctimeNs === after.ctimeNs
309
+ );
310
+ }
311
+
312
+ function matchesSecureInputIdentity(before: fsSync.BigIntStats, after: fsSync.BigIntStats): boolean {
313
+ return before.isFile() && after.isFile() && matchesSecurePathIdentity(before, after);
314
+ }
315
+
316
+ type SecureInputPathSnapshot = {
317
+ resolvedPath: string;
318
+ ancestors: readonly { path: string; stat: fsSync.BigIntStats }[];
319
+ };
320
+
321
+ async function secureInputPath(filePath: string): Promise<SecureInputPathSnapshot> {
322
+ const resolved = path.resolve(filePath);
323
+ const canonical = await fs.realpath(resolved);
324
+ if (canonical !== resolved)
325
+ throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
326
+ const ancestorPaths: string[] = [];
327
+ for (let current = path.dirname(resolved); ; ) {
328
+ ancestorPaths.unshift(current);
329
+ const parent = path.dirname(current);
330
+ if (parent === current) break;
331
+ current = parent;
332
+ }
333
+ const ancestors = [] as { path: string; stat: fsSync.BigIntStats }[];
334
+ for (const ancestor of ancestorPaths) {
335
+ const stat = await fs.lstat(ancestor, { bigint: true });
336
+ if (!stat.isDirectory())
337
+ throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
338
+ ancestors.push({ path: ancestor, stat });
339
+ }
340
+ return { resolvedPath: resolved, ancestors };
341
+ }
342
+
343
+ function sameSecureInputAncestors(
344
+ before: readonly { path: string; stat: fsSync.BigIntStats }[],
345
+ after: readonly { path: string; stat: fsSync.BigIntStats }[],
346
+ ): boolean {
347
+ return (
348
+ before.length === after.length &&
349
+ before.every(
350
+ (entry, index) =>
351
+ after[index]?.path === entry.path &&
352
+ after[index] !== undefined &&
353
+ matchesSecurePathIdentity(entry.stat, after[index]!.stat),
354
+ )
355
+ );
356
+ }
357
+
358
+ /** Reads a 0600 JSON input through one descriptor, never through a replaceable pathname. */
359
+ export async function readSecureJsonInputFile(filePath: string): Promise<string> {
360
+ let descriptor: fs.FileHandle | undefined;
361
+ try {
362
+ const beforePath = await secureInputPath(filePath);
363
+ const resolvedPath = beforePath.resolvedPath;
364
+ const noFollow = process.platform === "win32" ? 0 : fsSync.constants.O_NOFOLLOW;
365
+ descriptor = await fs.open(resolvedPath, fsSync.constants.O_RDONLY | noFollow);
366
+ const before = await descriptor.stat({ bigint: true });
367
+ const uid = process.getuid?.();
368
+ if (
369
+ !before.isFile() ||
370
+ (before.mode & 0o7777n) !== 0o600n ||
371
+ (process.platform !== "win32" && uid !== undefined && before.uid !== BigInt(uid))
372
+ )
373
+ throw new SdkSessionCliError(
374
+ "input_file_permissions",
375
+ "--json-input-file must be a regular file with 0600 permissions.",
376
+ 2,
377
+ );
378
+ if (before.size > BigInt(SDK_JSON_INPUT_FILE_MAX_BYTES))
379
+ throw new SdkSessionCliError(
380
+ "usage",
381
+ `--json-input-file must be at most ${SDK_JSON_INPUT_FILE_MAX_BYTES} bytes.`,
382
+ 2,
383
+ );
384
+
385
+ const pathIdentity = await fs.lstat(resolvedPath, { bigint: true });
386
+ const openedPath = await secureInputPath(resolvedPath);
387
+ if (
388
+ !matchesSecureInputIdentity(before, pathIdentity) ||
389
+ !sameSecureInputAncestors(beforePath.ancestors, openedPath.ancestors)
390
+ )
391
+ throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
392
+
393
+ const size = Number(before.size);
394
+ const bytes = Buffer.alloc(size);
395
+ let offset = 0;
396
+ while (offset < size) {
397
+ const read = await descriptor.read(bytes, offset, size - offset, offset);
398
+ if (read.bytesRead === 0) break;
399
+ offset += read.bytesRead;
400
+ }
401
+ if (offset !== size)
402
+ throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
403
+
404
+ const after = await descriptor.stat({ bigint: true });
405
+ const finalPath = await secureInputPath(resolvedPath);
406
+ const finalPathIdentity = await fs.lstat(resolvedPath, { bigint: true });
407
+ if (
408
+ !matchesSecureInputIdentity(before, after) ||
409
+ !matchesSecureInputIdentity(before, finalPathIdentity) ||
410
+ !sameSecureInputAncestors(beforePath.ancestors, finalPath.ancestors)
411
+ )
412
+ throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
413
+ return bytes.toString("utf8");
414
+ } catch (error) {
415
+ if (error instanceof SdkSessionCliError) throw error;
416
+ throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
417
+ } finally {
418
+ if (descriptor !== undefined) await descriptor.close().catch(() => {});
419
+ }
420
+ }
421
+
189
422
  function containsSecretField(value: unknown): boolean {
190
423
  if (Array.isArray(value)) return value.some(containsSecretField);
191
424
  if (!isRecord(value)) return false;
@@ -211,14 +444,7 @@ async function inputFromArgs(args: SdkSessionCliArgs): Promise<JsonRecord> {
211
444
  }
212
445
  if (args.jsonInputFile !== undefined) {
213
446
  try {
214
- const stat = await fs.stat(args.jsonInputFile);
215
- if (!stat.isFile() || (stat.mode & 0o077) !== 0)
216
- throw new SdkSessionCliError(
217
- "input_file_permissions",
218
- "--json-input-file must be a regular file with 0600 permissions.",
219
- 2,
220
- );
221
- return parseInput(await Bun.file(args.jsonInputFile).text(), "--json-input-file");
447
+ return parseInput(await readSecureJsonInputFile(args.jsonInputFile), "--json-input-file");
222
448
  } catch (error) {
223
449
  if (error instanceof SdkSessionCliError) throw error;
224
450
  throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
@@ -274,9 +500,8 @@ async function bounded<T>(promise: Promise<T>, timeoutMs: number, message: strin
274
500
  }
275
501
  }
276
502
 
277
- function reportRouterCleanupFailure(error: unknown): void {
278
- const message = error instanceof Error ? error.message : String(error);
279
- process.stderr.write(`SDK session Router cleanup failed: ${message}\n`);
503
+ function reportRouterCleanupFailure(): void {
504
+ logger.warn("SDK session Router cleanup failed");
280
505
  }
281
506
 
282
507
  /**
@@ -314,8 +539,14 @@ async function withRouter<T>(
314
539
  }
315
540
  try {
316
541
  await bounded(router.stop(), ROUTER_STOP_TIMEOUT_MS, "SDK session Router shutdown timed out.");
317
- } catch (error) {
318
- reportRouterCleanupFailure(error);
542
+ } catch {
543
+ if (actionFailed) {
544
+ const failure = normalizeSessionFailure(actionError, {});
545
+ actionError = new PublicCommandFailure({
546
+ ...failure.input,
547
+ diagnostics: [...(failure.input.diagnostics ?? []), "router_cleanup_failed"],
548
+ });
549
+ } else reportRouterCleanupFailure();
319
550
  }
320
551
  if (actionFailed) throw actionError;
321
552
  return result;
@@ -336,12 +567,17 @@ function throwResponseFailure(response: unknown): void {
336
567
  const record = object(response);
337
568
  if (record?.ok !== false) return;
338
569
  const failure = object(record.error);
339
- throw new SdkSessionCliError(
340
- typeof failure?.code === "string" ? failure.code : "unavailable",
341
- typeof failure?.message === "string" ? failure.message : "SDK request failed.",
342
- 1,
570
+ const failureInput = sdkPublicFailure(
571
+ typeof failure?.code === "string" ? failure.code : "operation_failed",
343
572
  failure,
344
573
  );
574
+ throw new PublicCommandFailure({
575
+ ...failureInput.input,
576
+ references: [
577
+ ...(failureInput.input.references ?? []),
578
+ ...(sdkPublicFailure("operation_failed", record.result).input.references ?? []),
579
+ ],
580
+ });
345
581
  }
346
582
 
347
583
  async function paginatedSessionList(
@@ -488,9 +724,10 @@ async function probeSearchRows(agentDir: string, result: SdkSearchResultV1): Pro
488
724
  } catch {
489
725
  return {
490
726
  ...result,
727
+ warnings: [...result.warnings, "probe_unavailable"],
491
728
  rows: mergeProbedSearchRows(
492
729
  result.rows,
493
- rows.map(row => ({ ...row, probe: row.live ? "unreachable" : "stale" })),
730
+ rows.map(row => (row.live ? { ...row } : { ...row, probe: "stale" as const })),
494
731
  ),
495
732
  };
496
733
  }
@@ -501,54 +738,32 @@ export async function runSdkSearch(
501
738
  args: Pick<SdkSessionCliArgs, "agentDir" | "repo" | "scope" | "limit" | "cursor">,
502
739
  createService: (agentDir: string) => SessionLifecycleService = createBrokerSessionLifecycleService,
503
740
  probe: (agentDir: string, result: SdkSearchResultV1) => Promise<SdkSearchResultV1> = probeSearchRows,
504
- ): Promise<{ result: SdkSearchResultV1; exitCode: 0 | 1 }> {
505
- const agentDir = path.resolve(args.agentDir ?? getAgentDir());
506
- const locator = await resolveSessionLocator(args.repo ?? process.cwd(), agentDir);
507
- const scope: ScopeNameV1 =
508
- args.scope === undefined || args.scope === "repo" || args.scope === "pwd" || args.scope === "global"
509
- ? ((args.scope ?? "repo") as ScopeNameV1)
510
- : (() => {
511
- throw new SdkSessionCliError(
512
- "usage",
513
- `Invalid search scope "${args.scope}". Expected repo, pwd, or global.`,
514
- 2,
515
- );
516
- })();
517
- const request = searchScopeRequest(scope, locator);
518
- const resolved = await resolveScopeRequest(request);
519
- const outcome = await createService(agentDir).scopedList(request, args.limit, args.cursor);
520
- if (outcome.ok) {
521
- if ("rows" in outcome.result) {
522
- if (outcome.result.status === "not-in-git-worktree") return { result: outcome.result, exitCode: 0 };
523
- if (outcome.result.status === "unavailable") return { result: outcome.result, exitCode: 1 };
524
- return { result: await probe(agentDir, outcome.result), exitCode: 0 };
741
+ ): Promise<{ result: SdkSearchResultV1; exitCode: 0 }> {
742
+ try {
743
+ const agentDir = path.resolve(args.agentDir ?? getAgentDir());
744
+ const locator = await resolveSessionLocator(args.repo ?? process.cwd(), agentDir);
745
+ const scope: ScopeNameV1 =
746
+ args.scope === undefined || args.scope === "repo" || args.scope === "pwd" || args.scope === "global"
747
+ ? ((args.scope ?? "repo") as ScopeNameV1)
748
+ : (() => {
749
+ throw sdkPublicFailure("usage", undefined, "pre-effect");
750
+ })();
751
+ const request = searchScopeRequest(scope, locator);
752
+ await resolveScopeRequest(request);
753
+ const outcome = await createService(agentDir).scopedList(request, args.limit, args.cursor);
754
+ if (outcome.ok) {
755
+ if ("rows" in outcome.result) {
756
+ if (outcome.result.status === "not-in-git-worktree") return { result: outcome.result, exitCode: 0 };
757
+ if (outcome.result.status === "unavailable")
758
+ throw sdkPublicFailure(outcome.result.error?.code ?? "unavailable");
759
+ return { result: await probe(agentDir, outcome.result), exitCode: 0 };
760
+ }
761
+ throw sdkPublicFailure("operation_failed");
525
762
  }
526
- return {
527
- result: {
528
- version: 1,
529
- scope: resolved,
530
- status: "unavailable",
531
- observedAt: new Date().toISOString(),
532
- rows: [],
533
- warnings: [],
534
- error: { code: "malformed_response", message: "broker search returned an unscoped result" },
535
- },
536
- exitCode: 1,
537
- };
763
+ throw lifecyclePublicFailure(outcome);
764
+ } catch (error) {
765
+ throw normalizeSessionFailure(error, args);
538
766
  }
539
- if (!outcome.ok && outcome.result?.status === "unavailable") return { result: outcome.result, exitCode: 1 };
540
- return {
541
- result: {
542
- version: 1,
543
- scope: resolved,
544
- status: "unavailable",
545
- observedAt: new Date().toISOString(),
546
- rows: [],
547
- warnings: [],
548
- error: { code: outcome.error.code, message: outcome.error.message },
549
- },
550
- exitCode: 1,
551
- };
552
767
  }
553
768
 
554
769
  function searchScopeLabel(result: SdkSearchResultV1): string {
@@ -560,7 +775,10 @@ function searchScopeLabel(result: SdkSearchResultV1): string {
560
775
  }
561
776
 
562
777
  function safeSearchText(value: string): string {
563
- return truncateToWidth(replaceTabs(value).replaceAll(/[\r\n]/g, " "), SEARCH_TEXT_WIDTH);
778
+ return truncateToWidth(
779
+ replaceTabs(sanitizeDisplayLine(value).replaceAll(/[\u2028\u2029]/gu, " ")),
780
+ SEARCH_TEXT_WIDTH,
781
+ );
564
782
  }
565
783
 
566
784
  /** Renders a credential-free scope/status preamble and bounded search table. */
@@ -568,15 +786,15 @@ export function renderSdkSearchTable(result: SdkSearchResultV1): string {
568
786
  const lines = [
569
787
  `Scope requested: ${result.scope.requested}`,
570
788
  `Scope resolved: ${safeSearchText(searchScopeLabel(result))}`,
571
- `Status: ${result.status}`,
572
- `Observed at: ${result.observedAt}`,
789
+ `Status: ${safeSearchText(result.status)}`,
790
+ `Observed at: ${safeSearchText(result.observedAt)}`,
573
791
  ...(result.cursor === undefined ? [] : [`Continuation cursor: ${safeSearchText(result.cursor)}`]),
574
792
  ];
575
793
  if (result.rows.length === 0) return lines.join("\n");
576
794
  lines.push("ID PROBE LIVE CWD");
577
795
  for (const row of result.rows)
578
796
  lines.push(
579
- `${safeSearchText(row.id).padEnd(20)} ${(row.probe ?? "-").padEnd(11)} ${String(row.live).padEnd(4)} ${safeSearchText(row.locator.cwd)}`,
797
+ `${safeSearchText(row.id).padEnd(20)} ${safeSearchText(row.probe ?? "-").padEnd(11)} ${String(row.live).padEnd(4)} ${safeSearchText(row.locator.cwd)}`,
580
798
  );
581
799
  return lines.join("\n");
582
800
  }
@@ -844,22 +1062,35 @@ async function requestBrokerOperatorAbort(
844
1062
  timeoutMs,
845
1063
  reconnectAttempts: 0,
846
1064
  });
1065
+ let actionFailed = false;
1066
+ let actionError: unknown;
1067
+ let result!: JsonRecord;
847
1068
  try {
848
1069
  const response = await client.global("session.control", request, {
849
1070
  idempotencyKey,
850
1071
  timeoutMs,
851
1072
  });
852
- const result = object(response);
853
- if (!result) throw new SdkClientError("protocol_error", "SDK broker returned a malformed control response.");
854
- throwResponseFailure(result);
855
- return result;
856
- } finally {
857
- await client.close().catch(error => {
858
- process.stderr.write(
859
- `SDK broker control cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`,
860
- );
861
- });
1073
+ const record = object(response);
1074
+ if (!record) throw new SdkClientError("protocol_error", "SDK broker returned a malformed control response.");
1075
+ throwResponseFailure(record);
1076
+ result = record;
1077
+ } catch (error) {
1078
+ actionFailed = true;
1079
+ actionError = error;
1080
+ }
1081
+ try {
1082
+ await client.close();
1083
+ } catch {
1084
+ if (actionFailed) {
1085
+ const failure = normalizeSessionFailure(actionError, args);
1086
+ actionError = new PublicCommandFailure({
1087
+ ...failure.input,
1088
+ diagnostics: [...(failure.input.diagnostics ?? []), "broker_cleanup_failed"],
1089
+ });
1090
+ } else logger.warn("SDK broker client cleanup failed");
862
1091
  }
1092
+ if (actionFailed) throw actionError;
1093
+ return result;
863
1094
  }
864
1095
 
865
1096
  async function requestQuery(
@@ -939,6 +1170,8 @@ export async function waitForTerminalStatus(
939
1170
  * reports when a request that was already on the wire is abandoned.
940
1171
  */
941
1172
  function isWaitWindowFailure(error: unknown): boolean {
1173
+ if (error instanceof PublicCommandFailure)
1174
+ return error.input.kind === "timeout" || error.input.kind === "uncertain_after_send";
942
1175
  if (error instanceof SdkClientError) return error.code === "timeout" || error.code === "uncertain_after_send";
943
1176
  return error instanceof SdkSessionCliError && error.code === "timeout";
944
1177
  }
@@ -958,30 +1191,49 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
958
1191
  if (inputRef === undefined) promptInput.clientRef = clientRef;
959
1192
  const invalid = validateAdapterControl("turn.prompt", promptInput);
960
1193
  if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
961
- await ensureBroker({ agentDir });
1194
+ let accepted = false;
1195
+ try {
1196
+ await ensureBroker({ agentDir });
962
1197
 
963
- return await withRouter(agentDir, [sessionId], async router => {
964
- const response = await requestControl(router, sessionId, "turn.prompt", promptInput, args);
965
- const result: JsonRecord = {
966
- version: SESSION_ROWS_VERSION,
967
- operationRef: clientRef,
968
- status: "accepted",
969
- receipt: resultObject(response) ?? response,
970
- };
971
- if (args.wait === true) {
972
- const outcome = await waitForTerminalStatus(router, sessionId, clientRef, args.timeoutMs ?? 30_000);
973
- if (!outcome.terminal)
974
- throw new SdkSessionCliError(
975
- "wait_timeout",
976
- `Prompt ${clientRef} did not reach a terminal state within the wait window.`,
977
- 1,
978
- { operationRef: clientRef, status: outcome.status },
979
- );
980
- result.status = outcome.status;
981
- result.statusDetail = outcome.detail;
982
- }
983
- return { ok: true, result };
984
- });
1198
+ return await withRouter(agentDir, [sessionId], async router => {
1199
+ const response = await requestControl(router, sessionId, "turn.prompt", promptInput, args);
1200
+ accepted = true;
1201
+ const result: JsonRecord = {
1202
+ version: SESSION_ROWS_VERSION,
1203
+ operationRef: clientRef,
1204
+ status: "accepted",
1205
+ receipt: resultObject(response) ?? response,
1206
+ };
1207
+ if (args.wait === true) {
1208
+ const outcome = await waitForTerminalStatus(router, sessionId, clientRef, args.timeoutMs ?? 30_000);
1209
+ if (!outcome.terminal)
1210
+ throw new SdkSessionCliError(
1211
+ "wait_timeout",
1212
+ `Prompt ${clientRef} did not reach a terminal state within the wait window.`,
1213
+ 1,
1214
+ { operationRef: clientRef, status: outcome.status },
1215
+ );
1216
+ result.status = outcome.status;
1217
+ result.statusDetail = outcome.detail;
1218
+ }
1219
+ return { ok: true, result };
1220
+ });
1221
+ } catch (error) {
1222
+ const failure = normalizeSessionFailure(error, { ...args, sessionId, opRef: clientRef });
1223
+ throw new PublicCommandFailure({
1224
+ ...failure.input,
1225
+ ...(accepted
1226
+ ? {
1227
+ proof: "accepted" as const,
1228
+ kind:
1229
+ failure.input.kind === "uncertain_after_send" || failure.input.kind === "timeout"
1230
+ ? ("wait_timeout" as const)
1231
+ : failure.input.kind,
1232
+ }
1233
+ : {}),
1234
+ references: failure.input.references,
1235
+ });
1236
+ }
985
1237
  }
986
1238
 
987
1239
  async function runStatus(
@@ -1266,14 +1518,23 @@ function retainedTranscriptCorrupt(): RetainedTranscriptTailError {
1266
1518
  );
1267
1519
  }
1268
1520
 
1269
- export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailReader): Promise<unknown[]> {
1521
+ export async function scanRetainedTranscriptTail(
1522
+ reader: RetainedTranscriptTailReader,
1523
+ options: { boundaryId?: string } = {},
1524
+ ): Promise<unknown[]> {
1270
1525
  if (!Number.isSafeInteger(reader.size) || reader.size < 0) throw retainedTranscriptUnavailable();
1271
1526
  const entries: unknown[] = [];
1527
+ let rowsAfterBoundary = 0;
1528
+ let boundaryFound = false;
1272
1529
  let position = reader.size;
1273
1530
  let scannedBytes = 0;
1274
1531
  let scannedLines = 0;
1275
1532
  let trailingFragment = new Uint8Array();
1276
- while (position > 0 && entries.length < TAIL_OFFLINE_MAX_ENTRIES) {
1533
+ while (
1534
+ position > 0 &&
1535
+ (entries.length < TAIL_OFFLINE_MAX_ENTRIES || (options.boundaryId !== undefined && !boundaryFound)) &&
1536
+ (options.boundaryId === undefined || !boundaryFound)
1537
+ ) {
1277
1538
  const remainingBytes = TAIL_OFFLINE_MAX_SCAN_BYTES - scannedBytes;
1278
1539
  if (remainingBytes <= 0)
1279
1540
  throw new RetainedTranscriptTailError(
@@ -1313,7 +1574,11 @@ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailR
1313
1574
  }
1314
1575
 
1315
1576
  let lineEnd = complete.byteLength;
1316
- while (lineEnd > 0 && entries.length < TAIL_OFFLINE_MAX_ENTRIES) {
1577
+ while (
1578
+ lineEnd > 0 &&
1579
+ (entries.length < TAIL_OFFLINE_MAX_ENTRIES || (options.boundaryId !== undefined && !boundaryFound)) &&
1580
+ (options.boundaryId === undefined || !boundaryFound)
1581
+ ) {
1317
1582
  const newline = complete.lastIndexOf(0x0a, lineEnd - 1);
1318
1583
  const lineStart = newline < 0 ? 0 : newline + 1;
1319
1584
  const line = complete.subarray(lineStart, lineEnd);
@@ -1331,12 +1596,24 @@ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailR
1331
1596
  "Retained transcript history exceeds the bounded tail replay limit.",
1332
1597
  );
1333
1598
  try {
1334
- entries.push(JSON.parse(transcriptDecoder.decode(line)));
1335
- } catch {
1599
+ const entry: unknown = JSON.parse(transcriptDecoder.decode(line));
1600
+ if (options.boundaryId !== undefined && isRecord(entry) && entry.id === options.boundaryId) {
1601
+ if (rowsAfterBoundary > TAIL_OFFLINE_MAX_ENTRIES)
1602
+ throw new RetainedTranscriptTailError(
1603
+ "boundary_out_of_window",
1604
+ "The requested transcript boundary is older than the bounded offline tail window.",
1605
+ );
1606
+ boundaryFound = true;
1607
+ } else if (options.boundaryId !== undefined && !boundaryFound) {
1608
+ rowsAfterBoundary++;
1609
+ }
1610
+ if (entries.length < TAIL_OFFLINE_MAX_ENTRIES) entries.push(entry);
1611
+ } catch (error) {
1612
+ if (error instanceof RetainedTranscriptTailError) throw error;
1336
1613
  throw retainedTranscriptCorrupt();
1337
1614
  }
1338
1615
  }
1339
- if (entries.length === TAIL_OFFLINE_MAX_ENTRIES) break;
1616
+ if (entries.length === TAIL_OFFLINE_MAX_ENTRIES && (options.boundaryId === undefined || boundaryFound)) break;
1340
1617
  if (start > 0) {
1341
1618
  if (partial.byteLength > TAIL_OFFLINE_MAX_LINE_BYTES)
1342
1619
  throw new RetainedTranscriptTailError(
@@ -1350,7 +1627,10 @@ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailR
1350
1627
  return entries.reverse();
1351
1628
  }
1352
1629
 
1353
- async function readRetainedTranscriptTail(savedSession: SessionLifecycleSavedSession): Promise<unknown[]> {
1630
+ async function readRetainedTranscriptTail(
1631
+ savedSession: SessionLifecycleSavedSession,
1632
+ afterTranscriptId?: string,
1633
+ ): Promise<unknown[]> {
1354
1634
  let descriptor: fs.FileHandle | undefined;
1355
1635
  try {
1356
1636
  descriptor = await fs.open(savedSession.path, retainedTranscriptOpenFlags());
@@ -1360,10 +1640,13 @@ async function readRetainedTranscriptTail(savedSession: SessionLifecycleSavedSes
1360
1640
  // Bind every range read to the opened descriptor so a later pathname replacement
1361
1641
  // cannot change the retained history selected by the lifecycle lookup.
1362
1642
  const file = Bun.file(descriptor.fd);
1363
- const entries = await scanRetainedTranscriptTail({
1364
- size: Number(before.size),
1365
- readRange: async (start, end) => new Uint8Array(await file.slice(start, end).arrayBuffer()),
1366
- });
1643
+ const entries = await scanRetainedTranscriptTail(
1644
+ {
1645
+ size: Number(before.size),
1646
+ readRange: async (start, end) => new Uint8Array(await file.slice(start, end).arrayBuffer()),
1647
+ },
1648
+ { boundaryId: afterTranscriptId },
1649
+ );
1367
1650
  const after = await descriptor.stat({ bigint: true });
1368
1651
  if (
1369
1652
  !matchesRetainedTranscriptIdentity(savedSession.identity, after) ||
@@ -1391,6 +1674,7 @@ async function offlineTailReplay(
1391
1674
  agentDir: string,
1392
1675
  sessionId: string,
1393
1676
  row: SdkSessionRowV1,
1677
+ afterTranscriptId?: string,
1394
1678
  ): Promise<unknown> {
1395
1679
  const lifecycle = createBrokerSessionLifecycleService(agentDir);
1396
1680
  const outcome = await lifecycle.list({
@@ -1398,8 +1682,7 @@ async function offlineTailReplay(
1398
1682
  capability: "session.list",
1399
1683
  target: { cwd: repo, resolveSessionId: sessionId },
1400
1684
  });
1401
- if (!outcome.ok)
1402
- throw new SdkSessionCliError(outcome.error.code, outcome.error.message, 1, { certainty: outcome.certainty });
1685
+ if (!outcome.ok) throw lifecyclePublicFailure(outcome);
1403
1686
  if ("rows" in outcome.result)
1404
1687
  throw new SdkSessionCliError(
1405
1688
  "malformed_response",
@@ -1415,7 +1698,7 @@ async function offlineTailReplay(
1415
1698
  );
1416
1699
  let entries: unknown[];
1417
1700
  try {
1418
- entries = await readRetainedTranscriptTail(savedSession);
1701
+ entries = await readRetainedTranscriptTail(savedSession, afterTranscriptId);
1419
1702
  } catch (error) {
1420
1703
  const retained = error instanceof RetainedTranscriptTailError ? error : retainedTranscriptUnavailable();
1421
1704
  throw new SdkSessionCliError("retention_gap", retained.message, 1, {
@@ -1423,15 +1706,21 @@ async function offlineTailReplay(
1423
1706
  reason: retained.reason,
1424
1707
  });
1425
1708
  }
1709
+ const items = entries.map((entry, index) =>
1710
+ toTailItemV1(entry, { kind: "transcript", revision: index + 1, seq: index }),
1711
+ );
1712
+ // The same contract as a live resume: rows up to and including the id the
1713
+ // caller already has are omitted, so a session that stopped between two
1714
+ // polls does not replay processed history on the next one. Unknown
1715
+ // boundary keeps everything (a duplicate is recoverable; a missing row is not).
1716
+ const boundary = afterTranscriptId === undefined ? -1 : items.findIndex(item => item.id === afterTranscriptId);
1426
1717
  return {
1427
1718
  ok: true,
1428
1719
  result: {
1429
1720
  version: SESSION_ROWS_VERSION,
1430
1721
  source: "offline",
1431
1722
  session: row,
1432
- items: entries.map((entry, index) =>
1433
- toTailItemV1(entry, { kind: "transcript", revision: index + 1, seq: index }),
1434
- ),
1723
+ items: boundary >= 0 ? items.slice(boundary + 1) : items,
1435
1724
  terminal: true,
1436
1725
  },
1437
1726
  };
@@ -1460,6 +1749,8 @@ async function runLiveTail(
1460
1749
  const seenEvents = new Set<string>();
1461
1750
  const liveRevisionBuffer = new TailRevisionBuffer();
1462
1751
  let checkpoint: SdkCheckpointRecordV1 | undefined;
1752
+ let transcriptCheckpoint: SdkCheckpointRecordV1 | undefined;
1753
+ let eventReplayCheckpoint: SdkCheckpointRecordV1 | undefined;
1463
1754
  let gap: SdkRetentionGapV1 | undefined;
1464
1755
  let liveReason: TailExitReason | undefined;
1465
1756
  let resolveLive: ((reason: TailExitReason) => void) | undefined;
@@ -1473,6 +1764,7 @@ async function runLiveTail(
1473
1764
  let turnIdle = false;
1474
1765
  let turnStateKey: TailSeqKey | undefined;
1475
1766
  let closed = false;
1767
+ let acceptingLiveFrames = true;
1476
1768
  // Lifecycle kind already claimed at each canonical position. A same-kind
1477
1769
  // duplicate never reaches here (dedupe keys include kind), so any entry that
1478
1770
  // disagrees is a host stating two different lifecycle kinds at one position.
@@ -1537,6 +1829,7 @@ async function runLiveTail(
1537
1829
  };
1538
1830
 
1539
1831
  const recordLiveFrame = (attachment: SessionAttachment, frame: SessionRouterFrame): void => {
1832
+ if (!acceptingLiveFrames) return;
1540
1833
  if (attachment.sessionId !== sessionId) return;
1541
1834
  const item = tailItemFromRouterFrame(frame);
1542
1835
  if (!item) return;
@@ -1572,6 +1865,8 @@ async function runLiveTail(
1572
1865
  throwResponseFailure(checkpointResponse);
1573
1866
  const extraction = extractCheckpoint(checkpointResponse);
1574
1867
  checkpoint = extraction.record;
1868
+ transcriptCheckpoint = checkpoint;
1869
+ eventReplayCheckpoint = checkpoint;
1575
1870
  gap = extraction.gap;
1576
1871
  if (checkpoint !== undefined)
1577
1872
  applyLifecycle(
@@ -1585,7 +1880,87 @@ async function runLiveTail(
1585
1880
  gap,
1586
1881
  );
1587
1882
 
1883
+ // Which snapshot to page. A fresh tail pages the checkpoint it was just
1884
+ // handed. A resumed tail (`--cursor`) must NOT page the exchanged cursor:
1885
+ // that one is pinned to the OLD snapshot at offset 0, so it would replay
1886
+ // every row the caller already processed. It pages a fresh snapshot
1887
+ // instead and drops rows up to the caller's `--after-transcript-id`, so
1888
+ // the result is exactly the transcript delta since the last tail - the
1889
+ // rows that carry tool calls and interim assistant text.
1588
1890
  let cursor = extraction.cursor;
1891
+ if (args.cursor !== undefined) {
1892
+ if (cursor === undefined)
1893
+ throw new SdkSessionCliError(
1894
+ "unavailable",
1895
+ "The host did not return the exchanged checkpoint needed to resume this tail.",
1896
+ 1,
1897
+ );
1898
+ const exchangedCursor = cursor;
1899
+ let freshPagingCursor: string | undefined;
1900
+ let freshPagingCheckpoint: SdkCheckpointRecordV1 | undefined;
1901
+ try {
1902
+ const fresh = await router.request(
1903
+ sessionId,
1904
+ { type: "query_request", query: "session.checkpoint", input: {} },
1905
+ attachment.generation,
1906
+ attachment,
1907
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
1908
+ );
1909
+ throwResponseFailure(fresh);
1910
+ const freshExtraction = extractCheckpoint(fresh);
1911
+ freshPagingCursor = freshExtraction.cursor;
1912
+ freshPagingCheckpoint = freshExtraction.record;
1913
+ if (freshPagingCursor === undefined || freshPagingCheckpoint === undefined)
1914
+ throw new Error("The host returned an incomplete transcript checkpoint.");
1915
+ } catch {
1916
+ // Keep the exchanged cursor as a complete fallback. Returning only live
1917
+ // frames after a failed fresh checkpoint would silently discard the
1918
+ // transcript delta the caller is resuming for.
1919
+ freshPagingCursor = undefined;
1920
+ freshPagingCheckpoint = undefined;
1921
+ }
1922
+ if (freshPagingCursor !== undefined && freshPagingCheckpoint !== undefined) {
1923
+ const pagingCursor = freshPagingCursor;
1924
+ const pagingCheckpoint = freshPagingCheckpoint;
1925
+ // The exchanged cursor is pinned to the old snapshot. Release it only
1926
+ // after the fresh snapshot has been acquired so a failed mint can still
1927
+ // page the exchanged snapshot instead of returning partial success.
1928
+ let drain: string | undefined = exchangedCursor;
1929
+ while (drain !== undefined) {
1930
+ try {
1931
+ const page = extractTranscriptPage(
1932
+ await router.request(
1933
+ sessionId,
1934
+ { type: "query_request", query: "transcript.list", input: {}, cursor: drain },
1935
+ attachment.generation,
1936
+ attachment,
1937
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
1938
+ ),
1939
+ );
1940
+ drain = page.complete ? undefined : page.cursor;
1941
+ } catch {
1942
+ // Releasing is best-effort; a pin that cannot be drained expires with its TTL.
1943
+ drain = undefined;
1944
+ }
1945
+ }
1946
+ cursor = pagingCursor;
1947
+ transcriptCheckpoint = pagingCheckpoint;
1948
+ eventReplayCheckpoint = pagingCheckpoint;
1949
+ applyLifecycle(
1950
+ mergeEventTailItems(
1951
+ eventItems,
1952
+ seenEvents,
1953
+ liveRevisionBuffer.resolve(pagingCheckpoint.revision),
1954
+ include,
1955
+ ),
1956
+ );
1957
+ } else {
1958
+ cursor = exchangedCursor;
1959
+ transcriptCheckpoint = checkpoint;
1960
+ eventReplayCheckpoint = checkpoint;
1961
+ }
1962
+ }
1963
+ const transcriptStart = transcriptItems.length;
1589
1964
  while (cursor !== undefined) {
1590
1965
  const response = await router.request(
1591
1966
  sessionId,
@@ -1596,7 +1971,7 @@ async function runLiveTail(
1596
1971
  );
1597
1972
  throwResponseFailure(response);
1598
1973
  const page = extractTranscriptPage(response);
1599
- const rev = checkpoint?.revision;
1974
+ const rev = transcriptCheckpoint?.revision;
1600
1975
  let nextSeq = transcriptItems.length;
1601
1976
  mergeTailItems(
1602
1977
  transcriptItems,
@@ -1604,7 +1979,7 @@ async function runLiveTail(
1604
1979
  page.items.map(item => {
1605
1980
  const it = toTailItemV1(item, { kind: "transcript" });
1606
1981
  if (it.revision === undefined && rev !== undefined) it.revision = rev;
1607
- if (it.generation === undefined) it.generation = checkpoint?.generation ?? 0;
1982
+ if (it.generation === undefined) it.generation = transcriptCheckpoint?.generation ?? 0;
1608
1983
  if (it.seq === undefined) it.seq = nextSeq++;
1609
1984
  return it;
1610
1985
  }),
@@ -1613,14 +1988,23 @@ async function runLiveTail(
1613
1988
  if (page.complete || page.cursor === undefined) break;
1614
1989
  cursor = page.cursor;
1615
1990
  }
1991
+ if (args.cursor !== undefined && args.afterTranscriptId !== undefined) {
1992
+ // Drop the rows the caller already has. Unknown boundary (row rotated
1993
+ // out, or a different session) → keep everything: a duplicate is
1994
+ // recoverable downstream, a silently missing row is not.
1995
+ const boundary = transcriptItems.findIndex(
1996
+ (item, index) => index >= transcriptStart && item.id === args.afterTranscriptId,
1997
+ );
1998
+ if (boundary >= 0) transcriptItems.splice(transcriptStart, boundary + 1 - transcriptStart);
1999
+ }
1616
2000
 
1617
2001
  const replayResponse = await router.request(
1618
2002
  sessionId,
1619
2003
  {
1620
2004
  type: "event_replay",
1621
- ...(checkpoint === undefined
2005
+ ...(eventReplayCheckpoint === undefined
1622
2006
  ? {}
1623
- : { sinceGeneration: checkpoint.generation, sinceSeq: checkpoint.seq }),
2007
+ : { sinceGeneration: eventReplayCheckpoint.generation, sinceSeq: eventReplayCheckpoint.seq }),
1624
2008
  },
1625
2009
  attachment.generation,
1626
2010
  attachment,
@@ -1628,7 +2012,7 @@ async function runLiveTail(
1628
2012
  );
1629
2013
  throwResponseFailure(replayResponse);
1630
2014
  const replay = object(replayResponse) ?? {};
1631
- const replayGap = eventGapToRetentionGap(replay.gap, replay, checkpoint);
2015
+ const replayGap = eventGapToRetentionGap(replay.gap, replay, eventReplayCheckpoint);
1632
2016
  if (replayGap !== undefined) {
1633
2017
  gap = replayGap;
1634
2018
  if (args.strict === true)
@@ -1655,20 +2039,21 @@ async function runLiveTail(
1655
2039
  }
1656
2040
  const replayItems = rawEvents.map(event => {
1657
2041
  const it = toTailItemV1(event, { kind: "event" });
1658
- if (it.revision === undefined && checkpoint?.revision !== undefined) it.revision = checkpoint.revision;
2042
+ if (it.revision === undefined && eventReplayCheckpoint?.revision !== undefined)
2043
+ it.revision = eventReplayCheckpoint.revision;
1659
2044
  return it;
1660
2045
  });
1661
2046
  applyLifecycle(mergeEventTailItems(eventItems, seenEvents, replayItems, include));
1662
2047
  if (malformed !== undefined) throw malformed;
1663
- if (checkpoint !== undefined) {
2048
+ if (eventReplayCheckpoint !== undefined) {
1664
2049
  const checkpointKey = {
1665
- revision: checkpoint.revision,
1666
- generation: checkpoint.generation,
1667
- seq: checkpoint.seq,
2050
+ revision: eventReplayCheckpoint.revision,
2051
+ generation: eventReplayCheckpoint.generation,
2052
+ seq: eventReplayCheckpoint.seq,
1668
2053
  };
1669
2054
  if (turnStateKey === undefined || compareTailSeqKeys(checkpointKey, turnStateKey) >= 0) {
1670
2055
  turnStateKey = checkpointKey;
1671
- turnIdle = checkpoint.idle;
2056
+ turnIdle = eventReplayCheckpoint.idle;
1672
2057
  }
1673
2058
  }
1674
2059
  if (liveReason === undefined && args.untilIdle === true && turnIdle) liveReason = "idle";
@@ -1677,18 +2062,14 @@ async function runLiveTail(
1677
2062
  resolveLive = completion.resolve;
1678
2063
  rejectLive = completion.reject;
1679
2064
  const timeoutMs = args.timeoutMs ?? 10_000;
1680
- const timer = setTimeout(
1681
- () =>
1682
- completion.reject(
1683
- new SdkSessionCliError(
1684
- "tail_timeout",
1685
- "Tail did not reach an exit condition within the wait window.",
1686
- 1,
1687
- { sessionId, timeoutMs },
1688
- ),
1689
- ),
1690
- timeoutMs,
1691
- );
2065
+ // The wait window closing is not a failure: it is the caller's bound on
2066
+ // one tail call, and everything collected inside it (transcript rows,
2067
+ // replayed and live events) is a valid, non-terminal observation.
2068
+ // Throwing here discarded that observation, so a poller watching a
2069
+ // running turn with `--until-idle` saw nothing until the turn ended -
2070
+ // every mid-turn tool call and interim line was lost, and the poll
2071
+ // itself blocked for the whole window. `terminal: false` says the rest.
2072
+ const timer = setTimeout(() => completion.resolve("timeout"), timeoutMs);
1692
2073
  try {
1693
2074
  liveReason = await completion.promise;
1694
2075
  } finally {
@@ -1697,6 +2078,48 @@ async function runLiveTail(
1697
2078
  rejectLive = undefined;
1698
2079
  }
1699
2080
  }
2081
+ // Freeze collection before minting the caller's continuation. A host can
2082
+ // capture the checkpoint watermark and publish a frame before the response
2083
+ // reaches us; that frame belongs to the next poll, not behind this cursor.
2084
+ acceptingLiveFrames = false;
2085
+ // Mint the caller's continuation only after replay and live follow have
2086
+ // reached their exit condition. A checkpoint minted earlier can point
2087
+ // behind events already included in this response, causing the next poll
2088
+ // to replay observations the caller already processed.
2089
+ let resumeCursor: string | undefined;
2090
+ try {
2091
+ const fresh = await router.request(
2092
+ sessionId,
2093
+ { type: "query_request", query: "session.checkpoint", input: {} },
2094
+ attachment.generation,
2095
+ attachment,
2096
+ args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
2097
+ );
2098
+ throwResponseFailure(fresh);
2099
+ resumeCursor = extractCheckpoint(fresh).cursor;
2100
+ } catch (error) {
2101
+ // A closed session has no next poll that could resume, so preserve the
2102
+ // complete terminal observation even when the host tears down the
2103
+ // connection before it can mint a final cursor. Active and timed-out
2104
+ // tails still fail closed: returning them without a continuation would
2105
+ // let the caller advance past observations it cannot resume.
2106
+ if (liveReason !== "close") {
2107
+ if (error instanceof SdkSessionCliError) throw error;
2108
+ throw new SdkSessionCliError(
2109
+ "unavailable",
2110
+ "The host did not return a continuation checkpoint for this tail.",
2111
+ 1,
2112
+ { sessionId },
2113
+ );
2114
+ }
2115
+ }
2116
+ if (resumeCursor === undefined && liveReason !== "close")
2117
+ throw new SdkSessionCliError(
2118
+ "unavailable",
2119
+ "The host did not return a continuation cursor for this tail.",
2120
+ 1,
2121
+ { sessionId },
2122
+ );
1700
2123
  return {
1701
2124
  ok: true,
1702
2125
  result: {
@@ -1704,6 +2127,13 @@ async function runLiveTail(
1704
2127
  source: "session",
1705
2128
  session: row,
1706
2129
  ...(checkpoint === undefined ? {} : { checkpoint }),
2130
+ // A signed, UNCONSUMED checkpoint cursor to resume from. Carried as
2131
+ // `cursor`, not `checkpointToken`: output passes through
2132
+ // stripSecretFields, whose /token/i matches the latter by name, so a
2133
+ // caller never saw it and no tail could ever be resumed (every poll
2134
+ // replayed the whole session). It is an opaque per-grant cursor, not a
2135
+ // credential - the same shape `list`/`transcript` already return.
2136
+ ...(resumeCursor === undefined ? {} : { cursor: resumeCursor }),
1707
2137
  ...(gap === undefined ? {} : { gap }),
1708
2138
  items: tailItems(),
1709
2139
  terminal: liveReason === "idle" || liveReason === "close",
@@ -1720,6 +2150,12 @@ export async function runTail(
1720
2150
  sessionId: string,
1721
2151
  args: SdkSessionCliArgs,
1722
2152
  ): Promise<unknown> {
2153
+ if (args.cursor !== undefined && args.afterTranscriptId === undefined)
2154
+ throw new SdkSessionCliError(
2155
+ "usage",
2156
+ "--after-transcript-id is required when resuming a session tail with --cursor.",
2157
+ 2,
2158
+ );
1723
2159
  const row = (await sessionRows(agentDir, { resolveSessionId: sessionId })).sessions.find(
1724
2160
  candidate => candidate.sessionId === sessionId,
1725
2161
  );
@@ -1727,7 +2163,10 @@ export async function runTail(
1727
2163
  throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
1728
2164
  if (row.deleted)
1729
2165
  throw new SdkSessionCliError("session_deleted", `Session ${sessionId} was deleted and has no tail.`, 1);
1730
- if (!row.live || row.terminalUncertain === true) return await offlineTailReplay(repo, agentDir, sessionId, row);
2166
+ if (row.live && row.terminalUncertain !== true && args.afterTranscriptId !== undefined && args.cursor === undefined)
2167
+ throw new SdkSessionCliError("usage", "--after-transcript-id requires --cursor for a live session tail.", 2);
2168
+ if (!row.live || row.terminalUncertain === true)
2169
+ return await offlineTailReplay(repo, agentDir, sessionId, row, args.afterTranscriptId);
1731
2170
  return await runLiveTail(agentDir, sessionId, row, args);
1732
2171
  }
1733
2172
 
@@ -1803,11 +2242,44 @@ function lifecycleMutationRequest(
1803
2242
  return { ...base, operation, capability: "session.delete", target };
1804
2243
  }
1805
2244
 
2245
+ async function resolveCloseAuthority(
2246
+ lifecycle: SessionLifecycleService,
2247
+ sessionId: string,
2248
+ ): Promise<{ endpointGeneration: number; endpointIncarnation: string }> {
2249
+ const listed = await lifecycle.list({
2250
+ actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
2251
+ capability: "session.list",
2252
+ target: { resolveSessionId: sessionId },
2253
+ });
2254
+ if (!listed.ok) throw new SdkSessionCliError(listed.error.code, listed.error.message, 1);
2255
+ if (!("sessions" in listed.result))
2256
+ throw new SdkSessionCliError("malformed_response", "broker returned a scoped result for session close", 1);
2257
+ const row = listed.result.sessions.find(session => session.sessionId === sessionId);
2258
+ if (
2259
+ row === undefined ||
2260
+ typeof row.endpointGeneration !== "number" ||
2261
+ !Number.isSafeInteger(row.endpointGeneration) ||
2262
+ row.endpointGeneration <= 0 ||
2263
+ typeof row.endpointIncarnation !== "string" ||
2264
+ !/^[a-f0-9]{64}$/u.test(row.endpointIncarnation)
2265
+ )
2266
+ throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} has no live endpoint authority.`, 1);
2267
+ return { endpointGeneration: row.endpointGeneration, endpointIncarnation: row.endpointIncarnation };
2268
+ }
2269
+
2270
+ function sessionCloseRequestKey(
2271
+ sessionId: string,
2272
+ authority: { endpointGeneration: number; endpointIncarnation: string },
2273
+ ): string {
2274
+ return `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.close:${sessionId}:${authority.endpointGeneration}:${authority.endpointIncarnation}`;
2275
+ }
2276
+
1806
2277
  async function runRawGlobal(
1807
2278
  agentDir: string,
1808
2279
  operation: string,
1809
2280
  input: JsonRecord,
1810
2281
  args: SdkSessionCliArgs,
2282
+ lifecycleOverride?: SessionLifecycleService,
1811
2283
  ): Promise<unknown> {
1812
2284
  if (args.page && operation !== "session.list")
1813
2285
  throw new SdkSessionCliError("usage", "--page is only available for raw global session.list.", 2);
@@ -1834,7 +2306,7 @@ async function runRawGlobal(
1834
2306
  if (operation === "session.lookup") {
1835
2307
  if (!args.idempotencyKey)
1836
2308
  throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for session lookup.", 2);
1837
- const lifecycle = createBrokerSessionLifecycleService(agentDir);
2309
+ const lifecycle = lifecycleOverride ?? createBrokerSessionLifecycleService(agentDir);
1838
2310
  return await lifecycle.lookup({
1839
2311
  actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
1840
2312
  capability: "session.lookup",
@@ -1847,10 +2319,17 @@ async function runRawGlobal(
1847
2319
  throw new SdkSessionCliError("unknown_operation", `Unknown global operation: ${operation}`, 1);
1848
2320
  if (!args.idempotencyKey)
1849
2321
  throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
1850
- const lifecycle = createBrokerSessionLifecycleService(agentDir);
2322
+ const lifecycle = lifecycleOverride ?? createBrokerSessionLifecycleService(agentDir);
1851
2323
  const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
1852
2324
  const response = await lifecycle.execute(lifecycleMutationRequest(operation, input, args.idempotencyKey, timeoutMs));
1853
- throwResponseFailure(response);
2325
+ try {
2326
+ if (!response.ok) throw lifecyclePublicFailure(response);
2327
+ } catch (error) {
2328
+ throw normalizeSessionFailure(error, {
2329
+ ...args,
2330
+ sessionId: typeof input.sessionId === "string" ? input.sessionId : args.sessionId,
2331
+ });
2332
+ }
1854
2333
  return response;
1855
2334
  }
1856
2335
 
@@ -1869,6 +2348,7 @@ export async function runSdkSessionCli(
1869
2348
  setExitCode: (exitCode: 1 | 2) => void = exitCode => {
1870
2349
  process.exitCode = exitCode;
1871
2350
  },
2351
+ deps: SdkSessionCliDependencies = {},
1872
2352
  ): Promise<void> {
1873
2353
  try {
1874
2354
  const action = args.action;
@@ -1879,6 +2359,7 @@ export async function runSdkSessionCli(
1879
2359
  action !== "send" &&
1880
2360
  action !== "status" &&
1881
2361
  action !== "tail" &&
2362
+ action !== "close" &&
1882
2363
  action !== "retire" &&
1883
2364
  action !== "raw" &&
1884
2365
  action !== "control" &&
@@ -1887,7 +2368,7 @@ export async function runSdkSessionCli(
1887
2368
  )
1888
2369
  throw new SdkSessionCliError(
1889
2370
  "usage",
1890
- "Expected one of: list, inspect, send, status, tail, retire, raw (control|query|global).",
2371
+ "Expected one of: list, inspect, send, status, tail, close, retire, raw (control|query|global).",
1891
2372
  2,
1892
2373
  );
1893
2374
  const agentDir = path.resolve(args.agentDir ?? getAgentDir());
@@ -1898,7 +2379,6 @@ export async function runSdkSessionCli(
1898
2379
  if (action === "search") {
1899
2380
  const search = await runSdkSearch(args);
1900
2381
  writeOutput(args.json === true ? search.result : renderSdkSearchTable(search.result));
1901
- if (search.exitCode !== 0) setExitCode(search.exitCode);
1902
2382
  return;
1903
2383
  }
1904
2384
  if (action === "inspect") {
@@ -1930,6 +2410,57 @@ export async function runSdkSessionCli(
1930
2410
  );
1931
2411
  return;
1932
2412
  }
2413
+ if (action === "close") {
2414
+ const sessionId = requireValue(args.sessionId, "<sessionId>");
2415
+ const input = await inputFromArgs(args);
2416
+ if (input.sessionId !== undefined && input.sessionId !== sessionId)
2417
+ throw new SdkSessionCliError("invalid_input", "Close sessionId does not match the selected session.", 2);
2418
+ const secretError = validateAdapterSecretFields("session.close", input);
2419
+ if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
2420
+ const lifecycle = deps.lifecycleService ?? createBrokerSessionLifecycleService(agentDir);
2421
+ const hasExplicitAuthority =
2422
+ Object.hasOwn(input, "endpointGeneration") || Object.hasOwn(input, "endpointIncarnation");
2423
+ const authority = hasExplicitAuthority
2424
+ ? typeof input.endpointGeneration === "number" &&
2425
+ Number.isSafeInteger(input.endpointGeneration) &&
2426
+ input.endpointGeneration > 0 &&
2427
+ typeof input.endpointIncarnation === "string" &&
2428
+ /^[a-f0-9]{64}$/u.test(input.endpointIncarnation)
2429
+ ? { endpointGeneration: input.endpointGeneration, endpointIncarnation: input.endpointIncarnation }
2430
+ : undefined
2431
+ : await resolveCloseAuthority(lifecycle, sessionId);
2432
+ if (hasExplicitAuthority && authority === undefined)
2433
+ throw new SdkSessionCliError(
2434
+ "invalid_input",
2435
+ "endpointGeneration and endpointIncarnation must identify a valid endpoint authority.",
2436
+ 2,
2437
+ );
2438
+ const target = { ...input, sessionId, ...(authority ?? {}) };
2439
+ const requestKey = args.idempotencyKey ?? sessionCloseRequestKey(sessionId, authority!);
2440
+ // Closing never attaches: a Router attachment registers this process as a
2441
+ // live client and renews the host's abandonment window, which is the
2442
+ // opposite of what a close is for. The Broker answers the lifecycle
2443
+ // mutation over its own client.
2444
+ //
2445
+ // The default request key is scoped to the current endpoint generation and
2446
+ // incarnation. A retry for one live host replays, while a resumed host with
2447
+ // the same session id receives a fresh lifecycle identity.
2448
+ writeOutput(
2449
+ stripSecretFields(
2450
+ await runRawGlobal(
2451
+ agentDir,
2452
+ "session.close",
2453
+ target,
2454
+ {
2455
+ ...args,
2456
+ idempotencyKey: requestKey,
2457
+ },
2458
+ lifecycle,
2459
+ ),
2460
+ ),
2461
+ );
2462
+ return;
2463
+ }
1933
2464
  if (action === "retire") {
1934
2465
  const sessionId = requireValue(args.sessionId, "<sessionId>");
1935
2466
  const input = await inputFromArgs(args);
@@ -1953,19 +2484,11 @@ export async function runSdkSessionCli(
1953
2484
  if (!kind) throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
1954
2485
  const operation = kind === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
1955
2486
  if (isRawSpawnOperation(kind, operation))
1956
- throw new SdkSessionCliError(
1957
- "adapter_operation_prohibited",
1958
- "session.spawn is unavailable through the SDK session CLI.",
1959
- 1,
1960
- );
2487
+ throw sdkPublicFailure("adapter_operation_prohibited", undefined, "pre-effect");
1961
2488
  const dispositionError = cliOperationError(kind, operation);
1962
- if (dispositionError) throw new SdkSessionCliError(dispositionError.code, dispositionError.message, 1);
2489
+ if (dispositionError) throw sdkPublicFailure(dispositionError.code, undefined, "pre-effect");
1963
2490
  if (isEndpointOperation(operation))
1964
- throw new SdkSessionCliError(
1965
- "endpoint_credential_forbidden",
1966
- "session.get_endpoint is not available through the SDK session CLI.",
1967
- 1,
1968
- );
2491
+ throw sdkPublicFailure("endpoint_credential_forbidden", undefined, "pre-effect");
1969
2492
  const input = await inputFromArgs(args);
1970
2493
  const secretError = validateAdapterSecretFields(operation, input);
1971
2494
  if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
@@ -1984,26 +2507,6 @@ export async function runSdkSessionCli(
1984
2507
  ),
1985
2508
  );
1986
2509
  } catch (error) {
1987
- const cliError =
1988
- error instanceof SdkSessionCliError
1989
- ? error
1990
- : error instanceof SessionRouterError
1991
- ? new SdkSessionCliError(error.phase, error.message, 1)
1992
- : error instanceof SdkClientError
1993
- ? new SdkSessionCliError(error.code, error.message, 1, error.details)
1994
- : new SdkSessionCliError(
1995
- "operation_failed",
1996
- error instanceof Error ? error.message : "SDK operation failed.",
1997
- 1,
1998
- );
1999
- writeOutput({
2000
- ok: false,
2001
- error: {
2002
- code: cliError.code,
2003
- message: cliError.message,
2004
- ...(cliError.details === undefined ? {} : { details: stripSecretFields(cliError.details) }),
2005
- },
2006
- });
2007
- setExitCode(cliError.exitCode);
2510
+ throw normalizeSessionFailure(error, args);
2008
2511
  }
2009
2512
  }