zidane 7.2.4 → 8.0.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 (376) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -6
  3. package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
  4. package/dist/acp-DQDhi2dj.js.map +1 -0
  5. package/dist/acp-cli.d.ts +1 -1
  6. package/dist/acp-cli.js +9 -66
  7. package/dist/acp-cli.js.map +1 -1
  8. package/dist/acp.d.ts +10 -590
  9. package/dist/acp.d.ts.map +1 -1
  10. package/dist/acp.js +3 -3
  11. package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
  12. package/dist/agent-DSLNmBp0.js.map +1 -0
  13. package/dist/agent.d.ts +1 -1
  14. package/dist/agent.js +3 -2
  15. package/dist/aliasing-lVFbm81n.js +217 -0
  16. package/dist/aliasing-lVFbm81n.js.map +1 -0
  17. package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
  18. package/dist/anthropic-CD00LhUP.js.map +1 -0
  19. package/dist/artifacts-CV2hUKnL.js.map +1 -1
  20. package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
  21. package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
  22. package/dist/call-tool-dHlbGOIY.js +70 -0
  23. package/dist/call-tool-dHlbGOIY.js.map +1 -0
  24. package/dist/chat/pure.d.ts +372 -6
  25. package/dist/chat/pure.d.ts.map +1 -1
  26. package/dist/chat/pure.js +35 -244
  27. package/dist/chat/pure.js.map +1 -1
  28. package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
  29. package/dist/chat-JPJJ8uu5.js.map +1 -0
  30. package/dist/chat.d.ts +4 -6
  31. package/dist/chat.js +23 -12
  32. package/dist/cli-shared-C-YCaglL.js +159 -0
  33. package/dist/cli-shared-C-YCaglL.js.map +1 -0
  34. package/dist/client-core-BQSyDALT.d.ts +2115 -0
  35. package/dist/client-core-BQSyDALT.d.ts.map +1 -0
  36. package/dist/codemode-worker.d.ts +1 -0
  37. package/dist/codemode-worker.js +155 -0
  38. package/dist/codemode-worker.js.map +1 -0
  39. package/dist/codemode.d.ts +352 -0
  40. package/dist/codemode.d.ts.map +1 -0
  41. package/dist/codemode.js +5 -0
  42. package/dist/connection-Dr-Js4yZ.d.ts +28 -0
  43. package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
  44. package/dist/connection-loop-gdh22LTI.js +112 -0
  45. package/dist/connection-loop-gdh22LTI.js.map +1 -0
  46. package/dist/context-segments-BHKlagL6.js +1016 -0
  47. package/dist/context-segments-BHKlagL6.js.map +1 -0
  48. package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
  49. package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
  50. package/dist/contexts/daytona.d.ts +19 -8
  51. package/dist/contexts/daytona.d.ts.map +1 -1
  52. package/dist/contexts/daytona.js +1 -440
  53. package/dist/contexts/docker.d.ts +36 -7
  54. package/dist/contexts/docker.d.ts.map +1 -1
  55. package/dist/contexts/docker.js +1 -456
  56. package/dist/contexts/e2b.d.ts +68 -13
  57. package/dist/contexts/e2b.d.ts.map +1 -1
  58. package/dist/contexts/e2b.js +1 -382
  59. package/dist/contexts/sandbox.d.ts +1 -1
  60. package/dist/contexts/sandbox.js +5 -0
  61. package/dist/contexts/sandbox.js.map +1 -1
  62. package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
  63. package/dist/contexts-d7UolYot.js.map +1 -0
  64. package/dist/contexts.d.ts +3 -3
  65. package/dist/contexts.js +1 -1
  66. package/dist/cred-keys-GpBg71cP.js +105 -0
  67. package/dist/cred-keys-GpBg71cP.js.map +1 -0
  68. package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
  69. package/dist/credentials-JttEuRON.js.map +1 -0
  70. package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
  71. package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
  72. package/dist/daemon-cli.d.ts +1 -1
  73. package/dist/daemon-cli.js +3 -2
  74. package/dist/daemon-cli.js.map +1 -1
  75. package/dist/daytona-DG3omipZ.js +513 -0
  76. package/dist/daytona-DG3omipZ.js.map +1 -0
  77. package/dist/defaults-Cx3zvDOV.js +23 -0
  78. package/dist/defaults-Cx3zvDOV.js.map +1 -0
  79. package/dist/dispatcher-BMlu2BmZ.js +4941 -0
  80. package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
  81. package/dist/docker-BZhMMJUp.js +727 -0
  82. package/dist/docker-BZhMMJUp.js.map +1 -0
  83. package/dist/e2b-CEVOdRib.js +480 -0
  84. package/dist/e2b-CEVOdRib.js.map +1 -0
  85. package/dist/edit-utils-EGosADZq.js +234 -0
  86. package/dist/edit-utils-EGosADZq.js.map +1 -0
  87. package/dist/environment-8wsKE2Ln.js +214 -0
  88. package/dist/environment-8wsKE2Ln.js.map +1 -0
  89. package/dist/eval-BNcvle_J.js +1595 -0
  90. package/dist/eval-BNcvle_J.js.map +1 -0
  91. package/dist/eval.d.ts +2 -2
  92. package/dist/eval.js +1 -1489
  93. package/dist/executor-Cx5-8yqi.js +621 -0
  94. package/dist/executor-Cx5-8yqi.js.map +1 -0
  95. package/dist/extension-authoring-DyGGmq9V.js +385 -0
  96. package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
  97. package/dist/extensions.d.ts +2 -2
  98. package/dist/extensions.js +3 -3
  99. package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
  100. package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
  101. package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
  102. package/dist/format-C2NQEVUH.js.map +1 -0
  103. package/dist/format-bytes-C7tfc3Zm.js +55 -0
  104. package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
  105. package/dist/headless-C_rNUuYC.js +960 -0
  106. package/dist/headless-C_rNUuYC.js.map +1 -0
  107. package/dist/headless.d.ts +1 -1
  108. package/dist/headless.js +1 -413
  109. package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
  110. package/dist/history-search-CcYzF8-0.js.map +1 -0
  111. package/dist/history-search-worker.d.ts +1 -1
  112. package/dist/history-search-worker.js +1 -1
  113. package/dist/host-D7NrI03V.js +378 -0
  114. package/dist/host-D7NrI03V.js.map +1 -0
  115. package/dist/host-signals-BcP5QKUD.js +118 -0
  116. package/dist/host-signals-BcP5QKUD.js.map +1 -0
  117. package/dist/identity-CwfO8K-t.js +26 -0
  118. package/dist/identity-CwfO8K-t.js.map +1 -0
  119. package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
  120. package/dist/index-BGe1fQT7.d.ts.map +1 -0
  121. package/dist/index-BVQsIKGn.d.ts +448 -0
  122. package/dist/index-BVQsIKGn.d.ts.map +1 -0
  123. package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
  124. package/dist/index-CHABJd-L.d.ts.map +1 -0
  125. package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
  126. package/dist/index-D6a7qXpg.d.ts.map +1 -0
  127. package/dist/index.d.ts +6 -6
  128. package/dist/index.js +27 -23
  129. package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
  130. package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
  131. package/dist/ipc/remote.d.ts +313 -0
  132. package/dist/ipc/remote.d.ts.map +1 -0
  133. package/dist/ipc/remote.js +4 -0
  134. package/dist/ipc.d.ts +6 -964
  135. package/dist/ipc.js +403 -4
  136. package/dist/ipc.js.map +1 -0
  137. package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
  138. package/dist/isolation-D9L650W5.d.ts.map +1 -0
  139. package/dist/json-rpc--HXQti1E.js +350 -0
  140. package/dist/json-rpc--HXQti1E.js.map +1 -0
  141. package/dist/links-j8rED1sR.js +603 -0
  142. package/dist/links-j8rED1sR.js.map +1 -0
  143. package/dist/local-terminal-D8QThwo_.js +252 -0
  144. package/dist/local-terminal-D8QThwo_.js.map +1 -0
  145. package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
  146. package/dist/logger-Gtqlayx8.js.map +1 -0
  147. package/dist/logs-view-CWG8aUAG.js.map +1 -1
  148. package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
  149. package/dist/mcp-_wecH9Qe.js.map +1 -0
  150. package/dist/mcp.d.ts +2 -2
  151. package/dist/mcp.js +2 -2
  152. package/dist/media-sniff-UKijEaiQ.js.map +1 -1
  153. package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
  154. package/dist/messages-BqiwcD2Q.js.map +1 -0
  155. package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
  156. package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
  157. package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
  158. package/dist/openai-compat-CFrpH7uq.js.map +1 -0
  159. package/dist/orchestration-BCGmB8Ps.js +511 -0
  160. package/dist/orchestration-BCGmB8Ps.js.map +1 -0
  161. package/dist/output/stream-json.d.ts +1 -2
  162. package/dist/output/stream-json.d.ts.map +1 -1
  163. package/dist/output/stream-json.js +1 -1
  164. package/dist/output/terminal.d.ts +1 -2
  165. package/dist/output/terminal.d.ts.map +1 -1
  166. package/dist/output/terminal.js +13 -7
  167. package/dist/output/terminal.js.map +1 -1
  168. package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
  169. package/dist/plugins-DS7mahzZ.js.map +1 -0
  170. package/dist/plugins.d.ts +1 -1
  171. package/dist/plugins.js +1 -2
  172. package/dist/prelude-Cxo4t74B.js +517 -0
  173. package/dist/prelude-Cxo4t74B.js.map +1 -0
  174. package/dist/presets.d.ts +1 -1
  175. package/dist/presets.js +1 -2
  176. package/dist/project-root-B8zADr4o.js +167 -0
  177. package/dist/project-root-B8zADr4o.js.map +1 -0
  178. package/dist/project-user-paths-BSgtrRxl.js +58 -0
  179. package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
  180. package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
  181. package/dist/prompts-DYcvqShf.d.ts.map +1 -0
  182. package/dist/protocol-DnhqqSmE.js +13 -0
  183. package/dist/protocol-DnhqqSmE.js.map +1 -0
  184. package/dist/providers/anthropic.d.ts +2 -2
  185. package/dist/providers/anthropic.js +1 -2
  186. package/dist/providers/arcee.d.ts +1 -1
  187. package/dist/providers/arcee.js +1 -1
  188. package/dist/providers/baseten.d.ts +1 -1
  189. package/dist/providers/baseten.js +1 -1
  190. package/dist/providers/cerebras.d.ts +1 -1
  191. package/dist/providers/cerebras.js +3 -2
  192. package/dist/providers/cerebras.js.map +1 -1
  193. package/dist/providers/local.d.ts +1 -1
  194. package/dist/providers/local.js +1 -1
  195. package/dist/providers/openai-compat.d.ts +1 -1
  196. package/dist/providers/openai-compat.js +1 -2
  197. package/dist/providers/openai.d.ts +1 -1
  198. package/dist/providers/openai.js +15 -12
  199. package/dist/providers/openai.js.map +1 -1
  200. package/dist/providers/openrouter.d.ts +1 -1
  201. package/dist/providers/openrouter.js +3 -2
  202. package/dist/providers/openrouter.js.map +1 -1
  203. package/dist/providers/xai.d.ts +1 -1
  204. package/dist/providers/xai.js +3 -3
  205. package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
  206. package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
  207. package/dist/providers.d.ts +1 -1
  208. package/dist/providers.js +4 -4
  209. package/dist/registry-D6ShBWJU.js +5234 -0
  210. package/dist/registry-D6ShBWJU.js.map +1 -0
  211. package/dist/remote-hosts-BtA_9ZGp.js +97 -0
  212. package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
  213. package/dist/restate.d.ts +2 -3
  214. package/dist/restate.d.ts.map +1 -1
  215. package/dist/restate.js +631 -572
  216. package/dist/restate.js.map +1 -1
  217. package/dist/run-summary-CklLiZ4y.js +288 -0
  218. package/dist/run-summary-CklLiZ4y.js.map +1 -0
  219. package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
  220. package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
  221. package/dist/session/sqlite.d.ts +1 -2
  222. package/dist/session/sqlite.d.ts.map +1 -1
  223. package/dist/session/sqlite.js +1 -616
  224. package/dist/session-factory-BUGY5P40.js +1576 -0
  225. package/dist/session-factory-BUGY5P40.js.map +1 -0
  226. package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
  227. package/dist/session-xh9JXu6i.js.map +1 -0
  228. package/dist/session.d.ts +1 -1
  229. package/dist/session.js +2 -2
  230. package/dist/settings-keys-Ckf29NeM.js +677 -0
  231. package/dist/settings-keys-Ckf29NeM.js.map +1 -0
  232. package/dist/shell-path-Dqaoow2J.js +412 -0
  233. package/dist/shell-path-Dqaoow2J.js.map +1 -0
  234. package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
  235. package/dist/skills-DKqZJm4X.js.map +1 -0
  236. package/dist/skills.d.ts +2 -2
  237. package/dist/skills.js +1 -2
  238. package/dist/source-mPExaUm8.js +381 -0
  239. package/dist/source-mPExaUm8.js.map +1 -0
  240. package/dist/sqlite-DV1DQzN2.js +698 -0
  241. package/dist/sqlite-DV1DQzN2.js.map +1 -0
  242. package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
  243. package/dist/src-BRMYRJib.js.map +1 -0
  244. package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
  245. package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
  246. package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
  247. package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
  248. package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
  249. package/dist/timeout-DEaqslLO.d.ts.map +1 -1
  250. package/dist/todo-edits-Dn251YUP.js +1790 -0
  251. package/dist/todo-edits-Dn251YUP.js.map +1 -0
  252. package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
  253. package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
  254. package/dist/tool-state-Dku6YqAB.js +989 -0
  255. package/dist/tool-state-Dku6YqAB.js.map +1 -0
  256. package/dist/tools/fetch-url.d.ts +1 -2
  257. package/dist/tools/fetch-url.d.ts.map +1 -1
  258. package/dist/tools/fetch-url.js +1 -2
  259. package/dist/tools/prompts.d.ts +1 -1
  260. package/dist/tools/prompts.js +7 -7
  261. package/dist/tools/prompts.js.map +1 -1
  262. package/dist/tools/web-search.d.ts +1 -2
  263. package/dist/tools/web-search.d.ts.map +1 -1
  264. package/dist/tools/web-search.js +2 -2
  265. package/dist/tools/web-search.js.map +1 -1
  266. package/dist/tools-_DnT3AGM.js +578 -0
  267. package/dist/tools-_DnT3AGM.js.map +1 -0
  268. package/dist/tools.d.ts +3 -3
  269. package/dist/tools.js +4 -2
  270. package/dist/tui.d.ts +534 -517
  271. package/dist/tui.d.ts.map +1 -1
  272. package/dist/tui.js +34933 -27242
  273. package/dist/tui.js.map +1 -1
  274. package/dist/turn-operations-B8sokwi5.js +333 -0
  275. package/dist/turn-operations-B8sokwi5.js.map +1 -0
  276. package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
  277. package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
  278. package/dist/types-C7ZFOztE.d.ts +132 -0
  279. package/dist/types-C7ZFOztE.d.ts.map +1 -0
  280. package/dist/types-CH5XpIHw.js +114 -0
  281. package/dist/types-CH5XpIHw.js.map +1 -0
  282. package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
  283. package/dist/types-Cu0ZGDu9.js.map +1 -0
  284. package/dist/types-IqegbJEE.d.ts +624 -0
  285. package/dist/types-IqegbJEE.d.ts.map +1 -0
  286. package/dist/types.d.ts +4 -4
  287. package/dist/types.js +1 -1
  288. package/dist/url-D8HCl65b.js +1259 -0
  289. package/dist/url-D8HCl65b.js.map +1 -0
  290. package/dist/wasm-BVT-j5cl.js +33 -0
  291. package/dist/wasm-BVT-j5cl.js.map +1 -0
  292. package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
  293. package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
  294. package/dist/ws-CIHMvlSh.js +952 -0
  295. package/dist/ws-CIHMvlSh.js.map +1 -0
  296. package/dist/ws-client-N7NxzvQp.js +230 -0
  297. package/dist/ws-client-N7NxzvQp.js.map +1 -0
  298. package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
  299. package/dist/xai-CvJTeLrI.js.map +1 -0
  300. package/dist/xdg-zweed0-t.js +117 -0
  301. package/dist/xdg-zweed0-t.js.map +1 -0
  302. package/docs/ACP.md +52 -0
  303. package/docs/ARCHITECTURE.md +103 -6
  304. package/docs/ARTIFACTS.md +8 -4
  305. package/docs/CHAT.md +165 -14
  306. package/docs/EXECUTION_CONTEXT.md +8 -0
  307. package/docs/EXTENSIONS.md +123 -2
  308. package/docs/HEADLESS.md +2 -0
  309. package/docs/IPC.md +775 -64
  310. package/docs/MACHINES.md +456 -0
  311. package/docs/PANELS.md +3 -0
  312. package/docs/README.md +4 -0
  313. package/docs/RESTATE.md +3 -2
  314. package/docs/RUN_IN_BACKGROUND.md +10 -2
  315. package/docs/SERVER.md +1085 -0
  316. package/docs/SKILL.md +6 -1
  317. package/docs/TERMINAL.md +75 -0
  318. package/docs/TUI.md +65 -14
  319. package/package.json +58 -26
  320. package/scripts/eval.ts +4 -0
  321. package/dist/acp-D-tVyUjw.js.map +0 -1
  322. package/dist/agent-D0zZnGbt.js.map +0 -1
  323. package/dist/anthropic-C2aole0G.js.map +0 -1
  324. package/dist/approvals-DKaJZYlm.js.map +0 -1
  325. package/dist/atomic-write-B7zr1ddF.js.map +0 -1
  326. package/dist/auth-BG2rVKSt.js.map +0 -1
  327. package/dist/completion-core-BRo86R_L.js.map +0 -1
  328. package/dist/contexts/daytona.js.map +0 -1
  329. package/dist/contexts/docker.js.map +0 -1
  330. package/dist/contexts/e2b.js.map +0 -1
  331. package/dist/contexts-CRE9pw07.js.map +0 -1
  332. package/dist/eval.js.map +0 -1
  333. package/dist/format-BF745pLL.js.map +0 -1
  334. package/dist/glob-DluQFSw1.js +0 -270
  335. package/dist/glob-DluQFSw1.js.map +0 -1
  336. package/dist/headless.js.map +0 -1
  337. package/dist/history-search-C4bk1Wz_.js.map +0 -1
  338. package/dist/index-BgN1T3lk.d.ts.map +0 -1
  339. package/dist/index-C37_ppWv.d.ts.map +0 -1
  340. package/dist/index-C6NEd5y_.d.ts.map +0 -1
  341. package/dist/ipc-zVSyo6K1.js +0 -331
  342. package/dist/ipc-zVSyo6K1.js.map +0 -1
  343. package/dist/ipc.d.ts.map +0 -1
  344. package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
  345. package/dist/json-rpc-rSWMUh9b.js +0 -192
  346. package/dist/json-rpc-rSWMUh9b.js.map +0 -1
  347. package/dist/logger--C1VQjwJ.js.map +0 -1
  348. package/dist/mcp-CgNd-f3F.js.map +0 -1
  349. package/dist/messages-2jcOA_5R.js.map +0 -1
  350. package/dist/openai-compat-Dy56weK8.js.map +0 -1
  351. package/dist/output-cap-C9zukcCj.js +0 -100
  352. package/dist/output-cap-C9zukcCj.js.map +0 -1
  353. package/dist/panels-B6gRk4bf.js +0 -789
  354. package/dist/panels-B6gRk4bf.js.map +0 -1
  355. package/dist/plugins-gbI7SBA6.js.map +0 -1
  356. package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
  357. package/dist/run-summary-BU_NgwUT.js +0 -269
  358. package/dist/run-summary-BU_NgwUT.js.map +0 -1
  359. package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
  360. package/dist/session/sqlite.js.map +0 -1
  361. package/dist/session-BqRP1hS7.js.map +0 -1
  362. package/dist/shell-path-h5czzllR.js +0 -1549
  363. package/dist/shell-path-h5czzllR.js.map +0 -1
  364. package/dist/skills-BC3dodWp.js.map +0 -1
  365. package/dist/src-CkNQKFKr.js.map +0 -1
  366. package/dist/todo-edits-UOwgxGKu.js +0 -528
  367. package/dist/todo-edits-UOwgxGKu.js.map +0 -1
  368. package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
  369. package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
  370. package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
  371. package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
  372. package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
  373. package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
  374. package/dist/turn-operations-DrbJlpq9.js.map +0 -1
  375. package/dist/types-CWgENVLY.js.map +0 -1
  376. package/dist/xai-CazjStOK.js.map +0 -1
