aicodeman 1.43.0 → 1.45.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 (353) hide show
  1. package/README.md +9 -4
  2. package/dist/case-rename.d.ts +65 -0
  3. package/dist/case-rename.d.ts.map +1 -0
  4. package/dist/case-rename.js +198 -0
  5. package/dist/case-rename.js.map +1 -0
  6. package/dist/cli-agent.d.ts +63 -4
  7. package/dist/cli-agent.d.ts.map +1 -1
  8. package/dist/cli-agent.js +233 -19
  9. package/dist/cli-agent.js.map +1 -1
  10. package/dist/config/auth-config.d.ts +16 -1
  11. package/dist/config/auth-config.d.ts.map +1 -1
  12. package/dist/config/auth-config.js +16 -1
  13. package/dist/config/auth-config.js.map +1 -1
  14. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  15. package/dist/config/cli-registry/stock.js +8 -3
  16. package/dist/config/cli-registry/stock.js.map +1 -1
  17. package/dist/config/cli-registry/types.d.ts +2 -2
  18. package/dist/config/map-limits.d.ts +13 -1
  19. package/dist/config/map-limits.d.ts.map +1 -1
  20. package/dist/config/map-limits.js +28 -1
  21. package/dist/config/map-limits.js.map +1 -1
  22. package/dist/config-backup.d.ts +279 -0
  23. package/dist/config-backup.d.ts.map +1 -0
  24. package/dist/config-backup.js +794 -0
  25. package/dist/config-backup.js.map +1 -0
  26. package/dist/cron/cron-service.d.ts +5 -5
  27. package/dist/cron/cron-service.js +6 -6
  28. package/dist/cron/cron-service.js.map +1 -1
  29. package/dist/custom-model-hosts.d.ts +16 -0
  30. package/dist/custom-model-hosts.d.ts.map +1 -1
  31. package/dist/custom-model-hosts.js.map +1 -1
  32. package/dist/hooks-config.d.ts +15 -0
  33. package/dist/hooks-config.d.ts.map +1 -1
  34. package/dist/hooks-config.js +35 -0
  35. package/dist/hooks-config.js.map +1 -1
  36. package/dist/host-sensors.d.ts +115 -0
  37. package/dist/host-sensors.d.ts.map +1 -0
  38. package/dist/host-sensors.js +457 -0
  39. package/dist/host-sensors.js.map +1 -0
  40. package/dist/intent-store.d.ts +2 -0
  41. package/dist/intent-store.d.ts.map +1 -1
  42. package/dist/intent-store.js +9 -0
  43. package/dist/intent-store.js.map +1 -1
  44. package/dist/mux-interface.d.ts +21 -4
  45. package/dist/mux-interface.d.ts.map +1 -1
  46. package/dist/prompt-cache-keepwarm.d.ts +193 -0
  47. package/dist/prompt-cache-keepwarm.d.ts.map +1 -0
  48. package/dist/prompt-cache-keepwarm.js +359 -0
  49. package/dist/prompt-cache-keepwarm.js.map +1 -0
  50. package/dist/remote-hosts.d.ts +7 -0
  51. package/dist/remote-hosts.d.ts.map +1 -1
  52. package/dist/remote-hosts.js +17 -3
  53. package/dist/remote-hosts.js.map +1 -1
  54. package/dist/remote-pane-history.d.ts.map +1 -1
  55. package/dist/remote-pane-history.js +2 -2
  56. package/dist/remote-pane-history.js.map +1 -1
  57. package/dist/respawn-controller.d.ts.map +1 -1
  58. package/dist/respawn-controller.js +6 -5
  59. package/dist/respawn-controller.js.map +1 -1
  60. package/dist/session-auto-ops.d.ts.map +1 -1
  61. package/dist/session-auto-ops.js +6 -1
  62. package/dist/session-auto-ops.js.map +1 -1
  63. package/dist/session-cli-registry-bridge.d.ts +13 -0
  64. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  65. package/dist/session-cli-registry-bridge.js +20 -3
  66. package/dist/session-cli-registry-bridge.js.map +1 -1
  67. package/dist/session-submit-verifier.d.ts +19 -0
  68. package/dist/session-submit-verifier.d.ts.map +1 -1
  69. package/dist/session-submit-verifier.js +88 -0
  70. package/dist/session-submit-verifier.js.map +1 -1
  71. package/dist/session.d.ts +61 -5
  72. package/dist/session.d.ts.map +1 -1
  73. package/dist/session.js +125 -7
  74. package/dist/session.js.map +1 -1
  75. package/dist/state-store.d.ts +10 -0
  76. package/dist/state-store.d.ts.map +1 -1
  77. package/dist/state-store.js +35 -0
  78. package/dist/state-store.js.map +1 -1
  79. package/dist/tab-layout-service.d.ts +33 -0
  80. package/dist/tab-layout-service.d.ts.map +1 -1
  81. package/dist/tab-layout-service.js +75 -3
  82. package/dist/tab-layout-service.js.map +1 -1
  83. package/dist/tmux-manager.d.ts +57 -11
  84. package/dist/tmux-manager.d.ts.map +1 -1
  85. package/dist/tmux-manager.js +190 -42
  86. package/dist/tmux-manager.js.map +1 -1
  87. package/dist/tmux-prefix-table.d.ts +86 -0
  88. package/dist/tmux-prefix-table.d.ts.map +1 -0
  89. package/dist/tmux-prefix-table.js +172 -0
  90. package/dist/tmux-prefix-table.js.map +1 -0
  91. package/dist/tui/tui-app.d.ts +17 -2
  92. package/dist/tui/tui-app.d.ts.map +1 -1
  93. package/dist/tui/tui-app.js +70 -20
  94. package/dist/tui/tui-app.js.map +1 -1
  95. package/dist/tui/tui-client.d.ts +93 -5
  96. package/dist/tui/tui-client.d.ts.map +1 -1
  97. package/dist/tui/tui-client.js +198 -28
  98. package/dist/tui/tui-client.js.map +1 -1
  99. package/dist/types/app-state.d.ts +6 -0
  100. package/dist/types/app-state.d.ts.map +1 -1
  101. package/dist/types/app-state.js.map +1 -1
  102. package/dist/types/session.d.ts +40 -1
  103. package/dist/types/session.d.ts.map +1 -1
  104. package/dist/types/session.js.map +1 -1
  105. package/dist/usage-telemetry.d.ts +15 -0
  106. package/dist/usage-telemetry.d.ts.map +1 -1
  107. package/dist/usage-telemetry.js +45 -2
  108. package/dist/usage-telemetry.js.map +1 -1
  109. package/dist/utils/copilot-plan-usage.d.ts +46 -0
  110. package/dist/utils/copilot-plan-usage.d.ts.map +1 -0
  111. package/dist/utils/copilot-plan-usage.js +95 -0
  112. package/dist/utils/copilot-plan-usage.js.map +1 -0
  113. package/dist/utils/index.d.ts +2 -0
  114. package/dist/utils/index.d.ts.map +1 -1
  115. package/dist/utils/index.js +2 -0
  116. package/dist/utils/index.js.map +1 -1
  117. package/dist/utils/single-line.d.ts +22 -0
  118. package/dist/utils/single-line.d.ts.map +1 -0
  119. package/dist/utils/single-line.js +30 -0
  120. package/dist/utils/single-line.js.map +1 -0
  121. package/dist/web/agent-inbox-persistence.d.ts +52 -0
  122. package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
  123. package/dist/web/agent-inbox-persistence.js +139 -0
  124. package/dist/web/agent-inbox-persistence.js.map +1 -0
  125. package/dist/web/agent-inbox.d.ts +140 -0
  126. package/dist/web/agent-inbox.d.ts.map +1 -0
  127. package/dist/web/agent-inbox.js +258 -0
  128. package/dist/web/agent-inbox.js.map +1 -0
  129. package/dist/web/approval-inbox.d.ts +6 -0
  130. package/dist/web/approval-inbox.d.ts.map +1 -1
  131. package/dist/web/approval-inbox.js +2 -2
  132. package/dist/web/approval-inbox.js.map +1 -1
  133. package/dist/web/middleware/auth.d.ts +2 -0
  134. package/dist/web/middleware/auth.d.ts.map +1 -1
  135. package/dist/web/middleware/auth.js +145 -52
  136. package/dist/web/middleware/auth.js.map +1 -1
  137. package/dist/web/plan-usage-latest.d.ts +2 -1
  138. package/dist/web/plan-usage-latest.d.ts.map +1 -1
  139. package/dist/web/plan-usage-latest.js +12 -2
  140. package/dist/web/plan-usage-latest.js.map +1 -1
  141. package/dist/web/ports/index.d.ts +1 -0
  142. package/dist/web/ports/index.d.ts.map +1 -1
  143. package/dist/web/ports/keep-warm-port.d.ts +9 -0
  144. package/dist/web/ports/keep-warm-port.d.ts.map +1 -0
  145. package/dist/web/ports/keep-warm-port.js +6 -0
  146. package/dist/web/ports/keep-warm-port.js.map +1 -0
  147. package/dist/web/ports/session-port.d.ts +3 -1
  148. package/dist/web/ports/session-port.d.ts.map +1 -1
  149. package/dist/web/public/admin-ui.js.gz +0 -0
  150. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  151. package/dist/web/public/app.42d0abae.js +43 -0
  152. package/dist/web/public/app.42d0abae.js.br +0 -0
  153. package/dist/web/public/app.42d0abae.js.gz +0 -0
  154. package/dist/web/public/approvals-ui.js.gz +0 -0
  155. package/dist/web/public/{constants.e5e588db.js → constants.0b3cdde9.js} +60 -1
  156. package/dist/web/public/constants.0b3cdde9.js.br +0 -0
  157. package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
  158. package/dist/web/public/cron-ui.js.gz +0 -0
  159. package/dist/web/public/entrance-animations.js.gz +0 -0
  160. package/dist/web/public/git-status-ui.js.gz +0 -0
  161. package/dist/web/public/header-divider.eb2d2463.js +1 -0
  162. package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
  163. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  164. package/dist/web/public/home-sessions.js.gz +0 -0
  165. package/dist/web/public/host-wake-ui.js.gz +0 -0
  166. package/dist/web/public/i18n.1e68220d.js +2 -0
  167. package/dist/web/public/i18n.1e68220d.js.br +0 -0
  168. package/dist/web/public/i18n.1e68220d.js.gz +0 -0
  169. package/dist/web/public/image-input.941386e7.js.gz +0 -0
  170. package/dist/web/public/index.html +162 -14
  171. package/dist/web/public/index.html.br +0 -0
  172. package/dist/web/public/index.html.gz +0 -0
  173. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  174. package/dist/web/public/{keyboard-accessory.c283e827.js → keyboard-accessory.4e8b20ac.js} +23 -5
  175. package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
  176. package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
  177. package/dist/web/public/{mobile-handlers.0ef9c311.js → mobile-handlers.1c0cbcd8.js} +50 -16
  178. package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
  179. package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
  180. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  181. package/dist/web/public/mobile-overview.js.gz +0 -0
  182. package/dist/web/public/mobile.12fb18de.css +1 -0
  183. package/dist/web/public/mobile.12fb18de.css.br +0 -0
  184. package/dist/web/public/mobile.12fb18de.css.gz +0 -0
  185. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  186. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  187. package/dist/web/public/{panels-ui.b7bfb9da.js → panels-ui.78c3defb.js} +19 -13
  188. package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
  189. package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
  190. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  191. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  192. package/dist/web/public/readmymind-ui.js.gz +0 -0
  193. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  194. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  195. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  196. package/dist/web/public/session-lineage.js.gz +0 -0
  197. package/dist/web/public/{session-ui.be076c88.js → session-ui.ea6b1994.js} +33 -27
  198. package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
  199. package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
  200. package/dist/web/public/settings-ui.085dd202.js +106 -0
  201. package/dist/web/public/settings-ui.085dd202.js.br +0 -0
  202. package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
  203. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  204. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  205. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  206. package/dist/web/public/styles.3b59b41d.css +1 -0
  207. package/dist/web/public/styles.3b59b41d.css.br +0 -0
  208. package/dist/web/public/styles.3b59b41d.css.gz +0 -0
  209. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  210. package/dist/web/public/sw.js +2 -2
  211. package/dist/web/public/sw.js.br +0 -0
  212. package/dist/web/public/sw.js.gz +0 -0
  213. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  214. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  215. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  216. package/dist/web/public/terminal-split.js.gz +0 -0
  217. package/dist/web/public/terminal-tile.js +49 -23
  218. package/dist/web/public/terminal-tile.js.br +0 -0
  219. package/dist/web/public/terminal-tile.js.gz +0 -0
  220. package/dist/web/public/terminal-ui.fb2a27f5.js +2 -0
  221. package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
  222. package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
  223. package/dist/web/public/tile-grid.js.gz +0 -0
  224. package/dist/web/public/ultracode-panel.js.gz +0 -0
  225. package/dist/web/public/ultracode-windows.js.gz +0 -0
  226. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  227. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  228. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  229. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  230. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  231. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  232. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  233. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  234. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  235. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  236. package/dist/web/public/vendor/xterm.css.gz +0 -0
  237. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  238. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  239. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  240. package/dist/web/public/webview-tabs.js.gz +0 -0
  241. package/dist/web/public/whats-new.json +9 -9
  242. package/dist/web/route-helpers.d.ts +57 -6
  243. package/dist/web/route-helpers.d.ts.map +1 -1
  244. package/dist/web/route-helpers.js +72 -4
  245. package/dist/web/route-helpers.js.map +1 -1
  246. package/dist/web/routes/admin-routes.d.ts +2 -1
  247. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  248. package/dist/web/routes/admin-routes.js +3 -0
  249. package/dist/web/routes/admin-routes.js.map +1 -1
  250. package/dist/web/routes/case-routes.d.ts +2 -2
  251. package/dist/web/routes/case-routes.d.ts.map +1 -1
  252. package/dist/web/routes/case-routes.js +263 -4
  253. package/dist/web/routes/case-routes.js.map +1 -1
  254. package/dist/web/routes/config-backup-routes.d.ts +54 -0
  255. package/dist/web/routes/config-backup-routes.d.ts.map +1 -0
  256. package/dist/web/routes/config-backup-routes.js +268 -0
  257. package/dist/web/routes/config-backup-routes.js.map +1 -0
  258. package/dist/web/routes/custom-model-routes.d.ts +17 -1
  259. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  260. package/dist/web/routes/custom-model-routes.js +118 -13
  261. package/dist/web/routes/custom-model-routes.js.map +1 -1
  262. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  263. package/dist/web/routes/hook-event-routes.js +26 -5
  264. package/dist/web/routes/hook-event-routes.js.map +1 -1
  265. package/dist/web/routes/inbox-routes.d.ts +43 -0
  266. package/dist/web/routes/inbox-routes.d.ts.map +1 -0
  267. package/dist/web/routes/inbox-routes.js +148 -0
  268. package/dist/web/routes/inbox-routes.js.map +1 -0
  269. package/dist/web/routes/index.d.ts +2 -0
  270. package/dist/web/routes/index.d.ts.map +1 -1
  271. package/dist/web/routes/index.js +2 -0
  272. package/dist/web/routes/index.js.map +1 -1
  273. package/dist/web/routes/ralph-routes.js +2 -2
  274. package/dist/web/routes/ralph-routes.js.map +1 -1
  275. package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
  276. package/dist/web/routes/reboot-restore-routes.js +2 -0
  277. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  278. package/dist/web/routes/session-routes.d.ts +2 -2
  279. package/dist/web/routes/session-routes.d.ts.map +1 -1
  280. package/dist/web/routes/session-routes.js +110 -61
  281. package/dist/web/routes/session-routes.js.map +1 -1
  282. package/dist/web/routes/status-telemetry-routes.d.ts +2 -2
  283. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  284. package/dist/web/routes/status-telemetry-routes.js +8 -4
  285. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  286. package/dist/web/routes/system-routes.d.ts +2 -2
  287. package/dist/web/routes/system-routes.d.ts.map +1 -1
  288. package/dist/web/routes/system-routes.js +30 -2
  289. package/dist/web/routes/system-routes.js.map +1 -1
  290. package/dist/web/routes/webview-routes.d.ts.map +1 -1
  291. package/dist/web/routes/webview-routes.js +1 -0
  292. package/dist/web/routes/webview-routes.js.map +1 -1
  293. package/dist/web/schemas.d.ts +45 -0
  294. package/dist/web/schemas.d.ts.map +1 -1
  295. package/dist/web/schemas.js +79 -2
  296. package/dist/web/schemas.js.map +1 -1
  297. package/dist/web/sensitive-path.d.ts.map +1 -1
  298. package/dist/web/sensitive-path.js +2 -0
  299. package/dist/web/sensitive-path.js.map +1 -1
  300. package/dist/web/server.d.ts +14 -2
  301. package/dist/web/server.d.ts.map +1 -1
  302. package/dist/web/server.js +176 -7
  303. package/dist/web/server.js.map +1 -1
  304. package/dist/web/session-listener-wiring.d.ts +6 -0
  305. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  306. package/dist/web/session-listener-wiring.js +10 -3
  307. package/dist/web/session-listener-wiring.js.map +1 -1
  308. package/dist/web/session-wait-registry.d.ts +48 -4
  309. package/dist/web/session-wait-registry.d.ts.map +1 -1
  310. package/dist/web/session-wait-registry.js +144 -6
  311. package/dist/web/session-wait-registry.js.map +1 -1
  312. package/dist/web/sse-events.d.ts +9 -3
  313. package/dist/web/sse-events.d.ts.map +1 -1
  314. package/dist/web/sse-events.js +9 -3
  315. package/dist/web/sse-events.js.map +1 -1
  316. package/dist/web/webview-proxy.d.ts +8 -3
  317. package/dist/web/webview-proxy.d.ts.map +1 -1
  318. package/dist/web/webview-proxy.js +23 -5
  319. package/dist/web/webview-proxy.js.map +1 -1
  320. package/package.json +1 -1
  321. package/skills/codeman/SKILL.md +27 -13
  322. package/skills/codeman/preamble.sh +15 -5
  323. package/skills/codeman/reference/endpoints.md +32 -10
  324. package/skills/codeman/reference/recipes.md +1 -1
  325. package/skills/codeman/reference/verbs.md +68 -13
  326. package/dist/web/public/app.ae50029a.js +0 -43
  327. package/dist/web/public/app.ae50029a.js.br +0 -0
  328. package/dist/web/public/app.ae50029a.js.gz +0 -0
  329. package/dist/web/public/constants.e5e588db.js.br +0 -0
  330. package/dist/web/public/constants.e5e588db.js.gz +0 -0
  331. package/dist/web/public/i18n.8f291f06.js +0 -2
  332. package/dist/web/public/i18n.8f291f06.js.br +0 -0
  333. package/dist/web/public/i18n.8f291f06.js.gz +0 -0
  334. package/dist/web/public/keyboard-accessory.c283e827.js.br +0 -0
  335. package/dist/web/public/keyboard-accessory.c283e827.js.gz +0 -0
  336. package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
  337. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  338. package/dist/web/public/mobile.13514863.css +0 -1
  339. package/dist/web/public/mobile.13514863.css.br +0 -0
  340. package/dist/web/public/mobile.13514863.css.gz +0 -0
  341. package/dist/web/public/panels-ui.b7bfb9da.js.br +0 -0
  342. package/dist/web/public/panels-ui.b7bfb9da.js.gz +0 -0
  343. package/dist/web/public/session-ui.be076c88.js.br +0 -0
  344. package/dist/web/public/session-ui.be076c88.js.gz +0 -0
  345. package/dist/web/public/settings-ui.fc4cc50a.js +0 -97
  346. package/dist/web/public/settings-ui.fc4cc50a.js.br +0 -0
  347. package/dist/web/public/settings-ui.fc4cc50a.js.gz +0 -0
  348. package/dist/web/public/styles.dcfa907f.css +0 -1
  349. package/dist/web/public/styles.dcfa907f.css.br +0 -0
  350. package/dist/web/public/styles.dcfa907f.css.gz +0 -0
  351. package/dist/web/public/terminal-ui.ae01a7d1.js +0 -2
  352. package/dist/web/public/terminal-ui.ae01a7d1.js.br +0 -0
  353. package/dist/web/public/terminal-ui.ae01a7d1.js.gz +0 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @fileoverview Config backups (src/config-backup.ts).
