pim-agent 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/package.json +3 -3
  2. package/packages/core/src/attachments/AttachmentStore.ts +8 -13
  3. package/packages/core/src/attachments/Attachments.ts +2 -4
  4. package/packages/core/src/extensions/CoreExtensions.ts +1 -1
  5. package/packages/core/src/extensions/apply-patch/coordinator.ts +1 -1
  6. package/packages/core/src/extensions/apply-patch/executor.ts +21 -25
  7. package/packages/core/src/extensions/apply-patch/matcher.ts +1 -1
  8. package/packages/core/src/extensions/apply-patch/model.ts +2 -6
  9. package/packages/core/src/extensions/apply-patch/parser.ts +60 -162
  10. package/packages/core/src/extensions/apply-patch/schema.ts +0 -1
  11. package/packages/core/src/extensions/bash/MemoryCap.ts +5 -4
  12. package/packages/core/src/extensions/bash/capture.ts +16 -23
  13. package/packages/core/src/extensions/bash/format.ts +2 -2
  14. package/packages/core/src/extensions/bash/image.ts +4 -8
  15. package/packages/core/src/extensions/bash/index.ts +1 -1
  16. package/packages/core/src/extensions/bash/render.ts +1 -1
  17. package/packages/core/src/extensions/bash/run.ts +9 -15
  18. package/packages/core/src/extensions/bash/schema.ts +4 -4
  19. package/packages/core/src/extensions/edit/edit.ts +14 -33
  20. package/packages/core/src/extensions/glob/glob.ts +1 -3
  21. package/packages/core/src/extensions/glob/index.ts +7 -9
  22. package/packages/core/src/extensions/glob/render.ts +2 -4
  23. package/packages/core/src/extensions/glob/schema.ts +1 -1
  24. package/packages/core/src/extensions/grep/grep.ts +7 -7
  25. package/packages/core/src/extensions/grep/render.ts +5 -6
  26. package/packages/core/src/extensions/grep/schema.ts +3 -7
  27. package/packages/core/src/extensions/read/ImageMemory.ts +2 -8
  28. package/packages/core/src/extensions/read/image.ts +3 -3
  29. package/packages/core/src/extensions/read/index.ts +2 -14
  30. package/packages/core/src/extensions/read/read.ts +5 -6
  31. package/packages/core/src/extensions/read/render.ts +1 -1
  32. package/packages/core/src/extensions/read/schema.ts +2 -2
  33. package/packages/core/src/extensions/subagent/render.ts +2 -2
  34. package/packages/core/src/extensions/subagent/subagent.ts +10 -21
  35. package/packages/core/src/extensions/system-prompt/index.ts +1 -2
  36. package/packages/core/src/extensions/system-prompt/prompt.ts +2 -4
  37. package/packages/core/src/extensions/todo/render.ts +1 -4
  38. package/packages/core/src/extensions/todo/todo.ts +16 -23
  39. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +3 -11
  40. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +1 -3
  41. package/packages/core/src/extensions/web-fetch/fetch.ts +4 -12
  42. package/packages/core/src/extensions/web-fetch/image.ts +3 -4
  43. package/packages/core/src/extensions/web-fetch/index.ts +2 -13
  44. package/packages/core/src/extensions/web-fetch/render.ts +3 -4
  45. package/packages/core/src/extensions/web-fetch/schema.ts +3 -4
  46. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +23 -31
  47. package/packages/core/src/extensions/web-search/SearchBreaker.ts +1 -1
  48. package/packages/core/src/extensions/web-search/SearchChain.ts +6 -5
  49. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +2 -2
  50. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +1 -2
  51. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +3 -3
  52. package/packages/core/src/extensions/write/write.ts +1 -1
  53. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +1 -3
  54. package/packages/core/src/picker/PickerItem.ts +1 -1
  55. package/packages/core/src/picker/PickerService.ts +3 -7
  56. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +1 -2
  57. package/packages/core/src/picker/catalog.ts +7 -15
  58. package/packages/core/src/picker/filePickerWorker.ts +1 -3
  59. package/packages/core/src/picker/ranker.ts +1 -1
  60. package/packages/core/src/picker/token.ts +3 -4
  61. package/packages/core/src/session/AgentRuntime.ts +5 -8
  62. package/packages/core/src/session/EventLog.ts +6 -12
  63. package/packages/core/src/session/ReadCursors.ts +3 -4
  64. package/packages/core/src/session/SearchIndex.ts +49 -86
  65. package/packages/core/src/session/SearchTokens.ts +4 -7
  66. package/packages/core/src/session/SessionCache.ts +8 -6
  67. package/packages/core/src/session/SessionDigest.ts +10 -19
  68. package/packages/core/src/session/SessionHost.ts +53 -83
  69. package/packages/core/src/session/SessionLease.ts +12 -15
  70. package/packages/core/src/session/SessionMeta.ts +16 -32
  71. package/packages/core/src/session/SessionName.ts +3 -7
  72. package/packages/core/src/session/SessionRegistry.ts +13 -29
  73. package/packages/core/src/session/SessionUi.ts +12 -16
  74. package/packages/core/src/session/WriteMark.ts +7 -16
  75. package/packages/core/src/shared/Cli.ts +1 -1
  76. package/packages/core/src/shared/DaemonUnit.ts +1 -2
  77. package/packages/core/src/shared/DiffLines.ts +16 -34
  78. package/packages/core/src/shared/DiffRenderer.ts +8 -27
  79. package/packages/core/src/shared/Directories.ts +4 -4
  80. package/packages/core/src/shared/EditMatcher.ts +18 -50
  81. package/packages/core/src/shared/ExtensionToggles.ts +3 -6
  82. package/packages/core/src/shared/FileEnumerator.ts +73 -57
  83. package/packages/core/src/shared/FileScanner.ts +5 -13
  84. package/packages/core/src/shared/FileWatch.ts +13 -17
  85. package/packages/core/src/shared/Format.ts +2 -3
  86. package/packages/core/src/shared/FuzzyMatcher.ts +18 -19
  87. package/packages/core/src/shared/Git.ts +22 -47
  88. package/packages/core/src/shared/GitMonitor.ts +12 -23
  89. package/packages/core/src/shared/GlobExclusions.ts +0 -4
  90. package/packages/core/src/shared/Http.ts +1 -1
  91. package/packages/core/src/shared/ImageMime.ts +3 -5
  92. package/packages/core/src/shared/Images.ts +17 -27
  93. package/packages/core/src/shared/Languages.ts +2 -2
  94. package/packages/core/src/shared/Levenshtein.ts +1 -6
  95. package/packages/core/src/shared/Lines.ts +1 -1
  96. package/packages/core/src/shared/McpClient.ts +2 -2
  97. package/packages/core/src/shared/PatchSummary.ts +1 -1
  98. package/packages/core/src/shared/Paths.ts +4 -1
  99. package/packages/core/src/shared/PiExtensions.ts +5 -12
  100. package/packages/core/src/shared/PimSettings.ts +14 -15
  101. package/packages/core/src/shared/Proc.ts +2 -3
  102. package/packages/core/src/shared/RateLimiter.ts +1 -1
  103. package/packages/core/src/shared/Renderer.ts +1 -3
  104. package/packages/core/src/shared/RepoDiff.ts +24 -46
  105. package/packages/core/src/shared/SearchRender.ts +3 -4
  106. package/packages/core/src/shared/SpillCache.ts +2 -3
  107. package/packages/core/src/shared/SubagentLogs.ts +4 -6
  108. package/packages/core/src/shared/Supervisor.ts +19 -32
  109. package/packages/core/src/shared/Surface.ts +1 -1
  110. package/packages/core/src/shared/Sweeper.ts +1 -1
  111. package/packages/core/src/shared/Tools.ts +17 -30
  112. package/packages/core/src/shared/Updater.ts +15 -15
  113. package/packages/core/src/view/BodyRenderer.ts +1 -1
  114. package/packages/core/src/view/DiffBlocks.ts +0 -1
  115. package/packages/core/src/view/DiffExpand.ts +10 -25
  116. package/packages/core/src/view/DiffLayout.ts +2 -2
  117. package/packages/core/src/view/DiffPairs.ts +2 -2
  118. package/packages/core/src/view/MarkdownPainter.ts +2 -19
  119. package/packages/core/src/view/MovePath.ts +4 -17
  120. package/packages/core/src/view/Painting.ts +4 -4
  121. package/packages/core/src/view/ViewBlock.ts +11 -12
  122. package/packages/daemon/src/Daemon.ts +2 -7
  123. package/packages/daemon/src/DaemonInstall.ts +5 -12
  124. package/packages/daemon/src/Surfaces.ts +3 -12
  125. package/packages/daemon/src/TelegramSurface.ts +0 -1
  126. package/packages/daemon/src/index.ts +0 -1
  127. package/packages/protocol/src/Command.ts +33 -41
  128. package/packages/protocol/src/Diff.ts +0 -5
  129. package/packages/protocol/src/ImageRoute.ts +2 -5
  130. package/packages/protocol/src/ServerEvent.ts +68 -105
  131. package/packages/server/src/AttachmentEndpoint.ts +45 -19
  132. package/packages/server/src/ClientConnection.ts +11 -12
  133. package/packages/server/src/ImageEndpoint.ts +3 -4
  134. package/packages/server/src/ProbeClient.ts +26 -59
  135. package/packages/server/src/Reloader.ts +4 -7
  136. package/packages/server/src/SendFileTool.ts +1 -1
  137. package/packages/server/src/SessionCatalogue.ts +35 -47
  138. package/packages/server/src/SessionProjection.ts +7 -12
  139. package/packages/server/src/SessionStream.ts +33 -80
  140. package/packages/server/src/StaticClient.ts +7 -8
  141. package/packages/server/src/WebOptions.ts +2 -6
  142. package/packages/server/src/WsGateway.ts +35 -96
  143. package/packages/server/src/probe.ts +6 -18
  144. package/packages/telegram/src/Bot.ts +2 -2
  145. package/packages/telegram/src/Commands.ts +108 -65
  146. package/packages/telegram/src/Config.ts +1 -6
  147. package/packages/telegram/src/Markdown.ts +4 -13
  148. package/packages/telegram/src/Renderer.ts +1 -1
  149. package/packages/telegram/src/Session.ts +3 -10
  150. package/packages/telegram/src/TaskScheduler.ts +31 -10
  151. package/packages/telegram/src/TaskStore.ts +11 -1
  152. package/packages/telegram/src/TaskTool.ts +1 -1
  153. package/packages/telegram/src/UpdateConfirm.ts +2 -2
  154. package/packages/tui/src/extensions/_init/index.ts +2 -5
  155. package/packages/tui/src/extensions/file-picker/index.ts +2 -6
  156. package/packages/tui/src/extensions/footer/index.ts +0 -1
  157. package/packages/tui/src/extensions/footer/powerline.ts +16 -28
  158. package/packages/tui/src/extensions/pim/index.ts +1 -1
  159. package/packages/tui/src/extensions/session-lease/index.ts +5 -6
  160. package/packages/tui/src/extensions/working-indicator/index.ts +1 -2
  161. package/packages/tui/src/themes/themeCliArgs.ts +1 -1
  162. package/packages/web/dist/client/assets/{abnfDiagram-O67JEVCF-CZ7V84ot.js → abnfDiagram-O67JEVCF-WweOU5DK.js} +1 -1
  163. package/packages/web/dist/client/assets/{arc-DiHGkUvc.js → arc-O2PmZXWt.js} +1 -1
  164. package/packages/web/dist/client/assets/architecture-7GRP2DOG-COBxVilU.js +1 -0
  165. package/packages/web/dist/client/assets/{architectureDiagram-NJMV4G6O-XS5DhfJB.js → architectureDiagram-NJMV4G6O-DwYq1YaM.js} +1 -1
  166. package/packages/web/dist/client/assets/{blockDiagram-BEXU5L5S-BCsgLvy1.js → blockDiagram-BEXU5L5S-CVab041n.js} +1 -1
  167. package/packages/web/dist/client/assets/{c4Diagram-YGBWAQC7-Bj9LcWNm.js → c4Diagram-YGBWAQC7-BmotHez3.js} +1 -1
  168. package/packages/web/dist/client/assets/channel-BKV-IcTA.js +1 -0
  169. package/packages/web/dist/client/assets/{chunk-3FUC2YCW-BmDurFxp.js → chunk-3FUC2YCW-CWNS7-n4.js} +1 -1
  170. package/packages/web/dist/client/assets/{chunk-5DYCD2WN-DAjG-pqn.js → chunk-5DYCD2WN-DflvXohu.js} +1 -1
  171. package/packages/web/dist/client/assets/{chunk-742MDFTN-Bmxs5puY.js → chunk-742MDFTN-B_0t6_RE.js} +1 -1
  172. package/packages/web/dist/client/assets/{chunk-7INBJB4K-Di7kIxrQ.js → chunk-7INBJB4K-wKj0Q3Kv.js} +1 -1
  173. package/packages/web/dist/client/assets/{chunk-7M6MHVWA-CSc8sR0b.js → chunk-7M6MHVWA-BMvUghc8.js} +1 -1
  174. package/packages/web/dist/client/assets/{chunk-7PRAP22T-C8kd5QvY.js → chunk-7PRAP22T-BY41wkcT.js} +1 -1
  175. package/packages/web/dist/client/assets/{chunk-GTNCS2PH-DG2b2LYE.js → chunk-GTNCS2PH-PqY_YoS0.js} +1 -1
  176. package/packages/web/dist/client/assets/{chunk-GWA4HPMP-DdLaLQCu.js → chunk-GWA4HPMP-fozSU5ty.js} +1 -1
  177. package/packages/web/dist/client/assets/{chunk-MBY4JIJT-C1j0yQlk.js → chunk-MBY4JIJT-CrHrC3jw.js} +1 -1
  178. package/packages/web/dist/client/assets/{chunk-NETBCI7D-Bfq9H74p.js → chunk-NETBCI7D-DWdAxieG.js} +1 -1
  179. package/packages/web/dist/client/assets/{chunk-O7XYJQB3-ZHvBmBO-.js → chunk-O7XYJQB3-B6uOn9iS.js} +1 -1
  180. package/packages/web/dist/client/assets/{chunk-UA2S7LBM-ku1JeHSK.js → chunk-UA2S7LBM-BxYSHqyy.js} +1 -1
  181. package/packages/web/dist/client/assets/{chunk-WEXAMYUT-BP1iga_7.js → chunk-WEXAMYUT-CtwIaQDa.js} +1 -1
  182. package/packages/web/dist/client/assets/{chunk-XXDRQBXY-BBeccPcX.js → chunk-XXDRQBXY-fgBFe-PX.js} +1 -1
  183. package/packages/web/dist/client/assets/{chunk-Z7XXMR3K-SMHZugaR.js → chunk-Z7XXMR3K-CPonoV3p.js} +1 -1
  184. package/packages/web/dist/client/assets/{chunk-ZIGJFQKS-CFU_JLG4.js → chunk-ZIGJFQKS-Cdu87cTj.js} +1 -1
  185. package/packages/web/dist/client/assets/{classDiagram-v2-NBCMYWYE-CYviuQzJ.js → classDiagram-v2-NBCMYWYE-DSWsPaa7.js} +1 -1
  186. package/packages/web/dist/client/assets/{core-DyHsgs5k.js → core-CBoS7AST.js} +1 -1
  187. package/packages/web/dist/client/assets/{cose-bilkent-JH36ORCC-9v2ZkJzo.js → cose-bilkent-JH36ORCC-CTwlldtJ.js} +1 -1
  188. package/packages/web/dist/client/assets/{cynefin-OW5HDTMX-Bx5xRadi.js → cynefin-OW5HDTMX-CZQ4EPNJ.js} +1 -1
  189. package/packages/web/dist/client/assets/{cynefinDiagram-VND7K2PF-BRfGDKqa.js → cynefinDiagram-VND7K2PF-C2gp6H_Q.js} +1 -1
  190. package/packages/web/dist/client/assets/{dagre-6A5THRUB-C4l-2L2e.js → dagre-6A5THRUB-CDxTP47t.js} +1 -1
  191. package/packages/web/dist/client/assets/{diagram-22UHCM2B-d_hQ3kyi.js → diagram-22UHCM2B-BIohE1lI.js} +1 -1
  192. package/packages/web/dist/client/assets/{diagram-3UASUU5V-DrEeguPI.js → diagram-3UASUU5V-TwAUir-w.js} +1 -1
  193. package/packages/web/dist/client/assets/{diagram-ATOU4E4O-BmfQ22pD.js → diagram-ATOU4E4O-BM1JQrIu.js} +1 -1
  194. package/packages/web/dist/client/assets/{diagram-CDSNMT55-BjVWjuPF.js → diagram-CDSNMT55-BKIqhxSI.js} +1 -1
  195. package/packages/web/dist/client/assets/{diagram-MLGK6HIB-wDjk-O8i.js → diagram-MLGK6HIB-CVe3QDHr.js} +1 -1
  196. package/packages/web/dist/client/assets/{diagram-MPIPVDR6-DqXQFE1s.js → diagram-MPIPVDR6-Bj-MQIFI.js} +1 -1
  197. package/packages/web/dist/client/assets/{dist-BddGOpie.js → dist-DlZpcyc2.js} +1 -1
  198. package/packages/web/dist/client/assets/{ebnfDiagram-ZINNZB2B-UB8kOITM.js → ebnfDiagram-ZINNZB2B-C06mmvmz.js} +1 -1
  199. package/packages/web/dist/client/assets/{elk-276RUBZZ-D4miYQnD.js → elk-276RUBZZ-CizJSNEj.js} +1 -1
  200. package/packages/web/dist/client/assets/{erDiagram-OPXOYQCR--RXKv7KH.js → erDiagram-OPXOYQCR-M3FQkKI1.js} +1 -1
  201. package/packages/web/dist/client/assets/eventmodeling-NTZA5JFV-D_UXLpIu.js +1 -0
  202. package/packages/web/dist/client/assets/flowDiagram-KWPJA3E3-BT4eEEVm.js +1 -0
  203. package/packages/web/dist/client/assets/{ganttDiagram-FUAMR5RP-C0CFpE5x.js → ganttDiagram-FUAMR5RP-CojF9Q16.js} +1 -1
  204. package/packages/web/dist/client/assets/{gitGraph-4MIJSDKK-VPL7nv2c.js → gitGraph-4MIJSDKK-YvKqGEPk.js} +1 -1
  205. package/packages/web/dist/client/assets/{gitGraphDiagram-X574FWY7-CgxJA-T8.js → gitGraphDiagram-X574FWY7-XqNqIFo9.js} +1 -1
  206. package/packages/web/dist/client/assets/index--2CDtzvY.css +1 -0
  207. package/packages/web/dist/client/assets/{index-XTlTnQmx.js → index-bT6QPJZs.js} +7 -7
  208. package/packages/web/dist/client/assets/{info-A6RAGUB7-P0-o68pj.js → info-A6RAGUB7-B_1BCzM2.js} +1 -1
  209. package/packages/web/dist/client/assets/{infoDiagram-VRGFBTTK-DFJZNRUP.js → infoDiagram-VRGFBTTK-zFX8B-dP.js} +1 -1
  210. package/packages/web/dist/client/assets/{ishikawaDiagram-OU5B5YK6-ZkyhNd1T.js → ishikawaDiagram-OU5B5YK6-DLzQ_Uyf.js} +1 -1
  211. package/packages/web/dist/client/assets/{journeyDiagram-ZHPQQLJL-DFevH8my.js → journeyDiagram-ZHPQQLJL-S3WRMmoa.js} +1 -1
  212. package/packages/web/dist/client/assets/{kanban-definition-PNTS6WVX-l7e7ZZDC.js → kanban-definition-PNTS6WVX-B5Z8I-kM.js} +1 -1
  213. package/packages/web/dist/client/assets/{line-DO_s6Ujd.js → line-BGMH-i2X.js} +1 -1
  214. package/packages/web/dist/client/assets/{linear-C4xH5PPc.js → linear-KsrestXG.js} +1 -1
  215. package/packages/web/dist/client/assets/{mermaid-parser.core-nQaTP3VY.js → mermaid-parser.core-DNffyJJA.js} +3 -3
  216. package/packages/web/dist/client/assets/{mermaid.core-Cz05yTZh.js → mermaid.core-Cz3vJeQG.js} +4 -4
  217. package/packages/web/dist/client/assets/{mindmap-definition-NLK3R4M7-CHOEuHm2.js → mindmap-definition-NLK3R4M7-CzA61XUf.js} +1 -1
  218. package/packages/web/dist/client/assets/{packet-AYTQ26CC-BH3ehPMo.js → packet-AYTQ26CC-BO55e7cR.js} +1 -1
  219. package/packages/web/dist/client/assets/{pegDiagram-GJSIUBJH-CkWBLtad.js → pegDiagram-GJSIUBJH-DeRiXYNd.js} +1 -1
  220. package/packages/web/dist/client/assets/{pie-WAS4IAKB-nNVEq3dx.js → pie-WAS4IAKB-D9ibrQRe.js} +1 -1
  221. package/packages/web/dist/client/assets/{pieDiagram-5QR66LMP-DimUBAgN.js → pieDiagram-5QR66LMP-BaBpPP33.js} +1 -1
  222. package/packages/web/dist/client/assets/{quadrantDiagram-O4NWA36T-BR0XpRGQ.js → quadrantDiagram-O4NWA36T-DIfn4GjM.js} +1 -1
  223. package/packages/web/dist/client/assets/{radar-RG4KPBEZ-paA-H3mb.js → radar-RG4KPBEZ-Dmn5-nJq.js} +1 -1
  224. package/packages/web/dist/client/assets/{railroad-74A4TZTK-yEiKB3rz.js → railroad-74A4TZTK-ZCWNEd2Z.js} +1 -1
  225. package/packages/web/dist/client/assets/railroad-abnf-HS5TGJTU-_swqhy0y.js +1 -0
  226. package/packages/web/dist/client/assets/railroad-ebnf-LZEXJU2U-CytbazPx.js +1 -0
  227. package/packages/web/dist/client/assets/railroad-peg-WCYAUIDC-Bg8QW17T.js +1 -0
  228. package/packages/web/dist/client/assets/{railroadDiagram-XR7U4H2S--TXe6rdU.js → railroadDiagram-XR7U4H2S-CDPZ39sy.js} +1 -1
  229. package/packages/web/dist/client/assets/{requirementDiagram-PLB6GJNP-DyqH36Za.js → requirementDiagram-PLB6GJNP-CyCI8Yul.js} +1 -1
  230. package/packages/web/dist/client/assets/{sankeyDiagram-IPEJSGJF-Bg6e3BlM.js → sankeyDiagram-IPEJSGJF-p_XAPv72.js} +1 -1
  231. package/packages/web/dist/client/assets/{sequenceDiagram-PO4LG4MO-ClGf85My.js → sequenceDiagram-PO4LG4MO-DmR6IP_o.js} +1 -1
  232. package/packages/web/dist/client/assets/{src-DF7hQfMh.js → src-gnv7pJ1z.js} +1 -1
  233. package/packages/web/dist/client/assets/{stateDiagram-v2-GCMORJYK-BK8TpxNo.js → stateDiagram-v2-GCMORJYK-7ZTHl6ih.js} +1 -1
  234. package/packages/web/dist/client/assets/{swimlanes-2SLR337P-DDnTIT9M.js → swimlanes-2SLR337P-N6bj71pw.js} +1 -1
  235. package/packages/web/dist/client/assets/swimlanesDiagram-TC7HE7FX-BJQ2CBsp.js +8 -0
  236. package/packages/web/dist/client/assets/{timeline-definition-EJHVYXUP-mBBbH3bk.js → timeline-definition-EJHVYXUP-GNez0kW8.js} +1 -1
  237. package/packages/web/dist/client/assets/{treeView-Q6P3EWNA-CTK7tRwR.js → treeView-Q6P3EWNA-DyQZkboR.js} +1 -1
  238. package/packages/web/dist/client/assets/{treemap-WGGIJYW6-vOI-Ef_m.js → treemap-WGGIJYW6-_J8Pw2TM.js} +1 -1
  239. package/packages/web/dist/client/assets/{usecaseDiagram-POWQR4AR-COcii8x-.js → usecaseDiagram-POWQR4AR-Bg-CzKjl.js} +1 -1
  240. package/packages/web/dist/client/assets/{vennDiagram-UO4OBE2U-D6kmDGpM.js → vennDiagram-UO4OBE2U-DBQKHXhK.js} +1 -1
  241. package/packages/web/dist/client/assets/{wardley-WFR3VGLG-CtE2KI9r.js → wardley-WFR3VGLG-B5aAsQ3F.js} +1 -1
  242. package/packages/web/dist/client/assets/{wardleyDiagram-VNRHLVJA-ncxNGWnW.js → wardleyDiagram-VNRHLVJA-BKUQ1LK6.js} +1 -1
  243. package/packages/web/dist/client/assets/{xychartDiagram-PMCCYNJV-DUvd2Qze.js → xychartDiagram-PMCCYNJV-BI2sVbks.js} +1 -1
  244. package/packages/web/dist/client/index.html +2 -2
  245. package/packages/web/dist/client/assets/architecture-7GRP2DOG-HIzEIkwQ.js +0 -1
  246. package/packages/web/dist/client/assets/channel-BCA6qe9I.js +0 -1
  247. package/packages/web/dist/client/assets/eventmodeling-NTZA5JFV-l6SYFw1s.js +0 -1
  248. package/packages/web/dist/client/assets/flowDiagram-KWPJA3E3-CWW9qnqm.js +0 -1
  249. package/packages/web/dist/client/assets/index-D9_XJLSc.css +0 -1
  250. package/packages/web/dist/client/assets/railroad-abnf-HS5TGJTU-mVQx9aVM.js +0 -1
  251. package/packages/web/dist/client/assets/railroad-ebnf-LZEXJU2U-CllXx1OJ.js +0 -1
  252. package/packages/web/dist/client/assets/railroad-peg-WCYAUIDC-BBD2Q5W_.js +0 -1
  253. package/packages/web/dist/client/assets/swimlanesDiagram-TC7HE7FX-CM8UnP5t.js +0 -8