package/docs/IPC.md CHANGED
@@ -7,8 +7,8 @@ prompts. The engine (`createChatHost`) is transport-agnostic; the daemon
7
7
  is one embedding of it, reached over a unix socket with JSON-RPC.
8
8
 
9
9
  Entry points: `src/ipc/daemon/server.ts` (`startIpcDaemon`), `src/ipc/client.ts`
10
- (`connectIpcClient` / `connectOrSpawnDaemon`), `src/tui/runtime/daemon.ts`
11
- (`connectDaemonSession`, the TUI's daemon-mode controller).
10
+ (`connectIpcClient` / `connectOrSpawnDaemon`), `src/tui/runtime/host-session.ts`
11
+ (`connectHostSession`, the TUI's session controller, in-process or daemon).
12
12
 
13
13
  ```
14
14
  ┌────────────┐ ┌────────────┐ ┌─────────────┐
@@ -30,9 +30,10 @@ Entry points: `src/ipc/daemon/server.ts` (`startIpcDaemon`), `src/ipc/client.ts`
30
30
  Contents: [Pieces](#the-pieces) · [Host environment](#host-environment) ·
31
31
  [RPC methods](#rpc-methods) · [Signals](#signals-and-convergence) ·
32
32
  [Concurrent sends](#concurrent-sends) · [Extension bus](#extension-bus-ctxbus) ·
33
- [Daemon lifecycle](#daemon-lifecycle) · [Turning it off](#turning-it-off-containment-guarantees) ·
33
+ [Daemon lifecycle](#daemon-lifecycle) · [Remote server](#remote-server-zidane-server) ·
34
+ [Reconnect and resume](#reconnect-and-resume) · [Turning it off](#turning-it-off-containment-guarantees) ·
34
35
  [Execution semantics](#execution-semantics-read-this) · [Testing](#testing) ·
35
- [Surface matrix](#surface-cabling-matrix-daemon-runtime) · [Removability](#removability) ·
36
+ [Surface matrix](#surface-cabling-matrix-daemon-runtime) · [TUI remote mode](#tui-remote-mode) · [Removability](#removability) ·
36
37
  [Status](#status--follow-ups)
37
38
 
38
39
  ## The pieces
@@ -41,8 +42,15 @@ Contents: [Pieces](#the-pieces) · [Host environment](#host-environment) ·
41
42
  | --- | --- |
42
43
  | `src/ipc/protocol.ts` | Transport contract: typed `IpcMethods` map, `IPC_PROTOCOL_VERSION`, handshake shapes. Re-exports `HostSignal` & co. from `src/chat/host/signals.ts`. |
43
44
  | `src/chat/host/` | `createChatHost(env)` — the transport-agnostic engine. Owns live `Agent`s, forwards loop hooks as signals, runs the approval + interaction hubs. |
44
- | `src/ipc/daemon/` | Unix-socket JSON-RPC server (`startIpcDaemon`), disk-backed environment, lifecycle helpers (socket path, liveness probe, spawn-on-demand). |
45
+ | `src/ipc/daemon/` | `dispatcher.ts`: the transport-free method table, per-peer state, signal fan-out and `ipc.hello` handling (`createHostDispatcher`); a connection is a `HostPeer` with a `send`. `handshake.ts`: the pure version / build / release fences. `server.ts`: the unix-socket transport around it (`startIpcDaemon`: framing, socket file, idle exit, retiring, drain; write backpressure is `../transports/send-budget.ts`). Plus the disk-backed environment and lifecycle helpers (socket path, liveness probe, spawn-on-demand). |
45
46
  | `src/ipc/client.ts` | `connectIpcClient` / `connectOrSpawnDaemon` — typed `request()`, `onSignal()`, version handshake with stale-daemon drain. |
47
+ | `src/ipc/transports/` | Wire pieces shared across transports: `peer-rpc.ts` (the host side of one JSON-RPC connection: `ipc.signal` fan-out, requests routed to the dispatcher), `send-budget.ts` (one connection's outgoing budget, socket and WebSocket alike: a peer with more than 8 MiB of pushed signals unsent is dropped, reply bytes set aside; a reply over the message size (WebSocket 32 MiB, socket 256 MiB) goes out as an `IpcErrorCode.ResponseTooLarge` error and the connection stays), `client-core.ts` (the client side: hello with its fences, request deadlines, signals). The WebSocket transport: `ws-server.ts` (`startWsServer`, `Bun.serve`, one JSON-RPC message per text frame; Bun-only, so exported nowhere), `ws-client.ts` (`connectWsClient`, the runtime's global `WebSocket`; never drains a host on mismatch; exported from `zidane/ipc/remote` with the saved-hosts registry (`src/ipc/remote.ts`), not from the `zidane/ipc` barrel, so a client that keeps remote hosts opt-in loads it only on demand), `ws-auth.ts` (token carriers: `Authorization: Bearer`, or a `zidane.bearer.<token>` subprotocol entry next to `zidane` for clients that can't set headers; not enforced unless the server is given `authorize`). |
48
+ | `src/ipc/daemon/signal-log.ts` | Per-session signal numbering and the bounded replay log `sessions.resume` reads; see [Reconnect and resume](#reconnect-and-resume). |
49
+ | `src/server/` | zidane-server: `startZidaneServer` (`server.ts`: one host behind the WebSocket transport with device tokens and a unix socket), the token store (`tokens.ts`), configuration (`config.ts`), upgrade policy (`security.ts`), the checks before the token and the failure accounting (`upgrade-guard.ts`), the audit log (`audit.ts`), and `zidane server …` (`cli.ts`, standalone `main.ts`). May import `src/ipc` and `src/chat`; nothing in `src/` imports it (the `zidane` binary routes `server` to `cli.ts`). See [Remote server](#remote-server-zidane-server). |
50
+ | `src/ipc/daemon/identity.ts` | Host identity: a UUID written once to `<userDataDir>/host-id`. The daemon and a window's in-process host share the user dir, so they share the id (one computer, one store); a remote server has its own. |
51
+ | `src/ipc/memory.ts` | `startInProcessHost` — the same dispatcher in the caller's process, with `IpcClient`s that call it directly (no socket, no spawn). Wire-faithful: JSON copies (a top-level `undefined` result → `null`, nested `undefined` fields dropped), `JsonRpcRemoteError` codes, `JsonRpcConnectionClosedError` for closed connections (in-flight requests included), responses and signals through one queue in production order, `close()` detaches quietly, host shutdown drains runs and fires `onClose`. No request deadlines (nothing to reconnect to) and `daemon.shutdown` does not stop it: its lifetime belongs to the embedding window. |
52
+ | `src/tui/runtime/transport.ts` | `HostTransport`: how a TUI window reaches its host. `socketTransport` (connect-or-spawn for session controllers, a passive connect for the presence watcher) or `inProcessTransport(host)`. `connectHostSession({ transport })` and the presence watcher speak only `IpcClient`. |
53
+ | `src/chat/hosts/` | The client's saved hosts (`createHostRegistry`): entries in `<userDir>/hosts.json` (0600, atomic), tokens in `credentials.json` under `hosts:<id>`. `add` verifies a host before saving it (the entry id is the server's `hostId`); `test` says who answers, or why not (`auth`, `unreachable`, `tls`, `protocol`, `identity`, …). Connecting is injected: `src/ipc/hosts/probe.ts` (`createWsHostRegistry`, `probeWsHost`) wires the WebSocket client. The host index (`host-index.ts`: which host an id lives on, never guessed) and the presence merge (`presence.ts`) are here too. Types and URL rules are in `zidane/chat/pure`. |
46
54
  | `src/ipc/attach.ts` | `zidane attach` — a thin readline client proving the multi-window contract end-to-end. |
47
55
 
48
56
  ## Host environment
@@ -57,6 +65,15 @@ process supplies a `ChatHostEnvironment`:
57
65
  same `state.json` settings slot; pending approvals in
58
66
  `pending-approvals.json`; per-cwd extension discovery with the same
59
67
  allowlist + trust contract as every other surface.
68
+ - **A host in the window's own process**: `createHostEnvironment({
69
+ broadcast, shared: { config, runtimeConfig, hostRuntime?, extensionBus?,
70
+ multiProcess? } })`, the daemon's environment reusing the window's boot
71
+ config (one store and one writer to `state.json` in the process), its
72
+ per-cwd runtime config (no second extension discovery, so no extension
73
+ `setup` runs twice in the process; `invalidate` / `forget` keep the
74
+ daemon's reload contract) and its extension bus. Single-process unless
75
+ `multiProcess`. `createDaemonEnvironment` is the same function without
76
+ `shared`: own config, discovery, multi-process.
60
77
 
61
78
  Both run the same engine — the gate flow, edit-approval pipeline,
62
79
  compaction, and hook forwarding cannot drift between surfaces.
@@ -71,28 +88,53 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
71
88
  | --- | --- |
72
89
  | `chat.send` | Send a prompt; starts a run (errors if already running elsewhere). |
73
90
  | `chat.interrupt` | Stop the session's active run. |
74
- | `chat.steer` | Inject a message into the live run. |
75
- | `chat.followUp` | Queue a message on the agent to run after the current turn (no steer). |
91
+ | `chat.steer` | Inject a message into the live run. Resolves `false` when no run took it. |
92
+ | `chat.followUp` | Queue a message on the agent to run after the current turn (no steer). Resolves `false` when no run took it. |
76
93
  | `chat.pushQueued` | Race-free send: steers a live run, sends otherwise. |
77
94
  | `chat.isRunning` | Whether the session has a run in flight. |
78
95
  | `chat.rebuildAgent` | Retire the cached agent so the next run rebuilds (fresh MCP/creds/extensions). |
79
96
  | `chat.listWork` | List cancellable tool calls / background tasks for the session. |
80
97
  | `chat.cancelWork` | Cancel one work item, or `'all'`. |
81
98
  | `sessions.list` | List sessions, filtered by project root / archive state. |
82
- | `sessions.create` | Create a session. |
83
- | `sessions.delete` | Delete a session. |
99
+ | `sessions.listMeta` | The sessions list as a window renders it (`SessionMeta`: title, turn / message / run counts, project, cwd, environment, artifact count, main flag), by project root (`null` = untagged) or all. |
100
+ | `sessions.getMeta` | One `SessionMeta` by id, or `null`. |
101
+ | `sessions.get` | The whole session document: the live copy when an agent holds it, else the stored one (stale `running` marker demoted), or `null`. `turns: false` leaves the turns out (read them with `sessions.getTurns`) and names the snapshot's `version`. |
102
+ | `sessions.getTurns` | `{ sessionId, from?, limit? }` → `{ turns, total, version }`: a slice of the same copy's turns (default 200, at most 1000), or `null`. Pages and the `turns: false` head come from one cached snapshot per content version (the session is read once per version, not per page), named by `version`. `loadSessionDocument(request, sessionId)` (`zidane/ipc`) reads a whole document a page at a time with it (halving a page refused as too large), starts over when the version changes mid-read (a few times, then the whole document in one reply), and reads in one reply from an older host. What the GUI exports a server session with. |
103
+ | `sessions.getToolResult` | `{ sessionId, callId, turnId? }` → the full text of one tool result, or `null`: what a clipped tool row shows on expand, without the session crossing the wire. |
104
+ | `sessions.getHeader` | One session's header: its `SessionMeta` row plus `metadata`, `agentId`, `status`, `createdAt`, without turns or runs (the live copy when an agent holds it; from the store's header index when it has one). What a window re-reads on `sessions:changed`. `usage: true` adds `contextTokens` (the last parent turn's context size: the footer gauge) and `costUsd` (summed run costs: the cost pill), computed host-side from the turns and runs, so a client needs no document for them. |
105
+ | `sessions.setExtensionState` | Write (or, `value` omitted, delete) one key of an extension's per-session state in the host's copy (renderer `useSessionState`). |
106
+ | `sessions.create` | Create a session through the shared lifecycle factory (`src/chat/session-factory.ts`): cwd, derived project root, agent, title, model + fallbacks + `effort` / `modelOptions`, `extensionEdit` / `skillEdit` markers, `pinnedSkills`, `environment` (`{ kind, branch?, base? }`; omitted = local), and an `id` (refused when taken or not a plain token). A kind that provisions returns at once with the row's `environment.state` = `provisioning`. |
107
+ | `sessions.delete` | Delete a session: retire its live agent + pending HITL state, tear down the environment it owns, then remove the row and its on-disk artifacts (`ChatHost.deleteSession`). Resolves with `{ environment? }` — `removed: false` + `reason` when the provider kept it. Clients in daemon mode must delete through this, never behind the daemon's back. |
108
+ | `sessions.environmentActions` | The per-session commands the session's environment offers right now (push, open PR). |
109
+ | `sessions.runEnvironmentAction` | Run one in the session's environment (resumed when paused); `{ message, url? }`. |
110
+ | `sessions.workspaceExec` | Run a shell command in the session's workspace: its execution context, rooted inside the environment when the files live there (resumed when paused), else at the session's cwd. Optional `cwd` (same path rules), `env`, `timeoutSeconds` (default 30): `ExecResult`. Backs `!` shell mode for sandboxed sessions and window-side `ctx.workspace` in the TUI's daemon mode. |
111
+ | `sessions.workspaceReadFile` | Read a file from the session's workspace. Paths resolve against the workspace root the way the execution context resolves them (absolute paths as given); containment is the context's own: a sandbox confines to its workspace, the local process context doesn't, except on a confined host (a server), where a path on the host's filesystem must resolve (real path) inside the workspace roots or the session's own environment folder (a worktree), and never into the data dir (`ChatHost.workspace.checkPath`); the same holds for `workspaceWriteFile` / `ListFiles` / `FileSize` and `workspaceExec`'s `cwd`. Backs the files panel and diffs for sandboxed sessions. |
112
+ | `sessions.workspaceWriteFile` | Write a file in the session's workspace, same path rules. |
113
+ | `sessions.workspaceExecStream` | The same command streamed (`!` on a remote host): `{ execId }`, then `workspace:exec:data { execId, stream, chunk }` signals in arrival order and one `workspace:exec:exit { execId, code, signal, truncated?, cancelled? }`, to the requesting connection only (never fanned out or replayed; signals may beat the reply). Runs the session context's `execStream` (a machine's too: `exec.stream*`); a context without one runs the buffered exec and reports it the same way. Bounded per connection: 4 running commands, 64 KiB chunks, 1 MiB of output (then the process group is stopped and the exit says `truncated`). Same `cwd` confinement as `workspaceExec`. A dropped connection kills what it started. |
114
+ | `sessions.workspaceExecCancel` | Kill a streamed command's process group (`SIGTERM`, `SIGKILL` after the grace): `{ cancelled }`, `false` for another connection's exec or one that ended. |
115
+ | `sessions.workspaceListFiles` | List a directory (or, with `options.glob`, match a pattern) in the session's workspace: `ListFilesOptions` semantics, same path rules. |
116
+ | `sessions.workspaceFileSize` | A file's byte size in the session's workspace, or `null`. |
117
+ | `sessions.setLaunchOverrides` | A window's overrides for the session agent: `zidane run --system / --mcp / --context` (`system` replaces the base prompt, extension fragments still append; `mcp` adds servers after the discovered ones; `execution` runs the session on the launch context) and `cwd`, a folder picked mid-session (moves the live agent without a rebuild). In memory and per client: the session runs the most recent connected setter's, and a client clearing (a call with no field) or disconnecting drops only its own, so the previous setter's apply again. |
118
+ | `sessions.retryEnvironment` | Provision a failed (or restart-stranded) environment again, in the background; progress arrives as `sessions:changed`. |
84
119
  | `sessions.setTitle` | Rename a session. |
120
+ | `sessions.generateTitle` | Title a session on the host, with the host's providers and credentials (`{ providerKey?, model? }`: the window's pick, used when the host has that provider; else the session's own). Written as a rename; refuses Main. |
85
121
  | `sessions.setArchived` | Archive / unarchive a session. |
86
122
  | `sessions.setAgentId` | Change a session's agent profile. |
87
123
  | `sessions.setEffort` | Change a session's thinking-effort level. |
88
124
  | `sessions.setModel` | Set provider/model; `fallbacks` patches the chain (omit = untouched, `[]` = clear). |
89
125
  | `sessions.setModelOptions` | Set per-model boolean options. |
90
- | `sessions.loadEvents` | Canonical transcript replay — the re-convergence source of truth. |
126
+ | `sessions.loadEvents` | Canonical transcript replay — the re-convergence source of truth. Optional paging: `{ before?, limit? }` (turn positions; `limit` default 200, at most 1000) answers `{ events, cursor?, exact: true, start }`, whole runs before `before` (a run: a user message to the turn before the next one): the run the page ends in, and the runs before it while the page stays within `limit` turns, so scroll-back never brings part of a run. A run longer than `5 × limit` turns on its own comes in pieces of that size, each starting on a turn that keeps a tool call with its result; `cursor` is the next page's `before`, absent at the first turn. A page's events are its turns' slice of the WHOLE replay (`exact`), so pages concatenate to exactly one whole load: exchange separators, run receipts, subagent markers and command state as the whole replay has them. `start` is how many events of the whole replay come before the page (absent from older hosts). `loadAllEvents` joins them as they are, and puts the exchange separator back at the joins of an older host's (unflagged) pages; `loadEventsTail` reads from the end only until a predicate is met, returning how many events it left out. Without them, the whole transcript as a plain array (the old shape). Typed callers page through `loadEventsPage(client.request, params)` (`zidane/ipc`), which also reads an older host's whole array as one page. |
127
+ | `sessions.resume` | Reattach without a reload: `{ sessionId, epoch, afterSeq }` → the session's numbered signals after `afterSeq`, or `{ ok: false, reason: 'epoch' \| 'gap' }` (then `loadEvents`). See [Reconnect and resume](#reconnect-and-resume). |
91
128
  | `sessions.getTurn` | Fetch one turn. |
92
129
  | `sessions.contextBreakdown` | Context-usage breakdown for the context panel. |
93
130
  | `sessions.artifacts` | One session's artifacts — see [ARTIFACTS.md](./ARTIFACTS.md). |
94
131
  | `sessions.touch` | Announce an out-of-band local `/`/`!` append; fans out `sessions:changed`. |
95
- | `sessions.compact` | Trigger manual compaction. |
132
+ | `sessions.fork` | Fork at a turn into a new session (`ChatHost.forkSession`); announced as a `sessions:changed` create. |
133
+ | `sessions.deleteTurn` / `sessions.editTurnText` | History edits in the host: refused while the session runs (here or by another process's fresh run marker), one atomic rewrite when the store has `mutateTurns`, cached agent retired, `sessions:changed`. |
134
+ | `sessions.setFolder` | Bind a session to a folder (`cwd` + derived project root), or back to a plain chat with `cwd: ''`. Refused while running and for the Main session. |
135
+ | `sessions.appendTurns` | Turns written outside a run (`!` shell output, a window's `/` built-in output and notices), through the live session when one exists; fans out `sessions:changed`. |
136
+ | `sessions.appendNote` | `ctx.appendNote` outside a run: a display-only note persisted as a `command` turn and broadcast under the same turn id. `false` for an unknown session or no blocks. |
137
+ | `sessions.compact` | Compact a session now (loads its agent when no window has run it). Returns a `CompactionReport` (replaced/dropped turn counts, restored files and skills, summarizer model and usage, post-compact `effectiveTokens`), or `null` when there was nothing to compact. |
96
138
  | `sessions.systemMd` | Attach-time backfill of the resolved `SYSTEM.md` override. |
97
139
  | `sessions.contextSegments` | Every prompt segment with provenance (Instructions tab). |
98
140
  | `sessions.createSystemOverride` | Seed a user-scope `SYSTEM.md`; throws when no override could apply. |
@@ -102,7 +144,7 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
102
144
  | `history.searchSession` | Transcript search over one session, against authoritative mid-run-fresh turns. |
103
145
  | `history.searchAll` | Cross-session content search; one shared index built in the daemon. |
104
146
  | `todos.get` | Current todos snapshot for a session. |
105
- | `todos.set` | Apply a user todo edit to the live agent's `Session` (what `todowrite` reads). |
147
+ | `todos.set` | Apply a user todo edit to the live agent's `Session` (what `todowrite` reads); with `persistIdle`, to the stored session when no agent is live. |
106
148
  | `orchestration.main` | Resolve/create the pinned "main" session id. Gated on the `orchestration` setting. |
107
149
  | `orchestration.createWorker` | Create a worker session under a main. |
108
150
  | `orchestration.list` | List a main's workers. |
@@ -111,12 +153,18 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
111
153
  | `orchestration.steer` | Steer a worker's live run. |
112
154
  | `orchestration.cancel` | Cancel a worker's run. |
113
155
  | `orchestration.transcript` | Fetch a worker's transcript text. |
114
- | `commands.list` | Extension-contributed `/` commands discovered for a cwd. |
156
+ | `commands.list` | The `/` commands the host runs for a cwd: its extensions' (the registry for that folder there) then its built-ins (`/help`). A TUI session on a server completes and sends these (`useHostCommands`). |
115
157
  | `commands.completeArgs` | Argument suggestions for `/name <prefix>`. |
116
158
  | `interactions.list` | Pending `present_plan` / `ask_user` requests. |
117
159
  | `interactions.respond` | Answer an interaction. |
118
160
  | `interactions.cancel` | Cancel an interaction. |
119
161
  | `interactions.rehydrate` | Re-queue orphaned interactions from a reopened session. |
162
+ | `terminal.state` | A session's terminals, whether this host can run them, and its environment kind. See [TERMINAL.md](./TERMINAL.md). |
163
+ | `terminal.open` | Open a terminal in the session's environment (or `target: 'local'`). |
164
+ | `terminal.attach` / `terminal.detach` | Start / stop showing a terminal in one view (`clientId`): history plus the offset live output resumes at. Views are detached when their connection drops. |
165
+ | `terminal.write` / `terminal.resize` / `terminal.ack` | Input; size (`claim: true` when the user focuses or types); acknowledge written output to keep it flowing. |
166
+ | `terminal.restart` / `terminal.close` / `terminal.setHidden` | Restart a user terminal; close one (an agent terminal's task is killed); hide one from the agent's tools. |
167
+ | `terminal.registerViewer` / `terminal.unregisterViewer` | This connection can show terminals (the desktop app). Agents get the terminal tools only while a viewer is attached; the registration ends with the connection. |
120
168
  | `approvals.resolve` | Resolve a safe-mode approval request. |
121
169
  | `approvals.denyAll` | Deny every pending approval (optionally scoped to a session). |
122
170
  | `approvals.listPending` | Pending approvals — backfill for a late attacher. |
@@ -133,15 +181,55 @@ path through the same `ChatHost` method. Source of truth: `IpcMethods` in
133
181
  | `suggest.resolve` | Resolve inline ghost-text autocomplete daemon-side; latest `seq` per session wins. |
134
182
  | `settings.get` | Read host settings. |
135
183
  | `settings.set` | Patch host settings; fans out `settings:changed`. |
184
+ | `environments.list` | Every registered environment kind described for a `cwd`: enabled, default, availability + reason, branch suggestion, option fields (secrets as `configured` / `fromEnv` only), and for a machine's kinds `machine: { id, name, status }`. |
185
+ | `environments.verify` | "Test connection" for a kind with its current options: `{ ok, detail }` or `{ ok: false, reason }`. |
186
+ | `environments.update` | One Settings → Environments edit (enable, default, option values, secrets), validated against the kind's schema; settings go through `settings.set`, secrets to the daemon's environment credential store. |
136
187
  | `presence.list` | Current presence snapshot — backfill for a freshly opened session list. |
137
188
  | `presence.watch` | Tag this connection as attached to a session; detach is implicit on close. |
189
+ | `fs.roots` | Where session folders may be: a server's workspace roots (`confined: true`), or the user's home on a local host (unconfined). |
190
+ | `fs.listDirs` | The folders in `path` (default: the first root) for a picker on the host's machine: real paths inside the roots only (`..` and symlinks out refused), no dotfiles, `isGitRepo` per entry, capped at 2000 (`truncated`). |
191
+ | `auth.status` | The host's provider credentials: per provider, `configured`, `oauth`, `envKey`, and its sources (stored API key, OAuth accounts with labels, env var), which is active. Never a secret. |
192
+ | `auth.setApiKey` | Store an API key as the provider's active credential in the host's `credentials.json`, re-apply the host's auth env, rebuild agents (idle now, busy at run end). OAuth sign-in: `auth.oauth.*`. |
193
+ | `auth.remove` | Remove one stored credential (`account`, a key from `auth.status`) or all of a provider's; a removed key also leaves the host's environment (one it only has from its own environment stays). |
194
+ | `auth.oauth.begin` | OAuth sign-in with the browser on the client and the tokens on the host: `{ provider }` or `{ mcpServer, cwd?, kind?, redirectUri }` (an MCP server of `cwd`'s catalog; `redirectUri` a `http://127.0.0.1:<port>/…` loopback the client listens on). The host runs the login (PKCE verifier, `state`, discovery and dynamic registration stay there) and answers `{ flowId, url, mode, redirectUri?, userCode?, instructions?, expiresAt }`: `loopback` (the client listens at `redirectUri`: a provider's fixed port, e.g. Anthropic's `http://localhost:53692/callback`; a busy port falls back to pasting), `browser` (open `url`, maybe paste a code), `device` (show `userCode`). zidane's providers bind no listener on the host (`callbackServer: false`); a provider that would (it ignores the flag) is refused. A question the login asks before its URL comes back as `{ mode: 'step', url: '', step }` (`HostOAuthStep`: a `select` with options, or a `prompt`): answer it with `auth.oauth.respond`. The flow is bound to this connection, at most 4 open, expires after 10 minutes (`oauthFlowTtlMs`). |
195
+ | `auth.oauth.complete` | `{ flowId, code?, state?, input? }`: the code from the loopback (its `state` must be the flow's, else the flow ends refused) or what the user pasted (a callback URL, `code=…&state=…`, Anthropic's `code#state`; a `state` in it is checked too), delivered once; then waits for the token exchange. Without a code it only waits (device and polling flows). Another connection's flow is refused. Result: `{ provider?: HostProviderAuth }` or `{ mcpServer? }`, never a token, or `{ step }` when the login asks something after its URL (answer with `auth.oauth.respond`, then call `complete` again); tokens go to the host's `credentials.json` / `mcp-credentials.json` and agents rebuild. |
196
+ | `auth.oauth.respond` | `{ flowId, stepId, value? }`: answer the flow's pending step (a `select`'s option id, none to cancel; a `prompt`'s text). Answers with where the flow stands (`HostOAuthBegin`: its URL, or the next step). Additive. |
197
+ | `auth.oauth.cancel` | Abandon a flow of this connection: `{ cancelled }`. |
198
+ | `catalog.install` / `catalog.remove` / `catalog.update` | Install a skill or an extension in the host user's scope from a GitHub (`owner/repo[#ref][:dir]`, GitHub / skills.sh URLs) or npm spec (paths refused; `scope` only `'user'`), remove a user-scope one by name, update an extension from its source. Gated by `remoteInstalls` (a zidane-server's `allowRemoteInstalls`; refused on any other host), every attempt audited with the token, bounded to 5 minutes; progress lines come back with the result. Extensions join an explicit `enabledExtensions` list; removals leave the enabled lists. Then agents rebuild and every window gets `catalog:changed`. |
199
+ | `mcp.upsert` / `mcp.remove` | Add or replace / remove an MCP server in the host user's `mcps.json` (`{ name, created }` / `{ removed }`); validated (`command` or http(s) `url`, string `env` / `headers`), its secrets stored there and never echoed (`catalog.mcps` redacts them). Same gate, audit and signal as `catalog.install`. |
200
+ | `catalog.skills` / `catalog.mcps` / `catalog.extensions` / `catalog.plugins` | What the host discovered for the settings tabs, with the same scans and gates its agents use, each entry with `enabled` (its settings' allowlist; unset = all) and `toggleable` (its own allowlist governs it: not a plugin's or extension's contribution, not broken). `cwd` adds that folder's project scope (inside the workspace roots on a server), else the host user's scope. Skills carry origin (`disk`, `plugin`, `extension`); MCP servers their transport, command, args, URL, file, with `<redacted>` in place of: every `env` / `headers` value; in the URL and in every URL-shaped argument, the userinfo, query values, fragment values (`#k=v`, or a token-shaped fragment) and token-shaped path segments; the value of a secret-looking flag (`--token x`, `--api-key=x`), of a header flag (`-H` / `--header` / `--header=`: `Name: <redacted>`) and of a secret-looking header anywhere (`Authorization: …`); the value of a secret-looking `KEY=VALUE` (`-e GITHUB_TOKEN=…`, `env API_KEY=…`, `--env=…`); known token shapes anywhere in an argument (`sk-`, `sk_live_`, `ghp_` / `gho_` / `ghu_` / `ghs_` / `ghr_`, `github_pat_`, `xox?-`, `AKIA` / `ASIA`, `glpat-`, `zds_`, `AIza`); and a whole argument or path segment that looks random (20+ characters: hex / UUID-like, or mixed case and digits with high entropy). File paths, package names and dash-joined words are kept. The rules are `src/ipc/daemon/redact.ts`; they are a best effort over free-form arguments, not a guarantee. Plus the config files that failed to parse; extensions their source, version, error, capabilities; plugins their origin, contributions, rejection. Toggles write the enabled lists with `settings.set`; installs, removal and MCP sign-in stay on the host's machine (`src/ipc/daemon/handlers/catalogs.ts`, `catalog-entries.ts`). |
201
+ | `models.list` | The host's credentialed providers (`detectAuth` on its data dir) and their models (`ModelInfo`), for the model picker of a session on that host; `cwd` adds what its extensions contribute there. |
202
+ | `machines.list` / `machines.forget` | The machines lending this host their execution (online or not), and forgetting one: it disconnects, its `local@` / `worktree@<name>` kinds stop resolving, and the token it registered with can't register it again (a new token can). `machine:status` signals each change. See `docs/MACHINES.md`. |
203
+ | `machines.listDirs` | A machine's folders for a picker (`path`, default its first root), listed on the machine (`fs.list`, confined there); never waits for an offline machine. |
204
+ | `machines.share.start` / `machines.share.stop` / `machines.useServer` / `machines.links.status` | This (local) host's machine links, behind remote hosts (refused while off; always refused on a zidane-server): share this computer with a saved server, use a saved server as a machine; `machine:links` signals each change. See `docs/MACHINES.md` § Client UI. |
205
+ | `machine.attach` / `machine.call` / `machine.detach` | The reverse direction, on a server with `allowAsMachine`: a client's host asks the server to lend itself over this connection, then sends each machine request; notifications come back as `machine:notification` signals to that connection only. The server keys its executor by this connection's token and the `hostId` (another token never reaches it; 4 per token); a full one answers `MachineErrorCode.Busy` (retry). Refused while off. See `docs/MACHINES.md` § Reverse direction. |
206
+ | `tokens.createMachineToken` | A zidane-server mints a `machine` token for the asking client (one-step "share this computer"), at most 16 live per client token, audited with it; other hosts refuse. |
207
+ | `sync.status` / `sync.claim` / `sync.push` / `sync.reset` / `sync.release` | Settings sync from a client's computer to a zidane-server ([SERVER.md § Settings sync](./SERVER.md#settings-sync)); other hosts refuse, and `ipc.hello` says `sync: true` on a server that takes it. `status`: the record (source device, categories, opted-in keys, synced fields, overrides; no values). `claim { deviceId, deviceName, takeover?, categories?, keys? }`: `{ claimed: false, status }` when another device is the source and `takeover` is not set. `push { deviceId, category, payload }`: one category's snapshot (`SyncPayloads`), source only, applied through the host's own setters (`settings.set` for syncable host-scoped keys, never window-only ones; the user-scope `SYSTEM.md` and skills as files; `mcp.upsert` / `remove`; `catalog.install`, behind `allowRemoteInstalls`; `auth.setApiKey` for opted-in providers), skipping overrides: `{ applied, unchanged, skipped: [{ field, reason }], warnings }`. `reset { field }`: drop an override. `release { deviceId }`: the source stops (values stay). A window's `settings.set` / `mcp.*` / `catalog.remove` / `update` / `auth.setApiKey` / `remove` on a synced field marks it an override. Each changes the record → `sync:changed` to every window; each call is audited. |
138
208
  | `daemon.info` | Handshake / diagnostics info. |
139
209
  | `daemon.shutdown` | Graceful drain: deny pending approvals, abort live runs, close the socket. |
140
210
 
141
- All methods above `IPC_PROTOCOL_VERSION` 9 are additive: an older daemon
211
+ Without a version bump (additive; an older daemon answers "method not found", which clients treat as unsupported): `catalog.*` and `models.list` (the TUI keeps its enabled-list panels and this window's providers), `sessions.loadEvents` paging and `exact`, `sessions.getHeader` `usage` (an older host ignores the fields), `sessions.getTurns`, `sessions.getToolResult` and `sessions.get` `turns: false` (a client reads the whole document from an older host), `sessions.getHeader` (the TUI falls back to `sessions.get`), `fs.roots`, `fs.listDirs`, `auth.status`, `auth.setApiKey`, `auth.remove`, `sessions.resume`, `signalEpoch` on `ipc.hello` and the `$seq` signal envelope field, the `$/ping` heartbeat request (answered outside the method table, before the handshake too), host identity (`hostId` / `hostKind` / `hostName` on `ipc.hello` and `daemon.info`, `hostId` on `SessionRow`; `hostRefOf(hello)` is `null` against an older host), `sessions.setLaunchOverrides`, `sessions.workspaceListFiles`, `sessions.workspaceFileSize`, `cwd` / `env` on `sessions.workspaceExec`, workspace calls for local sessions, the `sessions.compact` report, `sessions.appendNote`, `sessions.workspaceExecStream` / `workspaceExecCancel` and the `workspace:exec:*` signals (a client falls back to the buffered `workspaceExec`), `auth.oauth.*`, `catalog.install` / `remove` / `update` and `mcp.upsert` / `remove` (offered when `ipc.hello` says `remoteInstalls`), the `catalog:changed` signal, `cwd` / `execution` on `sessions.setLaunchOverrides`, and the `mcp:status` and `mcp:settled` signals. Version 13 added the `terminal.*` methods and the `terminal:*` signals. Version 12 added `sessions.workspaceExec`, `sessions.workspaceReadFile` and `sessions.workspaceWriteFile`. Version 11 added `environments.verify`, `sessions.environmentActions` and `sessions.runEnvironmentAction`, and `suspended` on row environments. Version 10 added `sessions.retryEnvironment`, `environments.list`, `environments.update`, `environment` on `sessions.create` / `SessionRow`, and the `sessions.delete` result. All methods above `IPC_PROTOCOL_VERSION` 9 are additive: an older daemon
142
212
  answers "method not found" and clients degrade (local compute, or a
143
213
  store-level fallback) instead of failing. `optional()` in
144
- `src/tui/runtime/daemon.ts` is the TUI's wrapper for that pattern.
214
+ `src/tui/runtime/host-session.ts` is the TUI's wrapper for that pattern.
215
+
216
+ ### Errors
217
+
218
+ A failed request rejects with a `JsonRpcRemoteError` carrying one of these
219
+ codes (`AcpErrorCode` in `src/acp/types.ts`, `IpcErrorCode` in
220
+ `src/ipc/protocol.ts`). Clients branch only on the codes that call for a
221
+ different action (`VersionMismatch`, `ResponseTooLarge`, method not found);
222
+ the rest are a plain remote error with a message.
223
+
224
+ | Code | Name | When |
225
+ | --- | --- | --- |
226
+ | -32601 | method not found | The host lacks the method (an older host: degrade). |
227
+ | -32602 | invalid params | The params are malformed (`InvalidParamsError`: not an object, a bad session id, a wrong type); `message` names the field. |
228
+ | -32010 | `VersionMismatch` | `ipc.hello` across protocol versions or builds. |
229
+ | -32011 | `NotHandshaken` | A method before `ipc.hello` (`daemon.info` / `daemon.shutdown` excepted). |
230
+ | -32012 | `RateLimited` | A network transport's per-connection request budget is spent; retry later. |
231
+ | -32013 | `ResponseTooLarge` | The reply would exceed the transport's message size; ask for less. |
232
+ | -32014 | `HandlerFailed` | The handler threw: the host refused or failed the call (`message` says why). Hosts before it answered -32000, ACP's `AuthenticationRequired`; clients never branched on either. |
145
233
 
146
234
  ## Signals and convergence
147
235
 
@@ -153,7 +241,7 @@ Host → every connected client, one push channel. Source of truth:
153
241
  | `run:start` | A run began. |
154
242
  | `run:end` | A run ended (`ok`, optional `error`). |
155
243
  | `turn:end` | A turn finished; carries `TurnUsage` (footer context gauge + live cost). |
156
- | `event` | Canonical `StreamEvent`; `echo: true` marks the sender's own send/steer echo. |
244
+ | `event` | Canonical `StreamEvent`; `echo: true` marks the sender's own send/steer echo. A failed call's `tool-result` carries `isError: true` (live and on replay); there is no separate `error` row for it. |
157
245
  | `compacting:start` | Manual or auto-compaction began. |
158
246
  | `compacting:progress` | Live summary-token estimate during an in-flight compaction. |
159
247
  | `stream:progress` | Live counters for the streaming parent turn (`outputTokens`, `estimated`, `elapsedMs`, optional `estCostUsd`), throttled ~4/s plus one final tick before the turn settles. Clients fold `estCostUsd` into the session cost until the matching `turn:end`. |
@@ -166,6 +254,10 @@ Host → every connected client, one push channel. Source of truth:
166
254
  | `artifact:changed` | An artifact was deleted / refreshed — see [ARTIFACTS.md](./ARTIFACTS.md). |
167
255
  | `interaction:request` | A `present_plan` / `ask_user` call is pending. |
168
256
  | `interaction:resolved` | An interaction left the pending queue. |
257
+ | `terminal:changed` | A session's terminal list changed (full list). |
258
+ | `terminal:output` / `terminal:reset` | Output batch starting at `offset`, or a view reset from history; sent only to connections that attached that terminal. |
259
+ | `workspace:exec:data` / `workspace:exec:exit` | A streamed workspace command's output and its end (`sessions.workspaceExecStream`); sent only to the connection that started it. |
260
+ | `catalog:changed` | `{ catalog }`: a client installed, updated or removed something on the host (`catalog.install`, `mcp.upsert`, …); windows re-read that catalog. |
169
261
  | `approval:request` | A safe-mode approval is pending. |
170
262
  | `approval:resolved` | An approval left the pending queue. |
171
263
  | `sessions:changed` | Session created / updated / deleted. |
@@ -176,8 +268,10 @@ Host → every connected client, one push channel. Source of truth:
176
268
  | `suggest:changed` | Another window's ghost-text autocomplete resolved (origin excluded). |
177
269
  | `skills:snapshot` | The daemon-side agent's active-skill set changed. |
178
270
  | `mcp:auth` | A daemon-side MCP server needs (re-)authentication. |
271
+ | `mcp:status` | A daemon-side MCP server connected (`status: 'connected'`) or failed to bootstrap (`status: 'failed'`, with `transport`, `error`, optional `code`). |
272
+ | `mcp:settled` | The session agent's MCP bootstrap finished (after every server's `mcp:status` / `mcp:auth`); a window's deadline for its one-shot auto-login. |
179
273
  | `stream:retry` | Provider retry in progress; `source` is `'parent'` or a retrying child's id. |
180
- | `stream:retry:cleared` | A retried stream reached a terminal state for `source`. |
274
+ | `stream:retry:cleared` | A retried stream for `source` resumed streaming or reached a terminal state. |
181
275
  | `system-md` | Which `SYSTEM.md` override (if any) composed this session's prompt. |
182
276
  | `cache:break` | A cache-prefix break was detected for the session's last request. |
183
277
  | `presence:changed` | Full presence snapshot (attached windows, running, attention). |
@@ -186,16 +280,19 @@ Host → every connected client, one push channel. Source of truth:
186
280
  | `queue:changed` | The shared type-ahead queue changed — broadcast to every peer, origin included. |
187
281
  | `extension:signal` | Extension-bus message (`ctx.bus.emit`), private per `extensionId`, 64 KiB cap. |
188
282
 
283
+ **Numbering.** Every signal with a `sessionId` that goes to every peer (not the origin-excluded drafts / search / suggestions, not terminal output) carries its number in its session as an extra `$seq` field on the `ipc.signal` params: from 1, gap-free within one host process (`HelloResult.signalEpoch`). Clients predating it ignore the field; `IpcClient` strips it and hands it to listeners as `onSignal((signal, meta) => ...)`, `meta.seq`.
284
+
189
285
  `event { kind: 'session-event' }` is not a separate signal: a worker's
190
286
  outcome to its main session rides the existing `event` kind as a
191
287
  `StreamEvent` variant, clickable in both clients.
192
288
 
193
289
  Cross-cutting contracts worth knowing before you touch a consumer:
194
290
 
195
- - **Re-convergence guarantee**: at every run end and on reconnect, windows
196
- replace their transcript via `sessions.loadEvents` — a signal missed to
197
- a disconnect, attach gap, or daemon respawn reconciles at the next run
198
- boundary.
291
+ - **Re-convergence guarantee**: at every run end, and on a reconnect that
292
+ cannot resume, windows replace their transcript via `sessions.loadEvents`
293
+ — a signal missed to a disconnect, attach gap, or daemon respawn
294
+ reconciles at the next run boundary. A reconnect to the same host process
295
+ replays exactly what was missed instead ([Reconnect and resume](#reconnect-and-resume)).
199
296
  - **User messages are part of the stream**: the host broadcasts
200
297
  `user-prompt` (inside `event`) for every send/steer, before `run:start`,
201
298
  tagging the sender's own copy `echo: true`. A client that appends its
@@ -214,12 +311,12 @@ Cross-cutting contracts worth knowing before you touch a consumer:
214
311
  (any window) and sends otherwise; the host drains FIFO at run end, so a
215
312
  queued message still runs after the queuing window closes.
216
313
 
217
- > **Adding a signal.** Both `src/tui/runtime/daemon.ts` and
314
+ > **Adding a signal.** Both `src/tui/runtime/host-session.ts` and
218
315
  > `gui/src/main/chat/host.ts` end their dispatch with
219
316
  > `signal satisfies never`, so a new `HostSignal` kind fails to compile
220
317
  > until routed explicitly. That catches an unrouted *kind*, not an
221
318
  > unconsumed *callback* — `onModelFallback`/`onRetry`/`onRetryCleared` are
222
- > therefore required (not optional) on `DaemonSessionCallbacks`: they were
319
+ > therefore required (not optional) on `HostSessionCallbacks`: they were
223
320
  > once dispatched while the TUI silently never supplied them, quietly
224
321
  > dropping the fallback notice and the retry indicator.
225
322
 
@@ -230,9 +327,8 @@ loser gets an explicit error. `chat.pushQueued` is the race-free
230
327
  spelling — it steers the live run when one is active and sends otherwise.
231
328
 
232
329
  > **Cross-process run guard.** The canonical loop persists session-level
233
- > `status: 'running'`, so the daemon refuses a session a local in-process
234
- > window is running, and the local TUI refuses a session the daemon is
235
- > running — each with an explicit error. A 5-minute freshness bound
330
+ > `status: 'running'`, so the daemon refuses a session a local-mode window's
331
+ > in-process host is running, and the reverse — each with an explicit error. A 5-minute freshness bound
236
332
  > against `updatedAt` means a crashed process's leftover marker never
237
333
  > blocks new runs; a live run refreshes the marker every 60s.
238
334
 
@@ -262,11 +358,12 @@ is `undefined`.
262
358
  (honors `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX`), override with
263
359
  `ZIDANE_DAEMON_SOCKET`. Dev builds (`ZIDANE_DEV=1`) use `dev.sock` in the
264
360
  same dir so a checkout never attaches to or drains the installed daemon.
265
- - **`useDaemon` setting** (Settings → agent, experimental): in-process
266
- (default) vs. the shared daemon. On, two terminals on one session share
267
- the live stream — either can steer/interrupt/answer approvals, and
268
- closing a window leaves the run executing. Implementation:
269
- `connectDaemonSession` in `src/tui/runtime/daemon.ts`.
361
+ - **`useDaemon` setting** (Settings → agent, experimental): the window's
362
+ own in-process host (default) vs. the shared daemon. On, two terminals on
363
+ one session share the live stream — either can steer/interrupt/answer
364
+ approvals, and closing a window leaves the run executing. Both go through
365
+ `connectHostSession` in `src/tui/runtime/host-session.ts`; only the transport
366
+ differs (`src/tui/runtime/transport.ts`).
270
367
  - Start explicitly: `zidane daemon [--socket <path>] [--verbose]` or the
271
368
  published `zidane-daemon` bin (`--idle-exit <ms>`, default 30 min, `0`
272
369
  disables). `connectOrSpawnDaemon()` / `zidane attach` spawn one
@@ -290,28 +387,200 @@ is `undefined`.
290
387
  may ignore `codeVersion`, so a recognizable `appVersion` is the fallback.
291
388
  A successful legacy hello reporting an older release is refused without
292
389
  shutting down its active runs. Unknown versions cannot establish release age.
390
+ - **A network server** (`hostKind: 'server'`) keeps only the protocol fence:
391
+ a newer-release or newer-build client attaches and degrades (`optional()`)
392
+ like an older one, the server never retires for it, and no client ever
393
+ drains it (`client-core` refuses to, whatever its `drain` option). A
394
+ protocol mismatch is refused both ways with both versions and the side to
395
+ upgrade (`Protocol version mismatch: the server speaks v13, the client v14.
396
+ Upgrade the server.`; the client words it from its side).
397
+
398
+ ## Remote server (zidane-server)
399
+
400
+ Running one (Docker, configuration, proxies, tokens, operations):
401
+ [SERVER.md](./SERVER.md). This section is the internals.
402
+
403
+ `startZidaneServer` (`src/server/server.ts`) serves ONE host over two
404
+ front-ends: the WebSocket transport (`src/ipc/transports/ws-server.ts`,
405
+ Bun only) for remote windows, and a unix socket (`startIpcDaemon`) for
406
+ local tools on the server's machine. Both attach to the same dispatcher,
407
+ built by `createServerHostDispatcher` (`ws-server.ts`, around
408
+ `createHostDispatcher`) and handed to each as the `dispatcher` option of
409
+ `startWsServer` / `startIpcDaemon` (a transport given a dispatcher serves
410
+ it and leaves draining it to its owner), so a session created over one is
411
+ live on the other. The host is a `server` (`hostKind`): multi-process (`env.multiProcess`
412
+ forced on: the run guard engages, runs outlive their windows), no idle
413
+ exit, never drained or retired by a client. The socket is unauthenticated
414
+ like the daemon's (0600 socket in the data dir) and not audited.
415
+
416
+ - **Run it**: `zidane server start` (`src/server/cli.ts`; standalone
417
+ `bun src/server/main.ts start`, compiled into the `zidane-server`
418
+ binary by `scripts/build-server.ts`). `zidane server health` asks
419
+ `daemon.info` over the unix socket; `health --http` GETs `/healthz` on
420
+ the configured port (`healthUrl`: loopback for a wildcard host; the
421
+ image's healthcheck). Tokens: `zidane server token create
422
+ --name <device>` (prints the token once, on stdout), `token list` (id,
423
+ name, scope, created, last used, revoked; never secrets), `token revoke
424
+ <id|name>`, `token generate` (a fresh token, stored nowhere). The token
425
+ commands edit the data dir's files directly, so they work while the
426
+ server runs. At start, `ZIDANE_SERVER_BOOTSTRAP_TOKEN` (or
427
+ `ZIDANE_SERVER_BOOTSTRAP_TOKEN_FILE`, a file holding it; both set is a
428
+ boot error; well-formed or a boot error) is adopted as the live client
429
+ token `bootstrap` (`TokenStore.adopt`: created, unchanged, rotated with
430
+ the old one revoked, or ignored with a warning when that value was
431
+ revoked); both variables are removed from the process environment
432
+ (children spawned later don't inherit them; the container's own
433
+ configuration and `/proc/1/environ` still hold an inline value). In a
434
+ Linux container, a data dir on no mount, an anonymous volume or a tmpfs
435
+ gets a start-up warning (`src/server/storage-check.ts`, from
436
+ `/proc/self/mountinfo`).
437
+ - **Configuration** (`src/server/config.ts`): flags > `ZIDANE_SERVER_*` env
438
+ (empty values are unset; the generic `PORT` is never read)
439
+ > `<data dir>/server.json` > defaults, validated at boot (errors name the
440
+ flag, variable or file key; unknown file keys and wrongly typed JSON
441
+ values are errors). Keys: `host` (127.0.0.1), `port` (4280), `tlsCert` /
442
+ `tlsKey` (relative to the data dir in the file; with `behindProxy` a
443
+ boot error), `behindProxy`, `drainTimeoutMs` (8000; env
444
+ `ZIDANE_SERVER_DRAIN_TIMEOUT_MS`), `name`
445
+ (the `hostName` clients see), `heartbeatMs`, `maxMessageBytes` (32 MiB),
446
+ `rateLimit` `{ perSecond: 50, burst: 200 }` (env `_RATE_LIMIT` /
447
+ `_RATE_BURST`; 0 disables), `allowedOrigins` (none), `socket` (default
448
+ `<data dir>/server.sock`, `off` for none), `workspaceRoots` (default
449
+ `[<data dir>/workspaces]`; flag `--workspace-root`, repeatable; env
450
+ `ZIDANE_SERVER_WORKSPACE_ROOTS`, path-delimited, absolute; relative
451
+ entries in the file resolve against the data dir; missing ones are
452
+ created at start and handed to the host as `DaemonEnvironment.workspaceRoots`). The data dir is the zidane
453
+ user dir (sessions, settings, `host-id`, and the server's files);
454
+ `--data-dir` / `ZIDANE_SERVER_DATA_DIR` set the storage root as
455
+ `ZIDANE_STORAGE_DIR` does, so `--data-dir /data` keeps everything in
456
+ `/data/.zidane`.
457
+ - **Exposure**: a non-loopback `host` without TLS is refused at boot
458
+ unless `behindProxy` (the error lists the three ways out). With
459
+ `tlsCert` / `tlsKey` the server speaks `wss://`. Behind a proxy, every
460
+ upgrade must carry `X-Forwarded-Proto` with only `https` entries, from
461
+ any peer, loopback included (local tools use the unix socket); with
462
+ `trustedProxies` (IPs / CIDRs, or `private` for every private range:
463
+ `PRIVATE_NETWORKS`) any other peer is a 403; without them every
464
+ peer's forwarded headers are believed, and the server warns at start
465
+ (`startupWarnings`: stderr, and `warnings` on the audit's `server.start`).
466
+ A 403 carries its reason in `X-Zidane-Refusal` (`origin`,
467
+ `insecure-transport`, `untrusted-proxy`, `scope`). An upgrade
468
+ carrying an `Origin` not in `allowedOrigins` is a 403 (browsers send
469
+ one, CLI clients don't; the default list is empty). These checks
470
+ (`src/server/security.ts`, `src/server/upgrade-guard.ts`) run before the
471
+ token. Behind a proxy the client address is the last `X-Forwarded-For`
472
+ entry (skipping trusted proxies from the right), never the first.
473
+ `GET` / `HEAD /healthz` (`SERVER_HEALTH_PATH`, the transport's
474
+ `healthPath` option) is answered before all of these: `200 ok`, `503`
475
+ while closing, no token, no rate limit, no audit, no host details.
476
+ - **Limits**: a frame over `maxMessageBytes` closes the connection with
477
+ 1009; a reply over it goes out as an `IpcErrorCode.ResponseTooLarge`
478
+ error and the connection stays (clients page what can be big:
479
+ `loadAllEvents`, `loadSessionDocument`). Each connection has a token bucket over its incoming messages;
480
+ past it a request gets JSON-RPC error `IpcErrorCode.RateLimited`
481
+ (-32012) and the connection lives on (notifications over budget are
482
+ dropped; a message over 4 KiB is not parsed, its id is read from its
483
+ head). Upgrades are rate-limited per client address
484
+ (`upgradeRateLimit`, 429), and open connections capped per token
485
+ (`maxConnectionsPerToken`) and per client address
486
+ (`maxConnectionsPerAddress`), 429. A token revoked between its upgrade
487
+ and the socket opening is closed with 1008.
488
+ - **Audit log** (`src/server/audit.ts`): append-only JSONL, 0600, rotated
489
+ to `.1` past 5 MB, written asynchronously in order and flushed at
490
+ shutdown. `<data dir>/server-audit.jsonl`: `connect` (token id and
491
+ name, remote address, client `appVersion` from its hello), `disconnect`
492
+ (and why: client close, heartbeat, slow consumer, oversized frame, token
493
+ revoked, shutdown), `token.create` / `token.revoke` (written by the
494
+ CLI, and at start for the bootstrap token), `server.start` /
495
+ `server.stop`.
496
+ `server-audit.failures.jsonl`: `auth.failure` (address, `missing` |
497
+ `invalid` | `scope` | `origin` | `insecure-transport` |
498
+ `untrusted-proxy` | `rate-limited` | `too-many-connections`, the
499
+ forwarded client behind a proxy), the first per address and minute;
500
+ the rest as one `auth.failure.summary` (count by reason). Never token
501
+ material.
502
+ - **Shutdown** (SIGINT / SIGTERM, or `close()`): stop accepting (upgrades
503
+ get 503, the socket stops listening), close every connection on both
504
+ front-ends (clients' `onClose` fires), drain the host within
505
+ `drainTimeoutMs` (8 s by default, under Docker's 10 s stop timeout),
506
+ exit 0. A second signal at least a second
507
+ after the first exits at once (one Ctrl-C delivered twice does not).
508
+
509
+ - **Device tokens** (`src/server/tokens.ts`): `zds_` + 32 random bytes
510
+ base64url, handed out once by `create(name, scope)`. The store keeps only
511
+ a SHA-256 (constant-time compare) in `<userDir>/server-tokens.json`, mode
512
+ 0600, written atomically (`writeJsonFileAtomic`): id, name, scope,
513
+ `createdAt`, `revokedAt`. `lastUsedAt` lives apart in
514
+ `server-tokens.usage.json` (written at most once a minute per token,
515
+ merged with what is on disk), so the server never rewrites the token
516
+ file and can't undo another process's revocation. `list()` never
517
+ returns hashes; `revoke(idOrName)`; `verify(token)` refuses revoked ones;
518
+ `adopt(name, token, scope)` stores a value chosen elsewhere (the
519
+ bootstrap token). Another process writing the file (a CLI) is picked up
520
+ on the next verify. An unreadable or corrupt file keeps the last good
521
+ tokens in force (never "no tokens").
522
+ - **Auth on every upgrade**: no token, an unknown or a revoked one is the
523
+ same plain HTTP 401, before any JSON-RPC (no echo, no telling them apart).
524
+ `client` tokens open client connections; `machine` tokens open a machine
525
+ connection ([MACHINES.md](./MACHINES.md)): never a dispatcher peer (no
526
+ signals), only `ipc.hello`, `$/ping` and `machine.register`, then the
527
+ host sends it execution requests (`src/ipc/daemon/machine-gateway.ts`).
528
+ A client calling `machine.*` gets `MachineErrorCode.MethodNotAllowed`. Revoking a token (here, or by another process: the server watches the
529
+ file and re-reads it every 2 s) drops the connections it opened (close
530
+ 1008); the server tracks connection → token id. Logs name token id and
531
+ name, never the token. `auth: { disabled: true }` is refused off loopback.
532
+ - **Heartbeat** (both ends, default 20 s, `heartbeatMs`): the server sends
533
+ WS ping frames, which every WebSocket client answers on its own; the
534
+ client sends a `$/ping` JSON-RPC request (a browser's WebSocket cannot send
535
+ ping frames), which every host answers. Two ticks in a row without an
536
+ answer (a pong, or for the server any message) and the peer is dead: the
537
+ client aborts (its `onClose` fires, its owner reconnects), the server
538
+ terminates the connection and removes the peer (presence follows).
539
+ - **Slow consumers** (`src/ipc/transports/send-budget.ts`, the socket's
540
+ too): a peer with more than 8 MiB (`maxBufferedBytes`) of pushed signals
541
+ unsent, or one Bun drops a frame for, is terminated; it reconnects and
542
+ resumes. Reply bytes it asked for don't count toward the bound (a big
543
+ page followed by a signal is not a slow consumer); the whole buffer has
544
+ a hard cap of `maxBufferedBytes + 2 × maxMessageBytes`.
545
+
546
+ ## Reconnect and resume
547
+
548
+ The host numbers session signals (see [Signals](#signals-and-convergence)) and
549
+ keeps the last ones per session (`src/ipc/daemon/signal-log.ts`: 2000
550
+ signals or 4 MiB per session, 32 MiB across sessions, least recently written
551
+ session evicted first; numbering survives eviction). The TUI's session
552
+ controller (`connectHostSession`, `src/tui/runtime/host-session-resume.ts`)
553
+ keeps a resume cursor per activation (session, epoch, last applied number).
554
+ On reattach to the same host process it calls `sessions.resume`, holding the
555
+ session's live signals meanwhile; it applies the replay, then the held ones
556
+ it did not carry: no duplicate, no gap, and `onInitialTranscript` does not
557
+ fire. Another process (`epoch`), a position the log dropped (`gap`), or an
558
+ older host fall back to the full `sessions.loadEvents` reload.
559
+ Transport-agnostic: it runs through the dispatcher on every transport, and
560
+ matters for the network one.
293
561
 
294
562
  ## Turning it off (containment guarantees)
295
563
 
296
564
  The daemon runtime is opt-in and inert when disabled:
297
565
 
298
- - **Default off**: `useDaemon` defaults to `false`; every daemon touchpoint
299
- (TUI and, on the same shared flag, GUI main) is gated on the live
300
- controller — off, no socket, no spawn, no signals, byte-identical to
301
- pre-IPC behavior. Toggling takes effect at the next session activation;
302
- detached daemons idle-exit on their own.
566
+ - **Default off**: `useDaemon` defaults to `false`; off, no socket, no
567
+ spawn, no other process: the TUI runs its sessions on a host inside its
568
+ own process (`startInProcessHost`, see [TUI local mode](#tui-local-mode))
569
+ and the GUI on its embedded `ChatHost`. Toggling re-activates the open
570
+ session on the other host; detached daemons idle-exit on their own.
303
571
  - **Run guard + heartbeat gated too**: the pre-send running-marker guard
304
572
  and its 60s heartbeat only engage when `useDaemon` is on, since with it
305
573
  off there is exactly one writer per session. `loadSession` heals stale
306
574
  (>5 min) on-disk `running` headers with a bounded targeted write.
307
575
  - **Emergency kill-switch**: `zidane --no-daemon` (or `ZIDANE_NO_DAEMON=1`)
308
- forces the in-process runtime for both TUI and GUI regardless of the
576
+ forces the in-process host for both TUI and GUI regardless of the
309
577
  setting.
310
- - **Fail-soft + zero footprint**: a spawn failure, socket problem, or
311
- handshake bug falls back to the in-process runtime with a visible
312
- notice; the daemon's extra state (socket, pidfile,
313
- `pending-approvals.json`, in-memory drafts/queues) exists only while a
314
- daemon is running.
578
+ - **Fail-visible + zero footprint**: in daemon mode a spawn failure, socket
579
+ problem, or handshake bug leaves the window reconnecting with a visible
580
+ notice (sends disabled until reattached); turning the setting off or
581
+ `--no-daemon` runs sessions in-process. The daemon's extra state
582
+ (socket, pidfile, `pending-approvals.json`, in-memory drafts/queues)
583
+ exists only while a daemon is running.
315
584
 
316
585
  ## Execution semantics (read this)
317
586
 
@@ -329,6 +598,13 @@ Moving the host out of the window process changes two things users notice:
329
598
  broadcast: run lifecycle, gate decisions, interaction flow, dispose/shutdown.
330
599
  - `test/ipc-host-commands.test.ts` — host-side `/` command execution and
331
600
  parity with the local runtime.
601
+ - `test/server-sync.test.ts` (a server's `sync.*`: claim and takeover,
602
+ each category, window-only keys, overrides and reset, path and size
603
+ checks, `allowRemoteInstalls`, opted-in keys, audit),
604
+ `test/chat-sync-engine.test.ts` (the client engine: watchers and the
605
+ settings stream, offline queue across restarts, import once, takeover,
606
+ off), `test/tui/app-settings-sync.test.tsx` (the real `<App>` with a
607
+ saved server, end to end).
332
608
  - `test/ipc-daemon.test.ts` — real unix-socket round-trips: handshake,
333
609
  version mismatch, stale socket, multi-peer fan-out, late-attach replay,
334
610
  cross-window approval convergence, the concurrent-send guard, and a
@@ -359,19 +635,29 @@ store at the next run-end resync; **gap** = known missing cabling.
359
635
  | Running work + ctrl+k cancel picker | synced | `work:snapshot` mirror; cancel via `chat.cancelWork` |
360
636
  | Context panel (ctrl+?) | synced | `sessions.contextBreakdown` RPC |
361
637
  | Artifacts (rail, badges, `#pr` completion) | synced | see [ARTIFACTS.md § IPC](./ARTIFACTS.md#ipc) |
362
- | History edits (selection modal) | synced | store refresh, `chat.rebuildAgent`, `sessions.touch` |
638
+ | History edits (selection modal), fork, folder attach/detach | synced | host-side (`sessions.deleteTurn` / `editTurnText` / `fork` / `setFolder`); refused mid-run on every surface |
363
639
  | History search (transcript + cross-session) | synced + accelerated | `history.searchSession`/`searchAll`, daemon LRU cache |
364
- | Provider-retry indicator | synced | `stream:retry`; clears on resume / run end |
640
+ | Provider-retry indicator | synced | `stream:retry`; `stream:retry:cleared` when the stream resumes, errors, or the run ends |
365
641
  | Run ceilings (turns/cost/tokens/wall clock/retry) | synced | host reads the same settings-driven caps as local |
366
642
  | Native server tools (e.g. web_search) | synced | host forwards `stream:server_tool_use/result` |
367
643
  | Footer context gauge + live session cost | synced | `turn:end` carries `TurnUsage` |
368
644
  | Subagent spawn banners | synced | host forwards `spawn:before/complete/error` |
369
645
  | Active-skill chips | synced | `skills:snapshot` on activation change |
370
646
  | MCP "needs login" badge | synced | `mcp:auth`; login flow itself runs window-local |
371
- | Manual compaction indicator | synced | `compacting:*` signals |
647
+ | MCP connect / failure status | synced | `mcp:status` |
648
+ | Manual compaction (trigger, indicator, "compacted N turns") | synced | `sessions.compact` runs host-side and returns the report; `compacting:*` signals |
649
+ | Repeat-guard notice | synced | host broadcasts and persists the notice row |
650
+ | Safe-mode "allow for this session" rules | synced | host-owned per session; survive agent rebuilds, dropped on delete |
651
+ | Auto-title after the first exchange | synced | generated host-side at run end (`sessions.generateTitle`: the host's providers and credentials, so a server session is titled with the server's); with several TUI windows on one new session each may generate one (last write wins) |
652
+ | `zidane run --context` (non-`process`) | local | forces the in-process host (`ZIDANE_NO_DAEMON=1`), which runs the session on the launch context (`sessions.setLaunchOverrides`) |
653
+ | `zidane run --system` / `--mcp` | synced | `sessions.setLaunchOverrides` on every activation, per window; the latest connected setter's apply |
654
+ | Cwd picked mid-session | synced | `sessions.setLaunchOverrides({ cwd })`: the live agent moves (tools at once, prompt next turn), not persisted |
655
+ | Extension `ctx.appendNote` outside a run | synced | `sessions.appendNote`: host persists and broadcasts the note |
656
+ | Auto-login after adding an MCP server | synced | window arms it; `mcp:auth` starts the login, `mcp:settled` disarms |
657
+ | Window-side extension `ctx.workspace` | synced | daemon session's `workspace` ops over `sessions.workspace*` |
372
658
  | Transcript convergence after any miss | synced | `loadEvents` replace at run end + reconnect |
373
659
  | `/` commands (incl. extension) | synced | host-side (`commands.ts`); TUI's own `/` stays local, converges via `sessions.touch` |
374
- | `!` shell mode | synced output | runs in the issuing window's cwd/env; `sessions.touch` on persist |
660
+ | `!` shell mode | synced output | runs in the issuing window's cwd/env (on a remote server: streamed from the session's folder there, `sessions.workspaceExecStream`, esc kills it there; an older server: the buffered `sessions.workspaceExec`); `sessions.touch` on persist |
375
661
  | Extension tools / hooks / MCPs | synced | daemon loads the same registry; tools run daemon-side |
376
662
  | Agent Plugins (`enabledPlugins`) | synced | host applies both plugin gates before composing the preset |
377
663
  | `SYSTEM.md` prompt override | synced | `system-md` carries the winning file + shadowed candidates |
@@ -384,36 +670,461 @@ store at the next run-end resync; **gap** = known missing cabling.
384
670
  | Steer / follow-up transcript order | synced | `user-prompt` fires at delivery via the `steer:inject` hook |
385
671
  | MCP login → daemon credential pickup | synced | `chat.rebuildAgent` after a window-local OAuth login |
386
672
  | Cross-process run guard | synced | store-status guard both ways (see [Concurrent sends](#concurrent-sends)) |
387
- | Agent-spawning `/` commands | **gap** | needs a local spawn seam, or a host-side spawn RPC |
673
+ | Agent-spawning `/` commands | synced | host-side spawn seam; children may pin any credentialed provider's model |
388
674
  | GUI windows on the shared daemon | synced | GUI main is a daemon guest under the same opt-in gate |
389
675
  | GUI composer draft/attachments/search/queue/presence | synced | renderer bound to daemon-mode channels; inert in-process |
390
676
 
677
+ ## TUI local mode
678
+
679
+ A single-process TUI window (the default) runs its sessions on a host in
680
+ its own process: `startInProcessHost` (`src/ipc/memory.ts`) around the same
681
+ dispatcher a daemon serves, with `createHostEnvironment({ shared })` built
682
+ from the window's setup (its config and state store, its live extension
683
+ registry and agent profiles, its HUD / bus / host-runtime bridges, its Logs
684
+ tab), so nothing is discovered or set up twice. The window reaches it
685
+ through `lazyInProcessTransport` (started by the first connect) with the
686
+ same `connectHostSession` code daemon mode uses, and drains it on quit
687
+ (`closeLocalHost`, bounded). Local semantics are kept: a run lives with its
688
+ window (opening another session mid-run is refused, and leaving a session
689
+ disposes it in the host, background tasks included); per-session artifacts
690
+ go to the window's cache dir. `isMultiProcess()` (another process shares
691
+ the sessions) gates what only exists across processes: presence, the run
692
+ guard and heartbeat, the daemon health dot.
693
+
694
+ ## TUI remote mode
695
+
696
+ `zidane --host <wss-url> --token-file <path|->` (or `--token <t|->`, or
697
+ `ZIDANE_HOST_URL` / `ZIDANE_HOST_TOKEN`) runs every session of the window on a zidane-server
698
+ through `wsTransport`: a whole-window choice for its lifetime. Which host a
699
+ window uses is one decision, `resolveHostTarget` (`src/tui/host-target.ts`):
700
+ `remote` (a launch's or the environment's remote host), else `daemon`
701
+ (`daemonGate`), else `local`. `isMultiProcess()` is `target !== local`, so
702
+ remote behaves like daemon mode everywhere it is consulted (runs outlive the
703
+ window, switching sessions mid-run is allowed, presence, the run guard, the
704
+ health dot); `useLocalHost` picks the transport from the same target; the
705
+ footer adds `host: <name>` (the server's `hostName` from `ipc.hello`), only
706
+ in remote mode.
707
+
708
+ - **Opt-in**: the flag (or the variable) is the explicit opt-in, as `zidane
709
+ attach` is for the daemon. The `remoteHosts` setting
710
+ (`remoteHostsGate`, default off; Settings → Sessions, "Remote hosts
711
+ (experimental)", inert under the kill switch) gates the several-hosts
712
+ window ([TUI.md § Several hosts](./TUI.md#several-hosts-remote-hosts)):
713
+ it connects only to the servers saved on Settings → Hosts. With several
714
+ hosts, `isMultiProcess()` and the footer label follow the active
715
+ session's host once it is not the window's default one
716
+ (`setActiveHostElsewhereSource` in `src/tui/live-settings.ts`). It stays in the window's `state.json`
717
+ even while a host owns the settings file (`WINDOW_LOCAL_SETTINGS`). `ZIDANE_NO_REMOTE=1`
718
+ wins over everything: the CLI refuses `--host` / `ZIDANE_HOST_URL` with a
719
+ message naming the switch, and `resolveHostTarget` never answers `remote`.
720
+ - **URLs**: `wss://`, or `ws://` to loopback only (a token never crosses
721
+ the network in the clear). The CLI keeps the URL and token in memory
722
+ (`runTui({ remoteHost })`) and removes `ZIDANE_HOST_URL` and
723
+ `ZIDANE_HOST_TOKEN` from its environment so child processes inherit
724
+ neither (`remoteHostFromCli`). A literal `--token` warns on stderr (ps,
725
+ shell history).
726
+ - **Connecting** (`connectWsClient`): bounded by `openTimeoutMs` (15 s),
727
+ abortable through `signal` up to the end of the handshake. A refused
728
+ upgrade is asked again over plain HTTP for its status: 401 / 403 reject
729
+ with `HostAuthError` (`token refused`, `forbidden`), with fixed messages:
730
+ the body is never read (a proxy may echo the token), and a 403's reason
731
+ comes only from the server's `X-Zidane-Refusal` code (`WS_REFUSAL_REASONS`
732
+ in `src/ipc/transports/ws-auth.ts`; unknown codes are ignored). That
733
+ request is `redirect: 'manual'`: a 3xx rejects with `HostRedirectError`
734
+ and the token never leaves the URL's origin (the TLS diagnosis probe in
735
+ `src/ipc/hosts/probe.ts` sends no token and follows no redirect either).
736
+ `expectHostId` (saved hosts: the entry id) closes a connection whose
737
+ hello names another host and rejects with `HostIdentityError`; the shared
738
+ connection loop (`src/ipc/hosts/connection-loop.ts`, `saved-hosts.ts`,
739
+ which checks every connection itself too) reports it as status
740
+ `identity` and does not retry. Messages name URLs by scheme, host and
741
+ path (`displayHostUrl`); `parseRemoteHostUrl` refuses userinfo, query
742
+ strings and fragments.
743
+ - **`zidane attach --host <wss-url> --token-file <path|->`**: the thin client on a
744
+ server (`runAttach({ host })`); nothing is spawned or drained.
745
+ - **Session data comes from the host**, in every mode
746
+ (`src/tui/runtime/session-data.ts`): the sessions list and the `#` chat
747
+ catalog (`sessions.listMeta` / `sessions.getMeta`), resume and activation
748
+ (`sessions.get`; header refreshes on `sessions:changed` and the model /
749
+ agent reflectors read `sessions.getHeader`, so opening a session moves
750
+ its turns once as a document and once as `sessions.loadEvents`, and later
751
+ header refreshes move none), creation (`sessions.create`, stamped host-side with the
752
+ cwd, profile, the window's whole model pick and an authoring session's
753
+ pinned skill), header / model / agent refreshes, titles, export, the
754
+ details modal, cross-session search, environment actions, artifacts,
755
+ out-of-band turns (`sessions.appendTurns`: command output, `!` shell,
756
+ notices), idle todo edits (`todos.set` with `persistIdle`) and renderer
757
+ state (`sessions.setExtensionState`). Requests ride the window's
758
+ app-lifetime host client, or a connection of their own while it is down.
759
+ The window keeps a `SessionSnapshot` of the open session
760
+ (`src/tui/runtime/session-snapshot.ts`): the host's document wrapped as a
761
+ `Session` with no store (mutators are memory-only mirrors, `save()`
762
+ throws), refreshed from the host on activation, history edits and a todos
763
+ signal for a run it doesn't know.
764
+ - **Older hosts**: a host on this machine (the in-process host, the local
765
+ daemon) shares the window's store, so when it predates a method (or can't
766
+ be reached) the window falls back to that store, as before. A remote
767
+ server has no such fallback: one too old to serve sessions is refused
768
+ with a message naming it ("runs an older zidane that can't serve this
769
+ window's sessions").
770
+ - **`!` shell on a server** runs on the server, in the session's folder
771
+ there, streamed like a local `!` (`sessions.workspaceExecStream`; esc
772
+ kills the process group on the server; 1 MiB of output, then it is
773
+ stopped). A server without the method runs it buffered
774
+ (`sessions.workspaceExec`, 10 min bound; esc stops waiting). On a host on
775
+ this machine it is the window's local shell, as before. Either way the
776
+ turns go to the host, in the same format.
777
+ - **Session folders on a server** are inside its workspace roots
778
+ (`--workspace-root`, default `<data dir>/workspaces`): `sessions.create`
779
+ without a folder takes the first root, one outside is refused, as is
780
+ `sessions.setFolder`; a launch-override `cwd` outside them is dropped (a
781
+ window's own path is on its machine). The chat host enforces this itself
782
+ (`ChatHostEnvironment.workspaceRoots` → `ChatHost.workspace`,
783
+ `src/chat/workspace-confinement.ts`) at every entry point taking a
784
+ folder: session creation (orchestration workers included),
785
+ `setSessionFolder`, `setLaunchOverrides`, `listCommands` /
786
+ `completeCommandArgs`, `listEnvironments`, and the workspace file / exec
787
+ methods; `catalog.*` / `models.list` and `fs.*` use the same instance. A
788
+ folderless session (the orchestration main) works in the first root. `fs.roots` / `fs.listDirs` browse
789
+ them. Local hosts are unconfined (`fs.*` rooted at the home; clients may
790
+ keep their local picker there).
791
+ - **Window-local, by design**: `state.json` (UI settings in local mode, the
792
+ last provider and session), keybindings, the window's credentials and MCP
793
+ tokens, `/` built-ins (they act on this window; their transcript turns go
794
+ to the host), session export files (written on
795
+ this machine), and in local / daemon mode the cleanup of this machine's
796
+ artifact blobs on delete. Still on the window's store: the Claude Code
797
+ import (`Settings → import`), which imports this machine's Claude data
798
+ into this machine's store, whatever the window's host
799
+ (`importFromClaudeCode` returns `target: 'this-computer'`; given the
800
+ window's `activeHost`, a remote one adds `claudeImportHostNote`'s "imported
801
+ to this computer" note). Against a remote server the imported sessions
802
+ don't show in its list.
803
+ - **The host's provider keys** are managed over `auth.*`: API keys, and
804
+ OAuth sign-ins with the browser on this computer and the tokens on the
805
+ server (`auth.oauth.*`; Settings › Authentication, `o`). MCP servers sign
806
+ in the same way (MCP servers tab, `o`), and with the server's
807
+ `remoteInstalls` skills / extensions install (`i`), user-scope ones are
808
+ removed (ctrl+x) or updated (ctrl+u), MCP servers added (`a`). The
809
+ window's own credentials stay its own.
810
+ - `test/tui/app-modes.test.tsx` remote mode gives the server its own store
811
+ and data dir (daemon mode keeps sharing the window's: same machine), so a
812
+ direct read of the window's store fails its scenarios; "sessions come from
813
+ the host" opens a session only the host has and checks that one only in
814
+ the window's store is not listed.
815
+
816
+ ## GUI several hosts
817
+
818
+ The desktop app's counterpart of [TUI.md § Several hosts](./TUI.md#several-hosts-remote-hosts),
819
+ behind the same gate (`remoteHosts`, Settings › General › Experimental;
820
+ `ZIDANE_NO_REMOTE=1` wins) and the same saved hosts (`hosts.json` + tokens
821
+ in the shared data dir).
822
+
823
+ - **Off** (default, or the kill switch): the app is the single-host app.
824
+ `gui/src/main/chat/hosts/gate.ts` never imports `./remote-hosts` (the only
825
+ module importing `zidane/ipc/remote`), so `hosts.json` is not read, no
826
+ registry is built, no WebSocket opens; the host directory holds This
827
+ computer only and every request goes where it went before.
828
+ `hosts/gate.containment.test.ts` boots `registerChatIpc` with the setting
829
+ off and a saved server on disk (also with the setting on under the kill
830
+ switch), and checks the static graph: no main module but
831
+ `remote-hosts.ts` imports `zidane/ipc/remote`, none imports
832
+ `remote-hosts` statically, and the `zidane` entries main imports
833
+ statically don't reach the WebSocket chunk.
834
+ - **Host directory** (`gui/src/main/chat/host-directory.ts`): This computer
835
+ (the embedded host or the daemon, through `hostRequest`, its in-process
836
+ fallback kept) plus one `ServerHost` per saved server (the WebSocket
837
+ client, reconnecting with backoff, its status: `connecting`,
838
+ `connected`, `reconnecting`, `offline`, `auth-refused`, `identity`: a
839
+ different server answers at its URL, not retried). A session is a
840
+ (host, id) pair: two hosts can hold the same id (a server sharing this
841
+ machine's data dir, a copied store). The app names a session by its
842
+ `SessionKey` (`gui/src/shared/session-key.ts`): the bare id for This
843
+ computer's (so local-only state is what it always was), `<hostId>␟<id>`
844
+ for a server's. Rows, the open chat, selection, the sidebar, persisted
845
+ view state and every signal main pushes carry keys (main keys a host's
846
+ signals by the host they came from); every session-scoped channel takes
847
+ one (branded type: a bare id doesn't type-check), and main routes with
848
+ `hostFor({ key })` or `hostFor({ hostId })` (list, create, environments,
849
+ settings, folders). A key of a server never known, or a local key This
850
+ computer has no session for, throws `UnknownHostIdError`: never a
851
+ guess. Saving a saved server again (a new token) reconnects it in place,
852
+ its sessions still routed to it. Removing one (Settings › Hosts, another
853
+ window, the gate turned off) marks its sessions and request ids removed
854
+ (`HostRemovedError` from then on), retracts its pending approvals and
855
+ questions on the renderer and sends `hosts:removed` with their keys
856
+ (`hosts/server-removal.ts`): a chat open on one of its sessions closes
857
+ with "<server> was removed", a twin on This computer stays open.
858
+ `sessionRequest(method, key, params)` / `sessionNotify` are the shorthands every
859
+ chat channel uses (send, steer, queue, approvals, interactions, todos,
860
+ context, model picks, history edits, terminals, workspace exec). Ids a
861
+ server hands out (approvals, questions, terminals) are noted and
862
+ answered there, and forgotten once settled (answered, withdrawn,
863
+ closed: `forgetOwner`); a late use of a settled id is refused
864
+ (`RequestSettledError`), never sent to This computer. A server that is down rejects with `HostOfflineError`:
865
+ never a fallback to this computer's store.
866
+ - **Signals**: every connected server's signals go through the same
867
+ `broadcast()` demux (`hosts/server-signals.ts`), minus what is this
868
+ computer's (its settings, extension bus, the dev browser, the
869
+ transcript-search mirror, the shared ghost text). Drafts another client
870
+ of the server types on a session (`draft:changed`, `draft:attachments`)
871
+ and the session's active skills (`skills:snapshot`) reach the window as
872
+ the local daemon's do, whether or not the local daemon is connected,
873
+ and the server's
874
+ `presence:changed` is merged with every other host's
875
+ (`presence-merge.ts`: the last snapshot per host, a server's entries
876
+ dropped when it goes away); `presence:list` asks every connected host.
877
+ `drafts.*` go to the session's host. `sessions:changed` from any host
878
+ re-reads every group of the sidebar.
879
+ - **Session data**: a server session's rows, transcript
880
+ (`sessions.loadEvents` pages of 200 turns, `loadEventsPage`: the last
881
+ page first, scrolling back passes the page's `cursor` as `before`),
882
+ footer numbers (`sessions.getHeader` `usage`: `contextTokens` for the
883
+ context gauge, `costUsd` for the cost pill; only a server too old for
884
+ `usage` has its document read), a clipped tool result's full text
885
+ (`sessions.getToolResult`), header, title, archive, turns and export
886
+ (`loadSessionDocument`: the document a page of turns at a time, written
887
+ as a copy on this computer) come from the server
888
+ (`hosts/server-sessions.ts`, `channels/history.ts`). No read asks a
889
+ current server for a whole session in one reply. Its last good list
890
+ stays in memory while it is offline (shown read-only, opening says the
891
+ server is offline). Rows carry `hostId` and a `zidane-env://<key>`
892
+ workspace root (the key URI-encoded), so the
893
+ Files / Diffs panels and `@` mentions read the server's folder through
894
+ `sessions.workspace*`, and `!` runs there, streamed
895
+ (`sessions.workspaceExecStream`, `server-shell.ts`; `shell:cancel` kills
896
+ it there; an older server: buffered), like the TUI.
897
+ - **Channels**: `hosts:status` (snapshot, also pushed on `hosts:status`),
898
+ `hosts:list` (with the registry's dates), `hosts:add` / `hosts:test`
899
+ (a saved id, or an unsaved URL + token; refusals come back as
900
+ `{ ok: false, error, message }` with the registry's codes),
901
+ `hosts:rename`, `hosts:remove`, `hosts:retry`, `hosts:listDirs`
902
+ (`fs.listDirs`), `hosts:authStatus` / `hosts:authSetApiKey` /
903
+ `hosts:authRemove` (`auth.*`), `hosts:catalog` (`catalog.*`, `null`
904
+ from a server too old for them). `sessions:list`, `sessions:create`,
905
+ `environments:list`, `settings:get` / `settings:set`,
906
+ `sessions:listModels`, `commands:list` / `commands:completeArgs`,
907
+ `extensions:completionProviders` / `extensions:completionSubmit` take
908
+ an optional host id.
909
+ - **Renderer**: Settings › Hosts (This computer + servers, status, server
910
+ name and version; add with the token masked, test before saving, a
911
+ message per refusal code; test, rename, remove with a confirm); the
912
+ sidebar grouped by host (no headers with This computer alone); a new
913
+ chat picks its host first, then that host's environments, then for a
914
+ server a folder on it; the chat header shows `on <server>` with its
915
+ status dot. On a server session, actions that need this machine's files
916
+ are not offered (reveal in Finder, open in editor, the local folder
917
+ picker, the dev browser tab, extension panels); there is no "download a
918
+ copy" of a single file yet, the session export writes a copy here. A
919
+ server terminal's loopback links (`localhost:5173`) are the server's:
920
+ they are not opened here, a tooltip says why (`serverOnlyLink`,
921
+ `terminal/links.ts`). A new chat that can't be created (its server went
922
+ offline) puts the typed prompt back with the reason above it
923
+ (`unsentDraft`, `ComposerNotice`).
924
+ - **Settings sync** ([SERVER.md § Settings sync](./SERVER.md#settings-sync)):
925
+ a server card's *Sync* (`ServerSync.tsx`): mode (Off · Import once ·
926
+ Keep in sync), the categories, each API key opted in after a
927
+ confirmation, *Take over* when another device syncs there (confirmed),
928
+ the server's overrides with *Sync again*, the connection / last push /
929
+ what waits / errors and what can't sync. Main: `hosts:syncState`,
930
+ `hosts:syncConfigure` (modes, categories and provider names checked),
931
+ `hosts:syncPush`, `hosts:syncTakeOver`, `hosts:syncReset`, and the
932
+ `hosts:syncChanged` push (the engine changed, or a server's
933
+ `sync:changed`). A server's Settings show `synced from <device>` under a
934
+ synced setting, or `edited here, not synced from <device>` with *Sync
935
+ again* (`SyncMarker.tsx`, `hosts:syncStatus`, any window). A local
936
+ `settings:set` tells the engine; its watchers cover the TUI's and other
937
+ windows' writes. Off with remote hosts: `hosts:syncState` answers
938
+ `enabled: false` and nothing loads (`gate.containment.test.ts`).
939
+ - **Settings on a server session**: `HostSettingsProvider` (renderer)
940
+ reads `settings:get(hostId)` (this window's settings overlaid with the
941
+ server's, `overlayHostSettings`) and writes `settings:set(patch,
942
+ hostId)` (main splits it with `splitHostSettingsPatch`: window-only keys
943
+ to this computer, the rest to the server, the server's part first: a
944
+ refused write applies nothing here and rejects with the server's error);
945
+ title `Settings · <server>`.
946
+ Authentication lists the server's providers: an API key is stored there,
947
+ an OAuth provider signs in (`hosts:oauth`, `ServerOAuthSignIn`: the
948
+ browser opens here through a loopback in the main process, a code can be
949
+ pasted, `hosts:oauthPaste` / `hosts:oauthCancel`, `hosts:oauthStart`
950
+ pushes the URL / device code), a stored credential is removed. Skills / MCPs / Extensions /
951
+ Plugins show the server's catalog for the session's folder there
952
+ (`HostCatalogPanel`, `hosts:catalog`): each entry with the server's
953
+ enabled state, a toggle writing the server's enabled list (the first
954
+ one seeds it from what the list governs; a plugin's or extension's
955
+ contribution or a broken entry shows its state and can't be toggled);
956
+ an HTTP MCP server signs in from here (`hosts:oauth`). When the server
957
+ takes installs (`hosts:installsAllowed`, its hello's `remoteInstalls`):
958
+ skills / extensions install from a spec (`hosts:install`), user-scope
959
+ entries are removed (two presses, `hosts:uninstall`), extensions updated
960
+ (`hosts:update`), MCP servers added / removed (`hosts:mcpUpsert` /
961
+ `hosts:mcpRemove`; an Authorization header stays on the server); a
962
+ `catalog:changed` from the server (`hosts:catalogChanged`) re-reads the
963
+ open tab. A server too old for `catalog.*` gets its enabled list. The summarizer picker
964
+ lists the server's models. Environments and Instructions are hidden.
965
+ The Claude Code import still targets this computer and says so when the
966
+ active chat is on a server.
967
+ - **Models, commands, search on a server session**: the model pickers
968
+ list the server's credentialed providers and models (`models.list`;
969
+ an older server: this computer's, as before). A pick there becomes this
970
+ window's remembered default (`lastProvider`, `lastModelChain`) only when
971
+ this computer can run its provider, without the fallbacks it can't
972
+ (`rememberableChain` in `zidane/chat/pure`, the TUI applies the same one); a server draft that picks a
973
+ provider only the server has launches under it without remembering it.
974
+ `/` completion, the palette's commands and the submit path's command
975
+ check use the server's `commands.list` / `commands.completeArgs` for the
976
+ session's folder there. Ghost text is the server's (`suggest.resolve`:
977
+ its path provider over its files, its extensions' providers). This
978
+ window's extension popup providers (`extensions:completionProviders`)
979
+ are not offered on a server session: they are this window's extensions,
980
+ run against this computer's files, and no RPC runs a server's. `@`
981
+ mentions list the server's files (`sessions.workspaceListFiles`). The
982
+ sidebar's content search also searches each connected server's group
983
+ there (`history.searchAll` over that group's rows), the hits merged with
984
+ this computer's. The `/` popover's skills are the ones the server
985
+ enables (`hosts:catalog` skills for the folder there).
986
+ - Tests: `host-directory.test.ts` (routing by key, twins),
987
+ `hosts/remote-hosts.server.test.ts` (the channels against a real
988
+ zidane-server spawned with Bun: per-code refusals, add, folders,
989
+ environments, create in a server folder, rename, `!` on the server,
990
+ transcript pages and scroll-back, the same id on this computer and the
991
+ server (both listed, each opened and edited on its host), a tool
992
+ result's text and an export without the whole document in one reply,
993
+ skills signals without the local daemon,
994
+ footer numbers from the header, the server's models, `/` commands,
995
+ ghost text and files, content search, drafts and presence from another
996
+ client, catalogs and a toggle, per-host settings and provider keys,
997
+ offline rows, remove; the server runs with its own HOME and no provider
998
+ keys of the developer's), `hosts/gate.containment.test.ts`, renderer
999
+ `HostsPanel.test.tsx`, `HostCatalogPanel.test.tsx`,
1000
+ `remember-model-pick.test.ts`, `sidebar-host-rows.test.ts`,
1001
+ `sidebar/hosts-sidebar.test.tsx`, `SessionsContext.twins.test.tsx` (two
1002
+ rows with one id, each opened and acted on with its key).
1003
+
1004
+ ### Machines in the desktop app
1005
+
1006
+ Behind the same gate (`docs/MACHINES.md` § Client UI). Main
1007
+ (`gui/src/main/chat/channels/machines.ts`) answers `machines:links`,
1008
+ `machines:shareStart` (minting the machine token on the server when This
1009
+ computer's host has none; `needsToken` asks the renderer for a pasted
1010
+ one, never echoed back in an error), `machines:shareStop`,
1011
+ `machines:useServer`, `machines:list`, `machines:forget`,
1012
+ `machines:listDirs`, all through This computer's host (`hostRequest`:
1013
+ the daemon, or the embedded host through the dispatcher's own handlers,
1014
+ `gui/src/main/chat/machine-links.ts`) or a saved server. It relays
1015
+ `machine:status` (any host, with its `hostId`) as `machines:status` and
1016
+ This computer's `machine:links` as `machines:links`; a server's
1017
+ `machine:notification` never reaches a window. `HostSummary.machineServe`
1018
+ says a server lends itself. Remote hosts coming on resume the embedded
1019
+ host's saved links. The renderer: Settings › Hosts › a server's
1020
+ *Machines* (`ServerMachines.tsx`), machine kinds grouped in the
1021
+ environment picker with a machine folder picker in the composer, and
1022
+ `MachineWaitingBanner` over the composer.
1023
+
1024
+ ## Several hosts: what the TUI and the GUI share
1025
+
1026
+ One implementation of the client side, each app keeping only its glue:
1027
+
1028
+ - `src/chat/hosts` (pure parts re-exported from `zidane/chat/pure`):
1029
+ `LOCAL_HOST_KEY` / `LOCAL_HOST_NAME`, `HostConnectionStatus`,
1030
+ `isMethodNotFound` (JSON-RPC -32601), the presence merge
1031
+ (`createPresenceMerge`, `mergePresenceMaps`), the id → host index
1032
+ (`createHostIndex`: unknown ids refused, a removed host's ids remembered
1033
+ as removed), and the registry with `checkHostInput` /
1034
+ `probeUnsavedHost` (the add form's unsaved test runs `add`'s checks).
1035
+ - `src/ipc/hosts` (exported from `zidane/ipc/remote`): `keepHostConnected`
1036
+ (the transport-agnostic connection loop: backoff, statuses, auth refusal
1037
+ not retried), `startSavedHosts` (registry ↔ directory sync through a
1038
+ `SavedHostsDirectory` the app supplies; a re-add reconfigures the entry
1039
+ in place), and `startWsSavedHosts` (the WebSocket registry, dial, unsaved
1040
+ probe and refusal classification). `connection-loop.ts` imports no
1041
+ WebSocket code, so the TUI loads it statically for This computer; the
1042
+ rest stays behind each app's gate.
1043
+ - TUI glue: `src/tui/host/host-directory.ts` (entries with transports,
1044
+ default/active pointers, session routing, `onRemoved`),
1045
+ `remote-hosts.ts`, `host-signals.ts`, `use-host-removal.ts`,
1046
+ `runtime/routed-session-data.ts`. GUI glue:
1047
+ `gui/src/main/chat/host-directory.ts`, `hosts/remote-hosts.ts`,
1048
+ `hosts/server-removal.ts`, `hosts/server-signals.ts`,
1049
+ `hosts/server-sessions.ts`, `presence-merge.ts`, and the renderer's
1050
+ `gui/src/shared/session-key.ts`.
1051
+ - Paged reads (`zidane/ipc`): `loadAllEvents` / `loadEventsPage` (the
1052
+ transcript, both apps) and `loadSessionDocument` (the whole document a
1053
+ page of turns at a time: the GUI's export of a server session; the TUI's
1054
+ own reads of a document still use `sessions.get`).
1055
+ - Settings sync to a server (`src/chat/sync`, loaded with the saved
1056
+ servers, behind the same gate): `types.ts` (pure: categories, modes,
1057
+ field names, `syncMarkerOf`; re-exported from `zidane/chat/pure`),
1058
+ `local.ts` (the device id in `device.json`, the per-server config and
1059
+ offline queue in `hosts-sync.json`, both 0600), `collect.ts` (each
1060
+ category's snapshot from this computer's files), `watchers.ts`,
1061
+ `pusher.ts` and `engine.ts` (`createSyncEngine`, exported from
1062
+ `zidane/ipc/remote`). Each app hands it its servers' connections and
1063
+ `sync:changed` signals: the TUI in `src/tui/host/remote-hosts.ts`, the
1064
+ GUI in `gui/src/main/chat/hosts/remote-hosts.ts`. UIs: the TUI's
1065
+ `src/tui/settings/server-sync-page.tsx` (Hosts → `ctrl+s`) and markers
1066
+ in `host-settings-scope.tsx`; the GUI's `ServerSync.tsx` (a server card's
1067
+ Sync) and `SyncMarker.tsx` (its Settings), over the `hosts:sync*`
1068
+ channels.
1069
+ - Settings on a server, both apps: the split is keyed on "the host is a
1070
+ server, not This computer" (`isWindowOnlySetting`). Window-only keys
1071
+ never go to a server and a server's never come in; the GUI's main splits
1072
+ `settings:set` and drops a server's `settings:changed`, the TUI applies a
1073
+ server's host-scoped keys only and persists only window-only ones while
1074
+ on a server (`--host` included).
1075
+
391
1076
  ## Removability
392
1077
 
393
1078
  The dependency direction is strictly `src/ipc` → `src/chat`: the host and
394
1079
  its wire shapes (`src/chat/host/**`, incl. `signals.ts`) import nothing
395
- from `src/ipc`. Deleting the whole transport — `src/ipc/**`,
396
- `src/daemon-cli.ts`, `src/tui/runtime/daemon.ts`, and the GUI's daemon
397
- glue (`daemon-client.ts` plus the daemon branches in
398
- `ipc.ts`/`engine.ts`/`interactions.ts`/`safeMode.ts`) — removes
399
- multi-window sync and nothing else: `createChatHost` stays, since it's
400
- the chat engine the GUI embeds unconditionally.
1080
+ from `src/ipc`. The TUI's local mode runs on `src/ipc`'s dispatcher, host
1081
+ environment and in-process transport, so those stay. Deleting the socket
1082
+ side — the daemon server and socket client, `src/daemon-cli.ts`, the
1083
+ socket transport, and the GUI's daemon glue (`daemon-client.ts`,
1084
+ `daemonHostClient` in `host-client.ts`, and the few remaining daemon
1085
+ branches in `ipc.ts`: the `!` guard, inline autocomplete, the settings
1086
+ write) — removes multi-window sync and nothing else: `createChatHost`
1087
+ stays, since it's the chat engine the GUI embeds unconditionally.
401
1088
 
402
1089
  ## Status / follow-ups
403
1090
 
404
- - The default TUI (`zidane`) still runs its agent in-process; `zidane
405
- attach` is the daemon-backed client. Migrating the full OpenTUI
406
- front-end onto `connectIpcClient` is the tracked follow-up.
1091
+ - Every TUI mode is host-backed ([TUI local mode](#tui-local-mode)).
1092
+ `test/tui/app-modes.test.tsx` drives the real `<App>` through every
1093
+ scenario on the in-process host and against a daemon built with the
1094
+ production environment around the window's setup, and pins the intended
1095
+ difference (local refuses to open another session mid-run; the daemon
1096
+ lets the run continue). `test/tui-runtime-parity.test.ts` runs every
1097
+ runtime scenario over the in-process and socket transports, which must
1098
+ match.
1099
+ - The daemon keeps per-session artifacts under the config dir
1100
+ (`paths.userDir`), the in-process host under the cache dir
1101
+ (`paths.cacheDir`, where the TUI always kept them). They coincide in the
1102
+ default layout and differ under an XDG layout.
1103
+ - `state.json` across processes: the daemon and a local-mode window on the
1104
+ same file both read-modify-write it without a lock (in daemon mode the
1105
+ window leaves settings to the daemon). A host in the window's process
1106
+ shares the window's state store, so that pair is one writer. Field-scoped
1107
+ merges or a lock would close the cross-process case.
407
1108
  - The GUI embeds `ChatHost` in Electron main and also speaks daemon: when
408
1109
  one is listening and `useDaemon` is on, `daemon-client.ts` connects as a
409
- guest (`drainOnMismatch: false`), routing execution, HITL, queue,
410
- drafts, search, presence, and settings writes over the wire; the
411
- embedded host is the fail-soft fallback.
1110
+ guest (`drainOnMismatch: false`). Every host operation goes through one
1111
+ `HostClient` (`gui/src/main/chat/host-client.ts`), named and typed by
1112
+ `IpcMethods`: `daemonHostClient` forwards to the socket,
1113
+ `embeddedHostClient` is a typed table of the in-process behavior
1114
+ (including where it differs: inert drafts / presence / search, artifacts
1115
+ in the local store). `hostRequest(method, params, { fallback })` picks
1116
+ the live one; with `fallback`, a dead socket (never a daemon answer)
1117
+ retries on the in-process host. A remote host would be a third
1118
+ implementation.
412
1119
  - `settings.set` in GUI daemon mode is wire-only (a local pre-write would
413
1120
  skip the daemon's agent-teardown ladder on extension/MCP toggles), with
414
1121
  a local-write fallback if the socket dies mid-write.
415
- - Wire-less session mutations (fork / deleteTurn / editTurnText /
416
- attachFolder / detachFolder) stay local-store + `sessions.touch` +
417
- `chat.rebuildAgent`, guarded by a `chat.isRunning` refusal on history
418
- edits. Remaining optional wire additions: paginated `sessions.loadEvents`
419
- and a SQL-narrowed cross-session search fast path (perf only).
1122
+ - Session edits (fork / deleteTurn / editTurnText / attach and detach a
1123
+ folder / `!` output) run in the host on every surface: the GUI calls
1124
+ the daemon's RPC or its in-process host, the TUI calls the RPC on its
1125
+ host (daemon or in-process), and no client writes the store for them. History and folder edits
1126
+ are refused while the session runs; the GUI greys the actions out, the
1127
+ TUI explains. Remaining optional wire addition: a SQL-narrowed
1128
+ cross-session search fast path (perf only). `sessions.loadEvents` pages
1129
+ from the host's loaded document (the store has no turn-range read), so
1130
+ paging saves the wire, not the host's disk read.