pim-agent 0.12.1 → 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 (264) hide show
  1. package/package.json +4 -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 +3 -2
  36. package/packages/core/src/extensions/system-prompt/prompt.ts +13 -3
  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-WweOU5DK.js +1 -0
  163. package/packages/web/dist/client/assets/arc-O2PmZXWt.js +1 -0
  164. package/packages/web/dist/client/assets/architecture-7GRP2DOG-COBxVilU.js +1 -0
  165. package/packages/web/dist/client/assets/architectureDiagram-NJMV4G6O-DwYq1YaM.js +36 -0
  166. package/packages/web/dist/client/assets/array-BifhSqXX.js +1 -0
  167. package/packages/web/dist/client/assets/blockDiagram-BEXU5L5S-CVab041n.js +140 -0
  168. package/packages/web/dist/client/assets/c4Diagram-YGBWAQC7-BmotHez3.js +38 -0
  169. package/packages/web/dist/client/assets/channel-BKV-IcTA.js +1 -0
  170. package/packages/web/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  171. package/packages/web/dist/client/assets/chunk-3FUC2YCW-CWNS7-n4.js +2 -0
  172. package/packages/web/dist/client/assets/chunk-5DYCD2WN-DflvXohu.js +1 -0
  173. package/packages/web/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  174. package/packages/web/dist/client/assets/chunk-742MDFTN-B_0t6_RE.js +1 -0
  175. package/packages/web/dist/client/assets/chunk-7INBJB4K-wKj0Q3Kv.js +62 -0
  176. package/packages/web/dist/client/assets/chunk-7M6MHVWA-BMvUghc8.js +213 -0
  177. package/packages/web/dist/client/assets/chunk-7PRAP22T-BY41wkcT.js +1 -0
  178. package/packages/web/dist/client/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  179. package/packages/web/dist/client/assets/chunk-FOHPRMQF-CoA4358r.js +161 -0
  180. package/packages/web/dist/client/assets/chunk-GTNCS2PH-PqY_YoS0.js +88 -0
  181. package/packages/web/dist/client/assets/chunk-GWA4HPMP-fozSU5ty.js +1 -0
  182. package/packages/web/dist/client/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  183. package/packages/web/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  184. package/packages/web/dist/client/assets/chunk-MBY4JIJT-CrHrC3jw.js +72 -0
  185. package/packages/web/dist/client/assets/chunk-NETBCI7D-DWdAxieG.js +1 -0
  186. package/packages/web/dist/client/assets/chunk-O7XYJQB3-B6uOn9iS.js +128 -0
  187. package/packages/web/dist/client/assets/chunk-UA2S7LBM-BxYSHqyy.js +1 -0
  188. package/packages/web/dist/client/assets/chunk-WEXAMYUT-CtwIaQDa.js +1 -0
  189. package/packages/web/dist/client/assets/chunk-XXDRQBXY-fgBFe-PX.js +1 -0
  190. package/packages/web/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  191. package/packages/web/dist/client/assets/chunk-Z7XXMR3K-CPonoV3p.js +10 -0
  192. package/packages/web/dist/client/assets/chunk-ZIGJFQKS-Cdu87cTj.js +2 -0
  193. package/packages/web/dist/client/assets/classDiagram-v2-NBCMYWYE-DSWsPaa7.js +217 -0
  194. package/packages/web/dist/client/assets/core-CBoS7AST.js +3 -0
  195. package/packages/web/dist/client/assets/cose-bilkent-JH36ORCC-CTwlldtJ.js +1 -0
  196. package/packages/web/dist/client/assets/cynefin-OW5HDTMX-CZQ4EPNJ.js +1 -0
  197. package/packages/web/dist/client/assets/cynefinDiagram-VND7K2PF-C2gp6H_Q.js +62 -0
  198. package/packages/web/dist/client/assets/cytoscape.esm-B-1iW2ft.js +321 -0
  199. package/packages/web/dist/client/assets/dagre-6A5THRUB-CDxTP47t.js +4 -0
  200. package/packages/web/dist/client/assets/defaultLocale-BFoDCU3G.js +1 -0
  201. package/packages/web/dist/client/assets/diagram-22UHCM2B-BIohE1lI.js +200 -0
  202. package/packages/web/dist/client/assets/diagram-3UASUU5V-TwAUir-w.js +24 -0
  203. package/packages/web/dist/client/assets/diagram-ATOU4E4O-BM1JQrIu.js +3 -0
  204. package/packages/web/dist/client/assets/diagram-CDSNMT55-BKIqhxSI.js +30 -0
  205. package/packages/web/dist/client/assets/diagram-MLGK6HIB-CVe3QDHr.js +24 -0
  206. package/packages/web/dist/client/assets/diagram-MPIPVDR6-Bj-MQIFI.js +41 -0
  207. package/packages/web/dist/client/assets/dist-DlZpcyc2.js +1 -0
  208. package/packages/web/dist/client/assets/ebnfDiagram-ZINNZB2B-C06mmvmz.js +1 -0
  209. package/packages/web/dist/client/assets/elk-276RUBZZ-CizJSNEj.js +27 -0
  210. package/packages/web/dist/client/assets/erDiagram-OPXOYQCR-M3FQkKI1.js +99 -0
  211. package/packages/web/dist/client/assets/eventmodeling-NTZA5JFV-D_UXLpIu.js +1 -0
  212. package/packages/web/dist/client/assets/flowDiagram-KWPJA3E3-BT4eEEVm.js +1 -0
  213. package/packages/web/dist/client/assets/ganttDiagram-FUAMR5RP-CojF9Q16.js +292 -0
  214. package/packages/web/dist/client/assets/gitGraph-4MIJSDKK-YvKqGEPk.js +1 -0
  215. package/packages/web/dist/client/assets/gitGraphDiagram-X574FWY7-XqNqIFo9.js +106 -0
  216. package/packages/web/dist/client/assets/graphlib-DS17s2tU.js +1 -0
  217. package/packages/web/dist/client/assets/index--2CDtzvY.css +1 -0
  218. package/packages/web/dist/client/assets/index-bT6QPJZs.js +47 -0
  219. package/packages/web/dist/client/assets/info-A6RAGUB7-B_1BCzM2.js +1 -0
  220. package/packages/web/dist/client/assets/infoDiagram-VRGFBTTK-zFX8B-dP.js +2 -0
  221. package/packages/web/dist/client/assets/init-C-OQMol4.js +1 -0
  222. package/packages/web/dist/client/assets/ishikawaDiagram-OU5B5YK6-DLzQ_Uyf.js +70 -0
  223. package/packages/web/dist/client/assets/journeyDiagram-ZHPQQLJL-S3WRMmoa.js +139 -0
  224. package/packages/web/dist/client/assets/kanban-definition-PNTS6WVX-B5Z8I-kM.js +89 -0
  225. package/packages/web/dist/client/assets/katex-ZlcWpGUi.js +257 -0
  226. package/packages/web/dist/client/assets/line-BGMH-i2X.js +1 -0
  227. package/packages/web/dist/client/assets/linear-KsrestXG.js +1 -0
  228. package/packages/web/dist/client/assets/mermaid-parser.core-DNffyJJA.js +7 -0
  229. package/packages/web/dist/client/assets/mermaid.core-Cz3vJeQG.js +44 -0
  230. package/packages/web/dist/client/assets/mindmap-definition-NLK3R4M7-CzA61XUf.js +96 -0
  231. package/packages/web/dist/client/assets/ordinal-BDEzSJ7C.js +1 -0
  232. package/packages/web/dist/client/assets/packet-AYTQ26CC-BO55e7cR.js +1 -0
  233. package/packages/web/dist/client/assets/path-fybaL0A-.js +1 -0
  234. package/packages/web/dist/client/assets/pegDiagram-GJSIUBJH-DeRiXYNd.js +1 -0
  235. package/packages/web/dist/client/assets/pie-WAS4IAKB-D9ibrQRe.js +1 -0
  236. package/packages/web/dist/client/assets/pieDiagram-5QR66LMP-BaBpPP33.js +39 -0
  237. package/packages/web/dist/client/assets/quadrantDiagram-O4NWA36T-DIfn4GjM.js +7 -0
  238. package/packages/web/dist/client/assets/radar-RG4KPBEZ-Dmn5-nJq.js +1 -0
  239. package/packages/web/dist/client/assets/railroad-74A4TZTK-ZCWNEd2Z.js +1 -0
  240. package/packages/web/dist/client/assets/railroad-abnf-HS5TGJTU-_swqhy0y.js +1 -0
  241. package/packages/web/dist/client/assets/railroad-ebnf-LZEXJU2U-CytbazPx.js +1 -0
  242. package/packages/web/dist/client/assets/railroad-peg-WCYAUIDC-Bg8QW17T.js +1 -0
  243. package/packages/web/dist/client/assets/railroadDiagram-XR7U4H2S-CDPZ39sy.js +1 -0
  244. package/packages/web/dist/client/assets/requirementDiagram-PLB6GJNP-CyCI8Yul.js +84 -0
  245. package/packages/web/dist/client/assets/rough.esm-Dy-Kn_BL.js +1 -0
  246. package/packages/web/dist/client/assets/sankeyDiagram-IPEJSGJF-p_XAPv72.js +40 -0
  247. package/packages/web/dist/client/assets/sequenceDiagram-PO4LG4MO-DmR6IP_o.js +169 -0
  248. package/packages/web/dist/client/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  249. package/packages/web/dist/client/assets/src-gnv7pJ1z.js +1 -0
  250. package/packages/web/dist/client/assets/stateDiagram-v2-GCMORJYK-7ZTHl6ih.js +291 -0
  251. package/packages/web/dist/client/assets/swimlanes-2SLR337P-N6bj71pw.js +1 -0
  252. package/packages/web/dist/client/assets/swimlanesDiagram-TC7HE7FX-BJQ2CBsp.js +8 -0
  253. package/packages/web/dist/client/assets/timeline-definition-EJHVYXUP-GNez0kW8.js +120 -0
  254. package/packages/web/dist/client/assets/treeView-Q6P3EWNA-DyQZkboR.js +1 -0
  255. package/packages/web/dist/client/assets/treemap-WGGIJYW6-_J8Pw2TM.js +1 -0
  256. package/packages/web/dist/client/assets/usecaseDiagram-POWQR4AR-Bg-CzKjl.js +328 -0
  257. package/packages/web/dist/client/assets/vennDiagram-UO4OBE2U-DBQKHXhK.js +34 -0
  258. package/packages/web/dist/client/assets/wardley-WFR3VGLG-B5aAsQ3F.js +1 -0
  259. package/packages/web/dist/client/assets/wardleyDiagram-VNRHLVJA-BKUQ1LK6.js +78 -0
  260. package/packages/web/dist/client/assets/xychartDiagram-PMCCYNJV-BI2sVbks.js +7 -0
  261. package/packages/web/dist/client/index.html +2 -2
  262. package/packages/web/dist/client/assets/core-C5fJJN75.js +0 -3
  263. package/packages/web/dist/client/assets/index-C5D7B_8d.js +0 -45
  264. package/packages/web/dist/client/assets/index-OVLzpES-.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pim-agent",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "A batteries-included distro of Pi, accessible in your terminal, browser and Telegram.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,6 +66,7 @@
