@thaut/claude-ide 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (559) hide show
  1. package/bin/claude-ide.js +23 -0
  2. package/package.json +75 -4
  3. package/server/add-dirs.ts +126 -0
  4. package/server/auth.test.ts +148 -0
  5. package/server/auth.ts +235 -0
  6. package/server/bang.ts +54 -0
  7. package/server/btw-instructions.ts +19 -0
  8. package/server/btw-policy.ts +17 -0
  9. package/server/btw.ts +143 -0
  10. package/server/child-env.ts +29 -0
  11. package/server/cli-control.ts +255 -0
  12. package/server/cli-files.ts +55 -0
  13. package/server/cli-info.ts +73 -0
  14. package/server/config/account.ts +65 -0
  15. package/server/config/agents.ts +186 -0
  16. package/server/config/mcp.ts +86 -0
  17. package/server/config/memory.ts +193 -0
  18. package/server/config/paths.ts +156 -0
  19. package/server/config/plugins.ts +204 -0
  20. package/server/config/routes.ts +95 -0
  21. package/server/config/settings.ts +246 -0
  22. package/server/config.ts +46 -0
  23. package/server/db.ts +121 -0
  24. package/server/directories.test.ts +180 -0
  25. package/server/directories.ts +87 -0
  26. package/server/dist-version.ts +69 -0
  27. package/server/durable-tasks.ts +61 -0
  28. package/server/file-transfers.ts +79 -0
  29. package/server/files.test.ts +127 -0
  30. package/server/files.ts +386 -0
  31. package/server/fs-queue.ts +14 -0
  32. package/server/fsutil.ts +144 -0
  33. package/server/git.ts +86 -0
  34. package/server/goal-state.test.ts +64 -0
  35. package/server/goal-state.ts +78 -0
  36. package/server/history-pages.test.ts +619 -0
  37. package/server/history-pages.ts +513 -0
  38. package/server/http-channel-manager.ts +468 -0
  39. package/server/http-channel-pull.test.ts +314 -0
  40. package/server/http-channel-push.test.ts +364 -0
  41. package/server/http-channel-registration.test.ts +291 -0
  42. package/server/http-channel-routes.ts +73 -0
  43. package/server/http-channel-store.ts +142 -0
  44. package/server/http-channel-test-fixture.ts +103 -0
  45. package/server/ide-messages-mcp.test.ts +82 -0
  46. package/server/ide-messages-mcp.ts +47 -0
  47. package/server/idecron-mcp.ts +65 -0
  48. package/server/idecron.test.ts +79 -0
  49. package/server/idecron.ts +271 -0
  50. package/server/index.ts +336 -0
  51. package/server/live-session.test.ts +1126 -0
  52. package/server/live-session.ts +1165 -0
  53. package/server/project-routes.ts +46 -0
  54. package/server/projects.test.ts +28 -0
  55. package/server/push.ts +315 -0
  56. package/server/report-cache.ts +28 -0
  57. package/server/session-aliases.test.ts +57 -0
  58. package/server/session-aliases.ts +50 -0
  59. package/server/session-branch.ts +71 -0
  60. package/server/session-dirs.ts +189 -0
  61. package/server/session-file.test.ts +134 -0
  62. package/server/session-file.ts +113 -0
  63. package/server/session-messaging.test.ts +297 -0
  64. package/server/session-messaging.ts +165 -0
  65. package/server/sessions.ts +638 -0
  66. package/server/subagents.test.ts +43 -0
  67. package/server/subagents.ts +58 -0
  68. package/server/tool-images.test.ts +76 -0
  69. package/server/tool-images.ts +45 -0
  70. package/server/turn-changes.ts +308 -0
  71. package/server/worktree-git.ts +87 -0
  72. package/server/worktrees.ts +138 -0
  73. package/server/ws.test.ts +195 -0
  74. package/server/ws.ts +197 -0
  75. package/shared/agent-model.ts +15 -0
  76. package/shared/agent-tasks.ts +12 -0
  77. package/shared/config.ts +175 -0
  78. package/shared/directories.ts +12 -0
  79. package/shared/files.ts +51 -0
  80. package/shared/http-channels.ts +63 -0
  81. package/shared/message-window.ts +189 -0
  82. package/shared/project.ts +77 -0
  83. package/shared/protocol.ts +203 -0
  84. package/shared/task-types.ts +4 -0
  85. package/shared/todos.ts +40 -0
  86. package/shared/write-policy.ts +9 -0
  87. package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
  88. package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
  89. package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
  90. package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
  91. package/web/dist/assets/Form-BKTwiYyw.js +1 -0
  92. package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
  93. package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
  94. package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
  95. package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
  96. package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
  97. package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
  98. package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
  99. package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
  100. package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
  101. package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
  102. package/web/dist/assets/abap-CLvhMVsD.js +1 -0
  103. package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
  104. package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
  105. package/web/dist/assets/ahk-DwADkxyd.js +1 -0
  106. package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
  107. package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  108. package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
  109. package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
  110. package/web/dist/assets/apache-U0d_L8uA.js +1 -0
  111. package/web/dist/assets/apex-KFGFyKwG.js +1 -0
  112. package/web/dist/assets/apl-xBchdRhy.js +1 -0
  113. package/web/dist/assets/applescript-CCn79oCD.js +1 -0
  114. package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
  115. package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
  116. package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
  117. package/web/dist/assets/astro-1q6DGuN3.js +1 -0
  118. package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  119. package/web/dist/assets/awk-BWXHIvNe.js +1 -0
  120. package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  121. package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  122. package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  123. package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  124. package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
  125. package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
  126. package/web/dist/assets/berry-DKpUyyne.js +1 -0
  127. package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  128. package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
  129. package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
  130. package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
  131. package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
  132. package/web/dist/assets/c-D9gIxSfx.js +1 -0
  133. package/web/dist/assets/c3-D0apG41V.js +1 -0
  134. package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
  135. package/web/dist/assets/cairo-DRAZz75c.js +1 -0
  136. package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  137. package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  138. package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  139. package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  140. package/web/dist/assets/chapel-CuseGurG.js +1 -0
  141. package/web/dist/assets/clarity-CtfBBspg.js +1 -0
  142. package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
  143. package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
  144. package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
  145. package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
  146. package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
  147. package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
  148. package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  149. package/web/dist/assets/coq-CGaSfhwR.js +1 -0
  150. package/web/dist/assets/core-BxI4V4-T.js +12 -0
  151. package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
  152. package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
  153. package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
  154. package/web/dist/assets/css-DpFbYmRh.js +1 -0
  155. package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
  156. package/web/dist/assets/cue-CE9AQfxI.js +1 -0
  157. package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
  158. package/web/dist/assets/d-qD-0Kul2.js +1 -0
  159. package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  160. package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
  161. package/web/dist/assets/dax-BkyTk9wS.js +1 -0
  162. package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
  163. package/web/dist/assets/diff-woXpYk--.js +1 -0
  164. package/web/dist/assets/docker-IyjqRm3v.js +1 -0
  165. package/web/dist/assets/dotenv-CuH5my32.js +1 -0
  166. package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
  167. package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  168. package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  169. package/web/dist/assets/edge-DXQWmXTB.js +1 -0
  170. package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
  171. package/web/dist/assets/elm-BslFEg8K.js +1 -0
  172. package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
  173. package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
  174. package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
  175. package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
  176. package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  177. package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  178. package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
  179. package/web/dist/assets/fish-BJitypiv.js +1 -0
  180. package/web/dist/assets/fluent-C03EYrpw.js +1 -0
  181. package/web/dist/assets/format-DrBC7w0A.js +1 -0
  182. package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
  183. package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  184. package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
  185. package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
  186. package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
  187. package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
  188. package/web/dist/assets/genie-CV2tkWYe.js +1 -0
  189. package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
  190. package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
  191. package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
  192. package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
  193. package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  194. package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  195. package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  196. package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
  197. package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
  198. package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  199. package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
  200. package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
  201. package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
  202. package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
  203. package/web/dist/assets/gn-CAXYAgV-.js +1 -0
  204. package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
  205. package/web/dist/assets/go-rLFTqkRN.js +1 -0
  206. package/web/dist/assets/graphql-CV_puxbB.js +1 -0
  207. package/web/dist/assets/groovy-CacY0gHj.js +1 -0
  208. package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  209. package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  210. package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  211. package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  212. package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  213. package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  214. package/web/dist/assets/hack-C5GPjiLV.js +1 -0
  215. package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
  216. package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
  217. package/web/dist/assets/haskell-D8IpX4py.js +1 -0
  218. package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
  219. package/web/dist/assets/hcl-Dh228itO.js +1 -0
  220. package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
  221. package/web/dist/assets/hjson-CxZEssPk.js +1 -0
  222. package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  223. package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
  224. package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  225. package/web/dist/assets/houston-CsvMBhTu.js +1 -0
  226. package/web/dist/assets/html-CmHiEKV9.js +1 -0
  227. package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
  228. package/web/dist/assets/http-DPjjqtcL.js +1 -0
  229. package/web/dist/assets/hurl-AMBpSloO.js +1 -0
  230. package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
  231. package/web/dist/assets/hy-CZbG8q4J.js +1 -0
  232. package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
  233. package/web/dist/assets/index-DNPnl4sh.css +2 -0
  234. package/web/dist/assets/index-DQh72lEx.js +14 -0
  235. package/web/dist/assets/ini-B5eOa1yu.js +1 -0
  236. package/web/dist/assets/java-CEM7Tdw_.js +1 -0
  237. package/web/dist/assets/javascript-CU3PLowd.js +1 -0
  238. package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
  239. package/web/dist/assets/jison-BoKsxs5M.js +1 -0
  240. package/web/dist/assets/json-BM2FWsRB.js +1 -0
  241. package/web/dist/assets/json5-BR5RXkoi.js +1 -0
  242. package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  243. package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  244. package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  245. package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
  246. package/web/dist/assets/jsx-B5fTecQP.js +1 -0
  247. package/web/dist/assets/julia-DGyDG39_.js +1 -0
  248. package/web/dist/assets/just-B-RyLcSR.js +1 -0
  249. package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  250. package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  251. package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
  252. package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
  253. package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
  254. package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
  255. package/web/dist/assets/langs-CciPKqaF.js +1 -0
  256. package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
  257. package/web/dist/assets/latex-CV1pQelm.js +1 -0
  258. package/web/dist/assets/lean-CewbzKMR.js +1 -0
  259. package/web/dist/assets/less-DVTAwKKz.js +1 -0
  260. package/web/dist/assets/lib-DlcVMUqT.js +1 -0
  261. package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
  262. package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
  263. package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
  264. package/web/dist/assets/log-BNLmms1o.js +1 -0
  265. package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
  266. package/web/dist/assets/lua-CIPVh0S-.js +1 -0
  267. package/web/dist/assets/luau-VGWVLoqX.js +1 -0
  268. package/web/dist/assets/make-Dixweg8N.js +1 -0
  269. package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  270. package/web/dist/assets/marko-h2h6YUMS.js +1 -0
  271. package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  272. package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  273. package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  274. package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  275. package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  276. package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
  277. package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
  278. package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
  279. package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
  280. package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
  281. package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
  282. package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
  283. package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  284. package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
  285. package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
  286. package/web/dist/assets/moonbit-D9W784E_.js +1 -0
  287. package/web/dist/assets/move-BYnGhZoM.js +1 -0
  288. package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
  289. package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
  290. package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
  291. package/web/dist/assets/nginx-jWINucNT.js +1 -0
  292. package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
  293. package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  294. package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
  295. package/web/dist/assets/nix-Cl_7024t.js +1 -0
  296. package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
  297. package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
  298. package/web/dist/assets/nushell--0fT3PI1.js +1 -0
  299. package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
  300. package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  301. package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
  302. package/web/dist/assets/odin-B1RWQWA5.js +1 -0
  303. package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
  304. package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
  305. package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
  306. package/web/dist/assets/org-ClD5huOa.js +1 -0
  307. package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
  308. package/web/dist/assets/perl-BFegxyeJ.js +1 -0
  309. package/web/dist/assets/php-B-qOA3Y4.js +1 -0
  310. package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
  311. package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
  312. package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
  313. package/web/dist/assets/po-BiJDBrnU.js +1 -0
  314. package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
  315. package/web/dist/assets/polar-C7UOKdEL.js +1 -0
  316. package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
  317. package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
  318. package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
  319. package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
  320. package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
  321. package/web/dist/assets/proto-DB4EqR-F.js +1 -0
  322. package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
  323. package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
  324. package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
  325. package/web/dist/assets/python-gzcpVVnB.js +1 -0
  326. package/web/dist/assets/qml-CYUsa87k.js +1 -0
  327. package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
  328. package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
  329. package/web/dist/assets/r-4xJhgbOf.js +1 -0
  330. package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
  331. package/web/dist/assets/raku-B3gFvitq.js +1 -0
  332. package/web/dist/assets/razor-BlHaOHx8.js +1 -0
  333. package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
  334. package/web/dist/assets/react-B4UGpcWC.js +1 -0
  335. package/web/dist/assets/red-CYlAq2b0.js +1 -0
  336. package/web/dist/assets/reg-CRGYupPL.js +1 -0
  337. package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
  338. package/web/dist/assets/rel-BtDbiS_P.js +1 -0
  339. package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
  340. package/web/dist/assets/ron-VUp2lXgN.js +1 -0
  341. package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
  342. package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  343. package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  344. package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
  345. package/web/dist/assets/rst-zjfguEh7.js +1 -0
  346. package/web/dist/assets/ruby-4oR9waBK.js +1 -0
  347. package/web/dist/assets/rust-DpiIf5JO.js +1 -0
  348. package/web/dist/assets/sas-Do4V_2i9.js +1 -0
  349. package/web/dist/assets/sass-DXrisJhu.js +1 -0
  350. package/web/dist/assets/scala-BiFt_7mu.js +1 -0
  351. package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
  352. package/web/dist/assets/scss-CW6JPtag.js +1 -0
  353. package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
  354. package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
  355. package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
  356. package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
  357. package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
  358. package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  359. package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
  360. package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
  361. package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  362. package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
  363. package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
  364. package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
  365. package/web/dist/assets/soy-DVtw20Yn.js +1 -0
  366. package/web/dist/assets/sparql-C0ia5K60.js +1 -0
  367. package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
  368. package/web/dist/assets/sql-DGCM-jag.js +1 -0
  369. package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  370. package/web/dist/assets/stata-DfuMOXna.js +1 -0
  371. package/web/dist/assets/stylus-B6D30XZt.js +1 -0
  372. package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
  373. package/web/dist/assets/svelte-g_uCUem5.js +1 -0
  374. package/web/dist/assets/swift-CyEgAFGc.js +1 -0
  375. package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  376. package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
  377. package/web/dist/assets/systemd-BxMlprV5.js +1 -0
  378. package/web/dist/assets/talonscript-CohzipZa.js +1 -0
  379. package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
  380. package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
  381. package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
  382. package/web/dist/assets/terraform-DswuEJGm.js +1 -0
  383. package/web/dist/assets/tex-mx6xDwuK.js +1 -0
  384. package/web/dist/assets/themes-CNElGaXw.js +1 -0
  385. package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  386. package/web/dist/assets/toml-CcmNWLt0.js +1 -0
  387. package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
  388. package/web/dist/assets/tsv-sltzmVWM.js +1 -0
  389. package/web/dist/assets/tsx-z04y7qR9.js +1 -0
  390. package/web/dist/assets/turtle-ByJddavk.js +1 -0
  391. package/web/dist/assets/twig-Bz57G_nl.js +1 -0
  392. package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
  393. package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
  394. package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
  395. package/web/dist/assets/v-DBj4K9d_.js +1 -0
  396. package/web/dist/assets/vala-zf12oZj6.js +1 -0
  397. package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
  398. package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
  399. package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
  400. package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
  401. package/web/dist/assets/viml-DvXPmvsu.js +1 -0
  402. package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  403. package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  404. package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  405. package/web/dist/assets/vue-CQgvQytb.js +1 -0
  406. package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
  407. package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
  408. package/web/dist/assets/vyper-CgoNMtux.js +1 -0
  409. package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
  410. package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
  411. package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
  412. package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
  413. package/web/dist/assets/wit-DdvCle-K.js +1 -0
  414. package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
  415. package/web/dist/assets/xml-B49klYQ3.js +1 -0
  416. package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
  417. package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
  418. package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
  419. package/web/dist/assets/zig-CMLA9XwU.js +1 -0
  420. package/web/dist/icons/apple-touch-icon.png +0 -0
  421. package/web/dist/icons/badge-96.png +0 -0
  422. package/web/dist/icons/icon-192.png +0 -0
  423. package/web/dist/icons/icon-512.png +0 -0
  424. package/web/dist/icons/icon.svg +4 -0
  425. package/web/dist/icons/maskable-192.png +0 -0
  426. package/web/dist/icons/maskable-512.png +0 -0
  427. package/web/dist/index.html +30 -0
  428. package/web/dist/manifest.webmanifest +20 -0
  429. package/web/dist/sw.js +176 -0
  430. package/web/dist/version.json +1 -0
  431. package/web/src/App.tsx +97 -0
  432. package/web/src/agent-history-flow.test.ts +191 -0
  433. package/web/src/agent-history-flow.ts +58 -0
  434. package/web/src/agent-owners.ts +30 -0
  435. package/web/src/annotation-selection.test.ts +36 -0
  436. package/web/src/annotation-selection.ts +123 -0
  437. package/web/src/annotations.test.ts +279 -0
  438. package/web/src/annotations.ts +191 -0
  439. package/web/src/api.ts +27 -0
  440. package/web/src/auth-events.ts +11 -0
  441. package/web/src/auth-lifecycle.test.ts +105 -0
  442. package/web/src/clipboard.ts +32 -0
  443. package/web/src/components/ActivityNotifier.tsx +22 -0
  444. package/web/src/components/AnnotationEditor.tsx +135 -0
  445. package/web/src/components/AnnotationSelection.tsx +122 -0
  446. package/web/src/components/Automation.tsx +390 -0
  447. package/web/src/components/CodeView.tsx +121 -0
  448. package/web/src/components/Composer.tsx +552 -0
  449. package/web/src/components/ComposerAnnotations.tsx +45 -0
  450. package/web/src/components/Confirmation.tsx +91 -0
  451. package/web/src/components/CopyButton.tsx +23 -0
  452. package/web/src/components/DiffView.tsx +94 -0
  453. package/web/src/components/DirectoryPicker.tsx +230 -0
  454. package/web/src/components/FileCopyMenu.tsx +58 -0
  455. package/web/src/components/FileEntryActions.tsx +95 -0
  456. package/web/src/components/ImagePreview.tsx +68 -0
  457. package/web/src/components/Items.tsx +608 -0
  458. package/web/src/components/Markdown.tsx +73 -0
  459. package/web/src/components/MessageList.tsx +458 -0
  460. package/web/src/components/Modal.tsx +72 -0
  461. package/web/src/components/NotificationEntry.tsx +31 -0
  462. package/web/src/components/NotificationSettings.tsx +226 -0
  463. package/web/src/components/NotificationToasts.tsx +59 -0
  464. package/web/src/components/OpenTabs.tsx +112 -0
  465. package/web/src/components/Pending.tsx +175 -0
  466. package/web/src/components/ProjectExtras.tsx +299 -0
  467. package/web/src/components/ProjectNav.tsx +47 -0
  468. package/web/src/components/SessionActions.tsx +271 -0
  469. package/web/src/components/SessionChannels.tsx +288 -0
  470. package/web/src/components/SessionExtras.tsx +263 -0
  471. package/web/src/components/SettingsLink.tsx +21 -0
  472. package/web/src/components/Shell.tsx +320 -0
  473. package/web/src/components/SvgPreview.tsx +18 -0
  474. package/web/src/components/TodoPanel.tsx +37 -0
  475. package/web/src/components/VersionNotice.tsx +40 -0
  476. package/web/src/components/icons.tsx +43 -0
  477. package/web/src/diff.ts +62 -0
  478. package/web/src/drafts.test.ts +80 -0
  479. package/web/src/drafts.ts +64 -0
  480. package/web/src/format.ts +5 -0
  481. package/web/src/highlight.worker.ts +49 -0
  482. package/web/src/hooks.ts +59 -0
  483. package/web/src/item-view-state.test.ts +51 -0
  484. package/web/src/item-view-state.ts +43 -0
  485. package/web/src/live-history-flow.test.ts +77 -0
  486. package/web/src/live-merge.test.ts +64 -0
  487. package/web/src/live-merge.ts +50 -0
  488. package/web/src/live-message-hydration.test.ts +253 -0
  489. package/web/src/live.ts +404 -0
  490. package/web/src/main.tsx +27 -0
  491. package/web/src/markdown-assets.test.ts +70 -0
  492. package/web/src/markdown-assets.ts +78 -0
  493. package/web/src/markdown-html.test.ts +84 -0
  494. package/web/src/markdown-html.ts +60 -0
  495. package/web/src/markdown-svg.test.ts +37 -0
  496. package/web/src/markdown-svg.ts +42 -0
  497. package/web/src/markdown-tables.test.ts +34 -0
  498. package/web/src/markdown-tables.ts +17 -0
  499. package/web/src/message-bodies.ts +117 -0
  500. package/web/src/message-cache.test.ts +208 -0
  501. package/web/src/message-cache.ts +158 -0
  502. package/web/src/message-font-size.test.ts +41 -0
  503. package/web/src/message-font-size.ts +57 -0
  504. package/web/src/message-time.test.ts +32 -0
  505. package/web/src/message-time.ts +26 -0
  506. package/web/src/notify.ts +132 -0
  507. package/web/src/open-tabs.test.ts +69 -0
  508. package/web/src/open-tabs.ts +110 -0
  509. package/web/src/pages/AgentView.tsx +138 -0
  510. package/web/src/pages/ChangesView.tsx +193 -0
  511. package/web/src/pages/FilesView.tsx +406 -0
  512. package/web/src/pages/HomePage.tsx +96 -0
  513. package/web/src/pages/LoginPage.tsx +56 -0
  514. package/web/src/pages/OverviewPage.tsx +124 -0
  515. package/web/src/pages/ProjectHome.tsx +136 -0
  516. package/web/src/pages/SessionView.tsx +238 -0
  517. package/web/src/pages/WorktreesView.tsx +197 -0
  518. package/web/src/pages/settings/AccountPage.tsx +149 -0
  519. package/web/src/pages/settings/AgentsPage.tsx +182 -0
  520. package/web/src/pages/settings/ConfigPage.tsx +181 -0
  521. package/web/src/pages/settings/GeneralPage.tsx +27 -0
  522. package/web/src/pages/settings/HooksPage.tsx +244 -0
  523. package/web/src/pages/settings/McpPage.tsx +310 -0
  524. package/web/src/pages/settings/MemoryPage.tsx +309 -0
  525. package/web/src/pages/settings/PermissionsPage.tsx +160 -0
  526. package/web/src/pages/settings/PluginsPage.tsx +251 -0
  527. package/web/src/pages/settings/SettingsPage.tsx +141 -0
  528. package/web/src/pages/settings/SkillsPage.tsx +142 -0
  529. package/web/src/pages/settings/ui.tsx +165 -0
  530. package/web/src/preference-document.ts +133 -0
  531. package/web/src/project-actions.ts +18 -0
  532. package/web/src/project-files-state.test.ts +49 -0
  533. package/web/src/project-files-state.ts +64 -0
  534. package/web/src/project-images.ts +13 -0
  535. package/web/src/pwa.ts +33 -0
  536. package/web/src/queryClient.ts +5 -0
  537. package/web/src/reasoning-preference.test.ts +39 -0
  538. package/web/src/reasoning-preference.ts +42 -0
  539. package/web/src/session-confirmations.ts +36 -0
  540. package/web/src/stream-replay.test.ts +163 -0
  541. package/web/src/styles.css +231 -0
  542. package/web/src/tool-result-body.test.ts +115 -0
  543. package/web/src/transcript.test.ts +186 -0
  544. package/web/src/transcript.ts +583 -0
  545. package/web/src/turns.test.ts +338 -0
  546. package/web/src/turns.ts +178 -0
  547. package/web/src/ui/Button.tsx +60 -0
  548. package/web/src/ui/Controls.tsx +227 -0
  549. package/web/src/ui/Form.tsx +203 -0
  550. package/web/src/ui/MenuPopover.tsx +144 -0
  551. package/web/src/ui/Popover.tsx +147 -0
  552. package/web/src/ui/ResizablePane.tsx +110 -0
  553. package/web/src/ui/Select.tsx +150 -0
  554. package/web/src/ui/icons.tsx +270 -0
  555. package/web/src/unsaved.tsx +99 -0
  556. package/web/src/user-text.test.ts +60 -0
  557. package/web/src/user-text.ts +35 -0
  558. package/web/src/uuid.ts +12 -0
  559. package/README.md +0 -3
