@sayknow-cli/coding-agent 0.4.6 → 0.5.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 (428) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/cli/read-cli.d.ts +1 -0
  6. package/dist/types/cli.d.ts +9 -1
  7. package/dist/types/commands/daemon.d.ts +2 -2
  8. package/dist/types/commands/deep-interview.d.ts +9 -0
  9. package/dist/types/commands/harness.d.ts +22 -0
  10. package/dist/types/commands/read.d.ts +6 -0
  11. package/dist/types/commands/sdk.d.ts +20 -2
  12. package/dist/types/config/keybindings.d.ts +34 -4
  13. package/dist/types/config/model-profile-contract.d.ts +40 -0
  14. package/dist/types/config/model-profiles.d.ts +2 -3
  15. package/dist/types/config/model-registry.d.ts +5 -0
  16. package/dist/types/config/provider-auth-health.d.ts +14 -0
  17. package/dist/types/config/provider-ranking.d.ts +59 -0
  18. package/dist/types/config/settings-schema.d.ts +156 -10
  19. package/dist/types/config/settings.d.ts +3 -0
  20. package/dist/types/edit/modes/patch.d.ts +13 -0
  21. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  22. package/dist/types/eval/py/executor.d.ts +1 -1
  23. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  24. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  25. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  26. package/dist/types/extensibility/shared-events.d.ts +6 -0
  27. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  28. package/dist/types/main.d.ts +33 -2
  29. package/dist/types/memories/index.d.ts +2 -0
  30. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  31. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  32. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  35. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  36. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  37. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  38. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  39. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  41. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  42. package/dist/types/modes/components/welcome.d.ts +5 -0
  43. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  45. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  46. package/dist/types/modes/interactive-mode.d.ts +13 -2
  47. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  48. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  49. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  50. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  51. package/dist/types/modes/types.d.ts +6 -0
  52. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  53. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  54. package/dist/types/runtime/memory-domain.d.ts +9 -0
  55. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  56. package/dist/types/runtime/memory-guard.d.ts +39 -0
  57. package/dist/types/runtime/memory-limit.d.ts +11 -0
  58. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  59. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
  60. package/dist/types/runtime-mcp/manager.d.ts +9 -1
  61. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  62. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  63. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  64. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  65. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  66. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  67. package/dist/types/sdk/broker/broker.d.ts +2 -0
  68. package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
  69. package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
  70. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  71. package/dist/types/sdk/bus/config.d.ts +2 -0
  72. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  73. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  74. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  75. package/dist/types/sdk/bus/index.d.ts +52 -4
  76. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  77. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  78. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  79. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  80. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  81. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
  82. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  83. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  84. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
  85. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  86. package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
  87. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  88. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  89. package/dist/types/sdk/client/discovery.d.ts +2 -0
  90. package/dist/types/sdk/client/liveness.d.ts +7 -0
  91. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  92. package/dist/types/sdk/host/host.d.ts +3 -1
  93. package/dist/types/sdk/host/query/handlers.d.ts +14 -0
  94. package/dist/types/sdk/index.d.ts +2 -0
  95. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  96. package/dist/types/sdk/prompt-status.d.ts +88 -0
  97. package/dist/types/sdk/session-directory.d.ts +1 -1
  98. package/dist/types/sdk/session.d.ts +7 -0
  99. package/dist/types/sdk/startup-capability.d.ts +9 -0
  100. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  101. package/dist/types/sdk/transport/index.d.ts +12 -0
  102. package/dist/types/sdk/transport/relay.d.ts +43 -0
  103. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  104. package/dist/types/sdk/transport/socket.d.ts +7 -0
  105. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  106. package/dist/types/session/agent-session.d.ts +83 -24
  107. package/dist/types/session/btw-contract.d.ts +16 -0
  108. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  109. package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
  110. package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
  111. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  112. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  113. package/dist/types/session/session-manager.d.ts +89 -18
  114. package/dist/types/session/session-storage.d.ts +31 -0
  115. package/dist/types/session/streaming-output.d.ts +60 -5
  116. package/dist/types/setup/credential-import.d.ts +5 -1
  117. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  118. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
  119. package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
  120. package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
  121. package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
  122. package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
  123. package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
  124. package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
  125. package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
  126. package/dist/types/skc-runtime/state-writer.d.ts +5 -1
  127. package/dist/types/skc-runtime/team-launch.d.ts +1 -1
  128. package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
  129. package/dist/types/skc-runtime/team-store.d.ts +12 -0
  130. package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
  131. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
  132. package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
  133. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
  134. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
  135. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  136. package/dist/types/ssh/utils.d.ts +1 -0
  137. package/dist/types/task/discovery.d.ts +3 -1
  138. package/dist/types/task/executor.d.ts +6 -0
  139. package/dist/types/task/index.d.ts +3 -1
  140. package/dist/types/task/provider-retry-status.d.ts +13 -0
  141. package/dist/types/task/receipt.d.ts +2 -0
  142. package/dist/types/task/types.d.ts +133 -0
  143. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  144. package/dist/types/tools/browser/launch.d.ts +7 -0
  145. package/dist/types/tools/fetch.d.ts +22 -9
  146. package/dist/types/tools/image-gen.d.ts +63 -1
  147. package/dist/types/tools/index.d.ts +7 -1
  148. package/dist/types/tools/output-meta.d.ts +21 -1
  149. package/dist/types/tools/read-internals.d.ts +12 -0
  150. package/dist/types/tools/read.d.ts +45 -1
  151. package/dist/types/tools/resource-gc.d.ts +23 -0
  152. package/dist/types/tools/skill-discovery.d.ts +7 -0
  153. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  154. package/dist/types/tools/tool-result.d.ts +7 -1
  155. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  156. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  157. package/dist/types/web/insane/bridge.d.ts +1 -0
  158. package/dist/types/web/insane/url-guard.d.ts +17 -0
  159. package/package.json +8 -12
  160. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  161. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  162. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  163. package/scripts/compile-args.ts +0 -1
  164. package/scripts/dogfood-repository-binding.ts +209 -0
  165. package/scripts/generate-docs-index.ts +4 -1
  166. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  167. package/scripts/run-test-manifest.ts +6 -4
  168. package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
  169. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  170. package/src/capability/index.ts +58 -42
  171. package/src/capability/types.ts +3 -0
  172. package/src/cli/args.ts +16 -100
  173. package/src/cli/fast-help.ts +1 -1
  174. package/src/cli/mcp-cli.ts +4 -21
  175. package/src/cli/notify-cli.ts +6 -38
  176. package/src/cli/read-cli.ts +2 -1
  177. package/src/cli/setup-cli.ts +10 -4
  178. package/src/cli.ts +133 -11
  179. package/src/commands/daemon.ts +22 -5
  180. package/src/commands/deep-interview.ts +26 -1
  181. package/src/commands/harness.ts +35 -12
  182. package/src/commands/read.ts +11 -2
  183. package/src/commands/sdk.ts +134 -17
  184. package/src/commands/team.ts +1 -1
  185. package/src/commit/agentic/index.ts +3 -3
  186. package/src/commit/map-reduce/index.ts +2 -2
  187. package/src/config/keybindings.ts +158 -25
  188. package/src/config/model-profile-contract.ts +174 -0
  189. package/src/config/model-profiles.ts +51 -10
  190. package/src/config/model-registry.ts +24 -4
  191. package/src/config/model-resolver.ts +2 -2
  192. package/src/config/provider-auth-health.ts +42 -0
  193. package/src/config/provider-ranking.ts +119 -0
  194. package/src/config/settings-schema.ts +157 -10
  195. package/src/config/settings.ts +254 -108
  196. package/src/coordinator-mcp/model-preset.ts +21 -74
  197. package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
  198. package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
  199. package/src/defaults/skc/skills/team/SKILL.md +1 -1
  200. package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
  201. package/src/discovery/ssh.ts +12 -1
  202. package/src/edit/index.ts +3 -6
  203. package/src/edit/modes/patch.ts +38 -1
  204. package/src/edit/modes/replace.ts +42 -0
  205. package/src/edit/path-mutation-lock.ts +112 -0
  206. package/src/eval/js/tool-bridge.ts +1 -1
  207. package/src/eval/py/executor.ts +37 -13
  208. package/src/eval/py/kernel.ts +1 -21
  209. package/src/eval/py/prelude.py +2 -2
  210. package/src/eval/py/runner-artifact.ts +84 -0
  211. package/src/eval/py/tool-bridge.ts +34 -18
  212. package/src/extensibility/extensions/runner.ts +4 -0
  213. package/src/extensibility/extensions/types.ts +42 -4
  214. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  215. package/src/extensibility/shared-events.ts +5 -0
  216. package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
  217. package/src/hooks/native-skill-hook.ts +17 -4
  218. package/src/internal-urls/docs-index.generated.ts +17 -16
  219. package/src/internal-urls/local-protocol.ts +87 -25
  220. package/src/internal-urls/local-root-gc.ts +127 -0
  221. package/src/internal-urls/mcp-protocol.ts +73 -11
  222. package/src/main.ts +120 -42
  223. package/src/memories/index.ts +10 -0
  224. package/src/modes/DESIGN.md +52 -0
  225. package/src/modes/acp/acp-agent.ts +647 -83
  226. package/src/modes/acp/acp-event-mapper.ts +106 -12
  227. package/src/modes/acp/acp-mode.ts +39 -3
  228. package/src/modes/action-registry.ts +1 -1
  229. package/src/modes/components/assistant-message.ts +123 -22
  230. package/src/modes/components/btw-panel.ts +82 -16
  231. package/src/modes/components/irc-sidebar.ts +176 -62
  232. package/src/modes/components/model-selector.ts +61 -4
  233. package/src/modes/components/notifications-settings-editor.ts +1 -1
  234. package/src/modes/components/oauth-selector.ts +42 -12
  235. package/src/modes/components/plan-preview-overlay.ts +1 -1
  236. package/src/modes/components/queue-pane.ts +55 -9
  237. package/src/modes/components/queued-message-selector.ts +53 -10
  238. package/src/modes/components/read-tool-group.ts +82 -14
  239. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  240. package/src/modes/components/sayknow-pet-widget.ts +27 -8
  241. package/src/modes/components/session-observer-overlay.ts +187 -32
  242. package/src/modes/components/settings-selector.ts +60 -18
  243. package/src/modes/components/tool-execution.ts +138 -27
  244. package/src/modes/components/tool-status-header.ts +26 -8
  245. package/src/modes/components/welcome.ts +50 -23
  246. package/src/modes/controllers/btw-controller.ts +119 -36
  247. package/src/modes/controllers/command-controller.ts +101 -54
  248. package/src/modes/controllers/event-controller.ts +119 -27
  249. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  250. package/src/modes/controllers/input-controller.ts +100 -8
  251. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  252. package/src/modes/controllers/selector-controller.ts +4 -1
  253. package/src/modes/interactive-mode.ts +201 -60
  254. package/src/modes/irc-observation-ledger.ts +71 -15
  255. package/src/modes/prompt-action-autocomplete.ts +32 -13
  256. package/src/modes/prompt-suggestion-controller.ts +96 -0
  257. package/src/modes/runtime-init.ts +20 -2
  258. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
  259. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  260. package/src/modes/types.ts +6 -0
  261. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  262. package/src/modes/utils/ui-helpers.ts +99 -4
  263. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  264. package/src/prompts/agents/architect.md +17 -3
  265. package/src/prompts/agents/critic.md +17 -2
  266. package/src/prompts/agents/executor.md +1 -1
  267. package/src/prompts/agents/planner.md +8 -1
  268. package/src/prompts/system/btw-user.md +3 -8
  269. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  270. package/src/prompts/system/system-prompt.md +6 -3
  271. package/src/prompts/tools/cron.md +3 -1
  272. package/src/prompts/tools/read.md +4 -2
  273. package/src/prompts/tools/skill-discovery.md +1 -0
  274. package/src/runtime/memory-domain.ts +66 -0
  275. package/src/runtime/memory-guard-contract.ts +73 -0
  276. package/src/runtime/memory-guard.ts +292 -0
  277. package/src/runtime/memory-limit.ts +50 -0
  278. package/src/runtime-mcp/client.ts +41 -55
  279. package/src/runtime-mcp/content-limits.ts +71 -0
  280. package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
  281. package/src/runtime-mcp/json-rpc.ts +28 -11
  282. package/src/runtime-mcp/manager.ts +77 -59
  283. package/src/runtime-mcp/oauth-flow.ts +0 -72
  284. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  285. package/src/runtime-mcp/redaction.ts +30 -0
  286. package/src/runtime-mcp/smithery-auth.ts +17 -2
  287. package/src/runtime-mcp/smithery-connect.ts +11 -1
  288. package/src/runtime-mcp/transports/http.ts +63 -22
  289. package/src/sdk/acp/adapter.ts +8 -1
  290. package/src/sdk/acp/final-text.ts +40 -0
  291. package/src/sdk/acp/mcp.ts +26 -0
  292. package/src/sdk/broker/broker.ts +34 -20
  293. package/src/sdk/broker/identity.ts +22 -12
  294. package/src/sdk/broker/lifecycle.ts +293 -43
  295. package/src/sdk/broker/session-index.ts +11 -1
  296. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  297. package/src/sdk/bus/chat-daemon-control.ts +22 -5
  298. package/src/sdk/bus/config-commands.ts +2 -2
  299. package/src/sdk/bus/config.ts +10 -2
  300. package/src/sdk/bus/control-drain-lease.ts +104 -0
  301. package/src/sdk/bus/conversation-store.ts +40 -6
  302. package/src/sdk/bus/daemon-paths.ts +2 -0
  303. package/src/sdk/bus/index.ts +793 -121
  304. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  305. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  306. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  307. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  308. package/src/sdk/bus/notification-orchestration.ts +12 -0
  309. package/src/sdk/bus/notification-service.ts +87 -25
  310. package/src/sdk/bus/operator-runtime.ts +31 -5
  311. package/src/sdk/bus/prompt-reconciliation.ts +240 -0
  312. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  313. package/src/sdk/bus/recent-activity.ts +90 -26
  314. package/src/sdk/bus/reconciliation-store.ts +224 -0
  315. package/src/sdk/bus/telegram-cli.ts +6 -13
  316. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  317. package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
  318. package/src/sdk/bus/telegram-daemon-control.ts +161 -40
  319. package/src/sdk/bus/telegram-daemon.ts +3649 -593
  320. package/src/sdk/bus/telegram-reference.ts +20 -2
  321. package/src/sdk/bus/topic-registry.ts +451 -12
  322. package/src/sdk/cli/session-cli.ts +15 -2
  323. package/src/sdk/client/discovery.ts +19 -3
  324. package/src/sdk/client/liveness.ts +49 -0
  325. package/src/sdk/host/control/dispatch.ts +8 -2
  326. package/src/sdk/host/control/operations.ts +3 -2
  327. package/src/sdk/host/host.ts +27 -9
  328. package/src/sdk/host/query/handlers.ts +125 -5
  329. package/src/sdk/index.ts +12 -0
  330. package/src/sdk/lifecycle-session.ts +19 -3
  331. package/src/sdk/mcp/server.ts +46 -18
  332. package/src/sdk/prompt-status.ts +97 -0
  333. package/src/sdk/protocol/operation-inventory.generated.json +145 -0
  334. package/src/sdk/protocol/operation-registry.ts +24 -4
  335. package/src/sdk/session-directory.ts +5 -1
  336. package/src/sdk/session.ts +69 -8
  337. package/src/sdk/startup-capability.ts +15 -1
  338. package/src/sdk/transport/auth-preface.ts +82 -0
  339. package/src/sdk/transport/index.ts +18 -0
  340. package/src/sdk/transport/relay.ts +252 -0
  341. package/src/sdk/transport/serve-cli.ts +105 -0
  342. package/src/sdk/transport/socket.ts +197 -0
  343. package/src/sdk/transport/stdio.ts +17 -0
  344. package/src/session/agent-session.ts +1112 -314
  345. package/src/session/blob-store.ts +25 -7
  346. package/src/session/btw-contract.ts +59 -0
  347. package/src/session/fallback-chain-controller.ts +47 -8
  348. package/src/session/internal/managed-session-scope.ts +1426 -259
  349. package/src/session/internal/managed-session-storage.ts +414 -238
  350. package/src/session/internal/native-publish-outcome.ts +271 -0
  351. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  352. package/src/session/session-manager.ts +2079 -469
  353. package/src/session/session-storage.ts +151 -11
  354. package/src/session/streaming-output.ts +262 -43
  355. package/src/setup/credential-import.ts +28 -3
  356. package/src/setup/provider-onboarding.ts +49 -5
  357. package/src/setup/provider-presets.json +2 -1
  358. package/src/skc-runtime/deep-interview-runtime.ts +303 -36
  359. package/src/skc-runtime/deep-interview-stage.ts +1016 -0
  360. package/src/skc-runtime/deep-interview-state.ts +8 -5
  361. package/src/skc-runtime/gc-render.ts +1 -0
  362. package/src/skc-runtime/gc-runtime.ts +11 -1
  363. package/src/skc-runtime/linux-proc.ts +70 -36
  364. package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
  365. package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
  366. package/src/skc-runtime/ralplan-runtime.ts +1057 -75
  367. package/src/skc-runtime/repository-binding.ts +267 -0
  368. package/src/skc-runtime/session-state-sidecar.ts +25 -9
  369. package/src/skc-runtime/state-runtime.ts +50 -12
  370. package/src/skc-runtime/state-writer.ts +68 -56
  371. package/src/skc-runtime/team-launch.ts +34 -5
  372. package/src/skc-runtime/team-runtime.ts +41 -1
  373. package/src/skc-runtime/team-store.ts +32 -0
  374. package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
  375. package/src/skc-runtime/team-workers.ts +1 -0
  376. package/src/skc-runtime/tmux-common.ts +13 -2
  377. package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
  378. package/src/skc-runtime/tmux-sessions.ts +45 -6
  379. package/src/skc-runtime/ultragoal-guard.ts +93 -15
  380. package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
  381. package/src/skc-runtime/ultragoal-runtime.ts +469 -38
  382. package/src/skc-runtime/workflow-manifest.generated.json +195 -5
  383. package/src/skc-runtime/workflow-manifest.ts +67 -3
  384. package/src/skill-state/workflow-hud.ts +25 -2
  385. package/src/skill-state/workflow-mutation-guard.ts +104 -12
  386. package/src/slash-commands/builtin-registry.ts +1 -1
  387. package/src/ssh/utils.ts +14 -0
  388. package/src/task/discovery.ts +8 -2
  389. package/src/task/executor.ts +165 -47
  390. package/src/task/index.ts +123 -15
  391. package/src/task/provider-retry-status.ts +88 -0
  392. package/src/task/receipt.ts +3 -0
  393. package/src/task/render.ts +45 -26
  394. package/src/task/skc-command.ts +2 -2
  395. package/src/task/types.ts +55 -10
  396. package/src/task/ultragoal-redteam-activation.ts +75 -0
  397. package/src/tools/ask.ts +1 -1
  398. package/src/tools/ast-edit.ts +2 -2
  399. package/src/tools/bash-allowed-prefixes.ts +45 -1
  400. package/src/tools/bash-pty-selection.ts +2 -2
  401. package/src/tools/browser/launch.ts +41 -6
  402. package/src/tools/browser/screenshot-format.ts +6 -1
  403. package/src/tools/cron.ts +1 -1
  404. package/src/tools/fetch.ts +127 -154
  405. package/src/tools/image-gen.ts +476 -23
  406. package/src/tools/index.ts +7 -1
  407. package/src/tools/output-meta.ts +176 -9
  408. package/src/tools/path-utils.ts +5 -1
  409. package/src/tools/read-internals.ts +44 -0
  410. package/src/tools/read.ts +1544 -310
  411. package/src/tools/resource-gc.ts +665 -125
  412. package/src/tools/skill-discovery.ts +39 -3
  413. package/src/tools/sqlite-reader.ts +63 -5
  414. package/src/tools/subagent-render.ts +74 -16
  415. package/src/tools/subagent.ts +2 -0
  416. package/src/tools/tool-result.ts +14 -1
  417. package/src/utils/edit-mode.ts +2 -2
  418. package/src/utils/pasted-image-path.ts +15 -6
  419. package/src/utils/prompt-suggestion.ts +265 -0
  420. package/src/utils/shell-snapshot.ts +173 -63
  421. package/src/utils/sixel.ts +3 -3
  422. package/src/web/insane/bridge.ts +5 -1
  423. package/src/web/insane/url-guard.ts +140 -27
  424. package/src/web/scrapers/docs-rs.ts +30 -5
  425. package/src/web/scrapers/types.ts +19 -49
  426. package/src/web/scrapers/utils.ts +12 -26
  427. package/src/web/search/providers/codex.ts +1 -1
  428. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -6,6 +6,7 @@ import {
6
6
  type AuthenticateRequest,
7
7
  type AuthenticateResponse,
8
8
  type AuthMethod,
9
+ type AvailableCommand,
9
10
  type ClientCapabilities,
10
11
  type CloseSessionRequest,
11
12
  type CloseSessionResponse,
@@ -24,6 +25,7 @@ import {
24
25
  PROTOCOL_VERSION,
25
26
  type PromptRequest,
26
27
  type PromptResponse,
28
+ RequestError,
27
29
  type ResumeSessionRequest,
28
30
  type ResumeSessionResponse,
29
31
  type SessionInfo,
@@ -33,7 +35,7 @@ import {
33
35
  type SetSessionModeRequest,
34
36
  type SetSessionModeResponse,
35
37
  } from "@agentclientprotocol/sdk";
36
- import { getAgentDir } from "@sayknow-cli/utils";
38
+ import { getAgentDir, logger } from "@sayknow-cli/utils";
37
39
  import packageJson from "../../../package.json" with { type: "json" };
38
40
  import {
39
41
  type AcpProviderRegistration,
@@ -41,9 +43,15 @@ import {
41
43
  AcpSdkAdapter,
42
44
  AcpSdkAdapterError,
43
45
  } from "../../sdk/acp";
46
+ import { resolveAcpFinalText } from "../../sdk/acp/final-text";
47
+ import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
44
48
  import { ensureBroker } from "../../sdk/broker/ensure";
45
- import { readSdkBrokerDiscovery, SdkClient } from "../../sdk/client";
46
- import { mapAgentWireEventPayloadToAcpSessionUpdates } from "./acp-event-mapper";
49
+ import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
50
+ import {
51
+ buildToolCallStartUpdate,
52
+ mapAgentSessionEventToAcpSessionUpdates,
53
+ mapAgentWireEventPayloadToAcpSessionUpdates,
54
+ } from "./acp-event-mapper";
47
55
  import { resolveAcpPermissionMode } from "./permission-mode";
48
56
  import type { AcpStartupOptions } from "./startup-options";
49
57
  import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
@@ -52,6 +60,8 @@ const ACP_DEFAULT_MODE_ID = "default";
52
60
  const ACP_PLAN_MODE_ID = "plan";
53
61
  const MODE_CONFIG_ID = "mode";
54
62
  const MODEL_CONFIG_ID = "model";
63
+ const MODEL_PRESET_CONFIG_KEY = "modelPreset";
64
+ const ACP_CUSTOM_MODEL_PRESET = "__custom__";
55
65
  const THINKING_CONFIG_ID = "thinking";
56
66
  const SESSION_PAGE_SIZE = 50;
57
67
  export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
@@ -59,11 +69,10 @@ const MAX_ACP_REPLAY_PAGES = 10_000;
59
69
 
60
70
  type JsonObject = Record<string, unknown>;
61
71
  /**
62
- * ACP prompt completion is tied to a post-acknowledgement lifecycle boundary.
63
- * AgentSession events do not carry the command identity themselves, so the
64
- * host stamps command/turn identities into its replay ring and ACP also keeps
65
- * a per-endpoint ingress sequence. A frame observed before an acknowledgement
66
- * can never settle the waiter that acknowledgement creates.
72
+ * ACP prompt completion is tied to lifecycle activity observed after dispatch.
73
+ * A fast host may stream and finish a turn before acknowledging `turn.prompt`,
74
+ * so pre-ack activity is retained but cannot resolve the waiter until the
75
+ * acknowledgement supplies its command/turn correlation.
67
76
  */
68
77
  interface PromptWaiter {
69
78
  cancelRequested: boolean;
@@ -75,6 +84,7 @@ interface PromptWaiter {
75
84
  boundary: number;
76
85
  correlation: PromptCorrelation;
77
86
  messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
87
+ emittedAssistantText: string;
78
88
  pendingTerminal?: PromptCorrelation;
79
89
  resolve: (response: PromptResponse) => void;
80
90
  reject: (error: Error) => void;
@@ -88,6 +98,7 @@ type PendingAttachment = { epoch: number; task: Promise<void> };
88
98
  type SessionRecord = {
89
99
  cwd: string;
90
100
  adapter: AcpSdkAdapter;
101
+ closeIdempotencyKey: string;
91
102
  unsubscribe: () => void;
92
103
  reconnectUnsubscribe: () => void;
93
104
  /** Per-session frame work queue; callbacks never race prompt ownership. */
@@ -96,6 +107,10 @@ type SessionRecord = {
96
107
  inboundSequence: number;
97
108
  /** Updated at ingress so a prompt acknowledgement can distinguish a steer from a fresh turn. */
98
109
  busy: boolean;
110
+ /** Start/update args retained because tool_execution_end does not carry them. */
111
+ toolArgs: Map<string, unknown>;
112
+ /** Actionable model-profile authentication failure detected before prompt dispatch. */
113
+ authFailure?: string;
99
114
  activePrompt?: PromptWaiter;
100
115
  };
101
116
  type Endpoint = { url: string; token: string };
@@ -105,6 +120,9 @@ type BrokerSession = {
105
120
  locator?: { repo?: string };
106
121
  live?: boolean;
107
122
  endpointGeneration?: number;
123
+ endpointMtimeMs?: number;
124
+ title?: string;
125
+ updatedAt?: string;
108
126
  };
109
127
 
110
128
  function parseAcpStartupOptions(value: unknown): AcpStartupOptions | undefined {
@@ -135,7 +153,12 @@ function aggregateAcpFailure(code: string, message: string, failures: unknown[])
135
153
  }
136
154
 
137
155
  /** Applies ACP's offset cursor after narrowing the broker listing to the requested cwd. */
138
- export function paginateAcpSessions(listed: unknown[], cwd: string | undefined, offset: number): ListSessionsResponse {
156
+ export function paginateAcpSessions(
157
+ listed: unknown[],
158
+ cwd: string | undefined,
159
+ offset: number,
160
+ sessionMetadata: ReadonlyMap<string, { title?: string; updatedAt?: string }> = new Map(),
161
+ ): ListSessionsResponse {
139
162
  const filtered = listed
140
163
  .map(value => object(value) as BrokerSession | undefined)
141
164
  .filter(
@@ -143,12 +166,28 @@ export function paginateAcpSessions(listed: unknown[], cwd: string | undefined,
143
166
  typeof value?.sessionId === "string" && typeof value.locator?.repo === "string",
144
167
  )
145
168
  .filter(value => !cwd || value.locator.repo === cwd);
146
- const sessions = filtered
147
- .slice(offset, offset + SESSION_PAGE_SIZE)
148
- .map(
149
- value =>
150
- ({ sessionId: value.sessionId, cwd: value.locator.repo, title: value.sessionId }) satisfies SessionInfo,
151
- );
169
+ const sessions = filtered.slice(offset, offset + SESSION_PAGE_SIZE).map(value => {
170
+ const metadata = sessionMetadata.get(value.sessionId);
171
+ const updatedAt =
172
+ typeof metadata?.updatedAt === "string"
173
+ ? metadata.updatedAt
174
+ : typeof value.updatedAt === "string"
175
+ ? value.updatedAt
176
+ : typeof value.endpointMtimeMs === "number" && Number.isFinite(value.endpointMtimeMs)
177
+ ? new Date(value.endpointMtimeMs).toISOString()
178
+ : undefined;
179
+ return {
180
+ sessionId: value.sessionId,
181
+ cwd: value.locator.repo,
182
+ title:
183
+ typeof metadata?.title === "string" && metadata.title
184
+ ? metadata.title
185
+ : typeof value.title === "string" && value.title
186
+ ? value.title
187
+ : value.sessionId,
188
+ ...(updatedAt ? { updatedAt } : {}),
189
+ } satisfies SessionInfo;
190
+ });
152
191
  return {
153
192
  sessions,
154
193
  nextCursor: offset + sessions.length < filtered.length ? String(offset + sessions.length) : undefined,
@@ -179,6 +218,26 @@ function pageItems(value: unknown): unknown[] {
179
218
  return Array.isArray(page?.items) ? page.items : [];
180
219
  }
181
220
 
221
+ /** Build the ACP command palette from the shared builtins and live SDK skill state. */
222
+ export function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[] {
223
+ const commands = new Map<string, AvailableCommand>();
224
+ for (const item of pageItems(query)) {
225
+ const skill = object(item);
226
+ if (typeof skill?.name !== "string" || !skill.name) continue;
227
+ const name = `skill:${skill.name}`;
228
+ if (commands.has(name)) continue;
229
+ commands.set(name, {
230
+ name,
231
+ description:
232
+ typeof skill.description === "string" && skill.description
233
+ ? skill.description
234
+ : `Run the ${skill.name} skill`,
235
+ input: { hint: "[request]" },
236
+ });
237
+ }
238
+ return [...commands.values()];
239
+ }
240
+
182
241
  function correlationFrom(...values: unknown[]): PromptCorrelation {
183
242
  const correlation: PromptCorrelation = {};
184
243
  for (const value of values) {
@@ -267,6 +326,7 @@ function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
267
326
  const type = frame.state === "busy" ? "agent_start" : frame.state === "idle" ? "agent_end" : undefined;
268
327
  return type ? { event: { type, ...correlationFrom(frame) } } : undefined;
269
328
  }
329
+ if (frame.type === "agent_start" || frame.type === "agent_end") return { event: frame };
270
330
  if (frame.type !== "event") return undefined;
271
331
  const payload = object(frame.payload);
272
332
  if (!payload) return undefined;
@@ -330,6 +390,19 @@ function configValues(query: unknown): Map<string, string> {
330
390
  return values;
331
391
  }
332
392
 
393
+ function modelPresetConfigOptions(query: unknown, current: string): { value: string; name: string }[] {
394
+ const options = new Map<string, string>();
395
+ for (const item of pageItems(query)) {
396
+ const profile = object(item);
397
+ if (!profile || typeof profile.id !== "string") continue;
398
+ if (profile.available === false && profile.id !== current) continue;
399
+ options.set(profile.id, typeof profile.displayName === "string" ? profile.displayName : profile.id);
400
+ }
401
+ if (current === ACP_CUSTOM_MODEL_PRESET) options.set(ACP_CUSTOM_MODEL_PRESET, "Custom (current model)");
402
+ else if (!options.has(current)) options.set(current, current);
403
+ return [...options].map(([value, name]) => ({ value, name }));
404
+ }
405
+
333
406
  function modelConfigOptions(query: unknown, current: string | undefined): { value: string; name: string }[] {
334
407
  const options = new Map<string, string>();
335
408
  for (const item of pageItems(query)) {
@@ -347,9 +420,10 @@ const THINKING_CONFIG_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhi
347
420
  name: value,
348
421
  }));
349
422
 
350
- /** Maps live canonical SDK config and model queries into the ACP 1.2.1 session state surface. */
351
- export function acpSessionStateFromConfig(query: unknown, modelsQuery?: unknown) {
423
+ /** Maps live canonical SDK config and the selected model catalog into the ACP 1.2.1 session state surface. */
424
+ export function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: unknown, modelPreset?: string) {
352
425
  const values = configValues(query);
426
+ const useModelPresets = modelPreset !== undefined;
353
427
  const currentModeId = values.get(MODE_CONFIG_ID) === ACP_PLAN_MODE_ID ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
354
428
  return {
355
429
  configOptions: [
@@ -364,15 +438,28 @@ export function acpSessionStateFromConfig(query: unknown, modelsQuery?: unknown)
364
438
  ],
365
439
  },
366
440
  ...ACP_CONFIG_OPTIONS.flatMap(option => {
367
- const value = values.get(option.id);
441
+ const value =
442
+ option.id === MODEL_CONFIG_ID && useModelPresets
443
+ ? (values.get(MODEL_PRESET_CONFIG_KEY) ?? ACP_CUSTOM_MODEL_PRESET)
444
+ : values.get(option.id);
368
445
  if (value === undefined) return [];
369
446
  const options =
370
447
  option.id === MODEL_CONFIG_ID
371
- ? modelConfigOptions(modelsQuery, value)
448
+ ? useModelPresets
449
+ ? modelPresetConfigOptions(modelCatalogQuery, value)
450
+ : modelConfigOptions(modelCatalogQuery, value)
372
451
  : option.id === THINKING_CONFIG_ID
373
452
  ? THINKING_CONFIG_OPTIONS
374
453
  : [...option.options];
375
- return [{ ...option, type: "select" as const, currentValue: value, options }];
454
+ return [
455
+ {
456
+ ...option,
457
+ ...(option.id === MODEL_CONFIG_ID && useModelPresets ? { name: "Preset" } : {}),
458
+ type: "select" as const,
459
+ currentValue: value,
460
+ options,
461
+ },
462
+ ];
376
463
  }),
377
464
  ],
378
465
  modes: {
@@ -446,6 +533,34 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
446
533
  return { text: text.join("\n"), images };
447
534
  }
448
535
 
536
+ /**
537
+ * `AcpSdkAdapterError.code` is an internal string, but the SDK only derives a
538
+ * JSON-RPC code from a `RequestError`. Everything else collapses to an opaque
539
+ * `-32603 Internal error`, which hides the reason and defeats client-side
540
+ * recovery (an ACP client cannot see that it must authenticate). Map the codes
541
+ * that have a defined ACP/JSON-RPC counterpart onto a real `RequestError`.
542
+ */
543
+ export function acpRequestFailure(error: unknown): unknown {
544
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
545
+ if (typeof code !== "string") return error;
546
+ const message = error instanceof Error ? error.message : code;
547
+ switch (code) {
548
+ case "authentication_failed":
549
+ return RequestError.authRequired({ code, details: message }, message);
550
+ case "not_found":
551
+ return RequestError.resourceNotFound(message);
552
+ case "invalid_input":
553
+ case "unsupported":
554
+ case "unsupported_content":
555
+ return RequestError.invalidParams({ code, details: message }, message);
556
+ default:
557
+ // The remaining internal codes (conflict, unavailable, busy, …) have no ACP
558
+ // counterpart and stay -32603. Keep the discriminator in `data` so a client can
559
+ // branch on retry/reconnect instead of parsing an English message.
560
+ return RequestError.internalError({ code, details: message }, message);
561
+ }
562
+ }
563
+
449
564
  /** Registers a permission provider only when the ACP client requires prompts. */
450
565
  export function acpProviderRegistrations(
451
566
  capabilities: ClientCapabilities | undefined,
@@ -571,8 +686,11 @@ export class AcpAgent implements Agent {
571
686
  readonly #attaching = new Map<string, PendingAttachment>();
572
687
  readonly #resolvingExisting = new Map<string, PendingAttachment>();
573
688
  readonly #knownSessionCwds = new Map<string, string>();
689
+ readonly #knownSessionMetadata = new Map<string, { title?: string; updatedAt?: string }>();
690
+ readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
574
691
  readonly #sessionEpochs = new Map<string, number>();
575
692
  readonly #tearingDown = new Map<string, number>();
693
+ readonly #closing = new Map<string, Promise<CloseSessionResponse>>();
576
694
  #clientCapabilities: ClientCapabilities | undefined;
577
695
  #broker: Promise<BrokerConnection> | undefined;
578
696
  readonly #startupOptions: AcpStartupOptions | undefined;
@@ -621,7 +739,14 @@ export class AcpAgent implements Agent {
621
739
  agentCapabilities: {
622
740
  loadSession: true,
623
741
  promptCapabilities: { embeddedContext: true, image: true },
624
- sessionCapabilities: { list: {}, fork: {}, resume: {}, close: {}, delete: {} },
742
+ mcpCapabilities: { http: true, sse: true },
743
+ sessionCapabilities: {
744
+ list: {},
745
+ fork: {},
746
+ resume: {},
747
+ close: {},
748
+ delete: {},
749
+ },
625
750
  },
626
751
  };
627
752
  }
@@ -633,16 +758,19 @@ export class AcpAgent implements Agent {
633
758
  }
634
759
 
635
760
  async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
636
- this.#assertNoMcpServers(params);
761
+ const mcpServers = this.#mcpServers(params);
637
762
  this.#assertAbsoluteCwd(params.cwd);
638
- const result = await (await this.#brokerAdapter()).global(
763
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
764
+ const result = await this.#launchSessionWithMcp(
639
765
  "session.create",
640
766
  {
641
767
  cwd: params.cwd,
642
768
  target: { path: params.cwd },
643
769
  ...(this.#startupOptions?.modelPreset ? { modelPreset: this.#startupOptions.modelPreset } : {}),
770
+ ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
644
771
  },
645
772
  randomUUID(),
773
+ mcpServers,
646
774
  );
647
775
  const id = sessionId(result);
648
776
  this.#knownSessionCwds.set(id, params.cwd);
@@ -650,7 +778,7 @@ export class AcpAgent implements Agent {
650
778
  await this.#attach(id, params.cwd, endpoint(result));
651
779
  await applyAcpStartupOptions(this.#adapter(id), this.#startupOptions);
652
780
  this.#scheduleBootstrap(id);
653
- return { sessionId: id, ...(await this.#sessionState(id)) };
781
+ return { sessionId: id, ...(await this.#sessionState(id, true)) };
654
782
  } catch (error) {
655
783
  await this.#discardNewSession(id);
656
784
  throw error;
@@ -658,35 +786,40 @@ export class AcpAgent implements Agent {
658
786
  }
659
787
 
660
788
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
661
- this.#assertNoMcpServers(params);
789
+ const mcpServers = this.#mcpServers(params);
662
790
  this.#assertAbsoluteCwd(params.cwd);
663
- await this.#attachExisting(params.sessionId, params.cwd);
791
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
792
+ await this.#attachExisting(params.sessionId, params.cwd, mcpServers);
664
793
  await this.#replaySession(params.sessionId);
665
794
  this.#scheduleBootstrap(params.sessionId);
666
795
  return await this.#sessionState(params.sessionId);
667
796
  }
668
797
 
669
798
  async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
670
- this.#assertNoMcpServers(params);
799
+ const mcpServers = this.#mcpServers(params);
671
800
  this.#assertAbsoluteCwd(params.cwd);
672
- await this.#attachExisting(params.sessionId, params.cwd);
801
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
802
+ await this.#attachExisting(params.sessionId, params.cwd, mcpServers);
673
803
  this.#scheduleBootstrap(params.sessionId);
674
804
  return await this.#sessionState(params.sessionId);
675
805
  }
676
806
 
677
807
  async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
678
- this.#assertNoMcpServers(params);
808
+ const mcpServers = this.#mcpServers(params);
679
809
  this.#assertAbsoluteCwd(params.cwd);
810
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
680
811
  const source = await this.#resolveSavedSession(params.sessionId, params.cwd);
681
- const result = await (await this.#brokerAdapter()).global(
812
+ const result = await this.#launchSessionWithMcp(
682
813
  "session.fork",
683
814
  {
684
815
  cwd: params.cwd,
685
816
  sourceSessionId: params.sessionId,
686
817
  sourceSessionPath: source,
687
818
  target: { path: params.cwd },
819
+ ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
688
820
  },
689
821
  randomUUID(),
822
+ mcpServers,
690
823
  );
691
824
  const id = sessionId(result);
692
825
  this.#knownSessionCwds.set(id, params.cwd);
@@ -727,22 +860,29 @@ export class AcpAgent implements Agent {
727
860
  this.#knownSessionCwds.set(candidate.sessionId, params.cwd);
728
861
  }
729
862
  }
730
- return paginateAcpSessions(listed, params.cwd ?? undefined, this.#cursor(params.cursor));
863
+ return paginateAcpSessions(
864
+ listed,
865
+ params.cwd ?? undefined,
866
+ this.#cursor(params.cursor),
867
+ this.#knownSessionMetadata,
868
+ );
731
869
  }
732
870
 
733
- async closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
871
+ closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
734
872
  const record = this.#sessions.get(params.sessionId);
735
873
  const cwd = record?.cwd ?? this.#knownSessionCwds.get(params.sessionId);
736
874
  // ACP close has no cwd. Only connection-owned sessions may reach broker lifecycle control.
737
- if (!cwd) return {};
738
- this.#beginTeardown(params.sessionId);
739
- try {
740
- await this.#teardownSession(params.sessionId, "closed", true);
741
- this.#knownSessionCwds.delete(params.sessionId);
742
- return {};
743
- } finally {
744
- this.#finishTeardown(params.sessionId);
745
- }
875
+ if (!cwd) return Promise.resolve({});
876
+ const existing = this.#closing.get(params.sessionId);
877
+ if (existing) return existing;
878
+ const deferred = Promise.withResolvers<CloseSessionResponse>();
879
+ this.#closing.set(params.sessionId, deferred.promise);
880
+ void this.#closeOwnedSession(params.sessionId).then(deferred.resolve, deferred.reject);
881
+ const cleanup = deferred.promise.finally(() => {
882
+ if (this.#closing.get(params.sessionId) === deferred.promise) this.#closing.delete(params.sessionId);
883
+ });
884
+ void cleanup.catch(() => undefined);
885
+ return deferred.promise;
746
886
  }
747
887
 
748
888
  async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
@@ -760,6 +900,7 @@ export class AcpAgent implements Agent {
760
900
  } catch (error) {
761
901
  if (error instanceof AcpSdkAdapterError && error.code === "not_found") {
762
902
  this.#knownSessionCwds.delete(params.sessionId);
903
+ this.#knownSessionMetadata.delete(params.sessionId);
763
904
  return {};
764
905
  }
765
906
  throw error;
@@ -770,6 +911,7 @@ export class AcpAgent implements Agent {
770
911
  this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
771
912
  );
772
913
  this.#knownSessionCwds.delete(params.sessionId);
914
+ this.#knownSessionMetadata.delete(params.sessionId);
773
915
  return {};
774
916
  } finally {
775
917
  this.#finishTeardown(params.sessionId);
@@ -795,7 +937,11 @@ export class AcpAgent implements Agent {
795
937
  await this.setSessionMode({ sessionId: params.sessionId, modeId: params.value });
796
938
  break;
797
939
  case MODEL_CONFIG_ID:
798
- await this.#adapter(params.sessionId).setModel(params.value);
940
+ if (this.#startupOptions?.modelPreset === undefined) {
941
+ await this.#adapter(params.sessionId).setModel(params.value);
942
+ } else if (params.value !== ACP_CUSTOM_MODEL_PRESET) {
943
+ await this.#adapter(params.sessionId).control("model.profile.set", { id: params.value });
944
+ }
799
945
  break;
800
946
  case THINKING_CONFIG_ID:
801
947
  await this.#adapter(params.sessionId).control("thinking.set", { level: params.value });
@@ -818,6 +964,7 @@ export class AcpAgent implements Agent {
818
964
  const record = this.#sessions.get(params.sessionId);
819
965
  if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
820
966
  if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
967
+ if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
821
968
  const payload = acpPromptPayload(params.prompt);
822
969
  let waiter!: PromptWaiter;
823
970
  const response = new Promise<PromptResponse>((resolve, reject) => {
@@ -828,6 +975,7 @@ export class AcpAgent implements Agent {
828
975
  steeringAtAcknowledgement: record.busy,
829
976
  boundary: record.inboundSequence,
830
977
  correlation: {},
978
+ emittedAssistantText: "",
831
979
  resolve,
832
980
  reject,
833
981
  };
@@ -869,18 +1017,33 @@ export class AcpAgent implements Agent {
869
1017
  }
870
1018
 
871
1019
  async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
1020
+ // An unrecognized extension method is a protocol failure, not an application
1021
+ // result: it must reach the client as JSON-RPC -32601 rather than a resolved
1022
+ // payload. Recognized `_skc/*` methods keep their `{ok:false}` result contract.
1023
+ if (
1024
+ method !== "session/set_model" &&
1025
+ method !== "_skc/sdk/global" &&
1026
+ method !== "_skc/sdk/control" &&
1027
+ method !== "_skc/sdk/query"
1028
+ )
1029
+ throw RequestError.methodNotFound(method);
872
1030
  try {
1031
+ if (method === "session/set_model") {
1032
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
1033
+ const modelId = typeof params.modelId === "string" ? params.modelId : undefined;
1034
+ if (!sessionId) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
1035
+ if (!modelId) throw new AcpSdkAdapterError("invalid_input", "modelId is required.");
1036
+ await this.setSessionConfigOption({ sessionId, configId: MODEL_CONFIG_ID, value: modelId });
1037
+ return {};
1038
+ }
873
1039
  if (method === "_skc/sdk/global") {
874
1040
  const result = await (await this.#brokerAdapter()).handle(method, params);
875
1041
  return object(result) ?? {};
876
1042
  }
877
- if (method === "_skc/sdk/control" || method === "_skc/sdk/query") {
878
- const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
879
- if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
880
- const result = await this.#adapter(id).handle(method, params);
881
- return object(result) ?? {};
882
- }
883
- throw new AcpSdkAdapterError("method_not_found", `Unknown ACP ext method: ${method}`);
1043
+ const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
1044
+ if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
1045
+ const result = await this.#adapter(id).handle(method, params);
1046
+ return object(result) ?? {};
884
1047
  } catch (error) {
885
1048
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "internal";
886
1049
  const message = error instanceof Error ? error.message : String(error);
@@ -932,12 +1095,23 @@ export class AcpAgent implements Agent {
932
1095
  );
933
1096
  }
934
1097
 
935
- async #attachExisting(id: string, cwd: string): Promise<void> {
1098
+ #isDefinitiveBrokerResponse(error: unknown): boolean {
1099
+ // Response-derived client errors retain the broker error as details. Responses
1100
+ // that represent ongoing or ambiguous lifecycle work must keep their key.
1101
+ if (!(error instanceof SdkClientError)) return false;
1102
+ const details = object(error.details);
1103
+ if (details?.code !== error.code || details.message !== error.message) return false;
1104
+ return !["terminal_uncertain", "cleanup_pending", "broker_restarting", "unavailable"].includes(error.code);
1105
+ }
1106
+
1107
+ async #attachExisting(id: string, cwd: string, mcpServers: SessionLifecycleMcpServer[] = []): Promise<void> {
936
1108
  const epoch = this.#sessionEpoch(id);
937
1109
  const attached = this.#sessions.get(id);
938
1110
  if (attached) {
939
1111
  if (path.resolve(attached.cwd) !== path.resolve(cwd))
940
1112
  throw new AcpSdkAdapterError("conflict", `ACP session ${id} has conflicting cwd authority.`);
1113
+ if (mcpServers.length > 0)
1114
+ throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
941
1115
  return;
942
1116
  }
943
1117
  const knownCwd = this.#knownSessionCwds.get(id);
@@ -954,7 +1128,7 @@ export class AcpAgent implements Agent {
954
1128
  return;
955
1129
  }
956
1130
 
957
- const task = this.#resolveExistingAttachment(id, cwd, epoch);
1131
+ const task = this.#resolveExistingAttachment(id, cwd, epoch, mcpServers);
958
1132
  const pending = { epoch, task };
959
1133
  this.#resolvingExisting.set(id, pending);
960
1134
  try {
@@ -965,11 +1139,18 @@ export class AcpAgent implements Agent {
965
1139
  }
966
1140
  }
967
1141
 
968
- async #resolveExistingAttachment(id: string, cwd: string, epoch: number): Promise<void> {
1142
+ async #resolveExistingAttachment(
1143
+ id: string,
1144
+ cwd: string,
1145
+ epoch: number,
1146
+ mcpServers: SessionLifecycleMcpServer[],
1147
+ ): Promise<void> {
969
1148
  this.#assertSessionEpoch(id, epoch);
970
1149
  const indexed = await this.#scopedBrokerSession(id, cwd);
971
1150
  this.#assertSessionEpoch(id, epoch);
972
1151
  if (indexed?.live) {
1152
+ if (mcpServers.length > 0)
1153
+ throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
973
1154
  const result = await this.#brokerEndpoint(id, indexed.endpointGeneration);
974
1155
  this.#assertSessionEpoch(id, epoch);
975
1156
  await this.#attach(id, cwd, endpoint(result), epoch);
@@ -978,10 +1159,17 @@ export class AcpAgent implements Agent {
978
1159
 
979
1160
  const saved = await this.#resolveSavedSession(id, cwd);
980
1161
  this.#assertSessionEpoch(id, epoch);
981
- const result = await (await this.#brokerAdapter()).global(
1162
+ const result = await this.#launchSessionWithMcp(
982
1163
  "session.resume",
983
- { cwd, sessionId: id, sessionPath: saved, target: { path: cwd } },
1164
+ {
1165
+ cwd,
1166
+ sessionId: id,
1167
+ sessionPath: saved,
1168
+ target: { path: cwd },
1169
+ ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
1170
+ },
984
1171
  randomUUID(),
1172
+ mcpServers,
985
1173
  );
986
1174
  this.#assertSessionEpoch(id, epoch);
987
1175
  await this.#attach(id, cwd, endpoint(result), epoch);
@@ -1045,11 +1233,13 @@ export class AcpAgent implements Agent {
1045
1233
  const record: SessionRecord = {
1046
1234
  cwd,
1047
1235
  adapter,
1236
+ closeIdempotencyKey: randomUUID(),
1048
1237
  unsubscribe: () => {},
1049
1238
  reconnectUnsubscribe: () => {},
1050
1239
  frameTail: Promise.resolve(),
1051
1240
  inboundSequence: 0,
1052
1241
  busy: false,
1242
+ toolArgs: new Map(),
1053
1243
  };
1054
1244
  record.unsubscribe = adapter.onFrame(frame => this.#enqueueSdkFrame(id, adapter!, frame));
1055
1245
  record.reconnectUnsubscribe = adapter.onReconnectFailed(error =>
@@ -1059,6 +1249,7 @@ export class AcpAgent implements Agent {
1059
1249
  this.#knownSessionCwds.set(id, cwd);
1060
1250
  await applyAcpPermissionMode(adapter, this.#clientCapabilities);
1061
1251
  this.#assertSessionEpoch(id, epoch);
1252
+ this.#pendingCloseIdempotencyKeys.delete(id);
1062
1253
  } catch (error) {
1063
1254
  if (adapter && this.#sessions.get(id)?.adapter === adapter) {
1064
1255
  try {
@@ -1101,6 +1292,23 @@ export class AcpAgent implements Agent {
1101
1292
  async #discardNewSession(id: string): Promise<void> {
1102
1293
  await this.#teardownSession(id, "discarded", true);
1103
1294
  this.#knownSessionCwds.delete(id);
1295
+ this.#knownSessionMetadata.delete(id);
1296
+ }
1297
+
1298
+ async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
1299
+ this.#beginTeardown(id);
1300
+ try {
1301
+ const attaching = this.#attaching.get(id);
1302
+ // The record is published before permission initialization. Let a canceled
1303
+ // provisional attachment retire it before selecting the generation key.
1304
+ if (attaching) await Promise.allSettled([attaching.task]);
1305
+ await this.#teardownSession(id, "closed", true);
1306
+ this.#knownSessionCwds.delete(id);
1307
+ this.#knownSessionMetadata.delete(id);
1308
+ return {};
1309
+ } finally {
1310
+ this.#finishTeardown(id);
1311
+ }
1104
1312
  }
1105
1313
 
1106
1314
  /**
@@ -1130,13 +1338,13 @@ export class AcpAgent implements Agent {
1130
1338
  failures.push(error);
1131
1339
  }
1132
1340
  if (closeRemote) {
1341
+ const closeIdempotencyKey =
1342
+ record?.closeIdempotencyKey ?? this.#pendingCloseIdempotencyKeys.get(id) ?? randomUUID();
1343
+ this.#pendingCloseIdempotencyKeys.set(id, closeIdempotencyKey);
1133
1344
  try {
1134
- await (await this.#brokerAdapter()).global(
1135
- "session.close",
1136
- { sessionId: id },
1137
- this.#lifecycleIdempotencyKey(id, "session.close"),
1138
- );
1345
+ await (await this.#brokerAdapter()).global("session.close", { sessionId: id }, closeIdempotencyKey);
1139
1346
  } catch (error) {
1347
+ if (this.#isDefinitiveBrokerResponse(error)) this.#pendingCloseIdempotencyKeys.delete(id);
1140
1348
  if (!(ownershipBound && this.#isAlreadyGone(error))) failures.push(error);
1141
1349
  }
1142
1350
  }
@@ -1146,6 +1354,7 @@ export class AcpAgent implements Agent {
1146
1354
  .join("; ");
1147
1355
  throw aggregateAcpFailure("terminal_uncertain", `ACP session cleanup is uncertain: ${detail}`, failures);
1148
1356
  }
1357
+ if (closeRemote) this.#pendingCloseIdempotencyKeys.delete(id);
1149
1358
  } finally {
1150
1359
  this.#finishTeardown(id);
1151
1360
  }
@@ -1275,13 +1484,23 @@ export class AcpAgent implements Agent {
1275
1484
  const received = receivedSdkEvent(frame);
1276
1485
  if (!received) return;
1277
1486
  const { event, wirePayload } = received;
1487
+ const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
1488
+ if (
1489
+ toolCallId &&
1490
+ (event.type === "tool_execution_start" || event.type === "tool_execution_update") &&
1491
+ "args" in event
1492
+ ) {
1493
+ record.toolArgs.set(toolCallId, event.args);
1494
+ }
1278
1495
  const correlation = correlationFrom(frame, event);
1279
1496
  const activePrompt = record.activePrompt;
1280
1497
  // Prompt ownership is updated before publishing client notifications, but a
1281
- // terminal waiter is resolved only after publication succeeds so a failed
1282
- // sessionUpdate rejects the prompt instead of reporting false completion.
1498
+ // terminal waiter is resolved only after acknowledgement and publication
1499
+ // succeed. Fast hosts may emit the whole turn before acknowledging the
1500
+ // control request; retaining that activity prevents an idle ACP prompt from
1501
+ // remaining open forever.
1283
1502
  if (
1284
- activePrompt?.acknowledged &&
1503
+ activePrompt &&
1285
1504
  sequence > activePrompt.boundary &&
1286
1505
  !correlationsConflict(activePrompt.correlation, correlation)
1287
1506
  ) {
@@ -1290,6 +1509,8 @@ export class AcpAgent implements Agent {
1290
1509
  } else if (event.type === "agent_end") {
1291
1510
  if (
1292
1511
  correlationsMatch(activePrompt.correlation, correlation) ||
1512
+ correlation.commandId !== undefined ||
1513
+ correlation.turnId !== undefined ||
1293
1514
  activePrompt.activityObserved ||
1294
1515
  activePrompt.steeringAtAcknowledgement
1295
1516
  )
@@ -1299,17 +1520,69 @@ export class AcpAgent implements Agent {
1299
1520
  if (wirePayload) {
1300
1521
  for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
1301
1522
  cwd: record.cwd,
1523
+ getToolArgs: id => record.toolArgs.get(id),
1302
1524
  getMessageProgress: message => {
1303
1525
  if (!activePrompt || !object(message)) return undefined;
1304
1526
  activePrompt.messageProgress ??= { textEmitted: false, thoughtEmitted: false };
1305
1527
  return activePrompt.messageProgress;
1306
1528
  },
1307
- }))
1529
+ })) {
1530
+ if (
1531
+ activePrompt &&
1532
+ notification.update.sessionUpdate === "agent_message_chunk" &&
1533
+ notification.update.content.type === "text"
1534
+ )
1535
+ activePrompt.emittedAssistantText += notification.update.content.text;
1308
1536
  await this.#publishSessionUpdate(id, notification, adapter);
1537
+ }
1538
+ }
1539
+ if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
1540
+ if (event.type === "agent_start") {
1541
+ await this.#publishSessionUpdate(
1542
+ id,
1543
+ {
1544
+ sessionId: id,
1545
+ update: {
1546
+ sessionUpdate: "session_info_update",
1547
+ updatedAt: new Date().toISOString(),
1548
+ _meta: { skcPhase: "working", running: true, skcRunning: true },
1549
+ },
1550
+ },
1551
+ adapter,
1552
+ );
1309
1553
  }
1310
1554
  if (event.type === "message_end" && object(event.message)?.role === "assistant" && activePrompt)
1311
1555
  activePrompt.messageProgress = undefined;
1312
- if (event.type === "agent_end") await this.#emitEndOfTurnUpdates(id, adapter);
1556
+ if (event.type === "agent_end") {
1557
+ const finalText = typeof event.finalText === "string" ? event.finalText : "";
1558
+ if (activePrompt && finalText) {
1559
+ const resolution = resolveAcpFinalText(activePrompt.emittedAssistantText, finalText);
1560
+ if (resolution.kind === "emit") {
1561
+ activePrompt.emittedAssistantText += resolution.text;
1562
+ await this.#publishSessionUpdate(
1563
+ id,
1564
+ {
1565
+ sessionId: id,
1566
+ update: {
1567
+ sessionUpdate: "agent_message_chunk",
1568
+ content: { type: "text", text: resolution.text },
1569
+ ...(resolution.final.truncated ? { _meta: { skcFinalTextTruncated: true } } : {}),
1570
+ },
1571
+ },
1572
+ adapter,
1573
+ );
1574
+ } else if (resolution.kind === "divergent") {
1575
+ logger.warn("acp_final_text_diverged", {
1576
+ sessionId: id,
1577
+ ...(activePrompt.correlation.commandId ? { commandId: activePrompt.correlation.commandId } : {}),
1578
+ ...(activePrompt.correlation.turnId ? { turnId: activePrompt.correlation.turnId } : {}),
1579
+ streamedLength: activePrompt.emittedAssistantText.length,
1580
+ finalLength: resolution.final.text.length,
1581
+ });
1582
+ }
1583
+ }
1584
+ await this.#emitEndOfTurnUpdates(id, adapter);
1585
+ }
1313
1586
  if (activePrompt) this.#settlePrompt(record, activePrompt);
1314
1587
  }
1315
1588
 
@@ -1328,6 +1601,7 @@ export class AcpAgent implements Agent {
1328
1601
 
1329
1602
  async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
1330
1603
  let usage: JsonObject | undefined;
1604
+ let title: string | undefined;
1331
1605
  try {
1332
1606
  const response = object(await adapter.query("context.get"));
1333
1607
  const result = object(response?.result) ?? response;
@@ -1335,6 +1609,15 @@ export class AcpAgent implements Agent {
1335
1609
  } catch {
1336
1610
  // Context usage is advisory ACP metadata; prompt completion remains authoritative.
1337
1611
  }
1612
+ try {
1613
+ const response = object(await adapter.query("session.metadata"));
1614
+ const result = object(response?.result) ?? response;
1615
+ const metadata = pageItems(result)[0];
1616
+ const item = object(metadata);
1617
+ if (typeof item?.name === "string" && item.name) title = item.name;
1618
+ } catch {
1619
+ // Session naming is advisory; prompt completion remains authoritative.
1620
+ }
1338
1621
  if (typeof usage?.tokens === "number" && typeof usage.contextWindow === "number") {
1339
1622
  await this.#publishSessionUpdate(
1340
1623
  id,
@@ -1349,13 +1632,16 @@ export class AcpAgent implements Agent {
1349
1632
  adapter,
1350
1633
  );
1351
1634
  }
1635
+ const updatedAt = new Date().toISOString();
1636
+ this.#knownSessionMetadata.set(id, { ...(title ? { title } : {}), updatedAt });
1352
1637
  await this.#publishSessionUpdate(
1353
1638
  id,
1354
1639
  {
1355
1640
  sessionId: id,
1356
1641
  update: {
1357
1642
  sessionUpdate: "session_info_update",
1358
- updatedAt: new Date().toISOString(),
1643
+ ...(title ? { title } : {}),
1644
+ updatedAt,
1359
1645
  _meta: { skcPhase: "idle", running: false, skcRunning: false },
1360
1646
  },
1361
1647
  },
@@ -1379,27 +1665,68 @@ export class AcpAgent implements Agent {
1379
1665
  }
1380
1666
  }
1381
1667
 
1382
- async #sessionState(id: string): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
1668
+ async #sessionState(
1669
+ id: string,
1670
+ rejectUnavailableStartupPreset = false,
1671
+ ): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
1383
1672
  const record = this.#sessions.get(id);
1384
1673
  if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
1385
- const [config, models] = await Promise.all([
1674
+ const modelPreset = this.#startupOptions?.modelPreset;
1675
+ const [config, modelCatalog] = await Promise.all([
1386
1676
  record.adapter.query("config.list/get"),
1387
- record.adapter.query("models.list/current"),
1677
+ record.adapter.query(modelPreset === undefined ? "models.list/current" : "models.profiles.list"),
1388
1678
  ]);
1389
- return acpSessionStateFromConfig(config, models);
1679
+ record.authFailure = undefined;
1680
+ if (modelPreset !== undefined) {
1681
+ const activePreset = configValues(config).get(MODEL_PRESET_CONFIG_KEY);
1682
+ const activeProfile = pageItems(modelCatalog)
1683
+ .map(item => object(item))
1684
+ .find(item => item?.id === activePreset);
1685
+ if (activePreset && activeProfile?.available === false) {
1686
+ record.authFailure =
1687
+ `Model preset "${activePreset}" has no usable provider credentials. ` +
1688
+ "Authenticate the required provider in Sayknow-CLI or select an available preset before prompting.";
1689
+ if (rejectUnavailableStartupPreset && activePreset === modelPreset)
1690
+ throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
1691
+ }
1692
+ }
1693
+ return acpSessionStateFromConfig(config, modelCatalog, modelPreset);
1694
+ }
1695
+
1696
+ async #publishAvailableCommands(id: string, adapter: AcpSdkAdapter): Promise<void> {
1697
+ let skills: unknown;
1698
+ try {
1699
+ skills = await adapter.query("skill.list/state");
1700
+ } catch {
1701
+ // Builtins remain useful when an older SDK host cannot expose skill state.
1702
+ }
1703
+ await this.#publishSessionUpdate(
1704
+ id,
1705
+ {
1706
+ sessionId: id,
1707
+ update: {
1708
+ sessionUpdate: "available_commands_update",
1709
+ availableCommands: acpAvailableCommandsFromSkills(skills),
1710
+ },
1711
+ },
1712
+ adapter,
1713
+ );
1390
1714
  }
1391
1715
 
1392
1716
  async #replaySession(id: string): Promise<void> {
1393
1717
  const adapter = this.#adapter(id);
1718
+ const record = this.#sessions.get(id);
1719
+ if (!record) return;
1394
1720
  let cursor: string | undefined;
1395
1721
  let imageLimitationReported = false;
1722
+ const replayTools = new Map<string, { name: string; args: unknown }>();
1396
1723
  for (let pageCount = 0; pageCount < MAX_ACP_REPLAY_PAGES; pageCount++) {
1397
1724
  const response = object(await adapter.query("transcript.list", {}, cursor));
1398
1725
  const result = object(response?.result) ?? response;
1399
1726
  const page = object(result?.page);
1400
1727
  for (const item of Array.isArray(page?.items) ? page.items : []) {
1401
1728
  const message = object(item);
1402
- if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
1729
+ if (!message) continue;
1403
1730
  const content = transcriptReplayContent(message);
1404
1731
  if (!imageLimitationReported) {
1405
1732
  imageLimitationReported = true;
@@ -1416,6 +1743,95 @@ export class AcpAgent implements Agent {
1416
1743
  );
1417
1744
  }
1418
1745
  const messageId = typeof message.id === "string" ? message.id : undefined;
1746
+ const richContent = Array.isArray(message.content) ? message.content : undefined;
1747
+ if ((message.role === "user" || message.role === "assistant") && richContent) {
1748
+ for (const rawBlock of richContent) {
1749
+ const block = object(rawBlock);
1750
+ if (!block || typeof block.type !== "string") continue;
1751
+ if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
1752
+ await this.#publishSessionUpdate(
1753
+ id,
1754
+ {
1755
+ sessionId: id,
1756
+ update: {
1757
+ sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk",
1758
+ content: { type: "text", text: block.text },
1759
+ ...(messageId ? { messageId } : {}),
1760
+ },
1761
+ },
1762
+ adapter,
1763
+ );
1764
+ } else if (
1765
+ message.role === "assistant" &&
1766
+ block.type === "thinking" &&
1767
+ typeof block.thinking === "string" &&
1768
+ block.thinking.length > 0
1769
+ ) {
1770
+ await this.#publishSessionUpdate(
1771
+ id,
1772
+ {
1773
+ sessionId: id,
1774
+ update: {
1775
+ sessionUpdate: "agent_thought_chunk",
1776
+ content: { type: "text", text: block.thinking },
1777
+ ...(messageId ? { messageId } : {}),
1778
+ },
1779
+ },
1780
+ adapter,
1781
+ );
1782
+ } else if (
1783
+ message.role === "assistant" &&
1784
+ block.type === "toolCall" &&
1785
+ typeof block.id === "string" &&
1786
+ typeof block.name === "string"
1787
+ ) {
1788
+ const args = block.arguments ?? {};
1789
+ replayTools.set(block.id, { name: block.name, args });
1790
+ await this.#publishSessionUpdate(
1791
+ id,
1792
+ {
1793
+ sessionId: id,
1794
+ update: buildToolCallStartUpdate({
1795
+ toolCallId: block.id,
1796
+ toolName: block.name,
1797
+ args,
1798
+ cwd: record.cwd,
1799
+ }),
1800
+ },
1801
+ adapter,
1802
+ );
1803
+ }
1804
+ }
1805
+ continue;
1806
+ }
1807
+ if (message.role === "toolResult" && typeof message.toolCallId === "string") {
1808
+ const replayTool = replayTools.get(message.toolCallId);
1809
+ const toolName = typeof message.toolName === "string" ? message.toolName : replayTool?.name;
1810
+ if (!toolName) continue;
1811
+ const resultContent = richContent
1812
+ ?.map(object)
1813
+ .filter(
1814
+ (block): block is JsonObject =>
1815
+ block !== undefined && block.type === "text" && typeof block.text === "string",
1816
+ )
1817
+ .map(block => ({ type: "text" as const, text: String(block.text) }));
1818
+ for (const notification of mapAgentSessionEventToAcpSessionUpdates(
1819
+ {
1820
+ type: "tool_execution_end",
1821
+ toolCallId: message.toolCallId,
1822
+ toolName,
1823
+ result: { content: resultContent ?? content.blocks },
1824
+ isError: message.isError === true,
1825
+ } as never,
1826
+ id,
1827
+ { cwd: record.cwd, getToolArgs: toolCallId => replayTools.get(toolCallId)?.args },
1828
+ )) {
1829
+ await this.#publishSessionUpdate(id, notification, adapter);
1830
+ }
1831
+ replayTools.delete(message.toolCallId);
1832
+ continue;
1833
+ }
1834
+ if (message.role !== "user" && message.role !== "assistant") continue;
1419
1835
  for (const block of content.blocks) {
1420
1836
  await this.#publishSessionUpdate(
1421
1837
  id,
@@ -1441,17 +1857,33 @@ export class AcpAgent implements Agent {
1441
1857
  setTimeout(() => {
1442
1858
  const record = this.#sessions.get(id);
1443
1859
  if (!record || this.#connection.signal.aborted) return;
1444
- void this.#publishSessionUpdate(
1445
- id,
1446
- {
1447
- sessionId: id,
1448
- update: {
1449
- sessionUpdate: "session_info_update",
1450
- _meta: { skcPhase: "idle", running: false, skcRunning: false },
1860
+ void (async () => {
1861
+ await this.#publishAvailableCommands(id, record.adapter);
1862
+ if (record.authFailure) {
1863
+ await this.#publishSessionUpdate(
1864
+ id,
1865
+ {
1866
+ sessionId: id,
1867
+ update: {
1868
+ sessionUpdate: "agent_thought_chunk",
1869
+ content: { type: "text", text: `[error:auth] ${record.authFailure}\n` },
1870
+ },
1871
+ },
1872
+ record.adapter,
1873
+ );
1874
+ }
1875
+ await this.#publishSessionUpdate(
1876
+ id,
1877
+ {
1878
+ sessionId: id,
1879
+ update: {
1880
+ sessionUpdate: "session_info_update",
1881
+ _meta: { skcPhase: "idle", running: false, skcRunning: false },
1882
+ },
1451
1883
  },
1452
- },
1453
- record.adapter,
1454
- ).catch(() => undefined);
1884
+ record.adapter,
1885
+ );
1886
+ })().catch(() => undefined);
1455
1887
  }, ACP_BOOTSTRAP_RACE_GUARD_MS);
1456
1888
  }
1457
1889
 
@@ -1466,9 +1898,138 @@ export class AcpAgent implements Agent {
1466
1898
  if (!path.isAbsolute(cwd)) throw new Error(`ACP cwd must be an absolute path: ${cwd}`);
1467
1899
  }
1468
1900
 
1469
- #assertNoMcpServers(params: { mcpServers?: unknown[] }): void {
1470
- if (params.mcpServers && params.mcpServers.length > 0)
1471
- throw new AcpSdkAdapterError("unsupported", "MCP servers are unsupported under SDK-backed ACP.");
1901
+ #assertNoAdditionalDirectories(directories: string[] | null | undefined): void {
1902
+ if (directories && directories.length > 0)
1903
+ throw new AcpSdkAdapterError("unsupported", "ACP additional directories are not supported.");
1904
+ }
1905
+
1906
+ #mcpServers(params: { mcpServers?: unknown[] }): SessionLifecycleMcpServer[] {
1907
+ const servers = params.mcpServers ?? [];
1908
+ if (servers.length > 64)
1909
+ throw new AcpSdkAdapterError("unsupported", "ACP supports at most 64 MCP servers per session.");
1910
+ const result: SessionLifecycleMcpServer[] = [];
1911
+ const names = new Set<string>();
1912
+ for (const value of servers) {
1913
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1914
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP server definitions must be objects.");
1915
+ const server = value as Record<string, unknown>;
1916
+ if (typeof server.name !== "string" || !/^[A-Za-z0-9_.-]{1,100}$/.test(server.name) || names.has(server.name))
1917
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP servers must have unique safe names.");
1918
+ names.add(server.name);
1919
+ if (server.type === "http" || server.type === "sse") {
1920
+ if (
1921
+ typeof server.url !== "string" ||
1922
+ server.url.length > 8_192 ||
1923
+ !Array.isArray(server.headers) ||
1924
+ server.headers.length > 100
1925
+ )
1926
+ throw new AcpSdkAdapterError("invalid_input", "ACP remote MCP servers require a valid URL and headers.");
1927
+ let parsedUrl: URL;
1928
+ try {
1929
+ parsedUrl = new URL(server.url);
1930
+ } catch {
1931
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP URL is invalid.");
1932
+ }
1933
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:")
1934
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP URLs must use HTTP or HTTPS.");
1935
+ const headers: Record<string, string> = {};
1936
+ for (const value of server.headers) {
1937
+ const header = object(value);
1938
+ if (
1939
+ typeof header?.name !== "string" ||
1940
+ header.name.length === 0 ||
1941
+ header.name.length > 256 ||
1942
+ header.name.includes("\r") ||
1943
+ header.name.includes("\n") ||
1944
+ typeof header.value !== "string" ||
1945
+ header.value.length > 8_192 ||
1946
+ header.value.includes("\r") ||
1947
+ header.value.includes("\n") ||
1948
+ Object.hasOwn(headers, header.name)
1949
+ )
1950
+ throw new AcpSdkAdapterError(
1951
+ "invalid_input",
1952
+ "ACP MCP headers must have unique valid names and values.",
1953
+ );
1954
+ headers[header.name] = header.value;
1955
+ }
1956
+ result.push({
1957
+ type: server.type,
1958
+ name: server.name,
1959
+ url: parsedUrl.toString(),
1960
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
1961
+ });
1962
+ continue;
1963
+ }
1964
+ if (
1965
+ (server.type !== undefined && server.type !== "stdio") ||
1966
+ typeof server.command !== "string" ||
1967
+ server.command.length > 4_096 ||
1968
+ !path.isAbsolute(server.command) ||
1969
+ !Array.isArray(server.args) ||
1970
+ server.args.length > 100 ||
1971
+ !server.args.every(argument => typeof argument === "string" && argument.length <= 8_192) ||
1972
+ !Array.isArray(server.env) ||
1973
+ server.env.length > 100
1974
+ )
1975
+ throw new AcpSdkAdapterError(
1976
+ "invalid_input",
1977
+ "ACP stdio MCP servers require an absolute command and bounded arguments and environment variables.",
1978
+ );
1979
+ const env: Record<string, string> = {};
1980
+ for (const value of server.env) {
1981
+ const variable = object(value);
1982
+ if (
1983
+ typeof variable?.name !== "string" ||
1984
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable.name) ||
1985
+ typeof variable.value !== "string" ||
1986
+ variable.value.length > 32_768 ||
1987
+ Object.hasOwn(env, variable.name)
1988
+ )
1989
+ throw new AcpSdkAdapterError(
1990
+ "invalid_input",
1991
+ "ACP MCP environment variables must have unique valid names and string values.",
1992
+ );
1993
+ env[variable.name] = variable.value;
1994
+ }
1995
+ result.push({
1996
+ name: server.name,
1997
+ command: server.command,
1998
+ args: server.args as string[],
1999
+ ...(Object.keys(env).length > 0 ? { env } : {}),
2000
+ });
2001
+ }
2002
+ return result;
2003
+ }
2004
+
2005
+ async #launchSessionWithMcp(
2006
+ operation: "session.create" | "session.fork" | "session.resume",
2007
+ input: JsonObject,
2008
+ idempotencyKey: string,
2009
+ mcpServers: SessionLifecycleMcpServer[],
2010
+ ): Promise<unknown> {
2011
+ try {
2012
+ return await (await this.#brokerAdapter()).global(operation, input, idempotencyKey);
2013
+ } catch (error) {
2014
+ const code =
2015
+ typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
2016
+ ? error.code
2017
+ : undefined;
2018
+ if (
2019
+ mcpServers.length === 0 ||
2020
+ code === "invalid_input" ||
2021
+ code === "authentication_failed" ||
2022
+ code === "unknown_model_profile" ||
2023
+ code === "model_profile_registry_error"
2024
+ )
2025
+ throw error;
2026
+ const names = mcpServers
2027
+ .slice(0, 8)
2028
+ .map(server => server.name)
2029
+ .join(", ");
2030
+ const suffix = mcpServers.length > 8 ? `, and ${mcpServers.length - 8} more` : "";
2031
+ throw new AcpSdkAdapterError("unavailable", `MCP server request failed to start (${names}${suffix}).`);
2032
+ }
1472
2033
  }
1473
2034
 
1474
2035
  #beginDispose(): void {
@@ -1494,6 +2055,9 @@ export class AcpAgent implements Agent {
1494
2055
  this.#attaching.clear();
1495
2056
  this.#resolvingExisting.clear();
1496
2057
  this.#knownSessionCwds.clear();
2058
+ this.#knownSessionMetadata.clear();
2059
+ this.#pendingCloseIdempotencyKeys.clear();
2060
+ if (this.#closing.size === 0) this.#closing.clear();
1497
2061
  this.#tearingDown.clear();
1498
2062
  if (this.#broker) {
1499
2063
  const broker = this.#broker;