66
66
  "@types/bun": "latest",
67
67
  "happy-dom": "^20.14.0",
68
68
  "highlight.js": "^11.12.0",
69
+ "mermaid": "^12.0.0",
69
70
  "oxfmt": "0.67.0",
70
71
  "oxlint": "^1.82.0",
71
72
  "solid-js": "^2.0.0-rc.7",
@@ -75,8 +76,8 @@
75
76
  "vite": "^8.2.2"
76
77
  },
77
78
  "dependencies": {
78
- "@earendil-works/pi-coding-agent": "0.87.1",
79
- "@earendil-works/pi-server": "0.87.1",
79
+ "@earendil-works/pi-coding-agent": "1.0.0",
80
+ "@earendil-works/pi-server": "1.0.0",
80
81
  "diff": "^9.0.0",
81
82
  "fzf": "^0.5.2",
82
83
  "grammy": "^1.46.0",
@@ -3,36 +3,32 @@ import { basename, extname } from "node:path";
3
3
 
4
4
  import { SafePath } from "../shared/SafePath";
5
5
 
6
- /** A file the store has taken a copy of, under a name it chose. */
7
6
  export type StoredFile = {
8
- /** Names the file within its scope; what a client sends back on a prompt. */
7
+ /** Unique within its scope. */
9
8
  readonly id: string;
10
- /** Absolute path on the server. The only path the agent is ever told. */
9
+ /** Absolute path on the server. */
11
10
  readonly path: string;
12
11
  readonly mimeType: string;
13
12
  };
14
13
 
15
- /** A file that now lives on the machine the agent runs on. */
16
14
  export type StoredAttachment = StoredFile & {
17
- /** Present only for images small enough to inline into the prompt. */
15
+ /** Set only for images up to IMAGE_BYTES_LIMIT. */
18
16
  readonly imageBase64: string | undefined;
19
17
  };
20
18
 
21
19
  export type AttachmentInput = {
22
20
  readonly bytes: ArrayBuffer;
23
21
  readonly mimeType: string;
24
- /** Client-supplied filename. Only its basename is ever used. */
22
+ /** Client-supplied filename; only its extension is used. */
25
23
  readonly name?: string;
26
- /** Stem of the stored filename; a random one when omitted. */
24
+ /** Defaults to a random UUID. */
27
25
  readonly stem?: string;
28
- /** Extension to fall back on when `name` carries none. */
26
+ /** Used when `name` has no extension. */
29
27
  readonly ext?: string;
30
28
  };
31
29
 
32
- /** Above this, an image is referenced by path instead of inlined. */
33
30
  const IMAGE_BYTES_LIMIT = 4 * 1024 * 1024;
34
31
 
35
- /** Materialises client bytes onto the server's filesystem; the agent is only ever told a server path. */
36
32
  export class AttachmentStore {
37
33
  public constructor(private readonly root: string) {}
38
34
 
@@ -59,12 +55,11 @@ export class AttachmentStore {
59
55
  };
60
56
  }
61
57
 
62
- /** Copies a file already on the agent's disk into the store so it can be served. */
63
58
  public async storeFile(scope: string, source: string): Promise<StoredFile> {
64
59
  const file = Bun.file(source);
65
60
  const name = basename(source);
66
61
  const ext = extname(name);
67
- // Keep `basename(name, ext)`: a regex strip leaves a dotfile like `.bashrc` with no name.
62
+ // `basename(name, ext)` keeps dotfiles like `.bashrc` named.
68
63
  const { id, path } = await this.place(scope, basename(name, ext), ext);
69
64
  await Bun.write(path, file);
70
65
  return { id, path, mimeType: file.type || "application/octet-stream" };
@@ -87,7 +82,7 @@ export class AttachmentStore {
87
82
  return SafePath.contain(this.root, SafePath.safeName(scope));
88
83
  }
89
84
 
90
- /** Where `store` put this id; the id is re-sanitised, so it can never name a file outside its scope. */
85
+ /** The id is re-sanitised, so it cannot escape its scope. */
91
86
  public locate(scope: string, id: string): string {
92
87
  return SafePath.contain(this.scopeDir(scope), SafePath.safeName(id));
93
88
  }
@@ -4,24 +4,22 @@ import { basename, extname } from "node:path";
4
4
  import type { StoredAttachment } from "./AttachmentStore";
5
5
 
6
6
  export type AttachmentPrompt = {
7
- /** One line per file, appended to the prompt text. */
8
7
  readonly lines: readonly string[];
9
8
  readonly images: NonNullable<PromptOptions["images"]>;
10
9
  };
11
10
 
12
- /** One file a prompt carried, read back out of the text that carried it. */
13
11
  export type PromptAttachment = {
14
12
  readonly path: string;
15
13
  readonly isImage: boolean;
16
14
  };
17
15
 
18
16
  export type ParsedPrompt = {
19
- /** The prompt with the marker lines removed: what a person actually said. */
17
+ /** The prompt with the marker lines removed. */
20
18
  readonly text: string;
21
19
  readonly files: readonly PromptAttachment[];
22
20
  };
23
21
 
24
- // Must keep matching every spelling `render` has ever produced: old session files still carry them.
22
+ // Old session files carry older spellings; keep matching them.
25
23
  const MARKER = /^\[(image )?attachment:\s*(.+)]$/i;
26
24
 
27
25
  const STAMP = /-\d{10,}$/;
@@ -26,7 +26,7 @@ export type PimInlineExtension = {
26
26
  readonly factory: ExtensionFactory;
27
27
  };
28
28
 
29
- // Enumerated, never globbed: the published tarball must not depend on a directory scan.
29
+ // Listed explicitly so the published package needs no directory scan.
30
30
  function list(surface?: Surface): readonly PimInlineExtension[] {
31
31
  return [
32
32
  { name: "apply-patch", factory: applyPatch },
@@ -1,7 +1,7 @@
1
1
  const EDIT_TOOL = "edit";
2
2
  const APPLY_PATCH_TOOL = "apply_patch";
3
3
 
4
- /** Reconcile the single `edit`/`apply_patch` slot; returns `active` by reference when nothing changes. */
4
+ /** Keeps at most one of `edit`/`apply_patch` active. Returns `active` itself when nothing changes. */
5
5
  export function computeActiveTools(
6
6
  available: readonly string[],
7
7
  active: readonly string[],
@@ -164,11 +164,7 @@ async function planUpdate(
164
164
  ): Promise<PlannedAction> {
165
165
  const absoluteSource = Paths.resolve(hunk.path, cwd);
166
166
  const metadata = await statPatchTarget(absoluteSource, hunk.path, "update");
167
- const canonicalSource = await realpathPatchTarget(
168
- absoluteSource,
169
- hunk.path,
170
- "update"
171
- );
167
+ const canonicalSource = await realpathUpdateTarget(absoluteSource, hunk.path);
172
168
  if (metadata.size > MAX_UPDATE_BYTES) {
173
169
  throw new Error(
174
170
  `Cannot update ${hunk.path}: file is too large (${metadata.size} bytes, max ${MAX_UPDATE_BYTES}). Use bash or another purpose-built tool for large-file edits.`
@@ -257,15 +253,14 @@ async function statPatchTarget(
257
253
  return metadata;
258
254
  }
259
255
 
260
- async function realpathPatchTarget(
256
+ async function realpathUpdateTarget(
261
257
  absolutePath: string,
262
- displayPath: string,
263
- operation: "delete" | "update"
258
+ displayPath: string
264
259
  ): Promise<string> {
265
260
  try {
266
261
  return await realpath(absolutePath);
267
262
  } catch (error) {
268
- throw new Error(formatStatFailure(displayPath, operation, error));
263
+ throw new Error(formatStatFailure(displayPath, "update", error));
269
264
  }
270
265
  }
271
266
 
@@ -275,23 +270,21 @@ function formatStatFailure(
275
270
  error: unknown
276
271
  ): string {
277
272
  const code = FsErrors.code(error);
278
- if (operation === "delete") {
279
- if (code === "ENOENT") {
280
- return `Failed to delete file ${displayPath}: file does not exist. Use glob to locate the file, or omit this delete hunk.`;
281
- }
282
- if (code === "EACCES" || code === "EPERM") {
283
- return `Failed to delete file ${displayPath}: permission denied.`;
284
- }
285
- return `Failed to delete file ${displayPath}: ${errorDetail(error)}.`;
286
- }
287
-
273
+ const prefix =
274
+ operation === "delete"
275
+ ? `Failed to delete file ${displayPath}`
276
+ : `Failed to read file to update ${displayPath}`;
288
277
  if (code === "ENOENT") {
289
- return `Failed to read file to update ${displayPath}: file does not exist. Use *** Add File to create a new file, or use glob to locate the existing file.`;
278
+ const hint =
279
+ operation === "delete"
280
+ ? "Use glob to locate the file, or omit this delete hunk."
281
+ : "Use *** Add File to create a new file, or use glob to locate the existing file.";
282
+ return `${prefix}: file does not exist. ${hint}`;
290
283
  }
291
284
  if (code === "EACCES" || code === "EPERM") {
292
- return `Failed to read file to update ${displayPath}: permission denied.`;
285
+ return `${prefix}: permission denied.`;
293
286
  }
294
- return `Failed to read file to update ${displayPath}: ${errorDetail(error)}.`;
287
+ return `${prefix}: ${errorDetail(error)}.`;
295
288
  }
296
289
 
297
290
  function errorDetail(error: unknown): string {
@@ -352,6 +345,7 @@ function applyChunks(
352
345
  readonly start: number;
353
346
  readonly oldLen: number;
354
347
  readonly newLines: readonly string[];
348
+ readonly order: number;
355
349
  }> = [];
356
350
  let lineIndex = 0;
357
351
 
@@ -373,11 +367,12 @@ function applyChunks(
373
367
  }
374
368
 
375
369
  if (chunk.oldLines.length === 0) {
376
- const insertionIdx = originalLines.length;
377
370
  replacements.push({
378
- start: insertionIdx,
371
+ start:
372
+ chunk.changeContext === undefined ? originalLines.length : lineIndex,
379
373
  oldLen: 0,
380
374
  newLines: chunk.newLines,
375
+ order: replacements.length,
381
376
  });
382
377
  continue;
383
378
  }
@@ -416,11 +411,12 @@ function applyChunks(
416
411
  start: found[0]!,
417
412
  oldLen: pattern.length,
418
413
  newLines: newSlice,
414
+ order: replacements.length,
419
415
  });
420
416
  lineIndex = found[0]! + pattern.length;
421
417
  }
422
418
 
423
- replacements.sort((a, b) => a.start - b.start);
419
+ replacements.sort((a, b) => a.start - b.start || a.order - b.order);
424
420
 
425
421
  const lines = [...originalLines];
426
422
  for (const { start, oldLen, newLines } of replacements.reverse()) {
@@ -4,7 +4,7 @@ const MATCHERS: ReadonlyArray<(a: string, b: string) => boolean> = [
4
4
  (a, b) => a.trim() === b.trim(),
5
5
  ];
6
6
 
7
- /** Locate `pattern` in `lines` at or after `start`, relaxing whitespace over three passes; `eof` searches flush against the end. */
7
+ /** Match starts of `pattern` at or after `start`, relaxing whitespace each pass. `eof` only tries the last possible position. */
8
8
  export function seekSequenceMatches(
9
9
  lines: readonly string[],
10
10
  pattern: readonly string[],
@@ -4,8 +4,8 @@ type ModelLike = {
4
4
  readonly id?: string;
5
5
  };
6
6
 
7
- // Never match a GPT-named non-OpenAI model (`eleutherai/gpt-neo`): a false positive hands it a tool it was not trained on.
8
- function isGptModel(model: ModelLike | undefined): boolean {
7
+ // Must not match GPT-named non-OpenAI models such as `eleutherai/gpt-neo`.
8
+ export function prefersApplyPatch(model: ModelLike | undefined): boolean {
9
9
  const provider = (model?.provider ?? "").toLowerCase();
10
10
  const api = (model?.api ?? "").toLowerCase();
11
11
  const id = (model?.id ?? "").toLowerCase();
@@ -22,7 +22,3 @@ function isGptModel(model: ModelLike | undefined): boolean {
22
22
  id.includes("gpt"))
23
23
  );
24
24
  }
25
-
26
- export function prefersApplyPatch(model: ModelLike | undefined): boolean {
27
- return isGptModel(model);
28
- }
@@ -12,25 +12,18 @@ const END_PATCH_MARKER = "*** End Patch";
12
12
  const EOF_MARKER = "*** End of File";
13
13
  const CHANGE_CONTEXT_MARKER = "@@ ";
14
14
  const EMPTY_CHANGE_CONTEXT_MARKER = "@@";
15
+ const EMPTY_CHUNK =
16
+ "Update hunk does not contain any context, added, or removed lines. Include at least one line starting with ' ', '+', or '-'.";
15
17
  const MOVE_DESTINATION_REQUIRED =
16
18
  "Invalid *** Move to directive: destination path is required.";
17
19
 
18
- type ParseErrorDetails =
19
- | { readonly type: "patch"; readonly message: string }
20
- | {
21
- readonly type: "hunk";
22
- readonly message: string;
23
- readonly lineNumber: number;
24
- };
20
+ // Matches Codex's ParseError text byte for byte; GPT models recover from it.
21
+ function patchError(message: string): Error {
22
+ return new Error(`invalid patch: ${message}`);
23
+ }
25
24
 
26
- // Keep these formats byte-identical to Codex's ParseError Display; GPT models recover from them.
27
- function formatParseError(error: ParseErrorDetails): string {
28
- switch (error.type) {
29
- case "patch":
30
- return `invalid patch: ${error.message}`;
31
- case "hunk":
32
- return `invalid hunk at line ${error.lineNumber}, ${error.message}`;
33
- }
25
+ function hunkError(lineNumber: number, message: string): Error {
26
+ return new Error(`invalid hunk at line ${lineNumber}, ${message}`);
34
27
  }
35
28
 
36
29
  export function parsePatch(text: string): Patch {
@@ -53,26 +46,15 @@ export function parsePatch(text: string): Patch {
53
46
  }
54
47
 
55
48
  function checkBoundaries(lines: readonly string[]): void {
56
- const first = lines[0]?.trim();
57
- const last = lines.at(-1)?.trim();
58
-
59
- if (first === undefined || !lines[0]!.trim().startsWith(BEGIN_PATCH_MARKER)) {
60
- throw new Error(
61
- formatParseError({
62
- type: "patch",
63
- message:
64
- "The first line of the patch must be '*** Begin Patch'. Do not include Markdown fences, prose, or shell heredoc text before it.",
65
- })
49
+ if (!lines[0]!.trim().startsWith(BEGIN_PATCH_MARKER)) {
50
+ throw patchError(
51
+ "The first line of the patch must be '*** Begin Patch'. Do not include Markdown fences, prose, or shell heredoc text before it."
66
52
  );
67
53
  }
68
54
 
69
- if (last !== END_PATCH_MARKER) {
70
- throw new Error(
71
- formatParseError({
72
- type: "patch",
73
- message:
74
- "The last line of the patch must be '*** End Patch'. Do not include Markdown fences or trailing prose after it.",
75
- })
55
+ if (lines.at(-1)!.trim() !== END_PATCH_MARKER) {
56
+ throw patchError(
57
+ "The last line of the patch must be '*** End Patch'. Do not include Markdown fences or trailing prose after it."
76
58
  );
77
59
  }
78
60
  }
@@ -98,15 +80,11 @@ function parseOneHunk(
98
80
  return parseUpdateHunk(lines, lineNumber, updatePath);
99
81
  }
100
82
 
101
- throw new Error(
102
- formatParseError({
103
- type: "hunk",
104
- lineNumber,
105
- message:
106
- `'${firstLine}' is not a valid hunk header. ` +
107
- "Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'. " +
108
- "Do not use unified-diff file headers like '---' or '+++' as hunk headers.",
109
- })
83
+ throw hunkError(
84
+ lineNumber,
85
+ `'${firstLine}' is not a valid hunk header. ` +
86
+ "Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'. " +
87
+ "Do not use unified-diff file headers like '---' or '+++' as hunk headers."
110
88
  );
111
89
  }
112
90
 
@@ -126,17 +104,10 @@ function parseAddHunk(
126
104
  }
127
105
  }
128
106
  const nextLine = lines[consumed];
129
- if (
130
- nextLine !== undefined &&
131
- !isHunkHeader(nextLine) &&
132
- !nextLine.trim().startsWith("*")
133
- ) {
134
- throw new Error(
135
- formatParseError({
136
- type: "hunk",
137
- lineNumber: lineNumber + consumed,
138
- message: `Invalid Add File body: '${nextLine}' must start with '+'. Added file content lines must start with '+'.`,
139
- })
107
+ if (nextLine !== undefined && !nextLine.trim().startsWith("*")) {
108
+ throw hunkError(
109
+ lineNumber + consumed,
110
+ `Invalid Add File body: '${nextLine}' must start with '+'. Added file content lines must start with '+'.`
140
111
  );
141
112
  }
142
113
  return {
@@ -151,17 +122,10 @@ function parseDeleteHunk(
151
122
  deletePath: string
152
123
  ): { readonly hunk: Hunk; readonly consumed: number } {
153
124
  const nextLine = lines[1];
154
- if (
155
- nextLine !== undefined &&
156
- !isHunkHeader(nextLine) &&
157
- !nextLine.trim().startsWith("*")
158
- ) {
159
- throw new Error(
160
- formatParseError({
161
- type: "hunk",
162
- lineNumber: lineNumber + 1,
163
- message: `Delete File hunks must not contain content lines, got: '${nextLine}'.`,
164
- })
125
+ if (nextLine !== undefined && !nextLine.trim().startsWith("*")) {
126
+ throw hunkError(
127
+ lineNumber + 1,
128
+ `Delete File hunks must not contain content lines, got: '${nextLine}'.`
165
129
  );
166
130
  }
167
131
  return {
@@ -182,46 +146,23 @@ function parseUpdateHunk(
182
146
  const moveLine = remaining[0]?.trim();
183
147
  if (moveLine?.startsWith(MOVE_TO_MARKER)) {
184
148
  const rawMovePath = moveLine.slice(MOVE_TO_MARKER.length);
185
- if (rawMovePath.length === 0) {
186
- throw new Error(
187
- formatParseError({
188
- type: "hunk",
189
- lineNumber: lineNumber + consumed,
190
- message: MOVE_DESTINATION_REQUIRED,
191
- })
192
- );
193
- }
194
- if (!rawMovePath.startsWith(" ")) {
195
- throw new Error(
196
- formatParseError({
197
- type: "hunk",
198
- lineNumber: lineNumber + consumed,
199
- message: `Invalid *** Move to directive: use '*** Move to: {path}'.`,
200
- })
149
+ if (rawMovePath.length > 0 && !rawMovePath.startsWith(" ")) {
150
+ throw hunkError(
151
+ lineNumber + consumed,
152
+ "Invalid *** Move to directive: use '*** Move to: {path}'."
201
153
  );
202
154
  }
203
155
  if (cleanPath(rawMovePath).length === 0) {
204
- throw new Error(
205
- formatParseError({
206
- type: "hunk",
207
- lineNumber: lineNumber + consumed,
208
- message: MOVE_DESTINATION_REQUIRED,
209
- })
210
- );
156
+ throw hunkError(lineNumber + consumed, MOVE_DESTINATION_REQUIRED);
211
157
  }
212
- movePath = rawMovePath;
213
- } else if (moveLine?.startsWith("*** Move")) {
214
- throw new Error(
215
- formatParseError({
216
- type: "hunk",
217
- lineNumber: lineNumber + consumed,
218
- message: `Invalid move directive '${moveLine}'. Use '*** Move to: {path}'.`,
219
- })
220
- );
221
- }
222
- if (movePath !== undefined) {
158
+ movePath = cleanPath(rawMovePath);
223
159
  remaining = remaining.slice(1);
224
160
  consumed += 1;
161
+ } else if (moveLine?.startsWith("*** Move")) {
162
+ throw hunkError(
163
+ lineNumber + consumed,
164
+ `Invalid move directive '${moveLine}'. Use '*** Move to: {path}'.`
165
+ );
225
166
  }
226
167
 
227
168
  const chunks: UpdateChunk[] = [];
@@ -246,12 +187,9 @@ function parseUpdateHunk(
246
187
  }
247
188
 
248
189
  if (chunks.length === 0 && movePath === undefined) {
249
- throw new Error(
250
- formatParseError({
251
- type: "hunk",
252
- lineNumber,
253
- message: `Update file hunk for path '${cleanPath(updatePath)}' is empty. Include @@ plus at least one context, added, or removed line, or add *** Move to for a pure rename.`,
254
- })
190
+ throw hunkError(
191
+ lineNumber,
192
+ `Update file hunk for path '${cleanPath(updatePath)}' is empty. Include @@ plus at least one context, added, or removed line, or add *** Move to for a pure rename.`
255
193
  );
256
194
  }
257
195
 
@@ -259,7 +197,7 @@ function parseUpdateHunk(
259
197
  hunk: {
260
198
  kind: "update",
261
199
  path: cleanPath(updatePath),
262
- movePath: movePath === undefined ? undefined : cleanPath(movePath),
200
+ movePath,
263
201
  chunks,
264
202
  },
265
203
  consumed,
@@ -271,41 +209,21 @@ function parseUpdateChunk(
271
209
  lineNumber: number,
272
210
  allowMissingContext: boolean
273
211
  ): { readonly chunk: UpdateChunk; readonly consumed: number } {
274
- let changeContext: string | undefined;
275
- let startIndex: number;
276
-
277
- if (lines[0] === EMPTY_CHANGE_CONTEXT_MARKER) {
278
- changeContext = undefined;
279
- startIndex = 1;
280
- } else {
281
- const ctx = stripPrefix(lines[0]!, CHANGE_CONTEXT_MARKER);
282
- if (ctx !== undefined) {
283
- changeContext = ctx;
284
- startIndex = 1;
285
- } else {
286
- if (!allowMissingContext) {
287
- throw new Error(
288
- formatParseError({
289
- type: "hunk",
290
- lineNumber,
291
- message: `Expected update hunk to start with a @@ context marker, got: '${lines[0]}'. Start each additional edit chunk with @@ or @@ followed by nearby context.`,
292
- })
293
- );
294
- }
295
- changeContext = undefined;
296
- startIndex = 0;
297
- }
212
+ const first = lines[0]!;
213
+ const hasMarker =
214
+ first === EMPTY_CHANGE_CONTEXT_MARKER ||
215
+ first.startsWith(CHANGE_CONTEXT_MARKER);
216
+ if (!hasMarker && !allowMissingContext) {
217
+ throw hunkError(
218
+ lineNumber,
219
+ `Expected update hunk to start with a @@ context marker, got: '${first}'. Start each additional edit chunk with @@ or @@ followed by nearby context.`
220
+ );
298
221
  }
222
+ const changeContext = stripPrefix(first, CHANGE_CONTEXT_MARKER);
223
+ const startIndex = hasMarker ? 1 : 0;
299
224
 
300
225
  if (startIndex >= lines.length) {
301
- throw new Error(
302
- formatParseError({
303
- type: "hunk",
304
- lineNumber,
305
- message:
306
- "Update hunk does not contain any context, added, or removed lines. Include at least one line starting with ' ', '+', or '-'.",
307
- })
308
- );
226
+ throw hunkError(lineNumber, EMPTY_CHUNK);
309
227
  }
310
228
 
311
229
  const oldLines: string[] = [];
@@ -316,14 +234,7 @@ function parseUpdateChunk(
316
234
  for (const line of lines.slice(startIndex)) {
317
235
  if (line === EOF_MARKER) {
318
236
  if (parsed === 0) {
319
- throw new Error(
320
- formatParseError({
321
- type: "hunk",
322
- lineNumber,
323
- message:
324
- "Update hunk does not contain any context, added, or removed lines. Include at least one line starting with ' ', '+', or '-'.",
325
- })
326
- );
237
+ throw hunkError(lineNumber, EMPTY_CHUNK);
327
238
  }
328
239
  isEndOfFile = true;
329
240
  parsed += 1;
@@ -343,15 +254,11 @@ function parseUpdateChunk(
343
254
  oldLines.push(line.slice(1));
344
255
  } else {
345
256
  if (parsed === 0) {
346
- throw new Error(
347
- formatParseError({
348
- type: "hunk",
349
- lineNumber,
350
- message:
351
- `Unexpected line found in update hunk: '${line}'. ` +
352
- "Every line should start with ' ' (context line), '+' (added line), or '-' (removed line). " +
353
- "Unchanged context lines must be prefixed with a single space.",
354
- })
257
+ throw hunkError(
258
+ lineNumber,
259
+ `Unexpected line found in update hunk: '${line}'. ` +
260
+ "Every line should start with ' ' (context line), '+' (added line), or '-' (removed line). " +
261
+ "Unchanged context lines must be prefixed with a single space."
355
262
  );
356
263
  }
357
264
  break;
@@ -368,12 +275,3 @@ function parseUpdateChunk(
368
275
  function stripPrefix(value: string, prefix: string): string | undefined {
369
276
  return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
370
277
  }
371
-
372
- function isHunkHeader(line: string): boolean {
373
- const trimmed = line.trim();
374
- return (
375
- trimmed.startsWith(ADD_FILE_MARKER) ||
376
- trimmed.startsWith(DELETE_FILE_MARKER) ||
377
- trimmed.startsWith(UPDATE_FILE_MARKER)
378
- );
379
- }
@@ -10,7 +10,6 @@ export type ApplyPatchInput = Static<typeof applyPatchSchema>;
10
10
 
11
11
  const ALIAS_KEYS = ["patch", "patchText", "patch_text"] as const;
12
12
 
13
- /** Normalize a bare string or a `patch`/`patchText`/`patch_text` key to `{input}`, dropping the alias. */
14
13
  export function prepareApplyPatchArguments(rawArgs: unknown): ApplyPatchInput {
15
14
  if (typeof rawArgs === "string") {
16
15
  return { input: rawArgs };
@@ -33,7 +33,7 @@ function limitBytes(): number | null {
33
33
  return Math.floor(Number(match[1]) * UNITS[unit]);
34
34
  }
35
35
 
36
- /** Whether a scope's MemoryMax is really enforced: without the memory controller delegated, systemd accepts it and ignores it. */
36
+ /** Whether MemoryMax is enforced; without memory controller delegation systemd silently ignores it. */
37
37
  async function probe(): Promise<boolean> {
38
38
  if (process.platform !== "linux") {
39
39
  return false;
@@ -65,7 +65,7 @@ async function probe(): Promise<boolean> {
65
65
  }
66
66
  }
67
67
 
68
- /** The user-manager service this process runs in, so a command's scope stops with it. */
68
+ /** The user service pim runs in, so command scopes stop with it. */
69
69
  async function findOwner(): Promise<string | null> {
70
70
  try {
71
71
  const cgroup = await readFile("/proc/self/cgroup", "utf8");
@@ -82,7 +82,7 @@ async function findOwner(): Promise<string | null> {
82
82
  }
83
83
  }
84
84
 
85
- /** The systemd-run prefix that puts one command in its own memory-capped scope, or null when it cannot be capped. */
85
+ /** systemd-run argv prefix for a memory-capped scope; null when capping is off or unsupported. */
86
86
  async function scope(): Promise<MemoryScope | null> {
87
87
  const limit = limitBytes();
88
88
  if (limit === null || !(await (supported ??= probe()))) {
@@ -104,6 +104,8 @@ async function scope(): Promise<MemoryScope | null> {
104
104
  `--property=MemoryMax=${limit}`,
105
105
  "--property=MemorySwapMax=0",
106
106
  "--property=OOMPolicy=kill",
107
+ // The kernel's group kill races systemd's OOM stop; both must SIGKILL so run.ts can tell.
108
+ "--property=KillSignal=SIGKILL",
107
109
  ...(bindTo
108
110
  ? [`--property=BindsTo=${bindTo}`, `--property=After=${bindTo}`]
109
111
  : []),
@@ -120,7 +122,6 @@ async function systemctl(args: readonly string[]): Promise<void> {
120
122
  }).exited;
121
123
  }
122
124
 
123
- /** Ends whatever of a killed command is still in its scope, including a scope systemd never saw empty. */
124
125
  async function stop(unit: string): Promise<void> {
125
126
  await systemctl(["--no-block", "stop", unit]);
126
127
  }