@@ -0,0 +1,619 @@
1
+ import { test, type TestContext } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { historyPage, historyMessage, HistoryError } from "./history-pages.ts";
7
+ import { PAGE_BYTES } from "../shared/message-window.ts";
8
+ import { getSessionMessages, getSubagentMessages, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
9
+
10
+ const rec = (uuid: string, parentUuid: string | null, type = "user", extra: object = {}) => ({ type, uuid, parentUuid, message: { role: type, content: uuid }, ...extra });
11
+ type HistoryRecord = { type: string; [key: string]: unknown };
12
+ async function fixture(t: TestContext, records: object[]) {
13
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "ide-history-"));
14
+ t.after(() => fs.rm(dir, { recursive: true, force: true }));
15
+ const file = path.join(dir, "session.jsonl");
16
+ await fs.writeFile(file, records.map((r) => JSON.stringify(r)).join("\n") + "\n");
17
+ return file;
18
+ }
19
+
20
+ test("pages walk both ways across the summary without duplicating boundaries", async (t) => {
21
+ const records: object[] = [];
22
+ let parent: string | null = null;
23
+ for (let i = 0; i < 250; i++) {
24
+ if (i === 130) {
25
+ records.push(rec("compact", null, "system", { subtype: "compact_boundary", logicalParentUuid: parent })); parent = "compact";
26
+ records.push(rec("summary", parent, "user", { isCompactSummary: true })); parent = "summary";
27
+ }
28
+ records.push(rec(`u${i}`, parent)); parent = `u${i}`;
29
+ }
30
+ const file = await fixture(t, records);
31
+ const newest = await historyPage(file);
32
+ assert.equal(newest.items.length, 100);
33
+ assert.equal(newest.items.at(-1)?.uuid, "u249");
34
+ const middle = await historyPage(file, { cursor: newest.older });
35
+ const oldest = await historyPage(file, { cursor: middle.older });
36
+ const ids = [...oldest.items, ...middle.items, ...newest.items].map((m) => m.uuid);
37
+ assert.equal(ids.length, 252);
38
+ assert.equal(new Set(ids).size, 252);
39
+ assert.equal(oldest.older, undefined);
40
+ const forward = await historyPage(file, { cursor: oldest.newer });
41
+ assert.deepEqual(forward.items.map((m) => m.uuid), middle.items.map((m) => m.uuid));
42
+ });
43
+
44
+ test("branch selection and pending rewind never expose the abandoned branch", async (t) => {
45
+ const file = await fixture(t, [rec("root", null), rec("old", "root", "assistant"), rec("new", "root"), rec("answer", "new", "assistant")]);
46
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), ["root", "new", "answer"]);
47
+ assert.deepEqual((await historyPage(file, { anchor: "root" })).items.map((m) => m.uuid), ["root"]);
48
+ await assert.rejects(historyMessage(file, "old"), (e) => e instanceof HistoryError && e.status === 404);
49
+ });
50
+
51
+ test("preserved message rewrites retain both the summary and the earlier conversation", async (t) => {
52
+ const file = await fixture(t, [rec("u0", null), rec("a0", "u0", "assistant"), rec("u1", "a0"), rec("a1", "u1", "assistant"),
53
+ rec("boundary", null, "system", { subtype: "compact_boundary", logicalParentUuid: "a1", compactMetadata: { preservedMessages: { anchorUuid: "summary", uuids: ["u1", "a1"] } } }),
54
+ rec("summary", "boundary", "user", { isCompactSummary: true }), rec("u2", "summary"), rec("a2", "u2", "assistant")]);
55
+ const ids = (await historyPage(file)).items.map((m) => m.uuid);
56
+ assert.deepEqual(new Set(ids), new Set(["u0", "a0", "u1", "a1", "boundary", "summary", "u2", "a2"]));
57
+ assert.equal(ids.length, 8);
58
+ });
59
+
60
+ test("append indexing retries partial lines and concurrent readers share the update", async (t) => {
61
+ const file = await fixture(t, [rec("u0", null)]);
62
+ await historyPage(file);
63
+ const next = JSON.stringify(rec("u1", "u0"));
64
+ await fs.appendFile(file, next.slice(0, 25));
65
+ assert.equal((await historyPage(file)).items.length, 1);
66
+ await fs.appendFile(file, next.slice(25) + "\n");
67
+ const pages = await Promise.all(Array.from({ length: 4 }, () => historyPage(file)));
68
+ assert.ok(pages.every((p) => p.items.length === 2));
69
+ });
70
+
71
+ test("file replacement invalidates old cursors", async (t) => {
72
+ const file = await fixture(t, Array.from({ length: 110 }, (_, i) => rec(`u${i}`, i ? `u${i - 1}` : null)));
73
+ const page = await historyPage(file);
74
+ await fs.writeFile(file, JSON.stringify(rec("replacement", null)) + "\n");
75
+ await assert.rejects(historyPage(file, { cursor: page.older }), (e) => e instanceof HistoryError && e.status === 409);
76
+ });
77
+
78
+ test("oversized inline images become placeholders and can be hydrated from the original record", async (t) => {
79
+ const record = rec("image", null, "user", { message: { content: [{ type: "image", source: { media_type: "image/jpeg", data: "x".repeat(800_000) } }, { type: "text", text: "hello" }] } });
80
+ const file = await fixture(t, [record]);
81
+ const page = await historyPage(file);
82
+ assert.ok(JSON.stringify(page).length < PAGE_BYTES);
83
+ assert.ok(page.items[0]?._deferred);
84
+ assert.deepEqual((await historyMessage(file, "image")).message, record.message);
85
+ });
86
+
87
+ test("visible tools get their results even when the result falls on a different page", async (t) => {
88
+ const records = [rec("call", null, "assistant", { message: { content: [{ type: "tool_use", id: "t1", name: "Bash", input: { command: "pwd" } }] } })];
89
+ for (let i = 0; i < 110; i++) records.push(rec(`u${i}`, i ? `u${i - 1}` : "call"));
90
+ records.push(rec("tool-result", "u109", "user", { message: { content: [{ type: "tool_result", tool_use_id: "t1", content: "output" }] } }));
91
+ const file = await fixture(t, records);
92
+ const page = await historyPage(file, { after: "call" });
93
+ const previous = await historyPage(file, { cursor: page.older });
94
+ assert.equal((previous.items[0]?.linkedToolResults as Record<string, { content: string }>)?.t1?.content, "output");
95
+ assert.deepEqual(previous.items[0]?._toolResultUuids, { t1: "tool-result" });
96
+ });
97
+
98
+ test("subagent transcripts may be sidechains without being hidden", async (t) => {
99
+ const file = await fixture(t, [rec("u0", null, "user", { isSidechain: true }), rec("a0", "u0", "assistant", { isSidechain: true })]);
100
+ assert.equal((await historyPage(file, { sidechain: true })).items.length, 2);
101
+ assert.equal((await historyPage(file)).items.length, 0);
102
+ assert.equal((await historyMessage(file, "a0", undefined, true)).uuid, "a0");
103
+ });
104
+
105
+ test("rewinding to a preserved message ignores compactions written after that anchor", async (t) => {
106
+ const file = await fixture(t, [rec("u0", null), rec("u1", "u0"), rec("future", "u1", "assistant"),
107
+ rec("boundary", null, "system", { subtype: "compact_boundary", logicalParentUuid: "future", compactMetadata: { preservedMessages: { anchorUuid: "summary", uuids: ["u1", "future"] } } }),
108
+ rec("summary", "boundary", "user"), rec("u2", "summary")]);
109
+ assert.deepEqual((await historyPage(file, { anchor: "u1" })).items.map((m) => m.uuid), ["u0", "u1"]);
110
+ });
111
+
112
+ test("todo state is independent of the currently loaded history page", async (t) => {
113
+ const records = [rec("create", null, "assistant", { message: { content: [{ type: "tool_use", id: "create-tool", name: "TaskCreate", input: { subject: "first task" } }] } }),
114
+ rec("created", "create", "user", { message: { content: [{ type: "tool_result", tool_use_id: "create-tool", content: "Task #42 created successfully" }] } }),
115
+ rec("update", "created", "assistant", { message: { content: [{ type: "tool_use", id: "update-tool", name: "TaskUpdate", input: { taskId: "42", status: "completed" } }] } })];
116
+ for (let i = 0; i < 110; i++) records.push(rec(`u${i}`, i ? `u${i - 1}` : "update"));
117
+ const file = await fixture(t, records);
118
+ const page = await historyPage(file);
119
+ assert.equal(page.items.some((m) => m.uuid === "create"), false);
120
+ assert.deepEqual(page.todos, [{ id: "42", subject: "first task", status: "completed", activeForm: undefined }]);
121
+ assert.deepEqual((await historyPage(file, { cursor: page.older })).todos, page.todos);
122
+ });
123
+
124
+ test("assistant sibling blocks and their results match the SDK across pages and message reads (CACHE-005)", async (t) => {
125
+ const assistant = (uuid: string, parent: string, id: string, content: unknown[], timestamp: string) => rec(uuid, parent, "assistant", { message: { id, role: "assistant", content }, timestamp });
126
+ const records = [rec("root", null),
127
+ assistant("thinking", "root", "api-message", [{ type: "thinking", thinking: "reasoning" }], "2026-10-01T00:00:01Z"),
128
+ assistant("tool", "root", "api-message", [{ type: "tool_use", id: "tool-id", name: "Bash", input: { command: "pwd" } }], "2026-10-01T00:00:02Z"),
129
+ assistant("text", "root", "api-message", [{ type: "text", text: "answer" }], "2026-10-01T00:00:03Z"),
130
+ rec("result", "tool", "user", { message: { content: [{ type: "tool_result", tool_use_id: "tool-id", content: "output" }] }, timestamp: "2026-10-01T00:00:04Z" }),
131
+ assistant("abandoned", "root", "other-message", [{ type: "text", text: "old branch" }], "2026-10-01T00:00:05Z")];
132
+ for (let i = 0; i < 110; i++) records.push(rec(`u${i}`, i ? `u${i - 1}` : "text"));
133
+ const file = await fixture(t, records);
134
+ const store = new InMemorySessionStore();
135
+ store.load = async () => records;
136
+ const expected = (await getSessionMessages("12345678-1234-1234-1234-123456789abc", { dir: "/tmp", sessionStore: store })).map((m) => m.uuid);
137
+ const latest = await historyPage(file);
138
+ const older = await historyPage(file, { cursor: latest.older });
139
+ assert.deepEqual([...older.items, ...latest.items].map((m) => m.uuid), expected);
140
+ assert.deepEqual((await historyPage(file, { cursor: older.newer })).items.map((m) => m.uuid), latest.items.map((m) => m.uuid));
141
+ assert.equal(new Set(expected).size, expected.length);
142
+ for (const uuid of ["thinking", "tool", "result"]) assert.equal((await historyMessage(file, uuid)).uuid, uuid);
143
+ assert.equal((older.items.find((m) => m.uuid === "tool")?.linkedToolResults as Record<string, { content: string }>)["tool-id"]?.content, "output");
144
+ await assert.rejects(historyMessage(file, "abandoned"), (e) => e instanceof HistoryError && e.status === 404);
145
+ const rewind = await historyPage(file, { anchor: "text" });
146
+ assert.deepEqual(rewind.items.map((m) => m.uuid), ["root", "text", "thinking", "tool"]);
147
+ await assert.rejects(historyMessage(file, "result", "text"), (e) => e instanceof HistoryError && e.status === 404);
148
+ assert.deepEqual((await historyPage(file, { anchor: "root" })).items.map((m) => m.uuid), ["root"]);
149
+ });
150
+
151
+ test("off-chain tool results use SDK source and tool-id association without crossing agents (CACHE-005)", async (t) => {
152
+ const records = [rec("root", null),
153
+ rec("call", "root", "assistant", { message: { id: "api-message", content: [{ type: "tool_use", id: "tool-id", name: "Bash", input: {} }] } }),
154
+ rec("result", "root", "user", { sourceToolAssistantUUID: "call", message: { content: [{ type: "tool_result", tool_use_id: "tool-id", content: "done" }] } }),
155
+ rec("foreign-result", "root", "user", { agentId: "foreign", sourceToolAssistantUUID: "call", message: { content: [{ type: "tool_result", tool_use_id: "tool-id", content: "foreign" }] } }),
156
+ rec("answer", "call", "assistant", { message: { id: "answer-message", content: [{ type: "text", text: "answer" }] } })];
157
+ const file = await fixture(t, records);
158
+ const store = new InMemorySessionStore(); store.load = async () => records;
159
+ const expected = (await getSessionMessages("12345678-1234-1234-1234-123456789abc", { dir: "/tmp", sessionStore: store })).map((m) => m.uuid);
160
+ const page = await historyPage(file);
161
+ assert.deepEqual(page.items.map((m) => m.uuid), expected);
162
+ assert.equal((await historyMessage(file, "result")).uuid, "result");
163
+ await assert.rejects(historyMessage(file, "foreign-result"), (e) => e instanceof HistoryError && e.status === 404);
164
+ });
165
+
166
+ test("LRU eviction preserves cursors for unchanged files and ordinary appends (CACHE-010)", async (t) => {
167
+ const file = await fixture(t, Array.from({ length: 110 }, (_, i) => rec(`u${i}`, i ? `u${i - 1}` : null)));
168
+ const page = await historyPage(file);
169
+ for (let i = 0; i < 9; i++) await historyPage(await fixture(t, [rec(`other-${i}`, null)]));
170
+ const older = await historyPage(file, { cursor: page.older });
171
+ assert.equal(older.generation, page.generation);
172
+ assert.equal(older.items[0]?.uuid, "u0");
173
+ await fs.appendFile(file, JSON.stringify(rec("appended", "u109")) + "\n");
174
+ for (let i = 0; i < 9; i++) await historyPage(await fixture(t, [rec(`later-${i}`, null)]));
175
+ const afterAppend = await historyPage(file, { cursor: page.older });
176
+ assert.equal(afterAppend.generation, page.generation);
177
+ assert.equal(afterAppend.items[0]?.uuid, "u0");
178
+ });
179
+
180
+ test("a self-evicted over-budget index can still page backwards and forwards (CACHE-010)", async (t) => {
181
+ const file = await fixture(t, Array.from({ length: 140_000 }, (_, i) => rec(String(i), i ? String(i - 1) : null)));
182
+ const page = await historyPage(file);
183
+ const older = await historyPage(file, { cursor: page.older });
184
+ const forward = await historyPage(file, { cursor: older.newer });
185
+ assert.equal(older.generation, page.generation);
186
+ assert.deepEqual(forward.items.map((m) => m.uuid), page.items.map((m) => m.uuid));
187
+ });
188
+
189
+ test("in-place rewrites after index eviction get a new generation and reject old cursors (CACHE-010)", async (t) => {
190
+ const records = Array.from({ length: 110 }, (_, i) => rec(`u${i}`, i ? `u${i - 1}` : null));
191
+ const file = await fixture(t, records);
192
+ const page = await historyPage(file);
193
+ for (let i = 0; i < 9; i++) await historyPage(await fixture(t, [rec(`other-${i}`, null)]));
194
+ // Preserve every UUID and the file size, so generation must detect a body rewrite.
195
+ const rewritten = records.map((r) => ({ ...r, message: { ...r.message, content: r.message.content.replace("u", "v") } }));
196
+ await fs.writeFile(file, rewritten.map((r) => JSON.stringify(r)).join("\n") + "\n");
197
+ await assert.rejects(historyPage(file, { cursor: page.older }), (e) => e instanceof HistoryError && e.status === 409);
198
+ const fresh = await historyPage(file);
199
+ assert.notEqual(fresh.generation, page.generation);
200
+ for (let i = 0; i < 9; i++) await historyPage(await fixture(t, [rec(`later-${i}`, null)]));
201
+ assert.equal((await historyPage(file, { cursor: fresh.older })).generation, fresh.generation);
202
+ // A larger replacement must not be mistaken for an append when its prefix changed.
203
+ const expanded = [...records.map((r) => ({ ...r, message: { ...r.message, content: `replaced ${r.message.content}` } })), rec("new-tail", "u109")];
204
+ await fs.writeFile(file, expanded.map((r) => JSON.stringify(r)).join("\n") + "\n");
205
+ await assert.rejects(historyPage(file, { cursor: fresh.older }), (e) => e instanceof HistoryError && e.status === 409);
206
+ const replaced = await historyPage(file);
207
+ assert.notEqual(replaced.generation, fresh.generation);
208
+ await fs.writeFile(file, JSON.stringify(rec("replacement", null)) + "\n");
209
+ const truncated = await historyPage(file);
210
+ assert.notEqual(truncated.generation, replaced.generation);
211
+ });
212
+
213
+
214
+ async function sdkMessages(records: HistoryRecord[], sidechain = false) {
215
+ const store = new InMemorySessionStore();
216
+ store.load = async () => records;
217
+ store.listSubkeys = async () => ["subagents/agent-test"];
218
+ const session = "12345678-1234-1234-1234-123456789abc";
219
+ return sidechain ? getSubagentMessages(session, "test", { dir: "/tmp", sessionStore: store })
220
+ : getSessionMessages(session, { dir: "/tmp", sessionStore: store });
221
+ }
222
+
223
+ test("fork briefing attachments cannot select or hide an abandoned branch (CACHE-014)", async (t) => {
224
+ const records = [rec("root", null), rec("old", "root", "assistant"), rec("new", "root"), rec("answer", "new", "assistant"),
225
+ rec("brief-old", "old", "attachment", { attachment: { type: "fork_briefing" } }),
226
+ rec("brief-current", "answer", "attachment", { attachment: { type: "fork_briefing" } })];
227
+ const file = await fixture(t, records);
228
+ const expected = (await sdkMessages(records)).map((m) => m.uuid);
229
+ assert.deepEqual(expected, ["root", "new", "answer"]);
230
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), expected);
231
+ await assert.rejects(historyMessage(file, "old"), (e) => e instanceof HistoryError && e.status === 404);
232
+ assert.deepEqual((await historyPage(file, { anchor: "root" })).items.map((m) => m.uuid), ["root"]);
233
+ });
234
+
235
+ test("sibling and nested pending queues match SDK pages, hydration and rewind (CACHE-015)", async (t) => {
236
+ const records = [rec("root", null), rec("answer", "root", "assistant"),
237
+ rec("abandoned-queue", "answer", "attachment", { attachment: { type: "queued_command", prompt: "old" } }),
238
+ rec("abandoned-answer", "abandoned-queue", "assistant"),
239
+ rec("latest", "answer", "assistant")];
240
+ for (let i = 0; i < 105; i++) records.push(rec(`queue-${i}`, i === 104 ? "queue-103" : "latest", "attachment", {
241
+ timestamp: `2026-10-01T00:${String(Math.floor(i / 60)).padStart(2, "0")}:${String(i % 60).padStart(2, "0")}Z`,
242
+ attachment: { type: "queued_command", prompt: `prompt ${i}`, source_uuid: `source-${i}` },
243
+ }));
244
+ const file = await fixture(t, records);
245
+ const expected = (await sdkMessages(records)).map((m) => m.uuid);
246
+ assert.equal(expected.length, 108);
247
+ assert.equal(expected.includes("abandoned-queue"), false);
248
+ const latest = await historyPage(file);
249
+ const older = await historyPage(file, { cursor: latest.older });
250
+ assert.deepEqual([...older.items, ...latest.items].map((m) => m.uuid), expected);
251
+ assert.deepEqual((await historyPage(file, { cursor: older.newer })).items.map((m) => m.uuid), latest.items.map((m) => m.uuid));
252
+ assert.equal((await historyMessage(file, "source-0")).uuid, "source-0");
253
+ assert.equal((await historyMessage(file, "queue-104")).uuid, "source-104");
254
+ assert.deepEqual((await historyPage(file, { anchor: "answer" })).items.map((m) => m.uuid), ["root", "answer"]);
255
+ await assert.rejects(historyMessage(file, "abandoned-queue"), (e) => e instanceof HistoryError && e.status === 404);
256
+ });
257
+
258
+ test("main external meta origins are visible and internal meta prompts stay hidden (CACHE-016)", async (t) => {
259
+ for (const kind of ["channel", "peer", "observer", "observer-activity", "slack-ping", "unknown"]) {
260
+ const origin = { kind, server: "probe-channel" };
261
+ const records = [rec("root", null), rec("meta", "root", "user", { isMeta: true, origin }),
262
+ rec("internal", "meta", "user", { isMeta: true }), rec("answer", "internal", "assistant"),
263
+ rec("queue", "answer", "attachment", { attachment: { type: "queued_command", prompt: "external", isMeta: true, origin } }),
264
+ rec("internal-queue", "answer", "attachment", { attachment: { type: "queued_command", prompt: "internal", isMeta: true } })];
265
+ const file = await fixture(t, records);
266
+ const expected = await sdkMessages(records);
267
+ const page = await historyPage(file);
268
+ assert.deepEqual(page.items.map((m) => m.uuid), expected.map((m) => m.uuid), kind);
269
+ const visible = kind !== "unknown";
270
+ assert.equal(page.items.some((m) => m.uuid === "meta"), visible, kind);
271
+ assert.equal(page.items.some((m) => m.uuid === "queue"), visible, kind);
272
+ assert.equal(page.items.some((m) => m.uuid === "internal" || m.uuid === "internal-queue"), false);
273
+ if (visible) {
274
+ assert.equal((await historyMessage(file, "meta")).is_meta, true);
275
+ assert.equal(((await historyMessage(file, "queue")).origin as { kind: string }).kind, kind);
276
+ } else await assert.rejects(historyMessage(file, "meta"), (e) => e instanceof HistoryError && e.status === 404);
277
+ await assert.rejects(historyMessage(file, "internal-queue"), (e) => e instanceof HistoryError && e.status === 404);
278
+ }
279
+ });
280
+
281
+ test("subagent meta prompts and delivered internal queues match SDK visibility (CACHE-016)", async (t) => {
282
+ const records = [rec("root", null, "user", { isMeta: true, isSidechain: true }),
283
+ rec("queue", "root", "attachment", { isSidechain: true, attachment: { type: "queued_command", prompt: "notification", isMeta: true } }),
284
+ rec("answer", "queue", "assistant", { isSidechain: true }),
285
+ rec("pending", "answer", "attachment", { isSidechain: true, attachment: { type: "queued_command", prompt: "pending", isMeta: true } })];
286
+ const file = await fixture(t, records);
287
+ const expected = await sdkMessages(records, true);
288
+ const page = await historyPage(file, { sidechain: true });
289
+ assert.deepEqual(page.items.map((m) => m.uuid), expected.map((m) => m.uuid));
290
+ assert.deepEqual(page.items.map((m) => m.uuid), ["root", "queue", "answer"]);
291
+ assert.equal((await historyMessage(file, "root", undefined, true)).is_meta, true);
292
+ await assert.rejects(historyMessage(file, "pending", undefined, true), (e) => e instanceof HistoryError && e.status === 404);
293
+ });
294
+
295
+ test("many assistant groups retain SDK result association without copying global result sets (CACHE-020)", async (t) => {
296
+ const records = [rec("root", null)];
297
+ let parent = "root";
298
+ for (let i = 0; i < 120; i++) {
299
+ const call = `call-${i}`, result = `result-${i}`, tool = `tool-${i}`;
300
+ records.push(rec(call, parent, "assistant", { message: { id: `api-${i}`, content: [{ type: "tool_use", id: tool, name: "Bash", input: {} }] } }),
301
+ rec(result, call, "user", { message: { content: [{ type: "tool_result", tool_use_id: tool, content: "done" }] } }),
302
+ rec(`duplicate-${i}`, "root", "user", { sourceToolAssistantUUID: call, message: { content: [{ type: "tool_result", tool_use_id: tool, content: "duplicate" }] } }));
303
+ parent = result;
304
+ }
305
+ records.push(rec("answer", parent, "assistant"));
306
+ const file = await fixture(t, records);
307
+ const expected = (await sdkMessages(records)).map((m) => m.uuid);
308
+ const pages = [];
309
+ let page = await historyPage(file);
310
+ pages.unshift(...page.items);
311
+ while (page.older) { page = await historyPage(file, { cursor: page.older }); pages.unshift(...page.items); }
312
+ assert.deepEqual(pages.map((m) => m.uuid), expected);
313
+ assert.equal(pages.some((m) => m.uuid?.startsWith("duplicate-")), false);
314
+ assert.equal(expected.length, 242);
315
+ });
316
+
317
+
318
+ test("queued tails traverse metadata nodes and suppress delivered or duplicate source UUIDs (CACHE-015/016)", async (t) => {
319
+ const records = [rec("root", null), rec("sent", "root"), rec("answer", "sent", "assistant"),
320
+ rec("progress", "answer", "progress"),
321
+ rec("fork", "progress", "attachment", { attachment: { type: "fork_briefing" } }),
322
+ rec("delivered", "fork", "attachment", { attachment: { type: "queued_command", prompt: "already sent", source_uuid: "sent" } }),
323
+ rec("queue-one", "fork", "attachment", { attachment: { type: "queued_command", prompt: "first", source_uuid: "pending" } }),
324
+ rec("queue-two", "fork", "attachment", { attachment: { type: "queued_command", prompt: "duplicate", source_uuid: "pending" } })];
325
+ const expected = (await sdkMessages(records)).map((m) => m.uuid);
326
+ const file = await fixture(t, records);
327
+ assert.deepEqual(expected, ["root", "sent", "answer", "pending"]);
328
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), expected);
329
+ assert.equal(((await historyMessage(file, "pending")).message as { content: string }).content, "first");
330
+ await assert.rejects(historyMessage(file, "delivered"), (e) => e instanceof HistoryError && e.status === 404);
331
+ await assert.rejects(historyMessage(file, "queue-two"), (e) => e instanceof HistoryError && e.status === 404);
332
+ });
333
+
334
+ test("forwarded queue intents follow SDK validation before becoming visible prompts (CACHE-016)", async (t) => {
335
+ const intents = [
336
+ { value: { lineage: "test-lineage", source: "typed" }, hidden: true },
337
+ { value: { lineage: "test-lineage", source: "relay" }, hidden: true },
338
+ { value: { lineage: "test-lineage", source: "owner" }, hidden: true },
339
+ { value: { lineage: " " }, hidden: true },
340
+ { value: { lineage: "test-lineage", source: { unknown: true } }, hidden: true },
341
+ { value: { lineage: "" }, hidden: false },
342
+ { value: { lineage: 1 }, hidden: false },
343
+ { value: { source: "typed" }, hidden: false },
344
+ { value: null, hidden: false },
345
+ { value: "test-lineage", hidden: false },
346
+ { value: [{ lineage: "test-lineage" }], hidden: false },
347
+ ];
348
+ for (const sidechain of [false, true]) for (const { value, hidden } of intents) {
349
+ const records = [rec("root", null, "user", { isSidechain: sidechain }),
350
+ rec("queue", "root", "attachment", { isSidechain: sidechain, attachment: {
351
+ type: "queued_command", prompt: "forwarded content", source_uuid: "source", forwardedIntent: value,
352
+ } }), rec("answer", "queue", "assistant", { isSidechain: sidechain })];
353
+ const expected = await sdkMessages(records, sidechain);
354
+ assert.equal(expected.some((m) => m.uuid === "source"), !hidden);
355
+ const file = await fixture(t, records);
356
+ assert.deepEqual((await historyPage(file, { sidechain })).items.map((m) => m.uuid), expected.map((m) => m.uuid));
357
+ if (hidden) {
358
+ for (const id of ["queue", "source"]) await assert.rejects(historyMessage(file, id, undefined, sidechain), (e) => e instanceof HistoryError && e.status === 404);
359
+ } else {
360
+ assert.equal((await historyMessage(file, "queue", undefined, sidechain)).uuid, "source");
361
+ assert.equal((await historyMessage(file, "source", undefined, sidechain)).uuid, "source");
362
+ }
363
+ }
364
+ });
365
+
366
+ test("consecutive queued inputs before interruption match SDK pages, hydration and rewind (CACHE-015/016)", async (t) => {
367
+ const records = [rec("root", null)];
368
+ for (let i = 0; i < 105; i++) records.push(rec(`queue-${i}`, i ? `queue-${i - 1}` : "root", "attachment", {
369
+ attachment: { type: "queued_command", prompt: `prompt ${i}`, source_uuid: `source-${i}` },
370
+ }));
371
+ records.push(rec("interrupted", "queue-104", "user", { message: { role: "user", content: "[Request interrupted by user]" } }));
372
+ const expected = await sdkMessages(records);
373
+ assert.equal(expected.length, 107);
374
+ const file = await fixture(t, records);
375
+ const latest = await historyPage(file);
376
+ const older = await historyPage(file, { cursor: latest.older });
377
+ assert.deepEqual([...older.items, ...latest.items].map((m) => m.uuid), expected.map((m) => m.uuid));
378
+ assert.deepEqual((await historyPage(file, { cursor: older.newer })).items.map((m) => m.uuid), latest.items.map((m) => m.uuid));
379
+ for (const id of ["queue-0", "source-0", "queue-104", "source-104"]) {
380
+ const result = await historyMessage(file, id);
381
+ assert.equal(result.uuid, id.replace("queue-", "source-"));
382
+ assert.equal(result.isQueuedCommand, true);
383
+ }
384
+ assert.deepEqual((await historyPage(file, { anchor: "root" })).items.map((m) => m.uuid), ["root"]);
385
+ await assert.rejects(historyMessage(file, "source-0", "root"), (e) => e instanceof HistoryError && e.status === 404);
386
+ });
387
+
388
+ test("interruption markers and genuine follow-up inputs keep different queued visibility (CACHE-016)", async (t) => {
389
+ const variants = [
390
+ { content: "[Request interrupted by user]", visible: true },
391
+ { content: "[Request interrupted by user for tool use] detail", visible: true },
392
+ { content: "[Tool call did not complete: the turn was ended to deliver the message that follows. Nothing refused it; re-run it if still needed.]", visible: true },
393
+ { content: "[Tool call interrupted: the session ended before this call's result was recorded, so its outcome is unknown. Check whether it took effect before relying on it or running it again.]", visible: true },
394
+ { content: "[Tool call result not in this copy: this session was copied from another session before that session recorded this call's result. The call may have finished there, may still be running there, or may never have run. Check whether it took effect before relying on it or running it again.]", visible: true },
395
+ { content: "The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed.", visible: true },
396
+ { content: "[Tool call skipped: the turn was stopped before this call ran, by the check whose denial is on another call in this batch. Nothing refused this call and it had no effects; re-run it if still needed.]", visible: true },
397
+ { content: "[Tool call skipped: the turn ended to deliver the message that follows before this call ran. Nothing refused it; re-run it if still needed.]", visible: true },
398
+ { content: [{ type: "text", text: "[Request interrupted by user]" }], visible: true },
399
+ { content: [{ type: "text", text: "[Request interrupted by user]" }, { type: "text", text: "new user request" }], visible: false },
400
+ { content: "new user request", visible: false },
401
+ { content: "[Request interrupted by userish]", visible: false },
402
+ { content: [], visible: false },
403
+ ];
404
+ for (const sidechain of [false, true]) for (const { content, visible } of variants) {
405
+ const records = [rec("root", null, "user", { isSidechain: sidechain }),
406
+ rec("queue", "root", "attachment", { isSidechain: sidechain, attachment: { type: "queued_command", prompt: "queued", source_uuid: "source" } }),
407
+ rec("followup", "queue", "user", { isSidechain: sidechain, message: { role: "user", content } }),
408
+ rec("answer", "followup", "assistant", { isSidechain: sidechain })];
409
+ const expected = await sdkMessages(records, sidechain);
410
+ assert.equal(expected.some((m) => m.uuid === "source"), visible);
411
+ const file = await fixture(t, records);
412
+ assert.deepEqual((await historyPage(file, { sidechain })).items.map((m) => m.uuid), expected.map((m) => m.uuid));
413
+ if (visible) assert.equal((await historyMessage(file, "source", undefined, sidechain)).uuid, "source");
414
+ else await assert.rejects(historyMessage(file, "source", undefined, sidechain), (e) => e instanceof HistoryError && e.status === 404);
415
+ }
416
+ });
417
+
418
+ test("meta, compact summaries and forwarded queues preserve SDK delivery boundaries (CACHE-016)", async (t) => {
419
+ for (const marker of [{ isCompactSummary: true }, { isMeta: true }, { isMeta: true, origin: { kind: "channel", server: "probe" } }, { isMeta: true, origin: { kind: "peer" } }]) {
420
+ const records = [rec("root", null),
421
+ rec("queue", "root", "attachment", { attachment: { type: "queued_command", prompt: "queued" } }),
422
+ rec("marker", "queue", "user", marker), rec("answer", "marker", "assistant")];
423
+ const expected = await sdkMessages(records);
424
+ const file = await fixture(t, records);
425
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), expected.map((m) => m.uuid));
426
+ assert.equal((await historyMessage(file, "queue")).uuid, "queue");
427
+ }
428
+ for (const kind of ["channel", "peer", "unknown"]) for (const forwarded of [true, false]) {
429
+ const records = [rec("root", null), rec("answer", "root", "assistant"),
430
+ rec("queue", "answer", "attachment", { attachment: { type: "queued_command", prompt: "queued", isMeta: true,
431
+ origin: { kind }, forwardedIntent: { lineage: forwarded ? "lineage" : "" },
432
+ } })];
433
+ const expected = await sdkMessages(records);
434
+ const file = await fixture(t, records);
435
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), expected.map((m) => m.uuid));
436
+ const visible = kind !== "unknown" && !forwarded;
437
+ assert.equal(expected.some((m) => m.uuid === "queue"), visible);
438
+ if (visible) assert.equal((await historyMessage(file, "queue")).is_meta, true);
439
+ else await assert.rejects(historyMessage(file, "queue"), (e) => e instanceof HistoryError && e.status === 404);
440
+ }
441
+ });
442
+
443
+ test("completed local commands preserve preceding queues and annotate record/output messages like SDK (CACHE-016)", async (t) => {
444
+ for (const sidechain of [false, true]) for (const format of ["string", "blocks"]) {
445
+ const content = (text: string) => format === "string" ? text : [{ type: "text", text: "leading" }, { type: "text", text }];
446
+ const records = [rec("root", null, "user", { isSidechain: sidechain }),
447
+ rec("queue", "root", "attachment", { isSidechain: sidechain, attachment: { type: "queued_command", prompt: "queued" } }),
448
+ rec("caveat", "queue", "user", { isSidechain: sidechain, isMeta: true, message: { role: "user", content: content("<local-command-caveat>Ignore command output") } }),
449
+ rec("record", "caveat", "user", { isSidechain: sidechain, message: { role: "user", content: content("<command-name>/x</command-name>") } }),
450
+ rec("stdout", "record", "user", { isSidechain: sidechain, message: { role: "user", content: content("<local-command-stdout>output") } }),
451
+ rec("stderr", "stdout", "user", { isSidechain: sidechain, message: { role: "user", content: content("<local-command-stderr>error") } }),
452
+ rec("answer", "stderr", "assistant", { isSidechain: sidechain })];
453
+ const expected = await sdkMessages(records, sidechain);
454
+ const file = await fixture(t, records);
455
+ const page = await historyPage(file, { sidechain });
456
+ assert.deepEqual(page.items.map((m) => [m.uuid, m.isCompletedLocalCommand]), expected.map((m) => [m.uuid, "isCompletedLocalCommand" in m ? m.isCompletedLocalCommand : undefined]));
457
+ assert.equal((await historyMessage(file, "queue", undefined, sidechain)).uuid, "queue");
458
+ assert.equal((await historyMessage(file, "stdout", undefined, sidechain)).isCompletedLocalCommand, true);
459
+ // A real new user input still cuts off an unconfirmed queued command.
460
+ const changed = records.map((r) => r.uuid === "record" ? { ...r, promptSource: "user" } : r);
461
+ const changedFile = await fixture(t, changed);
462
+ assert.deepEqual((await historyPage(changedFile, { sidechain })).items.map((m) => m.uuid), (await sdkMessages(changed, sidechain)).map((m) => m.uuid));
463
+ await assert.rejects(historyMessage(changedFile, "queue", undefined, sidechain), (e) => e instanceof HistoryError && e.status === 404);
464
+ }
465
+ });
466
+
467
+ test("delivery confirmations match SDK and remain bounded by rewind, duplicate copies and append offsets (CACHE-015/016)", async (t) => {
468
+ for (const delivery of [false, true]) {
469
+ const attachment = { type: "queued_command", prompt: "queued", source_uuid: "source", ...(delivery ? { delivery_id: "delivery" } : {}) };
470
+ const records: HistoryRecord[] = [rec("root", null), rec("queue-1", "root", "attachment", { attachment }),
471
+ rec("queue-2", "queue-1", "attachment", { attachment }), rec("followup", "queue-2"), rec("answer", "followup", "assistant")];
472
+ const operation = { type: "queue-operation", operation: "remove", reason: "absorbed_mid_turn", ...(delivery ? { deliveryId: "delivery" } : { commandUuid: "source" }) };
473
+ records.push(operation);
474
+ const file = await fixture(t, records);
475
+ const expected = await sdkMessages(records);
476
+ const page = await historyPage(file);
477
+ assert.deepEqual(page.items.map((m) => m.uuid), expected.map((m) => m.uuid));
478
+ assert.equal(((await historyMessage(file, "source")).message as { content: string }).content, "queued");
479
+ await assert.rejects(historyMessage(file, "queue-1"), (e) => e instanceof HistoryError && e.status === 404);
480
+ assert.equal((await historyMessage(file, "queue-2")).uuid, "source");
481
+ // Reading again must not count the same operation twice.
482
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), page.items.map((m) => m.uuid));
483
+ await assert.rejects(historyMessage(file, "queue-1"), (e) => e instanceof HistoryError && e.status === 404);
484
+ // Re-appending a known attachment UUID is an update, not a fresh copy.
485
+ await fs.appendFile(file, JSON.stringify(records[1]) + "\n"); records.push(records[1]!);
486
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), (await sdkMessages(records)).map((m) => m.uuid));
487
+ assert.equal((await historyMessage(file, "queue-2")).uuid, "source");
488
+ for (let i = 0; i < 9; i++) await historyPage(await fixture(t, [rec(`evict-${i}`, null)]));
489
+ assert.equal((await historyMessage(file, "queue-2")).uuid, "source");
490
+ // A later receipt cannot leak backwards into an anchored view.
491
+ assert.deepEqual((await historyPage(file, { anchor: "answer" })).items.map((m) => m.uuid), ["root", "followup", "answer"]);
492
+ await assert.rejects(historyMessage(file, "source", "answer"), (e) => e instanceof HistoryError && e.status === 404);
493
+ await fs.appendFile(file, JSON.stringify(operation) + "\n"); records.push(operation);
494
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), (await sdkMessages(records)).map((m) => m.uuid));
495
+ assert.equal((await historyMessage(file, "queue-1")).uuid, "source");
496
+ await assert.rejects(historyMessage(file, "queue-2"), (e) => e instanceof HistoryError && e.status === 404);
497
+ }
498
+ });
499
+
500
+ test("ambiguous legacy queue payloads ignore receipts, and unrelated operations cannot deliver a prompt (CACHE-016)", async (t) => {
501
+ for (const reason of ["absorbed_mid_turn", "cancelled"]) {
502
+ const records = [rec("root", null),
503
+ rec("queue-1", "root", "attachment", { attachment: { type: "queued_command", prompt: "first", source_uuid: "source" } }),
504
+ rec("queue-2", "queue-1", "attachment", { attachment: { type: "queued_command", prompt: "different", source_uuid: "source" } }),
505
+ rec("followup", "queue-2"), rec("answer", "followup", "assistant"),
506
+ { type: "queue-operation", operation: "remove", reason, commandUuid: "source" }];
507
+ const file = await fixture(t, records);
508
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), (await sdkMessages(records)).map((m) => m.uuid));
509
+ await assert.rejects(historyMessage(file, "source"), (e) => e instanceof HistoryError && e.status === 404);
510
+ }
511
+ // A conflicting overwrite of the same attachment UUID must retain the earlier
512
+ // ambiguity classification, even though the offset map now points at its tail.
513
+ const records: HistoryRecord[] = [rec("root", null),
514
+ rec("queue", "root", "attachment", { attachment: { type: "queued_command", prompt: "first", source_uuid: "source" } }),
515
+ rec("followup", "queue"), rec("answer", "followup", "assistant"),
516
+ { type: "queue-operation", operation: "remove", reason: "absorbed_mid_turn", commandUuid: "source" }];
517
+ const file = await fixture(t, records);
518
+ assert.equal((await historyMessage(file, "source")).uuid, "source");
519
+ const conflicting = rec("queue", "root", "attachment", { attachment: { type: "queued_command", prompt: "changed", source_uuid: "source" } });
520
+ await fs.appendFile(file, JSON.stringify(conflicting) + "\n"); records.push(conflicting);
521
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), (await sdkMessages(records)).map((m) => m.uuid));
522
+ await assert.rejects(historyMessage(file, "source"), (e) => e instanceof HistoryError && e.status === 404);
523
+ });
524
+
525
+ test("subagent queue-operation records do not override SDK reply/prompt visibility (CACHE-016)", async (t) => {
526
+ const records = [rec("root", null, "user", { isSidechain: true }),
527
+ rec("queue", "root", "attachment", { isSidechain: true, attachment: { type: "queued_command", prompt: "queued", source_uuid: "source", delivery_id: "delivery" } }),
528
+ rec("followup", "queue", "user", { isSidechain: true }), rec("answer", "followup", "assistant", { isSidechain: true }),
529
+ { type: "queue-operation", operation: "remove", reason: "absorbed_mid_turn", deliveryId: "delivery" }];
530
+ const expected = await sdkMessages(records, true);
531
+ const file = await fixture(t, records);
532
+ assert.deepEqual(expected.map((m) => m.uuid), ["root", "followup", "answer"]);
533
+ assert.deepEqual((await historyPage(file, { sidechain: true })).items.map((m) => m.uuid), expected.map((m) => m.uuid));
534
+ await assert.rejects(historyMessage(file, "source", undefined, true), (e) => e instanceof HistoryError && e.status === 404);
535
+ });
536
+
537
+ test("confirmed queued copies retain rewind membership after identical UUID rewrites (CACHE-015/016)", async (t) => {
538
+ for (const delivery of [false, true]) {
539
+ const attachment = { type: "queued_command", prompt: "queued", source_uuid: "source", ...(delivery ? { delivery_id: "delivery" } : {}) };
540
+ const queue = rec("queue", "root", "attachment", { attachment });
541
+ const operation = { type: "queue-operation", operation: "remove", reason: "absorbed_mid_turn", ...(delivery ? { deliveryId: "delivery" } : { commandUuid: "source" }) };
542
+ const records: HistoryRecord[] = [rec("root", null), queue, operation, rec("followup", "queue"), rec("answer", "followup", "assistant")];
543
+ const file = await fixture(t, records);
544
+ const expected = ["root", "source", "followup", "answer"];
545
+ const anchored = async () => (await historyPage(file, { anchor: "answer" })).items.map((m) => m.uuid);
546
+ assert.deepEqual(await anchored(), expected);
547
+ await fs.appendFile(file, JSON.stringify(queue) + "\n"); records.push(queue);
548
+ assert.deepEqual(await anchored(), expected);
549
+ assert.deepEqual((await sdkMessages(records)).map((m) => m.uuid), expected);
550
+ for (const id of ["queue", "source"]) assert.equal((await historyMessage(file, id, "answer")).uuid, "source");
551
+ for (let i = 0; i < 9; i++) await historyPage(await fixture(t, [rec(`rewind-evict-${i}`, null)]));
552
+ assert.deepEqual(await anchored(), expected);
553
+ assert.equal((await historyMessage(file, "source", "answer")).uuid, "source");
554
+
555
+ // A future, genuinely new copy must not compete with the pre-anchor copy
556
+ // for its earlier confirmation. Neither may a future receipt enter the view.
557
+ const future = rec("future-copy", "answer", "attachment", { attachment });
558
+ await fs.appendFile(file, [future, operation].map((r) => JSON.stringify(r)).join("\n") + "\n");
559
+ assert.deepEqual(await anchored(), expected);
560
+ assert.equal((await historyMessage(file, "queue", "answer")).uuid, "source");
561
+ await assert.rejects(historyMessage(file, "future-copy", "answer"), (e) => e instanceof HistoryError && e.status === 404);
562
+
563
+ // A changed body under an existing UUID is no longer the earlier copy. It
564
+ // must not gain pre-anchor membership by borrowing that copy's first offset.
565
+ const changed = rec("queue", "root", "attachment", { attachment: { ...attachment, prompt: "future changed payload" } });
566
+ await fs.appendFile(file, JSON.stringify(changed) + "\n");
567
+ assert.deepEqual(await anchored(), ["root", "followup", "answer"]);
568
+ await assert.rejects(historyMessage(file, "source", "answer"), (e) => e instanceof HistoryError && e.status === 404);
569
+ const changedKey = rec("queue", "root", "attachment", { attachment: { ...attachment, source_uuid: "future-source", ...(delivery ? { delivery_id: "future-delivery" } : {}) } });
570
+ await fs.appendFile(file, JSON.stringify(changedKey) + "\n");
571
+ assert.deepEqual(await anchored(), ["root", "followup", "answer"]);
572
+ await assert.rejects(historyMessage(file, "future-source", "answer"), (e) => e instanceof HistoryError && e.status === 404);
573
+
574
+ // A reply-shaped successor cannot make a future conflicting body visible
575
+ // through the normal heuristic either, even without a new user delimiter.
576
+ const replyFile = await fixture(t, [rec("root", null), queue, operation, rec("reply", "queue", "assistant")]);
577
+ await historyPage(replyFile, { anchor: "reply" });
578
+ await fs.appendFile(replyFile, JSON.stringify(changed) + "\n");
579
+ assert.deepEqual((await historyPage(replyFile, { anchor: "reply" })).items.map((m) => m.uuid), ["root", "reply"]);
580
+ await assert.rejects(historyMessage(replyFile, "source", "reply"), (e) => e instanceof HistoryError && e.status === 404);
581
+
582
+ const noReceiptRecords = [rec("root", null), queue, rec("followup", "queue"), rec("answer", "followup", "assistant")];
583
+ const noReceiptFile = await fixture(t, noReceiptRecords);
584
+ await historyPage(noReceiptFile, { anchor: "answer" });
585
+ await fs.appendFile(noReceiptFile, [queue, operation].map((r) => JSON.stringify(r)).join("\n") + "\n");
586
+ assert.deepEqual((await historyPage(noReceiptFile, { anchor: "answer" })).items.map((m) => m.uuid), ["root", "followup", "answer"]);
587
+ await assert.rejects(historyMessage(noReceiptFile, "source", "answer"), (e) => e instanceof HistoryError && e.status === 404);
588
+ }
589
+ // UUID-less native notifications cannot have a receipt key; their delivered
590
+ // attachment UUID must still survive an identical rewrite under the anchor.
591
+ const queue = rec("queue", "root", "attachment", { attachment: { type: "queued_command", prompt: "notification" } });
592
+ const file = await fixture(t, [rec("root", null), queue, rec("answer", "queue", "assistant")]);
593
+ await historyPage(file, { anchor: "answer" });
594
+ await fs.appendFile(file, JSON.stringify(queue) + "\n");
595
+ assert.deepEqual((await historyPage(file, { anchor: "answer" })).items.map((m) => m.uuid), ["root", "queue", "answer"]);
596
+ assert.equal((await historyMessage(file, "queue", "answer")).uuid, "queue");
597
+ });
598
+
599
+ test("another branch's confirmed copy does not deliver the current branch's unconfirmed copy (CACHE-015/016)", async (t) => {
600
+ for (const delivery of [false, true]) {
601
+ const attachment = { type: "queued_command", prompt: "queued", source_uuid: "source", ...(delivery ? { delivery_id: "delivery" } : {}) };
602
+ const records: HistoryRecord[] = [rec("root", null), rec("current-queue", "root", "attachment", { attachment }), rec("followup", "current-queue"),
603
+ rec("old-queue", "root", "attachment", { attachment }), rec("old-answer", "old-queue", "assistant"),
604
+ { type: "queue-operation", operation: "remove", reason: "absorbed_mid_turn", ...(delivery ? { deliveryId: "delivery" } : { commandUuid: "source" }) },
605
+ rec("answer", "followup", "assistant")];
606
+ const file = await fixture(t, records);
607
+ assert.deepEqual((await historyPage(file)).items.map((m) => m.uuid), ["root", "followup", "answer"]);
608
+ for (const id of ["source", "current-queue", "old-queue"]) await assert.rejects(historyMessage(file, id), (e) => e instanceof HistoryError && e.status === 404);
609
+ }
610
+ });
611
+
612
+ test("linked deferred tool results retain their original record UUID (CACHE-024)", async (t) => {
613
+ const file = await fixture(t, [rec("call", null, "assistant", { message: { content: [{ type: "tool_use", id: "tool", name: "Bash", input: {} }] } }),
614
+ rec("result", "call", "user", { message: { content: [{ type: "tool_result", tool_use_id: "tool", content: "x".repeat(20_000) }] } })]);
615
+ const page = await historyPage(file);
616
+ const call = page.items.find((m) => m.uuid === "call")!;
617
+ assert.deepEqual(call._toolResultUuids, { tool: "result" });
618
+ assert.equal((call.linkedToolResults as Record<string, { deferredUuid: string }>).tool?.deferredUuid, "result");
619
+ });