@@ -25,13 +25,13 @@ export type ToolCallView = {
25
25
 
26
26
  /** A file a user message carried; `url` is server-relative. */
27
27
  export type AttachmentView = {
28
- /** What to call it on screen; never a path. */
28
+ /** Display name; never a path. */
29
29
  readonly name: string;
30
30
  readonly url: string;
31
31
  readonly isImage: boolean;
32
32
  };
33
33
 
34
- /** Projected from pi's session JSONL; `seq` is the line ordinal, so one line must emit at most one durable event and a client resumes at `seq > n`. */
34
+ /** Projected from pi's session JSONL. `seq` is the line ordinal, so each line emits at most one durable event. */
35
35
  export type DurableEvent =
36
36
  | {
37
37
  readonly seq: number;
@@ -39,13 +39,13 @@ export type DurableEvent =
39
39
  readonly messageId: string;
40
40
  readonly role: "user" | "assistant";
41
41
  readonly text: string;
42
- /** When pi appended the entry, in epoch ms. */
42
+ /** Epoch ms. */
43
43
  readonly timestamp: number;
44
44
  readonly thinking?: string;
45
- /** Only ever set on a user message, which may carry nothing else. */
45
+ /** User messages only. */
46
46
  readonly attachments?: readonly AttachmentView[];
47
47
  readonly toolCalls?: readonly ToolCallView[];
48
- /** The model call failed, with what it said; only ever set on an assistant message. */
48
+ /** Model call error; assistant messages only. */
49
49
  readonly error?: string;
50
50
  }
51
51
  | {
@@ -63,7 +63,7 @@ export type DurableEvent =
63
63
  readonly text: string;
64
64
  };
65
65
 
66
- /** Progress of the one `reload` this server can have in flight. */
66
+ /** Progress of a `reload`. */
67
67
  export type UpdateStateEvent =
68
68
  | {
69
69
  readonly type: "update_state";
@@ -77,7 +77,7 @@ export type UpdateStateEvent =
77
77
  readonly to: string;
78
78
  readonly skipped: readonly UpdateSkip[];
79
79
  }
80
- /** New code is on disk but nothing will restart this process: no supervisor owns it. */
80
+ /** Updated on disk, but no supervisor will restart this process. */
81
81
  | {
82
82
  readonly type: "update_state";
83
83
  readonly phase: "stranded";
@@ -91,26 +91,26 @@ export type UpdateStateEvent =
91
91
  readonly error: string;
92
92
  };
93
93
 
94
- /** Unsequenced live state, never replayed by ordinal; live `tool_call`s reappear in a durable message's `toolCalls`, so dedupe on `callId`. */
94
+ /** Unsequenced live state. Live `tool_call`s reappear in a durable message's `toolCalls`, so dedupe on `callId`. */
95
95
  export type EphemeralEvent =
96
96
  | {
97
97
  readonly type: "attached";
98
98
  readonly sessionId: string;
99
99
  readonly cwd: string;
100
- /** Highest durable `seq` at attach time; replay follows immediately. */
100
+ /** Highest durable `seq` at attach time. */
101
101
  readonly head: number;
102
- /** The server's build; a client built from another one is stale and should reload. */
102
+ /** A client built from another version should reload. */
103
103
  readonly pimVersion: string;
104
104
  readonly piVersion: string;
105
105
  }
106
- /** Several events in one frame; apply them in order, each as if it had arrived alone. */
106
+ /** Apply in order, as if each arrived alone. */
107
107
  | { readonly type: "replay"; readonly events: readonly StreamEvent[] }
108
108
  | {
109
109
  readonly type: "message_start";
110
110
  readonly role: "assistant";
111
111
  readonly messageId: string;
112
112
  }
113
- /** Drop the live message with this id; the durable `message` superseding it was sent immediately before. */
113
+ /** Drop this live message; its durable `message` was sent just before. */
114
114
  | { readonly type: "message_retire"; readonly messageId: string }
115
115
  | {
116
116
  readonly type: "text_delta";
@@ -134,37 +134,36 @@ export type EphemeralEvent =
134
134
  readonly callId: string;
135
135
  readonly view: ToolView;
136
136
  }
137
- /** Settled view until pi appends the `tool_result` for the same `callId`, which supersedes it. */
137
+ /** Superseded by the `tool_result` with the same `callId`. */
138
138
  | {
139
139
  readonly type: "tool_end";
140
140
  readonly callId: string;
141
141
  readonly view: ToolView;
142
142
  readonly isError: boolean;
143
143
  }
144
- /** Every picker answer a client holds for this cwd is stale; drop the cache and re-query. */
144
+ /** Cached picker answers for this cwd are stale. */
145
145
  | {
146
146
  readonly type: "picker_invalidate";
147
147
  readonly scope: "files" | "commands" | "all";
148
148
  readonly cwd: string;
149
149
  }
150
- /** A child's events, applied against the child's transcript; sent only to the connection that asked, and never resumed. */
150
+ /** Sent only to the watching connection; never resumed. */
151
151
  | {
152
152
  readonly type: "subagent_events";
153
- /** The parent tool call the watch was opened on. */
154
153
  readonly callId: string;
155
154
  readonly events: readonly StreamEvent[];
156
155
  }
157
156
  | { readonly type: "turn_end"; readonly stats: TurnStats }
158
- /** Something an extension said, in Markdown; shown and then forgotten, never written to the session. */
157
+ /** Markdown from an extension; not written to the session. */
159
158
  | {
160
159
  readonly type: "ui_notice";
161
160
  readonly id: string;
162
161
  readonly severity: NoticeSeverity;
163
162
  readonly text: string;
164
- /** The command being dispatched when it was said, like `/login`; absent means nobody asked for it. */
163
+ /** The command being dispatched, like `/login`. */
165
164
  readonly command?: string;
166
165
  }
167
- /** An extension is waiting on a human; the first `ui_response` wins and the rest are refused. */
166
+ /** The first `ui_response` wins. */
168
167
  | {
169
168
  readonly type: "ui_request";
170
169
  readonly requestId: string;
@@ -173,48 +172,43 @@ export type EphemeralEvent =
173
172
  readonly message?: string;
174
173
  readonly options?: readonly string[];
175
174
  readonly placeholder?: string;
176
- /** The command that asked, like `/login`; absent where an extension asked unprompted. */
175
+ /** The command that asked, like `/login`. */
177
176
  readonly command?: string;
178
177
  }
179
- /** The request is settled, by whoever answered it or by the server answering for them; drop its control. */
180
178
  | { readonly type: "ui_request_done"; readonly requestId: string }
181
- /** Sent to every connection, not just those attached; only sessions this server holds open are reported. */
179
+ /** Broadcast; only for sessions this server holds open. */
182
180
  | {
183
181
  readonly type: "session_activity";
184
182
  readonly sessionId: string;
185
183
  readonly status: SessionStatus;
186
184
  }
187
- /** Sent to every connection; the read cursor is one per session, not one per client. */
185
+ /** Broadcast; the read cursor is per session, not per client. */
188
186
  | { readonly type: "session_read"; readonly sessionId: string }
189
- /** Sent to every connection: one session's name or overrides changed, so a listing a client holds can be patched in place. */
187
+ /** Broadcast patch: only changed fields are set. */
190
188
  | {
191
189
  readonly type: "session_meta";
192
190
  readonly sessionId: string;
193
- /** The session's own name, `null` once it is cleared. */
191
+ /** `null` once cleared. */
194
192
  readonly name?: string | null;
195
193
  readonly archived?: boolean;
196
194
  readonly unread?: boolean;
197
195
  }
198
- /**
199
- * Sent to every connection; keyed by absolute working directory, not by
200
- * session. A patch like `session_meta`: only what changed is said, so a
201
- * fold carries no claim about the pin beside it.
202
- */
196
+ /** Broadcast patch: only changed fields are set. */
203
197
  | {
204
198
  readonly type: "project_meta";
205
199
  readonly cwd: string;
206
200
  readonly pinned?: boolean;
207
201
  readonly expanded?: boolean;
208
- /** What the project is called in a listing, `null` once it is cleared. */
202
+ /** `null` once cleared. */
209
203
  readonly label?: string | null;
210
204
  }
211
- /** Sent to every connection: the pinned projects, in the order they are shown. */
205
+ /** Broadcast. */
212
206
  | { readonly type: "pins_changed"; readonly order: readonly string[] }
213
- /** Sent to every connection: the sessions on disk changed, so any listing a client holds is stale. */
207
+ /** Broadcast. */
214
208
  | { readonly type: "sessions_changed" }
215
- /** Sent to every connection: an extension was switched, so any roster a client holds is stale. */
209
+ /** Broadcast. */
216
210
  | { readonly type: "extensions_changed" }
217
- /** Sent to every connection; the restart it ends in drops every socket. */
211
+ /** Broadcast. */
218
212
  | UpdateStateEvent
219
213
  | {
220
214
  readonly type: "session_state";
@@ -227,155 +221,138 @@ export type EphemeralEvent =
227
221
  readonly status: SessionStatus;
228
222
  /** False while another process holds this session's turn lease. */
229
223
  readonly writable: boolean;
230
- /** A session in the same working directory is mid-turn, so nothing may move the repository under it. */
224
+ /** A session in the same cwd is mid-turn, so git actions are refused. */
231
225
  readonly repoBusy?: boolean;
232
- /** Who holds it; absent when nothing does, or when their record is torn. */
226
+ /** Absent when unheld or the lease record is corrupt. */
233
227
  readonly heldBy?: {
234
228
  readonly frontend: LeaseFrontend;
235
229
  readonly pid: number;
236
230
  };
237
231
  readonly tps?: number;
238
- /** Elapsed run time of the turn in flight, by the server's clock; absent when idle. */
232
+ /** Absent when idle. */
239
233
  readonly turnElapsedMs?: number;
240
234
  /** Context filled, 0–100. Absent until a turn has reported usage. */
241
235
  readonly contextPercent?: number;
242
236
  readonly contextWindow?: number;
243
- /** The cwd's git branch, absent outside a repository. */
237
+ /** Absent outside a git repository. */
244
238
  readonly branch?: string;
245
- /** Paths git reports as changed; zero is a clean tree. */
246
239
  readonly dirtyCount?: number;
247
240
  readonly ahead?: number;
248
241
  readonly behind?: number;
249
- /** Changes whenever the working copy does, content of a dirty file included. */
242
+ /** Changes whenever the working copy does, including dirty file content. */
250
243
  readonly repoRevision?: string;
251
244
  }
252
245
  /** A frame the server could not attribute to any command. */
253
246
  | { readonly type: "error"; readonly message: string };
254
247
 
255
- /** One row of the session catalogue. */
256
248
  export type SessionSummaryView = {
257
249
  readonly sessionId: string;
258
250
  readonly cwd: string;
259
251
  readonly createdAt: number;
260
- /** End of the last completed turn and the catalogue's sort key, never the file mtime; falls back to `createdAt`. */
252
+ /** End of the last completed turn (not file mtime); falls back to `createdAt`. Sort key. */
261
253
  readonly settledAt: number;
262
- /** The session's first user message, trimmed; absent when it has none. */
254
+ /** The first user message, trimmed. */
263
255
  readonly title?: string;
264
- /** True when `title` is a name somebody wrote, rather than the first message. */
256
+ /** `title` is a user-set name, not the first message. */
265
257
  readonly named?: true;
266
258
  readonly archived?: true;
267
- /** Has answered since anything last read it; absent means it has not. */
268
259
  readonly unread?: boolean;
269
- /** Absent when idle, and for a session this server does not hold open. */
260
+ /** Absent when idle or not held open by this server. */
270
261
  readonly status?: SessionStatus;
271
262
  };
272
263
 
273
- /** One working directory the catalogue holds sessions for, counted before any per-project cut. */
274
264
  export type ProjectView = {
275
265
  readonly cwd: string;
276
- /** Every session in it the listing's scope allows, including the ones the cut dropped. */
266
+ /** Counted before the per-project cut. */
277
267
  readonly count: number;
278
268
  readonly pinned?: true;
279
- /** Where it sorts among the pinned, 0 first; absent unless it is pinned. */
269
+ /** 0 first; only set when pinned. */
280
270
  readonly pinRank?: number;
281
- /** The sidebar group stands unfolded; absent is folded. */
282
271
  readonly expanded?: true;
283
- /** What to call it instead of its base name; absent when it goes by the directory. */
272
+ /** Overrides the directory's base name. */
284
273
  readonly label?: string;
285
274
  };
286
275
 
287
- /**
288
- * One session a search matched. Deliberately not a `SessionSummaryView`: what
289
- * a ranked list needs is why the row matched, and `unread` is the sidebar's
290
- * triage while `status` resolves itself the moment the session opens.
291
- */
292
276
  export type SearchHitView = {
293
277
  readonly sessionId: string;
294
278
  readonly cwd: string;
295
- /** Its name if it has one, else the clamped opening message; the digest's, so it agrees with the sidebar's. */
279
+ /** The session name, else the clamped first message. */
296
280
  readonly title?: string;
297
- /** Offsets into the clamped `title`, empty when the title did not match. */
281
+ /** Offsets into `title`. */
298
282
  readonly titleRanges: readonly SearchRange[];
299
- /** The session's opening ask, for a row whose name is all that matched and so has no snippet to show. */
283
+ /** The first message, for rows where only the name matched. */
300
284
  readonly opening?: string;
301
- /** End of the last completed turn and the row's clock, never the file mtime; falls back to when the session started, as the sidebar's does. */
285
+ /** As in `SessionSummaryView`. */
302
286
  readonly settledAt: number;
303
- /** Searched and found anyway: the badge that makes "archived are in scope" honest. */
304
287
  readonly archived?: true;
305
- /** What matched, in the words it was said in, each with its own ranges. */
306
288
  readonly snippets: readonly SearchSnippet[];
307
- /** Matching messages in this session, before the snippet cut. */
289
+ /** Matching messages, before the snippet cut. */
308
290
  readonly total: number;
309
291
  };
310
292
 
311
- /** One answer to `list_sessions`: the page of rows, and every directory that had one, counted whole. */
312
293
  export type SessionListing = {
313
294
  readonly sessions: readonly SessionSummaryView[];
314
- /** Counted before the per-project cut, so a group can say what a page of it leaves out. */
315
295
  readonly projects: readonly ProjectView[];
316
296
  };
317
297
 
318
- /** One answer to `search_sessions`: the ranked rows, the words no session held, and the scope it read. */
319
298
  export type SessionSearch = {
320
299
  readonly hits: readonly SearchHitView[];
321
300
  readonly dropped: readonly string[];
322
- /** Sessions searched, whole: what the empty state and the result footer say out loud. */
301
+ /** Sessions searched. */
323
302
  readonly scanned: number;
324
303
  };
325
304
 
326
- /** One model the server can be switched to, for the composer's model menu. */
327
305
  export type ModelView = {
328
306
  readonly id: string;
329
307
  readonly label: string;
330
- /** The provider half of `id`, so a client can tag a row without parsing it. */
308
+ /** The provider half of `id`. */
331
309
  readonly provider: string;
332
310
  };
333
311
 
334
- /** Answer to one `Command`, correlated by its `id`. Never sequenced. */
312
+ /** Answer to one `Command`, correlated by `id`. */
335
313
  export type ResponseEvent = {
336
314
  readonly type: "response";
337
315
  readonly id: string;
338
316
  readonly success: boolean;
339
317
  readonly error?: string;
340
- /** Ranked rows, for the commands that answer with data (`pick_*`). */
318
+ /** For `pick_*`. */
341
319
  readonly items?: readonly PickerItem[];
342
- /** The catalogue, for `list_sessions`. */
320
+ /** For `list_sessions`. */
343
321
  readonly sessions?: readonly SessionSummaryView[];
344
- /** The directories those sessions came from, for `list_sessions`. */
322
+ /** For `list_sessions`. */
345
323
  readonly projects?: readonly ProjectView[];
346
- /** The ranked sessions, for `search_sessions`. */
324
+ /** For `search_sessions`. */
347
325
  readonly hits?: readonly SearchHitView[];
348
- /** Query words no session held, dropped so the rest could match, for `search_sessions`. */
326
+ /** Query words no session held, for `search_sessions`. */
349
327
  readonly dropped?: readonly string[];
350
- /** Sessions the query actually searched, for `search_sessions`. */
328
+ /** For `search_sessions`. */
351
329
  readonly scanned?: number;
352
- /** The model catalogue, for `list_models`. */
330
+ /** For `list_models`. */
353
331
  readonly models?: readonly ModelView[];
354
- /** What the *current* model supports, on the same answer. */
332
+ /** The current model's levels, for `list_models`. */
355
333
  readonly thinkingLevels?: readonly string[];
356
- /** The extension roster, for `list_extensions`. */
334
+ /** For `list_extensions`. */
357
335
  readonly extensions?: readonly ExtensionEntry[];
358
- /** One directory's subdirectories, for `list_dirs`. */
336
+ /** For `list_dirs`. */
359
337
  readonly directory?: DirectoryListing;
360
- /** The cwd's local branches, for `list_branches`. */
338
+ /** For `list_branches`. */
361
339
  readonly branches?: readonly GitBranch[];
362
- /** The commit that landed, for `commit`. */
340
+ /** For `commit`. */
363
341
  readonly commit?: Pick<Extract<CommitResult, { readonly ok: true }>, "sha">;
364
- /** The change list, for `list_changes`. */
342
+ /** For `list_changes`. */
365
343
  readonly changes?: ChangeList;
366
- /** One file's hunks, for `file_diff`. */
344
+ /** For `file_diff`. */
367
345
  readonly fileDiff?: FileDiff;
368
- /** The lines behind one gap, for `read_lines`. */
346
+ /** For `read_lines`. */
369
347
  readonly fileLines?: FileLines;
370
- /** For `cancel` and `dequeue`: queued messages pi gave back, now owned by the client that asked. */
348
+ /** For `cancel` and `dequeue`: queued messages handed back to the client. */
371
349
  readonly restored?: readonly string[];
372
- /** For `user_message`: it named an extension command, so no turn started and no entry was written. */
350
+ /** For `user_message`: an extension command ran, so no turn started. */
373
351
  readonly dispatched?: boolean;
374
352
  };
375
353
 
376
354
  export type ServerEvent = DurableEvent | EphemeralEvent | ResponseEvent;
377
355
 
378
- /** Anything a session emits: everything on the wire but an answer to a command. */
379
356
  export type StreamEvent = DurableEvent | EphemeralEvent;
380
357
 
381
358
  export type ServerEventType = ServerEvent["type"];
@@ -384,17 +361,7 @@ export function isDurableEvent(event: ServerEvent): event is DurableEvent {
384
361
  return "seq" in event;
385
362
  }
386
363
 
387
- /**
388
- * Whether the frame speaks for the one session a connection is attached to,
389
- * and so says nothing to a client whose attach is still in flight. The rest
390
- * name their own subject — another session, a directory, or nothing at all —
391
- * and gating those on the attach window drops a broadcast every time a client
392
- * switches session.
393
- *
394
- * Exhaustive by construction: a new event is a type error here until it is
395
- * classified, which is the only reason this lives beside the wire types
396
- * rather than in the client that gates on it.
397
- */
364
+ /** Whether the event belongs to the attached session, so a client mid-attach should drop it. */
398
365
  export function isAttachScoped(event: ServerEvent): boolean {
399
366
  switch (event.type) {
400
367
  case "attached":
@@ -415,9 +382,7 @@ export function isAttachScoped(event: ServerEvent): boolean {
415
382
  case "ui_notice":
416
383
  case "ui_request":
417
384
  case "ui_request_done":
418
- // Names a cwd, but only ever reaches a client down the session stream it
419
- // is attached to, so the old session's is the only one that can arrive
420
- // mid-attach — and re-querying for it would warm the wrong cache.
385
+ // Only sent down the attached session's stream.
421
386
  case "picker_invalidate":
422
387
  return true;
423
388
  case "session_activity":
@@ -428,8 +393,6 @@ export function isAttachScoped(event: ServerEvent): boolean {
428
393
  case "sessions_changed":
429
394
  case "extensions_changed":
430
395
  case "update_state":
431
- // Sent for a frame the server could not read at all, which is likeliest
432
- // before an attach has settled: gating it swallows the diagnostic.
433
396
  case "error":
434
397
  case "response":
435
398
  return false;
@@ -11,42 +11,51 @@ export type AttachmentEndpointDeps = {
11
11
  /** Defaults to `~/.pim/attachments`. */
12
12
  readonly root?: string;
13
13
  readonly maxBytes?: number;
14
+ /** How long an upload no prompt has taken is kept. */
15
+ readonly ttlMs?: number;
16
+ readonly now?: () => number;
14
17
  };
15
18
 
19
+ type Pending = StoredAttachment & { readonly arrivedAt: number };
20
+
16
21
  const DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
17
22
 
23
+ const DEFAULT_TTL_MS = 60 * 60 * 1000;
24
+
18
25
  const PREFIX = "/attachment/";
19
26
 
20
- /** Where stored bytes live when nothing says otherwise. */
21
27
  export function defaultAttachmentsRoot(): string {
22
28
  return join(Paths.pimHomeDir(), "attachments");
23
29
  }
24
30
 
25
- // Every origin is allowed: in dev the client is served from vite's own port and its upload is cross-origin.
31
+ // Any origin: in dev the client is served from vite's port.
26
32
  function allow(response: Response): Response {
27
33
  response.headers.set("access-control-allow-origin", "*");
28
34
  response.headers.set("access-control-allow-headers", "content-type");
29
35
  return response;
30
36
  }
31
37
 
32
- /** Where `serve` answers for a stored file: the last two path segments only, never the server's layout. */
38
+ /** Uses only the last two path segments, never the server's layout. */
33
39
  export function attachmentUrl(path: string): string {
34
40
  const scope = encodeURIComponent(basename(dirname(path)));
35
41
  return `${PREFIX}${scope}/${encodeURIComponent(basename(path))}`;
36
42
  }
37
43
 
38
- /** `POST /upload?session=<id>` with a multipart `file` field, and `GET /attachment/<session>/<file>` to read it back. */
44
+ /** `POST /upload?session=<id>` with a multipart `file` field; `GET /attachment/<session>/<file>` reads it back. */
39
45
  export class AttachmentEndpoint {
40
46
  private readonly store: AttachmentStore;
41
47
  private readonly maxBytes: number;
42
- private readonly bySession = new Map<string, Map<string, StoredAttachment>>();
48
+ private readonly ttlMs: number;
49
+ private readonly now: () => number;
50
+ private readonly bySession = new Map<string, Map<string, Pending>>();
43
51
 
44
52
  public constructor(deps: AttachmentEndpointDeps = {}) {
45
53
  this.store = new AttachmentStore(deps.root ?? defaultAttachmentsRoot());
46
54
  this.maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
55
+ this.ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS;
56
+ this.now = deps.now ?? Date.now;
47
57
  }
48
58
 
49
- /** Whether this endpoint owns the path, so the gateway can route to it. */
50
59
  public static owns(pathname: string): boolean {
51
60
  return pathname === "/upload" || pathname.startsWith(PREFIX);
52
61
  }
@@ -63,7 +72,7 @@ export class AttachmentEndpoint {
63
72
  );
64
73
  }
65
74
 
66
- // Every segment goes back through the write's sanitising, so a crafted name cannot escape its scope.
75
+ // `locate` re-sanitises each segment, so a crafted name cannot escape its scope.
67
76
  private async serve(pathname: string): Promise<Response> {
68
77
  const [scope, name, ...rest] = pathname
69
78
  .slice(PREFIX.length)
@@ -91,10 +100,7 @@ export class AttachmentEndpoint {
91
100
  }
92
101
  const declared = Number(request.headers.get("content-length") ?? 0);
93
102
  if (declared > this.maxBytes) {
94
- return Response.json(
95
- { error: `upload exceeds ${this.maxBytes} bytes` },
96
- { status: 413 }
97
- );
103
+ return this.tooLarge();
98
104
  }
99
105
 
100
106
  let file: File | undefined;
@@ -112,10 +118,7 @@ export class AttachmentEndpoint {
112
118
  return Response.json({ error: "missing `file` field" }, { status: 400 });
113
119
  }
114
120
  if (file.size > this.maxBytes) {
115
- return Response.json(
116
- { error: `upload exceeds ${this.maxBytes} bytes` },
117
- { status: 413 }
118
- );
121
+ return this.tooLarge();
119
122
  }
120
123
 
121
124
  let stored: StoredAttachment;
@@ -140,7 +143,7 @@ export class AttachmentEndpoint {
140
143
  });
141
144
  }
142
145
 
143
- /** Consume the uploads a prompt referenced; unknown ids are dropped, so a replayed command cannot re-attach. */
146
+ /** Consumes the uploads a prompt referenced; unknown ids are ignored, so a replay cannot re-attach. */
144
147
  public take(
145
148
  sessionId: string,
146
149
  ids: readonly string[]
@@ -154,7 +157,8 @@ export class AttachmentEndpoint {
154
157
  const stored = pending.get(id);
155
158
  if (stored) {
156
159
  pending.delete(id);
157
- taken.push(stored);
160
+ const { arrivedAt: _, ...attachment } = stored;
161
+ taken.push(attachment);
158
162
  }
159
163
  }
160
164
  if (pending.size === 0) {
@@ -163,11 +167,33 @@ export class AttachmentEndpoint {
163
167
  return taken;
164
168
  }
165
169
 
170
+ private tooLarge(): Response {
171
+ return Response.json(
172
+ { error: `upload exceeds ${this.maxBytes} bytes` },
173
+ { status: 413 }
174
+ );
175
+ }
176
+
166
177
  private remember(sessionId: string, stored: StoredAttachment): void {
167
- const pending = this.bySession.get(sessionId) ?? new Map();
168
- pending.set(stored.id, stored);
178
+ const now = this.now();
179
+ this.sweep(now);
180
+ const pending = this.bySession.get(sessionId) ?? new Map<string, Pending>();
181
+ pending.set(stored.id, { ...stored, arrivedAt: now });
169
182
  this.bySession.set(sessionId, pending);
170
183
  }
184
+
185
+ private sweep(now: number): void {
186
+ for (const [sessionId, pending] of this.bySession) {
187
+ for (const [id, stored] of pending) {
188
+ if (now - stored.arrivedAt >= this.ttlMs) {
189
+ pending.delete(id);
190
+ }
191
+ }
192
+ if (pending.size === 0) {
193
+ this.bySession.delete(sessionId);
194
+ }
195
+ }
196
+ }
171
197
  }
172
198
 
173
199
  function stemOf(name: string): string | undefined {
@@ -7,7 +7,6 @@ import {
7
7
  } from "#protocol/ServerEvent";
8
8
  import type { SessionProjection } from "./SessionProjection";
9
9
 
10
- /** The half of `SessionStream` a connection needs: subscribe, and ask for everything missed. */
11
10
  export type AttachableStream = {
12
11
  readonly subscribe: (listener: (event: ServerEvent) => void) => () => void;
13
12
  readonly replay: (fromSeq: number) => Promise<readonly StreamEvent[]>;
@@ -20,11 +19,13 @@ type SubagentWatch = {
20
19
  cursor: number;
21
20
  };
22
21
 
22
+ /** Bytes buffered before the connection pauses. */
23
23
  const HIGH_WATER_MARK = 1 << 20;
24
24
 
25
+ /** Serialized once per event, shared across connections. */
25
26
  const FRAMES = new WeakMap<object, string>();
26
27
 
27
- export function frame(event: object): string {
28
+ function frame(event: object): string {
28
29
  const cached = FRAMES.get(event);
29
30
  if (cached !== undefined) {
30
31
  return cached;
@@ -34,7 +35,7 @@ export function frame(event: object): string {
34
35
  return text;
35
36
  }
36
37
 
37
- /** One WebSocket client, attached to at most one session; a lagging client is paused and re-derived on `drain`. */
38
+ /** One WebSocket client, attached to at most one session. A lagging client is paused and resynced on `drain`. */
38
39
  export class ClientConnection {
39
40
  private readonly ws: ServerWebSocket<undefined>;
40
41
  private stream: AttachableStream | undefined;
@@ -55,7 +56,6 @@ export class ClientConnection {
55
56
  return this.stream?.sessionId;
56
57
  }
57
58
 
58
- /** Whether the reader behind this socket is looking at the session right now. */
59
59
  public get attentive(): boolean {
60
60
  return this.reading;
61
61
  }
@@ -65,7 +65,6 @@ export class ClientConnection {
65
65
  return this.cursor;
66
66
  }
67
67
 
68
- /** The subagent this client is reading, if it is reading one. */
69
68
  public get watchedCallId(): string | undefined {
70
69
  return this.watch?.callId;
71
70
  }
@@ -89,7 +88,7 @@ export class ClientConnection {
89
88
  this.reading = attentive;
90
89
  }
91
90
 
92
- /** Read a subagent's log alongside the attached session; at most one watch at a time. */
91
+ /** At most one watch at a time. */
93
92
  public async watchSubagent(
94
93
  callId: string,
95
94
  projection: SessionProjection,
@@ -99,14 +98,14 @@ export class ClientConnection {
99
98
  await this.pumpWatch();
100
99
  }
101
100
 
102
- /** Closes only the named watch, so it cannot close the one that replaced it. */
101
+ /** Only closes the named watch, never one that replaced it. */
103
102
  public unwatchSubagent(callId: string): void {
104
103
  if (this.watch?.callId === callId) {
105
104
  this.watch = undefined;
106
105
  }
107
106
  }
108
107
 
109
- /** Send something that belongs to no stream, e.g. a command response. */
108
+ /** For events outside the stream, e.g. command responses. */
110
109
  public send(event: ServerEvent): void {
111
110
  if (this.closed) {
112
111
  return;
@@ -135,7 +134,7 @@ export class ClientConnection {
135
134
  this.detach();
136
135
  }
137
136
 
138
- // Gate live events during the read and reconcile by `seq`; ephemeral ones are already in the snapshot.
137
+ // Queue live events during replay, then keep only durable ones (ephemeral state is in the snapshot).
139
138
  private async sync(): Promise<void> {
140
139
  const stream = this.stream;
141
140
  if (!stream || this.closed) {
@@ -156,7 +155,7 @@ export class ClientConnection {
156
155
  if (this.touchesWatch(event)) {
157
156
  void this.pumpWatch();
158
157
  }
159
- // Gate a batch unwrapped: the durable filter cannot see into an envelope.
158
+ // Unwrap so the durable filter can see inside.
160
159
  if (event.type === "replay") {
161
160
  if (this.gated) {
162
161
  this.gate.push(...event.events);
@@ -172,12 +171,12 @@ export class ClientConnection {
172
171
  this.write(event);
173
172
  }
174
173
 
175
- // Sent whole or not at all: the cursor moves only once the frame is on the socket.
174
+ // The cursor only moves once the frame is sent.
176
175
  private writeBatch(events: readonly StreamEvent[]): void {
177
176
  if (this.closed || this.paused) {
178
177
  return;
179
178
  }
180
- // Filter against a cursor that moves inside the batch: replay and queue can name the same line.
179
+ // Replay and queue can overlap, so dedupe against a moving cursor.
181
180
  let cursor = this.cursor;
182
181
  const fresh: StreamEvent[] = [];
183
182
  for (const event of events) {