3
+ *
4
+ * GET /api/config-backups — settings in effect, the folder, and the snapshots in it.
5
+ * POST /api/config-backups — snapshot now (a manual request always makes one).
6
+ * POST /api/config-backups/:id/restore — put a snapshot's files back (after a `pre-restore` snapshot).
7
+ * GET /api/config-backups/diff?from=&to= — what differs between two snapshots, or a snapshot and the
8
+ * running config (`current`, the default for `to`).
9
+ *
10
+ * The files include credentials (`.env`, `users.json`, endpoint keys), so in multi-user mode all four
11
+ * are admin only. Responses carry file NAMES and sizes, never file content, except the diff, which shows
12
+ * changed values but never a credential's (`.env`, `users.json`, `push-keys.json`, `webhook.json`, `intents.json` and any key that looks
13
+ * like a token/secret/password are reported as "changed" only). Restore replaces files the running server
14
+ * may have cached: `restartRequired` says which ones only apply after a restart. A restore that could not
15
+ * write every file answers 500 with the error envelope plus `data` (`restored`, `failed`, `untouched`,
16
+ * `safetySnapshot`, `restartRequired`): the files it wrote stay restored and are reloaded like a full one.
17
+ */
18
+ import { readFile } from 'node:fs/promises';
19
+ import { createRequire } from 'node:module';
20
+ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
21
+ import { getAuthUser, isAdmin, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
22
+ import { appendAdminAudit } from '../admin-audit.js';
23
+ import { reloadCliRegistry } from '../../config/cli-registry/registry.js';
24
+ import { isMultiUserMode } from '../../config/multiuser.js';
25
+ import { getDataDir } from '../../config/instance.js';
26
+ import { intentStore } from '../../intent-store.js';
27
+ import { describeUnknownPath, probePathKind } from '../../utils/bounded-path-probe.js';
28
+ import { dirname } from 'node:path';
29
+ import { CONFIG_BACKUP_FILES, CONFIG_DIFF_CURRENT, ConfigBackupCorruptError, ConfigBackupNotFoundError, ConfigBackupPartialRestoreError, defaultConfigBackupDir, diffConfigBackups, listConfigBackups, readConfigBackupSettings, resolveConfigBackupDir, restoreConfigBackup, runConfigBackupPass, withRecordedRetention, readConfigBackupOrigin, } from '../../config-backup.js';
30
+ // Only recorded in each manifest; a missing package.json just leaves it blank.
31
+ const APP_VERSION = (() => {
32
+ try {
33
+ return createRequire(import.meta.url)('../../../package.json').version ?? '';
34
+ }
35
+ catch {
36
+ return '';
37
+ }
38
+ })();
39
+ /** Files whose running-process copy only reloads on restart (the rest are read per request; `users.json`
40
+ * is re-read within a second, `clis.json` is reloaded below, `intents.json` by `reload()`). */
41
+ const RESTART_REQUIRED = new Set(['.env', 'push-keys.json', 'settings.json']);
42
+ /**
43
+ * The settings in effect and the folder they name. A custom folder is probed (bounded, #516) BEFORE
44
+ * the recorded-retention fallback reads it, so this throws `BackupDirUnavailableError` for a folder
45
+ * that does not answer and never lists it; `present` is false for a custom folder that does not exist yet.
46
+ */
47
+ export async function readEffectiveBackupSettings() {
48
+ const raw = await readJsonConfig(SETTINGS_PATH, 'settings.json', {});
49
+ const base = readConfigBackupSettings(raw);
50
+ const dir = resolveConfigBackupDir(base, getDataDir());
51
+ const present = await probeBackupDir(base, dir);
52
+ // The same fallback the timer pass uses, so the form never shows 20 / 30 while a pass keeps 100 / 0.
53
+ const settings = present ? withRecordedRetention(base, raw, dir, readConfigBackupOrigin(getDataDir())) : base;
54
+ return { settings, dir, present };
55
+ }
56
+ /** A user-chosen backup folder that is not safe to touch (bounded probe, as for case paths, #516). */
57
+ export class BackupDirUnavailableError extends Error {
58
+ }
59
+ /**
60
+ * Probe a custom backup folder without blocking the event loop on a dead mount. The default folder
61
+ * needs no probe. Returns whether the folder exists; throws when it did not answer, or is not a
62
+ * folder. A missing custom folder is only creatable when its parent is present.
63
+ */
64
+ export async function probeBackupDir(settings, dir) {
65
+ if (!settings.dir || dir === defaultConfigBackupDir(getDataDir()))
66
+ return true;
67
+ const kind = await probePathKind(dir, { pastCap: true });
68
+ if (kind === 'unknown')
69
+ throw new BackupDirUnavailableError(describeUnknownPath('Backup folder', dir, { pastCap: true }));
70
+ if (kind === 'file')
71
+ throw new BackupDirUnavailableError(`Backup folder is a file, not a folder: ${dir}`);
72
+ if (kind === 'directory')
73
+ return true;
74
+ const parent = await probePathKind(dirname(dir), { pastCap: true });
75
+ if (parent === 'unknown') {
76
+ throw new BackupDirUnavailableError(describeUnknownPath('Backup folder', dirname(dir), { pastCap: true }));
77
+ }
78
+ if (parent !== 'directory')
79
+ throw new BackupDirUnavailableError(`Backup folder's parent is missing: ${dirname(dir)}`);
80
+ return false;
81
+ }
82
+ async function readSettingsForBackup() {
83
+ try {
84
+ const parsed = JSON.parse(await readFile(SETTINGS_PATH, 'utf8'));
85
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
86
+ ? parsed
87
+ : 'unreadable';
88
+ }
89
+ catch (err) {
90
+ return err.code === 'ENOENT' ? {} : 'unreadable';
91
+ }
92
+ }
93
+ /** Snapshot (when changed) and apply retention; shared by the timer and the manual route. */
94
+ export async function runConfigBackup(reason, opts = {}) {
95
+ // Read settings.json directly: readJsonConfig answers {} for ANY failure, so a syntax error or a read
96
+ // that lands inside PUT /api/settings's non-atomic write used to prune with the defaults (20 / 30
97
+ // days) and drop the longer history the user asked to keep.
98
+ const raw = await readSettingsForBackup();
99
+ const unreadable = raw === 'unreadable';
100
+ const fromFile = readConfigBackupSettings(unreadable ? {} : raw);
101
+ if (!fromFile.enabled && !opts.force)
102
+ return null;
103
+ const dir = resolveConfigBackupDir(fromFile, getDataDir());
104
+ await probeBackupDir(fromFile, dir);
105
+ return runConfigBackupPass({
106
+ dataDir: getDataDir(),
107
+ backupDir: dir,
108
+ reason,
109
+ appVersion: APP_VERSION,
110
+ force: opts.force,
111
+ settings: fromFile,
112
+ raw: unreadable ? {} : raw,
113
+ // An unreadable settings.json still gets its snapshot, but no pass deletes anything on its say-so.
114
+ prune: !unreadable,
115
+ });
116
+ }
117
+ export function registerConfigBackupRoutes(app) {
118
+ const deny = (req, reply) => {
119
+ if (isMultiUserMode() && !isAdmin(req)) {
120
+ reply.code(403);
121
+ return createErrorResponse(ApiErrorCode.FORBIDDEN, 'Admin only in multi-user mode');
122
+ }
123
+ return null;
124
+ };
125
+ app.get('/api/config-backups', async (req, reply) => {
126
+ const denied = deny(req, reply);
127
+ if (denied)
128
+ return denied;
129
+ let eff;
130
+ try {
131
+ eff = await readEffectiveBackupSettings();
132
+ }
133
+ catch (err) {
134
+ reply.code(503);
135
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
136
+ }
137
+ const { settings, dir, present } = eff;
138
+ // Read-only: a listing must not create config-backup-id, and without one nothing can be ours.
139
+ const origin = readConfigBackupOrigin(getDataDir());
140
+ return {
141
+ success: true,
142
+ data: {
143
+ settings,
144
+ dir,
145
+ defaultDir: defaultConfigBackupDir(getDataDir()),
146
+ tracked: CONFIG_BACKUP_FILES,
147
+ backups: present && origin ? listConfigBackups(dir, origin) : [],
148
+ },
149
+ };
150
+ });
151
+ app.post('/api/config-backups', async (req, reply) => {
152
+ const denied = deny(req, reply);
153
+ if (denied)
154
+ return denied;
155
+ try {
156
+ const out = await runConfigBackup('manual', { force: true });
157
+ return { success: true, data: { ...out.result, removed: out.removed } };
158
+ }
159
+ catch (err) {
160
+ reply.code(err instanceof BackupDirUnavailableError ? 503 : 500);
161
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Backup failed: ${getErrorMessage(err)}`);
162
+ }
163
+ });
164
+ app.get('/api/config-backups/diff', async (req, reply) => {
165
+ const denied = deny(req, reply);
166
+ if (denied)
167
+ return denied;
168
+ const q = req.query;
169
+ if (typeof q.from !== 'string' || !q.from) {
170
+ reply.code(400);
171
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, '`from` is required (a backup id or "current")');
172
+ }
173
+ try {
174
+ const { dir } = await readEffectiveBackupSettings();
175
+ const data = diffConfigBackups({
176
+ dataDir: getDataDir(),
177
+ backupDir: dir,
178
+ from: q.from,
179
+ to: typeof q.to === 'string' && q.to ? q.to : CONFIG_DIFF_CURRENT,
180
+ });
181
+ return { success: true, data };
182
+ }
183
+ catch (err) {
184
+ if (err instanceof ConfigBackupNotFoundError) {
185
+ reply.code(404);
186
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, err.message);
187
+ }
188
+ if (err instanceof ConfigBackupCorruptError) {
189
+ reply.code(409);
190
+ return createErrorResponse(ApiErrorCode.CONFLICT, err.message);
191
+ }
192
+ reply.code(err instanceof BackupDirUnavailableError ? 503 : 500);
193
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Diff failed: ${getErrorMessage(err)}`);
194
+ }
195
+ });
196
+ /**
197
+ * What a restore that wrote files must do, whether or not every file made it: reload what the running
198
+ * server caches, audit it, and name the files that only apply after a restart.
199
+ */
200
+ const afterRestore = (req, id, restored, failed = []) => {
201
+ // IntentStore caches the file and rewrites it whole on the next prompt, which would undo the restore.
202
+ if (restored.includes('intents.json'))
203
+ intentStore.reload();
204
+ // restoreConfigBackup writes clis.json directly (the one writer besides mutateRegistryFile).
205
+ if (restored.includes('clis.json'))
206
+ reloadCliRegistry();
207
+ // A restore of users.json replaces accounts, password hashes, roles and bypass grants.
208
+ if (isMultiUserMode()) {
209
+ void appendAdminAudit({
210
+ admin: getAuthUser(req).username,
211
+ action: 'config_backup_restore',
212
+ target: id,
213
+ ip: req.ip,
214
+ detail: { restored, ...(failed.length > 0 && { failed }) },
215
+ });
216
+ }
217
+ return restored.filter((n) => RESTART_REQUIRED.has(n));
218
+ };
219
+ app.post('/api/config-backups/:id/restore', async (req, reply) => {
220
+ const denied = deny(req, reply);
221
+ if (denied)
222
+ return denied;
223
+ const { id } = req.params;
224
+ try {
225
+ const { dir } = await readEffectiveBackupSettings();
226
+ const out = restoreConfigBackup({ dataDir: getDataDir(), backupDir: dir, id, appVersion: APP_VERSION });
227
+ return {
228
+ success: true,
229
+ data: {
230
+ restored: out.restored,
231
+ untouched: out.untouched,
232
+ safetySnapshot: out.safetySnapshot,
233
+ restartRequired: afterRestore(req, id, out.restored),
234
+ },
235
+ };
236
+ }
237
+ catch (err) {
238
+ if (err instanceof ConfigBackupNotFoundError) {
239
+ reply.code(404);
240
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, err.message);
241
+ }
242
+ if (err instanceof ConfigBackupCorruptError) {
243
+ reply.code(409);
244
+ return createErrorResponse(ApiErrorCode.CONFLICT, `${err.message}. Nothing was changed.`);
245
+ }
246
+ if (err instanceof ConfigBackupPartialRestoreError) {
247
+ // Some files are the backup's now: the client must learn which (Settings re-reads a restored
248
+ // settings.json, or its next Save writes the old values back) and where the undo copy is.
249
+ const failedNames = err.failed.map((f) => f.name);
250
+ const restartRequired = afterRestore(req, id, err.restored, failedNames);
251
+ reply.code(500);
252
+ return {
253
+ ...createErrorResponse(ApiErrorCode.OPERATION_FAILED, err.message),
254
+ data: {
255
+ restored: err.restored,
256
+ failed: err.failed,
257
+ untouched: err.untouched,
258
+ safetySnapshot: err.safetySnapshot,
259
+ restartRequired,
260
+ },
261
+ };
262
+ }
263
+ reply.code(err instanceof BackupDirUnavailableError ? 503 : 500);
264
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Restore failed: ${getErrorMessage(err)}`);
265
+ }
266
+ });
267
+ }
268
+ //# sourceMappingURL=config-backup-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-backup-routes.js","sourceRoot":"","sources":["../../../src/web/routes/config-backup-routes.ts"],"names":[],"mappings":"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"}
@@ -40,14 +40,30 @@ export declare const CLAUDE_MIN_SAFE_CONTEXT_TOKENS = 40000;
40
40
  * with a small enough baseline of its own to never trip this would have no reason to
41
41
  * declare the field in the first place, so the check simply never applies to it.
42
42
  */
43
- export declare function exceedsSafeContextFloor(entry: Pick<CliEntry, 'capabilities'>, contextLength: number | undefined): boolean;
43
+ export declare function exceedsSafeContextFloor(entry: Pick<CliEntry, 'capabilities'>, contextLength: number | undefined, slotCount?: number): boolean;
44
+ /**
45
+ * The context one request can rely on when the server runs `slotCount` parallel slots.
46
+ * llama-server divides its context across `--parallel` slots (by default they share ONE
47
+ * unified KV pool), and Claude Code sends concurrent requests: the main session plus every
48
+ * parallel subagent, each carrying the full `CLAUDE_MIN_SAFE_CONTEXT_TOKENS` of its own
49
+ * system prompt and tools. With N busy slots each request therefore gets roughly
50
+ * `contextLength / N`, and once the pool is spent llama.cpp answers HTTP 500 "Context size
51
+ * has been exceeded" (measured: 4 slots, ~131k pool, four simultaneous 40k-token requests
52
+ * all failed; the same requests against one slot all succeeded). An unknown or single slot
53
+ * count leaves the context whole.
54
+ */
55
+ export declare function perSlotContext(contextLength: number, slotCount: number | undefined): number;
44
56
  export interface DiscoveryResult {
45
57
  models: string[];
46
58
  /** See `CustomModelHost.modelContextLengths` — only ever populated for models already loaded. */
47
59
  contextLengths: Record<string, number>;
60
+ /** See `CustomModelHost.modelSlotCounts`; populated on the same loaded-models-only basis as `contextLengths`. */
61
+ slotCounts: Record<string, number>;
48
62
  /** See `CustomModelHost.modelSizesGB` — populated for every model whose own listing states one. */
49
63
  sizesGB: Record<string, number>;
50
64
  }
65
+ /** Forget what was probed (tests only: a changed launch command already probes again). */
66
+ export declare function clearPropsProbeCache(): void;
51
67
  export interface LlamaSwapRunningModel {
52
68
  model: string;
53
69
  state: string;
@@ -1 +1 @@
1
- {"version":3,"file":"custom-model-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/custom-model-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"custom-model-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/custom-model-routes.ts"],"names":[],"mappings":"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"}
@@ -48,11 +48,30 @@ export const CLAUDE_MIN_SAFE_CONTEXT_TOKENS = 40000;
48
48
  * with a small enough baseline of its own to never trip this would have no reason to
49
49
  * declare the field in the first place, so the check simply never applies to it.
50
50
  */
51
- export function exceedsSafeContextFloor(entry, contextLength) {
51
+ export function exceedsSafeContextFloor(entry, contextLength, slotCount) {
52
52
  const cap = entry.capabilities.customModelInjection;
53
53
  if (cap.kind !== 'env' || !cap.contextLengthVar)
54
54
  return false;
55
- return typeof contextLength === 'number' && contextLength < CLAUDE_MIN_SAFE_CONTEXT_TOKENS;
55
+ if (typeof contextLength !== 'number')
56
+ return false;
57
+ if (contextLength < CLAUDE_MIN_SAFE_CONTEXT_TOKENS)
58
+ return true;
59
+ return perSlotContext(contextLength, slotCount) < CLAUDE_MIN_SAFE_CONTEXT_TOKENS;
60
+ }
61
+ /**
62
+ * The context one request can rely on when the server runs `slotCount` parallel slots.
63
+ * llama-server divides its context across `--parallel` slots (by default they share ONE
64
+ * unified KV pool), and Claude Code sends concurrent requests: the main session plus every
65
+ * parallel subagent, each carrying the full `CLAUDE_MIN_SAFE_CONTEXT_TOKENS` of its own
66
+ * system prompt and tools. With N busy slots each request therefore gets roughly
67
+ * `contextLength / N`, and once the pool is spent llama.cpp answers HTTP 500 "Context size
68
+ * has been exceeded" (measured: 4 slots, ~131k pool, four simultaneous 40k-token requests
69
+ * all failed; the same requests against one slot all succeeded). An unknown or single slot
70
+ * count leaves the context whole.
71
+ */
72
+ export function perSlotContext(contextLength, slotCount) {
73
+ const slots = typeof slotCount === 'number' && Number.isFinite(slotCount) && slotCount > 1 ? Math.floor(slotCount) : 1;
74
+ return Math.floor(contextLength / slots);
56
75
  }
57
76
  function adminOnly(req, reply) {
58
77
  if (!isMultiUserMode() || isAdmin(req))
@@ -96,7 +115,7 @@ function applyStoredApiKey(incoming, existing) {
96
115
  return incoming.apiKey ? incoming : { ...incoming, apiKey: existing.apiKey };
97
116
  }
98
117
  /**
99
- * `modelContextLengths`/`modelSizesGB` are server-populated by discovery, never
118
+ * `modelContextLengths`/`modelSlotCounts`/`modelSizesGB` are server-populated by discovery, never
100
119
  * user-entered, and PUT replaces the whole record — so merge them back in from the
101
120
  * stored host rather than trust whatever the editor's body carried (or omitted).
102
121
  * The editor only ever sends `models`/`lastDiscoveredAt` verbatim from its cached
@@ -104,7 +123,12 @@ function applyStoredApiKey(incoming, existing) {
104
123
  * future caller forgets, same class of bug `applyStoredApiKey` exists to prevent.
105
124
  */
106
125
  function applyDiscoveredFields(incoming, existing) {
107
- return { ...incoming, modelContextLengths: existing.modelContextLengths, modelSizesGB: existing.modelSizesGB };
126
+ return {
127
+ ...incoming,
128
+ modelContextLengths: existing.modelContextLengths,
129
+ modelSlotCounts: existing.modelSlotCounts,
130
+ modelSizesGB: existing.modelSizesGB,
131
+ };
108
132
  }
109
133
  function authHeaders(host) {
110
134
  const headers = {};
@@ -137,7 +161,7 @@ function parseSizeGB(description) {
137
161
  }
138
162
  /**
139
163
  * Best-effort: fetches `GET /props?model=<id>` (llama.cpp-native, llama-swap-proxied) for
140
- * ONE already-loaded model and pulls its real `n_ctx` out. Never called for a model that
164
+ * ONE already-loaded model and pulls its `n_ctx` and `total_slots` out. Never called for a model that
141
165
  * isn't already loaded — see the caller and `CustomModelHost.modelContextLengths` for why
142
166
  * that's a hard safety requirement, not just a nicety: llama-swap treats this endpoint's
143
167
  * `?model=` as a routing hint, and asking it about an unloaded model risks triggering an
@@ -153,17 +177,31 @@ function parseSizeGB(description) {
153
177
  * (below), which reads the actual launch flag `/running` reports, is the primary source;
154
178
  * this is only used when that parse comes up empty (no recognized flag in `cmd`, or `cmd`
155
179
  * itself unavailable).
180
+ *
181
+ * ⚠️ `/props` only ever RAISES the slot warning, never skips it: its `n_ctx` is the context
182
+ * fallback when `cmd` states no context flag at all, and `total_slots` is the slot count when
183
+ * `cmd` has no `--parallel`. It never widens the pool the slot check divides. And it is not
184
+ * free: llama-swap routes it through the model's process handler, which refreshes the model's
185
+ * last-use time, so probing a model with a `ttl` on every 5-minute sweep would keep it loaded
186
+ * forever. `discoverModels` therefore probes once per launch command (`propsByCmd`).
187
+ *
188
+ * Returns `undefined` when the request itself failed (network error, timeout, unreadable body),
189
+ * which is retried next sweep; a non-2xx status, or a 2xx without the fields, gives an object with
190
+ * them unset, which `propsForLaunch` remembers.
156
191
  */
157
- async function fetchContextLength(host, modelId, headers) {
192
+ async function fetchServerProps(host, modelId, headers) {
158
193
  try {
159
194
  const url = new URL(`${host.baseUrl.replace(/\/+$/, '')}/props`);
160
195
  url.searchParams.set('model', modelId);
161
196
  const res = await webviewFetch(url, { headers, signal: AbortSignal.timeout(PROPS_TIMEOUT_MS) });
197
+ // A non-2xx answer (an upstream that does not serve /props) is a real answer: empty, so it is
198
+ // remembered per launch command instead of being re-probed every sweep.
162
199
  if (!res.ok)
163
- return undefined;
200
+ return {};
164
201
  const body = (await res.json());
165
202
  const nCtx = body.n_ctx ?? body.default_generation_settings?.n_ctx;
166
- return typeof nCtx === 'number' && Number.isFinite(nCtx) && nCtx > 0 ? nCtx : undefined;
203
+ const positive = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
204
+ return { nCtx: positive(nCtx), totalSlots: positive(body.total_slots) };
167
205
  }
168
206
  catch {
169
207
  return undefined;
@@ -173,7 +211,7 @@ async function fetchContextLength(host, modelId, headers) {
173
211
  * Parses the REAL configured context size out of llama-swap's own launch command for a
174
212
  * model (`/running`'s `cmd` field, e.g. `"llama-server -m ... --fit-ctx 16384 ..."`) —
175
213
  * the primary source for `modelContextLengths`, preferred over `/props`'s `n_ctx` (see
176
- * `fetchContextLength`'s own doc comment for why that field is unreliable here). Checks
214
+ * `fetchServerProps`'s own doc comment for why that field is unreliable here). Checks
177
215
  * `--fit-ctx` first (llama-swap's own auto-fit flag), then the plain llama.cpp
178
216
  * `-c`/`--ctx-size`/`--ctx_size` flags a hand-written launch command might use instead.
179
217
  * Returns `undefined` when `cmd` has none of these — not every launch command needs to
@@ -189,6 +227,46 @@ function parseCtxFromCmd(cmd) {
189
227
  const value = Number(match[1]);
190
228
  return Number.isFinite(value) && value > 0 ? value : undefined;
191
229
  }
230
+ /**
231
+ * Parses an explicit `--parallel N` / `-np N` (or `--parallel=N`) out of a launch command.
232
+ * Returns `undefined` when the command states none, including `-np -1` (llama.cpp's "pick
233
+ * automatically"), because a llama-server started without it chooses the slot count itself
234
+ * (4 on the build this was measured on) and only `/props`'s `total_slots` can say what it chose.
235
+ */
236
+ function parseSlotsFromCmd(cmd) {
237
+ if (typeof cmd !== 'string')
238
+ return undefined;
239
+ const match = /(?:^|\s)(?:-np|--parallel)(?:\s+|=)(\d+)(?=\s|$)/.exec(cmd);
240
+ if (!match)
241
+ return undefined;
242
+ const value = Number(match[1]);
243
+ return Number.isFinite(value) && value > 0 ? value : undefined;
244
+ }
245
+ /** The `/props` answer for the launch command a model was last probed under, by host and model id. */
246
+ const propsByCmd = new Map();
247
+ /** Forget what was probed (tests only: a changed launch command already probes again). */
248
+ export function clearPropsProbeCache() {
249
+ propsByCmd.clear();
250
+ }
251
+ /**
252
+ * `/props` for one loaded model, but only once per launch command: the probe refreshes the model's
253
+ * last-use time in llama-swap, so repeating it on every rediscovery sweep would stop a model with a
254
+ * `ttl` from ever unloading. A changed `cmd` (a reloaded or reconfigured model) probes again; a
255
+ * `cmd` that is not a string cannot be told apart, so it is probed each time; a probe that did not
256
+ * answer (network error, timeout) is not remembered; a non-2xx status is. Held in memory only, so a restart probes once more.
257
+ */
258
+ async function propsForLaunch(host, modelId, cmd, headers) {
259
+ const key = `${host.baseUrl}\0${modelId}`;
260
+ if (typeof cmd === 'string') {
261
+ const cached = propsByCmd.get(key);
262
+ if (cached?.cmd === cmd)
263
+ return cached.props;
264
+ }
265
+ const props = await fetchServerProps(host, modelId, headers);
266
+ if (props && typeof cmd === 'string')
267
+ propsByCmd.set(key, { cmd, props });
268
+ return props;
269
+ }
192
270
  async function discoverModels(host) {
193
271
  const headers = authHeaders(host);
194
272
  const res = await webviewFetch(new URL(`${host.baseUrl.replace(/\/+$/, '')}/v1/models`), {
@@ -214,6 +292,7 @@ async function discoverModels(host) {
214
292
  // fetchContextLength must only ever run against a model this server itself calls loaded.
215
293
  const hasStatusField = entries.some((m) => m && typeof m === 'object' && 'status' in m);
216
294
  const contextLengths = {};
295
+ const slotCounts = {};
217
296
  if (hasStatusField) {
218
297
  const loadedIds = entries
219
298
  .filter((m) => m.status && typeof m.status === 'object' && m.status.value === 'loaded')
@@ -227,14 +306,35 @@ async function discoverModels(host) {
227
306
  const swapStatus = await getLlamaSwapStatus(host);
228
307
  const cmdById = new Map(swapStatus.running.map((r) => [r.model, r.cmd]));
229
308
  for (const id of loadedIds) {
230
- const fromCmd = parseCtxFromCmd(cmdById.get(id));
231
- const ctx = fromCmd ?? (await fetchContextLength(host, id, headers));
309
+ const cmd = cmdById.get(id);
310
+ let ctx = parseCtxFromCmd(cmd);
311
+ const cmdSlots = parseSlotsFromCmd(cmd);
312
+ let slots = cmdSlots;
313
+ // /props is only needed for whatever the launch command did not answer. The context
314
+ // still comes from the command whenever it states one (see fetchServerProps's doc
315
+ // comment for why /props's n_ctx is only a fallback); the slot count usually cannot
316
+ // come from the command at all, since an unset --parallel is chosen by the server.
317
+ if (ctx === undefined || slots === undefined) {
318
+ const props = await propsForLaunch(host, id, cmd, headers);
319
+ const ctxFromProps = ctx === undefined && props?.nCtx !== undefined;
320
+ ctx ??= props?.nCtx;
321
+ slots ??= props?.totalSlots;
322
+ // With an explicit --parallel N, llama.cpp reports /props's n_ctx PER SLOT (the unified
323
+ // KV pool is only the default when the count is auto), so it is already the figure one
324
+ // request can use, so the count is 1: recording N would divide it a second time, and
325
+ // recording nothing would leave an earlier round's count in place (applyDiscoveredModels
326
+ // merges onto what was learned before).
327
+ if (cmdSlots !== undefined && ctxFromProps)
328
+ slots = 1;
329
+ }
232
330
  if (ctx !== undefined)
233
331
  contextLengths[id] = ctx;
332
+ if (slots !== undefined)
333
+ slotCounts[id] = slots;
234
334
  }
235
335
  }
236
336
  }
237
- return { models, contextLengths, sizesGB };
337
+ return { models, contextLengths, slotCounts, sizesGB };
238
338
  }
239
339
  /**
240
340
  * undici reports every network failure as `TypeError('fetch failed', { cause })`, with the
@@ -506,7 +606,7 @@ export function triggerLlamaSwapLoad(host, modelId) {
506
606
  });
507
607
  }
508
608
  function applyDiscoveredModels(host, result) {
509
- const { models, contextLengths, sizesGB } = result;
609
+ const { models, contextLengths, slotCounts, sizesGB } = result;
510
610
  const defaultModelId = host.defaultModelId && models.includes(host.defaultModelId) ? host.defaultModelId : undefined;
511
611
  // Merge onto what's already known rather than replacing: a model not probed this round
512
612
  // (not currently loaded) keeps whatever context length an earlier round already learned
@@ -514,6 +614,10 @@ function applyDiscoveredModels(host, result) {
514
614
  const merged = { ...host.modelContextLengths, ...contextLengths };
515
615
  const kept = Object.fromEntries(Object.entries(merged).filter(([id]) => models.includes(id)));
516
616
  const modelContextLengths = Object.keys(kept).length > 0 ? kept : undefined;
617
+ // Slot counts follow the same loaded-only, keep-what-an-earlier-round-learned rule.
618
+ const mergedSlots = { ...host.modelSlotCounts, ...slotCounts };
619
+ const keptSlots = Object.fromEntries(Object.entries(mergedSlots).filter(([id]) => models.includes(id)));
620
+ const modelSlotCounts = Object.keys(keptSlots).length > 0 ? keptSlots : undefined;
517
621
  // sizesGB, unlike contextLengths, is populated for every model in the SAME pass (no
518
622
  // loaded-only restriction — see parseSizeGB), so this is closer to a plain replace, but
519
623
  // still merges onto the previous round rather than dropping a size for a model whose
@@ -526,6 +630,7 @@ function applyDiscoveredModels(host, result) {
526
630
  models,
527
631
  defaultModelId,
528
632
  modelContextLengths,
633
+ modelSlotCounts,
529
634
  modelSizesGB,
530
635
  lastDiscoveredAt: new Date().toISOString(),
531
636
  };