synabun 2.0.0 → 2.0.7

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 (507) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/NOTICE +5 -0
  3. package/README.md +2 -1
  4. package/THIRD-PARTY-LICENSES.md +59 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +153 -18
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +26 -3
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/child-path.js +303 -0
  14. package/lib/claude-hooks.js +2 -1
  15. package/lib/data-home-migration.js +2 -0
  16. package/lib/desktop-app.js +265 -0
  17. package/lib/external-tools.js +7 -3
  18. package/lib/log-redact.js +102 -0
  19. package/lib/packaged-runtime.js +138 -11
  20. package/lib/start-launcher.js +88 -1
  21. package/mcp-server/dist/http.d.ts +3 -1
  22. package/mcp-server/dist/http.d.ts.map +1 -1
  23. package/mcp-server/dist/http.js +15 -4
  24. package/mcp-server/dist/http.js.map +1 -1
  25. package/mcp-server/dist/index.d.ts.map +1 -1
  26. package/mcp-server/dist/index.js +9 -6
  27. package/mcp-server/dist/index.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  33. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  34. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  35. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  36. package/mcp-server/dist/services/identity.d.ts +11 -0
  37. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  38. package/mcp-server/dist/services/identity.js +24 -0
  39. package/mcp-server/dist/services/identity.js.map +1 -1
  40. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  41. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  42. package/mcp-server/dist/services/neural-interface.js +34 -2
  43. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  44. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  45. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  46. package/mcp-server/dist/services/typesafe-config.js +4 -1
  47. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  48. package/mcp-server/dist/tools/agents.d.ts +10 -7
  49. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  50. package/mcp-server/dist/tools/agents.js +34 -13
  51. package/mcp-server/dist/tools/agents.js.map +1 -1
  52. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  53. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  54. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  55. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  56. package/mcp-server/dist/tui.js +2 -1
  57. package/mcp-server/dist/tui.js.map +1 -1
  58. package/mcp-server/dist/version.d.ts +5 -0
  59. package/mcp-server/dist/version.d.ts.map +1 -0
  60. package/mcp-server/dist/version.js +21 -0
  61. package/mcp-server/dist/version.js.map +1 -0
  62. package/mcp-server/package-lock.json +2 -2
  63. package/mcp-server/package.json +1 -3
  64. package/mcp-server/run.mjs +15 -9
  65. package/mcp-server/src/http.ts +18 -3
  66. package/mcp-server/src/index.ts +9 -6
  67. package/mcp-server/src/services/budget-judgments.ts +183 -0
  68. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  69. package/mcp-server/src/services/identity.ts +27 -0
  70. package/mcp-server/src/services/neural-interface.ts +34 -2
  71. package/mcp-server/src/services/typesafe-config.ts +4 -1
  72. package/mcp-server/src/tools/agents.ts +33 -14
  73. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  74. package/mcp-server/src/tui.ts +2 -1
  75. package/mcp-server/src/version.ts +22 -0
  76. package/neural-interface/i18n/en.json +2126 -209
  77. package/neural-interface/i18n/pt-BR.json +2123 -207
  78. package/neural-interface/lib/account-usage.js +163 -0
  79. package/neural-interface/lib/assistant-api.js +316 -57
  80. package/neural-interface/lib/assistant-brains/claude.js +19 -2
  81. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  82. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  83. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  84. package/neural-interface/lib/assistant-budget-split.js +408 -0
  85. package/neural-interface/lib/assistant-budget.js +94 -95
  86. package/neural-interface/lib/assistant-catalog.js +179 -27
  87. package/neural-interface/lib/assistant-config.js +91 -14
  88. package/neural-interface/lib/assistant-dispatch.js +494 -138
  89. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  90. package/neural-interface/lib/assistant-jev.js +26 -3
  91. package/neural-interface/lib/assistant-memory.js +24 -10
  92. package/neural-interface/lib/assistant-persona.js +73 -12
  93. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  94. package/neural-interface/lib/assistant-pricing.js +21 -6
  95. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  96. package/neural-interface/lib/assistant-route-gate.js +19 -3
  97. package/neural-interface/lib/assistant-router.js +373 -42
  98. package/neural-interface/lib/assistant-runtime.js +488 -59
  99. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  100. package/neural-interface/lib/assistant-test-density.js +142 -0
  101. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  102. package/neural-interface/lib/assistant-user-context.js +908 -0
  103. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  104. package/neural-interface/lib/assistant-user-learn.js +404 -0
  105. package/neural-interface/lib/assistant-user-threads.js +226 -0
  106. package/neural-interface/lib/assistant-user-writer.js +183 -0
  107. package/neural-interface/lib/augmented-path.js +317 -53
  108. package/neural-interface/lib/browser-background-tab.js +134 -0
  109. package/neural-interface/lib/browser-screenshot.js +678 -61
  110. package/neural-interface/lib/claude-accounts.js +342 -31
  111. package/neural-interface/lib/claude-agent-bridge.js +123 -9
  112. package/neural-interface/lib/claude-executable.js +6 -1
  113. package/neural-interface/lib/claude-model-catalog.js +33 -5
  114. package/neural-interface/lib/claude-panel-auth.js +221 -0
  115. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  116. package/neural-interface/lib/claude-temporary.js +46 -7
  117. package/neural-interface/lib/cli-install-api.js +137 -0
  118. package/neural-interface/lib/cli-install.js +797 -0
  119. package/neural-interface/lib/cli-path.js +130 -0
  120. package/neural-interface/lib/cli-resolver.js +744 -0
  121. package/neural-interface/lib/cli-update-planner.js +143 -7
  122. package/neural-interface/lib/cloudflared/install.js +533 -0
  123. package/neural-interface/lib/cloudflared/manager.js +545 -0
  124. package/neural-interface/lib/cloudflared/pins.js +115 -0
  125. package/neural-interface/lib/cloudflared/pins.json +63 -0
  126. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  127. package/neural-interface/lib/codex-account-auth.js +178 -0
  128. package/neural-interface/lib/codex-accounts.js +489 -0
  129. package/neural-interface/lib/codex-capabilities.js +2 -1
  130. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  131. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  132. package/neural-interface/lib/codex-panel-auth.js +62 -0
  133. package/neural-interface/lib/codex-session-settings.js +1 -1
  134. package/neural-interface/lib/codex-settings-schema.js +120 -0
  135. package/neural-interface/lib/desktop/config.js +3 -0
  136. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  137. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  138. package/neural-interface/lib/editor-completion.js +714 -0
  139. package/neural-interface/lib/editor-config.js +149 -0
  140. package/neural-interface/lib/editor-files-api.js +268 -0
  141. package/neural-interface/lib/editor-git-api.js +134 -0
  142. package/neural-interface/lib/file-explorer-api.js +1039 -0
  143. package/neural-interface/lib/file-explorer-system.js +355 -0
  144. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  145. package/neural-interface/lib/file-explorer-watch.js +239 -0
  146. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  147. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  148. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  149. package/neural-interface/lib/git-bash/install.js +529 -0
  150. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  151. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  152. package/neural-interface/lib/git-bash/manager.js +268 -0
  153. package/neural-interface/lib/git-bash/pins.js +206 -0
  154. package/neural-interface/lib/git-bash/pins.json +34 -0
  155. package/neural-interface/lib/git-bash/resolver.js +194 -0
  156. package/neural-interface/lib/git-diff-api.js +103 -0
  157. package/neural-interface/lib/git-read.js +234 -0
  158. package/neural-interface/lib/greeting/api.js +89 -0
  159. package/neural-interface/lib/greeting/budgets.js +90 -0
  160. package/neural-interface/lib/greeting/engine.js +461 -0
  161. package/neural-interface/lib/greeting/facts.js +320 -0
  162. package/neural-interface/lib/greeting/sources.js +252 -0
  163. package/neural-interface/lib/greeting/writer.js +139 -0
  164. package/neural-interface/lib/http-guards.js +189 -13
  165. package/neural-interface/lib/install-details.js +145 -0
  166. package/neural-interface/lib/machine-names.js +88 -0
  167. package/neural-interface/lib/mcp-installer.js +14 -2
  168. package/neural-interface/lib/memory-api.js +31 -7
  169. package/neural-interface/lib/native-loop-providers.js +26 -8
  170. package/neural-interface/lib/native-loop-runtime.js +9 -4
  171. package/neural-interface/lib/os-path.js +319 -0
  172. package/neural-interface/lib/plugin-installer.js +4 -2
  173. package/neural-interface/lib/project-storage.js +5 -7
  174. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  175. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  176. package/neural-interface/lib/pty-host/core.js +2 -1
  177. package/neural-interface/lib/pty-host/manager.js +27 -2
  178. package/neural-interface/lib/remote-policy.js +20 -11
  179. package/neural-interface/lib/request-gate.js +589 -0
  180. package/neural-interface/lib/session-title-generator.js +4 -3
  181. package/neural-interface/lib/system-about.js +340 -0
  182. package/neural-interface/lib/system-activity.js +66 -0
  183. package/neural-interface/lib/terminal-env.js +11 -3
  184. package/neural-interface/lib/terminal-path.js +55 -0
  185. package/neural-interface/lib/ui-blob-api.js +108 -0
  186. package/neural-interface/lib/ui-blob-store.js +68 -0
  187. package/neural-interface/lib/virtual-socket.js +12 -1
  188. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  189. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  190. package/neural-interface/lib/whatsapp/commands.js +13 -14
  191. package/neural-interface/lib/whatsapp/config.js +134 -20
  192. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  193. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  194. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  195. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  196. package/neural-interface/lib/whatsapp/manager.js +92 -32
  197. package/neural-interface/lib/whatsapp/paths.js +11 -1
  198. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  199. package/neural-interface/lib/whatsapp/redact.js +63 -0
  200. package/neural-interface/lib/whatsapp/service.js +343 -91
  201. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  202. package/neural-interface/package-lock.json +701 -2
  203. package/neural-interface/package.json +13 -2
  204. package/neural-interface/public/index.html +2 -0
  205. package/neural-interface/public/index2d.html +2 -0
  206. package/neural-interface/public/install-app.html +9 -6
  207. package/neural-interface/public/offline.html +19 -3
  208. package/neural-interface/public/onboarding.html +107 -2
  209. package/neural-interface/public/shared/account-i18n.js +7 -0
  210. package/neural-interface/public/shared/account-menu.js +539 -0
  211. package/neural-interface/public/shared/account-usage.js +19 -0
  212. package/neural-interface/public/shared/account-view.js +335 -0
  213. package/neural-interface/public/shared/agent-runtime-options.js +50 -14
  214. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  215. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  216. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  217. package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
  218. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  219. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  220. package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
  221. package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
  222. package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
  223. package/neural-interface/public/shared/assistant/asst-render.js +197 -23
  224. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  225. package/neural-interface/public/shared/assistant/asst-route.js +95 -343
  226. package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
  227. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  228. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  229. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  230. package/neural-interface/public/shared/assistant/asst-state.js +186 -2
  231. package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
  232. package/neural-interface/public/shared/blob-store.js +111 -22
  233. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  234. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  235. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  236. package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
  237. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  238. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  239. package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
  240. package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
  241. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  242. package/neural-interface/public/shared/claude-account-view.js +3 -0
  243. package/neural-interface/public/shared/cli-install-view.js +392 -0
  244. package/neural-interface/public/shared/cli-status.js +282 -22
  245. package/neural-interface/public/shared/constants.js +6 -1
  246. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  247. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  248. package/neural-interface/public/shared/cp/cp-styles.js +61 -0
  249. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  250. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  251. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  252. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  253. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  254. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  255. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  256. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  257. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  258. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  259. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  260. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  261. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  262. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  263. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  264. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  265. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  266. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  267. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  268. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  269. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  270. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  271. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  272. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  273. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  274. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  275. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  276. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  277. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  278. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  279. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  280. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  281. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  282. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  283. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  284. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  285. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  286. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  287. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  288. package/neural-interface/public/shared/file-links.js +339 -0
  289. package/neural-interface/public/shared/git-bash-view.js +145 -0
  290. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  291. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  292. package/neural-interface/public/shared/html-shell.js +161 -165
  293. package/neural-interface/public/shared/i18n.js +19 -1
  294. package/neural-interface/public/shared/install-details-view.js +86 -0
  295. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  296. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  297. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
  298. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  299. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
  300. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
  301. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  302. package/neural-interface/public/shared/pwa-install-page.js +32 -2
  303. package/neural-interface/public/shared/pwa-install.css +3 -3
  304. package/neural-interface/public/shared/pwa-install.js +100 -1
  305. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  306. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  307. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  308. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  309. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  310. package/neural-interface/public/shared/settings/settings-ia.js +33 -4
  311. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  312. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  313. package/neural-interface/public/shared/start-bridge.js +238 -19
  314. package/neural-interface/public/shared/storage.js +30 -3
  315. package/neural-interface/public/shared/styles.css +1710 -770
  316. package/neural-interface/public/shared/synabun-mascot.js +68 -58
  317. package/neural-interface/public/shared/tunnel-view.js +167 -0
  318. package/neural-interface/public/shared/ui-claude-panel.js +369 -229
  319. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  320. package/neural-interface/public/shared/ui-detail.js +15 -0
  321. package/neural-interface/public/shared/ui-explorer.js +22 -0
  322. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  323. package/neural-interface/public/shared/ui-file-links.js +103 -0
  324. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  325. package/neural-interface/public/shared/ui-help.js +26 -0
  326. package/neural-interface/public/shared/ui-invite.js +126 -116
  327. package/neural-interface/public/shared/ui-judgments.js +10 -1
  328. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  329. package/neural-interface/public/shared/ui-loading.js +187 -19
  330. package/neural-interface/public/shared/ui-menubar.js +25 -33
  331. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  332. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  333. package/neural-interface/public/shared/ui-navbar.js +48 -22
  334. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  335. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  336. package/neural-interface/public/shared/ui-sessions.js +9 -3
  337. package/neural-interface/public/shared/ui-settings.js +509 -490
  338. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  339. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  340. package/neural-interface/public/shared/ui-sync.js +22 -0
  341. package/neural-interface/public/shared/ui-terminal.js +40 -5
  342. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  343. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  344. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  345. package/neural-interface/public/shared/ui-update.js +594 -5
  346. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  347. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  348. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  349. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  350. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  351. package/neural-interface/public/shared/worker-end.js +253 -0
  352. package/neural-interface/public/sw.js +2 -2
  353. package/neural-interface/public/variant/2d/main.js +10 -1
  354. package/neural-interface/public/variant/3d/main.js +10 -1
  355. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  356. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  357. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  358. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  359. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  360. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  361. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  362. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  363. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  364. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  365. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  366. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  367. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  368. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  369. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  370. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  371. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  372. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  373. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  374. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  375. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  376. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  377. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  378. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  379. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  380. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  381. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  382. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  383. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  384. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  385. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  386. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  387. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  388. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  389. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  390. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  391. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  392. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  393. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  394. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  395. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  396. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  397. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  398. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  399. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  400. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  401. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  402. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  403. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  404. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  405. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  406. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  407. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  408. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  409. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  410. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  411. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  412. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  413. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  414. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  415. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  416. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  417. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  418. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  419. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  420. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  421. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  422. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  423. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  424. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  425. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  426. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  427. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  428. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  429. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  430. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  431. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  432. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  433. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  434. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  435. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  436. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  437. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  438. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  439. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  440. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  441. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  442. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  443. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  444. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  445. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  446. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  447. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  448. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  449. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  450. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  451. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  452. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  453. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  454. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  455. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  456. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  457. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  458. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  459. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  460. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  461. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  462. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  463. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  464. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  465. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  466. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  467. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  468. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  469. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  470. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  471. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  472. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  473. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  474. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  475. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  476. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  477. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  478. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  479. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  480. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  481. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  482. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  483. package/neural-interface/scripts/build-vendor.mjs +78 -2
  484. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  485. package/neural-interface/server.js +2085 -1629
  486. package/neural-interface/templates/rulesets/README.md +1 -1
  487. package/package.json +5 -8
  488. package/setup.js +53 -15
  489. package/benchmarks/browser-live-results.json +0 -79
  490. package/benchmarks/browser-v2-results.json +0 -258
  491. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  492. package/neural-interface/public/category-logos/claude.svg +0 -1
  493. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  494. package/neural-interface/public/category-logos/openai.svg +0 -1
  495. package/neural-interface/public/category-logos/synabun.png +0 -0
  496. package/neural-interface/public/claude.svg +0 -1
  497. package/neural-interface/public/gallery.svg +0 -6
  498. package/neural-interface/public/leonardoai.svg +0 -1
  499. package/neural-interface/public/mcp.svg +0 -4
  500. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  501. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  502. package/neural-interface/public/sessionicon.svg +0 -8
  503. package/neural-interface/public/toolbar-album.svg +0 -8
  504. package/neural-interface/public/toolbar-question.svg +0 -6
  505. package/neural-interface/public/toolbar-sessions.svg +0 -6
  506. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  507. package/skills/synabun/synabunicon.png +0 -0
@@ -12,18 +12,71 @@
12
12
  // Matching (lib/assistant-catalog.js hiddenModelId): the id, or the row's
13
13
  // resolved model at the same context window, case-insensitive; context
14
14
  // variants ("opus" / "opus[1m]", "gpt-x" / "gpt-x[extended]") hide
15
- // independently. OpenCode ids match full or bare. Re-read when the file's
16
- // mtime changes; writes are atomic.
15
+ // independently. OpenCode ids match full or bare. Re-read when the file
16
+ // changes (time, size or inode); writes are atomic.
17
+ //
18
+ // Every save is one read-modify-write of the file as it is on disk at that
19
+ // moment, alone among SynaBun's writers (two servers can share a data home):
20
+ // an OS-held mutex next to the data (hiddenModelsLockPath; an empty SQLite file,
21
+ // lib/rulesets/lock.js) and, right before the new file takes the old one's
22
+ // place, a second look at what is there. A document someone saved in between (a
23
+ // hand edit, an older build, which take no mutex) is the newer one: the change
24
+ // is made again on it, and an automatic one (the pin migration below) that
25
+ // cannot be made that way is not saved at all.
26
+ //
27
+ // A hidden alias hides a model, not a name. "haiku", "opus", "default" name
28
+ // whatever Claude Code points them at, and a CLI update moves them. So an alias
29
+ // entry carries a pin: the model and context window it named when the user hid
30
+ // it, its label and the time (`pins` in the file, next to `providers`; a build
31
+ // that knows nothing of pins still reads the lists). Each catalog build settles
32
+ // the pins against the rows the provider lists (reconcile → planHiddenPins):
33
+ // the alias still names the pinned model → hidden, as before
34
+ // the alias names another model → the alias is shown again and the
35
+ // pinned model stays hidden under
36
+ // its own id (the entry is rewritten)
37
+ // an alias entry with no pin (written by → pinned to what the alias names
38
+ // an older build, or hidden while the now, marked `inferred`
39
+ // list was not a verified one)
40
+ // an entry already rewritten, whose model → the entry takes the id the model is
41
+ // is listed under another id listed by (it was absent, or respelled)
42
+ // A pinned entry hides its own row only. What an alias names comes from the
43
+ // row's `upstream`, which is the `resolvedModel` the Claude Code CLI reports
44
+ // for it. Nothing is decided without it: while the list is the static
45
+ // stand-in, the provider does not answer, the alias is not listed, or the CLI
46
+ // names no versioned model behind it, an entry keeps hiding the alias by name.
17
47
 
18
- import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
19
- import { dirname } from 'node:path';
48
+ import { createHash } from 'node:crypto';
49
+ import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
50
+ import { basename, dirname, join } from 'node:path';
20
51
  import { Router } from 'express';
21
- import { CATALOG_PROVIDERS, baseModelId, canonicalModelId, contextSuffix } from './assistant-catalog.js';
52
+ import { CATALOG_PROVIDERS, aliasTargetOf, baseModelId, canonicalModelId, contextSuffix, contextVariantOf, modelIdentity, pinnedModelId, sameModelPin } from './assistant-catalog.js';
22
53
  import { HIDDEN_MODELS_SYNC_TYPE as OPENCODE_SYNC_TYPE } from './opencode-hidden-models.js';
54
+ import { createWriterLock } from './rulesets/lock.js';
23
55
 
24
56
  export const ASSISTANT_HIDDEN_SYNC_TYPE = 'assistant:hidden-models-changed';
25
57
  export const OWN_PROVIDERS = Object.freeze(['claude-code', 'codex']);
26
58
  export const MAX_HIDDEN_PER_PROVIDER = 2000;
59
+ /** How long a save waits for another process that is saving. */
60
+ export const HIDDEN_LOCK_TIMEOUT_MS = 2000;
61
+ /** The same for the pin migration, which runs inside a catalog build and is made again by the next one. */
62
+ export const HIDDEN_MIGRATION_WAIT_MS = 250;
63
+ /** How many times one save is made again on a file that changed under it before it gives up. */
64
+ export const HIDDEN_SAVE_ATTEMPTS = 3;
65
+
66
+ const FILE_IO = Object.freeze({ mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync });
67
+ const NO_FILE = 'absent';
68
+
69
+ /**
70
+ * The mutex that goes with a hidden-models file: <data home>/runtime/<name>.mutex
71
+ * for <data home>/data/<name>.json, outside data/ so no backup or restore ever
72
+ * opens it (see lib/rulesets/lock.js on why nothing may), and beside the file
73
+ * for any other layout.
74
+ */
75
+ export function hiddenModelsLockPath(file) {
76
+ const dir = dirname(file);
77
+ const name = `${basename(file).replace(/\.json$/i, '')}.mutex`;
78
+ return basename(dir) === 'data' ? join(dirname(dir), 'runtime', name) : join(dir, name);
79
+ }
27
80
 
28
81
  function hiddenError(code, message, status = 400, extra = {}) {
29
82
  const error = new Error(message);
@@ -46,66 +99,266 @@ export function normalizeModelIds(list) {
46
99
  return [...out.values()].sort();
47
100
  }
48
101
 
49
- /** The Claude Code / Codex store: { version, updatedAt, providers: { 'claude-code': [], codex: [] } }. */
50
- export function createAssistantHiddenModelsStore({ path, now = Date.now, log = () => {} } = {}) {
102
+ const text = (value, max = 300) => (typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null);
103
+
104
+ /** One pin record, or null when it names no model: { model, variant, label, at, alias?, movedAt?, inferred? }. */
105
+ export function normalizePin(raw) {
106
+ if (!raw || typeof raw !== 'object') return null;
107
+ const model = text(baseModelId(text(raw.model) || ''))?.toLowerCase() || null;
108
+ if (!model) return null;
109
+ const variant = contextVariantOf(`x[${String(raw.variant || '').toLowerCase()}]`);
110
+ const pin = { model, variant, label: text(raw.label, 200), at: text(raw.at, 40) };
111
+ if (text(raw.alias)) pin.alias = text(raw.alias);
112
+ if (text(raw.movedAt, 40)) pin.movedAt = text(raw.movedAt, 40);
113
+ if (raw.inferred === true) pin.inferred = true;
114
+ return pin;
115
+ }
116
+
117
+ /** The pins of the ids in `ids`, keyed by lower-case id; anything else is dropped. */
118
+ export function normalizePins(raw, ids) {
119
+ const out = {};
120
+ if (!raw || typeof raw !== 'object') return out;
121
+ const source = new Map(Object.entries(raw).map(([key, value]) => [String(key).toLowerCase(), value]));
122
+ for (const key of arr(ids).map((id) => String(id).toLowerCase()).sort()) {
123
+ const pin = normalizePin(source.get(key));
124
+ if (pin) out[key] = pin;
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /** The pin to record when `row` is hidden: what the alias names now, with its label. null for a row that is its own model. */
130
+ export function pinForRow(row) {
131
+ const target = aliasTargetOf(row);
132
+ return target ? { ...target, label: text(row.label, 200) } : null;
133
+ }
134
+
135
+ /**
136
+ * Settle one provider's hidden ids against the rows it lists (pure).
137
+ * ids, pins the stored list and its pin records
138
+ * rows every row of the provider, hidden ones included
139
+ * source how the list was obtained; only 'live' decides anything
140
+ * at the time to record (ISO)
141
+ * → { ids, pins, changed, moved: [{ alias, from, to, listed }], pinned: [ids] }
142
+ */
143
+ export function planHiddenPins({ ids = [], pins = {}, rows = [], source = null, at = null } = {}) {
144
+ const live = source === 'live';
145
+ const list = arr(rows).filter((row) => row?.id);
146
+ const byId = new Map(list.map((row) => [String(row.id).toLowerCase(), row]));
147
+ const current = normalizePins(pins, ids);
148
+ const nextIds = [];
149
+ const nextPins = {};
150
+ const seen = new Set();
151
+ const keep = (id, pin) => {
152
+ const key = String(id).toLowerCase();
153
+ if (seen.has(key)) return false;
154
+ seen.add(key);
155
+ nextIds.push(id);
156
+ if (pin) nextPins[key] = pin;
157
+ return true;
158
+ };
159
+ const moved = [];
160
+ const pinned = [];
161
+ const movedOn = [];
162
+ const adrift = [];
163
+ // The row that is the model a pin names, under the id the provider lists it by (a snapshot date is the same model).
164
+ const ownRowOf = (pin) => list.find((row) => !aliasTargetOf(row) && sameModelPin(pin, { model: baseModelId(row.id), variant: contextVariantOf(row.id) })) || null;
165
+ for (const id of arr(ids)) {
166
+ const key = String(id).toLowerCase();
167
+ const pin = current[key] || null;
168
+ const row = byId.get(key) || null;
169
+ // An entry already rewritten to its model's own id (`alias` says from where) is that model, never an
170
+ // alias again. It hides the model whatever id the provider lists it by today: with no row under the
171
+ // stored id it is settled below, by model.
172
+ if (live && pin?.alias) { if (row) keep(id, pin); else adrift.push({ id, pin }); continue; }
173
+ const target = live && row ? aliasTargetOf(row) : null;
174
+ // Not an alias with a model named behind it, or nothing verified to read it from: as it is.
175
+ if (!target) { keep(id, pin); continue; }
176
+ if (!pin) {
177
+ // Written before pins existed: it hides what the alias names now, as it did until this moment.
178
+ keep(id, { ...target, label: text(row.label, 200), at, inferred: true });
179
+ pinned.push(id);
180
+ continue;
181
+ }
182
+ if (sameModelPin(pin, target)) { keep(id, pin); continue; }
183
+ movedOn.push({ id, pin, target });
184
+ }
185
+ // After every entry that stays, so an entry the user made for the old model keeps its own meaning.
186
+ // The model is listed again, or under another id (it was absent when its alias moved on, or the provider
187
+ // respelled it): the entry follows it. Not listed at all: it waits under the id it has.
188
+ for (const { id, pin } of adrift) {
189
+ const own = ownRowOf(pin);
190
+ keep(own ? String(own.id) : id, pin);
191
+ }
192
+ for (const { id, pin, target } of movedOn) {
193
+ const own = ownRowOf(pin);
194
+ const concrete = own ? String(own.id) : pinnedModelId(pin);
195
+ keep(concrete, { model: pin.model, variant: pin.variant, label: pin.label, at: pin.at, alias: String(id), movedAt: at });
196
+ moved.push({ alias: String(id), from: concrete, to: pinnedModelId(target), listed: !!own });
197
+ }
198
+ const sameIds = normalizeModelIds(nextIds).join('\n') === normalizeModelIds(ids).join('\n');
199
+ const finalPins = normalizePins(nextPins, nextIds);
200
+ const changed = !sameIds || JSON.stringify(finalPins) !== JSON.stringify(normalizePins(pins, ids));
201
+ return { ids: normalizeModelIds(nextIds), pins: finalPins, changed, moved, pinned };
202
+ }
203
+
204
+ /**
205
+ * The Claude Code / Codex store:
206
+ * { version, updatedAt, providers: { 'claude-code': [], codex: [] }, pins: { 'claude-code': { [id]: pin }, codex: {} } }.
207
+ */
208
+ export function createAssistantHiddenModelsStore({
209
+ path, now = Date.now, log = () => {},
210
+ lockPath = path ? hiddenModelsLockPath(path) : null, lockTimeoutMs = HIDDEN_LOCK_TIMEOUT_MS,
211
+ // The file calls (node:fs by default): a seam for tests that save between two of them.
212
+ fs: io = FILE_IO,
213
+ } = {}) {
51
214
  if (!path) throw new Error('createAssistantHiddenModelsStore requires path');
52
215
  const emptyProviders = () => Object.fromEntries(OWN_PROVIDERS.map((p) => [p, []]));
53
- const empty = Object.freeze({ providers: emptyProviders(), version: 0, updatedAt: null });
54
- let cached = null; // { mtimeMs, value }
216
+ const emptyPins = () => Object.fromEntries(OWN_PROVIDERS.map((p) => [p, {}]));
217
+ const empty = Object.freeze({ providers: emptyProviders(), pins: emptyPins(), version: 0, updatedAt: null });
218
+ let cached = null; // { stamp, value }
55
219
  const listeners = new Set();
56
220
 
57
- function mtime() { try { return statSync(path).mtimeMs; } catch { return 0; } }
58
- function read() {
59
- const m = mtime();
60
- if (cached && cached.mtimeMs === m) return cached.value;
61
- let value = empty;
62
- if (m) {
63
- try {
64
- const raw = JSON.parse(readFileSync(path, 'utf8')) || {};
65
- const providers = emptyProviders();
66
- for (const p of OWN_PROVIDERS) providers[p] = normalizeModelIds(raw.providers?.[p]);
67
- value = { providers, version: Number(raw.version) || 0, updatedAt: raw.updatedAt || null };
68
- } catch (error) { log('hidden-models:unreadable', error?.message || String(error)); value = cached?.value || empty; }
221
+ /** Which file is there, by stat alone: '' when there is none. An atomic save is a new inode even inside one clock tick. */
222
+ function stamp() { try { const info = io.statSync(path); return `${info.mtimeMs}:${info.size}:${info.ino}`; } catch { return ''; } }
223
+ /** The revision of the file on disk: a digest of its bytes, NO_FILE when there is none. */
224
+ function revisionOf(raw) { return createHash('sha256').update(raw).digest('hex'); }
225
+ function revisionOnDisk() {
226
+ try { return revisionOf(io.readFileSync(path, 'utf8')); }
227
+ catch (error) { return error?.code === 'ENOENT' ? NO_FILE : `unreadable:${error?.code || 'read'}`; }
228
+ }
229
+ /** The document on disk, never the copy in memory: { value, rev, readable } (`readable` false = it is there and does not parse). */
230
+ function load() {
231
+ let raw;
232
+ try { raw = io.readFileSync(path, 'utf8'); }
233
+ catch (error) {
234
+ if (error?.code === 'ENOENT') return { value: empty, rev: NO_FILE, readable: true };
235
+ log('hidden-models:unreadable', error?.message || String(error));
236
+ return { value: cached?.value || empty, rev: `unreadable:${error?.code || 'read'}`, readable: false };
237
+ }
238
+ try {
239
+ const doc = JSON.parse(raw) || {};
240
+ const providers = emptyProviders();
241
+ const pins = emptyPins();
242
+ for (const p of OWN_PROVIDERS) {
243
+ providers[p] = normalizeModelIds(doc.providers?.[p]);
244
+ pins[p] = normalizePins(doc.pins?.[p], providers[p]);
245
+ }
246
+ return { value: { providers, pins, version: Number(doc.version) || 0, updatedAt: doc.updatedAt || null }, rev: revisionOf(raw), readable: true };
247
+ } catch (error) {
248
+ log('hidden-models:unreadable', error?.message || String(error));
249
+ return { value: cached?.value || empty, rev: revisionOf(raw), readable: false };
69
250
  }
70
- cached = { mtimeMs: m, value };
251
+ }
252
+ function read() {
253
+ const s = stamp();
254
+ if (cached && cached.stamp === s) return cached.value;
255
+ const value = s ? load().value : empty;
256
+ cached = { stamp: s, value };
71
257
  return value;
72
258
  }
73
259
  function list(provider) { return read().providers[provider] || []; }
260
+ function pins(provider) { return read().pins?.[provider] || {}; }
261
+
262
+ /** Run `fn` as the only SynaBun writer of the file. Throws (nothing was changed) when the mutex cannot be had within `waitMs`. */
263
+ function alone(fn, waitMs = lockTimeoutMs) {
264
+ const lock = createWriterLock(lockPath, { timeoutMs: Math.min(lockTimeoutMs, waitMs) });
265
+ try { lock.enter(); } catch (error) {
266
+ lock.close();
267
+ if (error?.code === 'BUSY') throw hiddenError('HIDDEN_BUSY', 'Another SynaBun process is saving the hidden-models list right now, so nothing was changed. Try again in a moment.', 503, { retryable: true });
268
+ throw hiddenError('HIDDEN_LOCK_UNAVAILABLE', `SynaBun could not lock the hidden-models list (${lockPath}): ${error?.cause?.message || error?.message || error}. Nothing was changed.`, 503);
269
+ }
270
+ // One connection per save, closed with it: nothing stays open on the mutex between two saves.
271
+ try { return fn(); } finally { lock.leave(); lock.close(); }
272
+ }
74
273
 
75
- /** Replace one provider's list. Returns { providers, version, updatedAt, changed }. */
76
- function set(provider, ids) {
274
+ /**
275
+ * One read-modify-write of one provider's list. `decide(document)` gets the document as it is
276
+ * on disk at that moment and answers { ids, pins? } (`pins` default: the ones the provider has; a
277
+ * pin whose id left the list goes with it) or null for no change. It runs again when the file
278
+ * changed before the new one could take its place, so it must be a function of the document it
279
+ * is given. Nothing is saved over a file that is there and does not parse (someone is in the
280
+ * middle of saving it, or it needs a hand). `reason` reaches the listeners; `waitMs` shortens
281
+ * the wait for another process that is saving (the server's event loop stands still meanwhile).
282
+ * Returns { providers, pins, version, updatedAt, changed }.
283
+ */
284
+ function change(provider, decide, { reason = null, waitMs = lockTimeoutMs } = {}) {
77
285
  if (!OWN_PROVIDERS.includes(provider)) throw hiddenError('PROVIDER_INVALID', `provider must be one of ${OWN_PROVIDERS.join(', ')}`);
78
- const current = read();
79
- const nextList = normalizeModelIds(ids);
80
- const changed = current.providers[provider].join('\n') !== nextList.join('\n');
81
- if (!changed) return { ...current, changed: false };
82
- const next = { version: current.version + 1, updatedAt: new Date(now()).toISOString(), providers: { ...current.providers, [provider]: nextList } };
83
- mkdirSync(dirname(path), { recursive: true });
84
- const tmp = `${path}.${process.pid}.${now()}.tmp`;
85
- writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', 'utf8');
86
- renameSync(tmp, path);
87
- cached = { mtimeMs: mtime(), value: next };
88
- for (const listener of listeners) { try { listener(next, { provider, changed: true }); } catch {} }
89
- return { ...next, changed: true };
286
+ const saved = alone(() => {
287
+ for (let attempt = 1; ; attempt += 1) {
288
+ const found = load();
289
+ if (!found.readable) throw hiddenError('HIDDEN_UNREADABLE', `The hidden-models file (${path}) could not be read as JSON right now, so it was left as it is. If it stays that way, fix or remove it.`, 409, { retryable: true });
290
+ const current = found.value;
291
+ const wanted = decide(current);
292
+ const nextList = wanted ? normalizeModelIds(wanted.ids) : current.providers[provider];
293
+ const nextPins = wanted ? normalizePins(wanted.pins === undefined ? current.pins?.[provider] : wanted.pins, nextList) : null;
294
+ const changed = !!wanted && (current.providers[provider].join('\n') !== nextList.join('\n')
295
+ || JSON.stringify(nextPins) !== JSON.stringify(current.pins?.[provider] || {}));
296
+ if (!changed) return { value: current, changed: false };
297
+ const next = {
298
+ version: current.version + 1, updatedAt: new Date(now()).toISOString(),
299
+ providers: { ...current.providers, [provider]: nextList },
300
+ pins: { ...emptyPins(), ...current.pins, [provider]: nextPins },
301
+ };
302
+ io.mkdirSync(dirname(path), { recursive: true });
303
+ const tmp = `${path}.${process.pid}.${now()}.tmp`;
304
+ try {
305
+ io.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', 'utf8');
306
+ // Once more, with the new file ready: a writer outside the mutex (a hand edit, an older build) may
307
+ // have saved since `found` was read. Its document is the newer one and is never replaced by a
308
+ // decision made without it.
309
+ if (revisionOnDisk() !== found.rev) {
310
+ if (attempt >= HIDDEN_SAVE_ATTEMPTS) throw hiddenError('HIDDEN_BUSY', 'The hidden-models file kept changing while SynaBun was saving it, so it was left as it is. Try again in a moment.', 503, { retryable: true });
311
+ continue;
312
+ }
313
+ io.renameSync(tmp, path);
314
+ } finally {
315
+ try { io.rmSync(tmp, { force: true }); } catch {}
316
+ }
317
+ cached = { stamp: stamp(), value: next };
318
+ return { value: next, changed: true };
319
+ }
320
+ }, waitMs);
321
+ // Outside the mutex: a listener may read or save in its turn.
322
+ if (saved.changed) for (const listener of listeners) { try { listener(saved.value, { provider, changed: true, reason }); } catch {} }
323
+ return { ...saved.value, changed: saved.changed };
324
+ }
325
+
326
+ /**
327
+ * Replace one provider's list. `pins` replaces its pin records (default: the ones it has);
328
+ * a pin whose id left the list goes with it. `reason` reaches the listeners.
329
+ * Returns { providers, pins, version, updatedAt, changed }.
330
+ */
331
+ function set(provider, ids, { pins: nextPinsRaw = undefined, reason = null } = {}) {
332
+ return change(provider, () => ({ ids, pins: nextPinsRaw }), { reason });
90
333
  }
91
334
 
92
335
  function onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); }
93
- return { read, list, set, onChange, path };
336
+ return { read, list, pins, set, change, onChange, path, lockPath };
94
337
  }
95
338
 
96
339
  /**
97
340
  * One view over both stores.
98
341
  * list(provider) → ids; all() → { 'claude-code', codex, opencode }
99
- * update(provider, { hide, show }) → { provider, hidden, changed, added, removed }
100
- * onChange(fn) → fn({ provider }) after either store changes
342
+ * pins(provider) → { [id]: pin }; allPins() → the same per provider (OpenCode has none)
343
+ * update(provider, { hide, show, pins }) → { provider, hidden, changed, added, removed };
344
+ * `pins` = { [id]: { model, variant, label } } for the aliases among `hide` (pinForRow)
345
+ * reconcile({ provider, rows, source }) → { ids, pins, moved, pinned } | null: settles the
346
+ * pins against the rows a catalog build lists (planHiddenPins) and saves what changed
347
+ * Both save through the store's `change`: on the document that is on disk at that moment.
348
+ * onChange(fn) → fn({ provider, reason, moved }) after either store changes; reason 'pins' =
349
+ * reconcile's own write (`moved` lists the aliases that were shown again)
101
350
  * OpenCode ids are "provider/model" (its store drops anything else); callers
102
351
  * resolve bare ids first (the router does, from the catalog).
103
352
  */
104
- export function createHiddenModels({ file, opencodeStore = null, now = Date.now, log = () => {} } = {}) {
105
- const own = createAssistantHiddenModelsStore({ path: file, now, log });
353
+ export function createHiddenModels({ file, opencodeStore = null, now = Date.now, log = () => {}, lockPath = undefined, lockTimeoutMs = undefined, fs = undefined } = {}) {
354
+ const own = createAssistantHiddenModelsStore({ path: file, now, log, lockPath, lockTimeoutMs, fs });
106
355
  const listeners = new Set();
107
- const emit = (provider) => { for (const fn of listeners) { try { fn({ provider }); } catch {} } };
108
- own.onChange((_value, meta) => emit(meta?.provider || null));
356
+ let settling = null; // what reconcile is saving right now, for its change event
357
+ const emit = (provider, reason = null) => {
358
+ const event = { provider, reason, moved: reason === 'pins' ? settling?.moved || [] : [] };
359
+ for (const fn of listeners) { try { fn(event); } catch {} }
360
+ };
361
+ own.onChange((_value, meta) => emit(meta?.provider || null, meta?.reason || null));
109
362
  opencodeStore?.onChange?.(() => emit('opencode'));
110
363
 
111
364
  function list(provider) {
@@ -113,29 +366,78 @@ export function createHiddenModels({ file, opencodeStore = null, now = Date.now,
113
366
  return OWN_PROVIDERS.includes(provider) ? own.list(provider) : [];
114
367
  }
115
368
  function all() { return Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, list(p)])); }
369
+ function pins(provider) { return OWN_PROVIDERS.includes(provider) ? own.pins(provider) : {}; }
370
+ function allPins() { return Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, pins(p)])); }
371
+
372
+ function reconcile({ provider, rows = [], source = null } = {}) {
373
+ if (!OWN_PROVIDERS.includes(provider)) return null;
374
+ const at = new Date(now()).toISOString();
375
+ const settle = (ids, pins) => planHiddenPins({ ids, pins, rows, source, at });
376
+ // The look every build takes, from the copy in memory: most have nothing to settle and save nothing.
377
+ let plan = settle(own.list(provider), own.pins(provider));
378
+ if (plan.changed) {
379
+ // Nobody asked for this change, so it is settled again on the document that is on disk when the
380
+ // save happens: an id someone added since this build read the list is part of what gets saved.
381
+ // A list that cannot be saved still applies to this build; the next one settles it again, so
382
+ // a build never waits long for another process.
383
+ try {
384
+ own.change(provider, (doc) => {
385
+ plan = settle(doc.providers[provider], doc.pins?.[provider] || {});
386
+ settling = plan;
387
+ return plan.changed ? { ids: plan.ids, pins: plan.pins } : null;
388
+ }, { reason: 'pins', waitMs: HIDDEN_MIGRATION_WAIT_MS });
389
+ } catch (error) { log('hidden-models:pins-unsaved', error?.message || String(error)); }
390
+ finally { settling = null; }
391
+ for (const m of plan.moved) log('hidden-models:alias-moved', `${provider}: "${m.alias}" now names ${m.to}; ${m.from} stays hidden, "${m.alias}" is shown`);
392
+ }
393
+ return { ids: plan.ids, pins: plan.pins, moved: plan.moved, pinned: plan.pinned };
394
+ }
116
395
 
117
- function update(provider, { hide = [], show = [] } = {}) {
396
+ function update(provider, { hide = [], show = [], pins: hidePins = null } = {}) {
118
397
  if (!CATALOG_PROVIDERS.includes(provider)) throw hiddenError('PROVIDER_INVALID', `provider must be one of ${CATALOG_PROVIDERS.join(', ')}`);
119
398
  if (provider === 'opencode' && !opencodeStore) throw hiddenError('PROVIDER_UNAVAILABLE', 'The OpenCode hidden-model store is not available.', 503);
120
- const current = list(provider);
121
399
  const key = (id) => String(id).trim().toLowerCase();
122
400
  const hideIds = arr(hide).map((id) => String(id || '').trim()).filter(Boolean);
123
401
  const showKeys = new Set(arr(show).map(key).filter(Boolean));
124
- const next = new Map(current.map((id) => [key(id), id]));
125
- for (const id of hideIds) if (!next.has(key(id))) next.set(key(id), id);
126
- for (const k of showKeys) next.delete(k);
127
- const nextList = [...next.values()];
128
- const before = new Set(current.map(key));
129
- const after = new Set(nextList.map(key));
130
- const added = nextList.filter((id) => !before.has(key(id)));
131
- const removed = current.filter((id) => !after.has(key(id)));
132
- if (!added.length && !removed.length) return { provider, hidden: current, changed: false, added, removed };
133
- const result = provider === 'opencode' ? opencodeStore.set(nextList) : own.set(provider, nextList);
134
- return { provider, hidden: provider === 'opencode' ? result.models : result.providers[provider], changed: true, added, removed, version: result.version };
402
+ /** This edit on the list it finds: the list after it, and what it added and removed. */
403
+ const edit = (current) => {
404
+ const next = new Map(current.map((id) => [key(id), id]));
405
+ for (const id of hideIds) if (!next.has(key(id))) next.set(key(id), id);
406
+ for (const k of showKeys) next.delete(k);
407
+ const nextList = [...next.values()];
408
+ const before = new Set(current.map(key));
409
+ const after = new Set(nextList.map(key));
410
+ return { current, nextList, added: nextList.filter((id) => !before.has(key(id))), removed: current.filter((id) => !after.has(key(id))) };
411
+ };
412
+ if (provider === 'opencode') {
413
+ const { current, nextList, added, removed } = edit(list(provider));
414
+ if (!added.length && !removed.length) return { provider, hidden: current, changed: false, added, removed };
415
+ const result = opencodeStore.set(nextList);
416
+ return { provider, hidden: result.models, changed: true, added, removed, version: result.version };
417
+ }
418
+ const given = hidePins && typeof hidePins === 'object' ? new Map(Object.entries(hidePins).map(([id, pin]) => [key(id), pin])) : new Map();
419
+ const at = new Date(now()).toISOString();
420
+ // An edit is a difference, so it is made on the list that is on disk when it is saved: what another
421
+ // writer added or removed in the meantime stays as that writer left it.
422
+ let made = null;
423
+ const result = own.change(provider, (doc) => {
424
+ made = edit(doc.providers[provider]);
425
+ if (!made.added.length && !made.removed.length) return null;
426
+ // An alias that is hidden now is pinned to the model it names now; an id already hidden keeps its pin.
427
+ const nextPins = { ...(doc.pins?.[provider] || {}) };
428
+ for (const id of made.added) {
429
+ const pin = normalizePin(given.get(key(id)));
430
+ if (pin) nextPins[key(id)] = { ...pin, at };
431
+ }
432
+ return { ids: made.nextList, pins: nextPins };
433
+ });
434
+ const { added, removed } = made;
435
+ if (!result.changed) return { provider, hidden: made.current, changed: false, added, removed };
436
+ return { provider, hidden: result.providers[provider], pins: result.pins[provider], changed: true, added, removed, version: result.version };
135
437
  }
136
438
 
137
439
  function onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); }
138
- return { list, all, update, onChange, store: own, path: file };
440
+ return { list, all, pins, allPins, update, reconcile, onChange, store: own, path: file };
139
441
  }
140
442
 
141
443
  // ── resolving user-typed ids against the unfiltered catalog ─────────────────
@@ -206,9 +508,13 @@ export function resolveHiddenModelId({ id, provider = null, action = 'hide', row
206
508
  }
207
509
 
208
510
  /**
209
- * GET / → { ok, providers: { 'claude-code': [], codex: [], opencode: [] } }
511
+ * GET / → { ok, providers: { 'claude-code': [], codex: [], opencode: [] }, pins }
210
512
  * PATCH / { provider?, hide?: [], show?: [] } → the new lists plus what changed.
211
- * A missing provider is inferred from the unfiltered catalog.
513
+ * A missing provider is inferred from the unfiltered catalog. Hiding an
514
+ * alias ("haiku") records the model it names at that moment (`pins`),
515
+ * when the catalog's list for the provider is a live one; else the
516
+ * next live build records it. 503 HIDDEN_BUSY / HIDDEN_LOCK_UNAVAILABLE
517
+ * and 409 HIDDEN_UNREADABLE: nothing was changed.
212
518
  * Owner only (guests get 403). Broadcasts assistant:hidden-models-changed, and
213
519
  * opencode:hidden-models-changed when the OpenCode list changed.
214
520
  */
@@ -220,7 +526,7 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
220
526
  next();
221
527
  });
222
528
  router.get('/', (req, res) => {
223
- try { res.json({ ok: true, providers: hiddenModels.all() }); }
529
+ try { res.json({ ok: true, providers: hiddenModels.all(), pins: hiddenModels.allPins?.() || {} }); }
224
530
  catch (error) { res.status(500).json({ ok: false, error: error.message }); }
225
531
  });
226
532
  router.patch('/', async (req, res) => {
@@ -233,16 +539,36 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
233
539
  if (hide.length + show.length > MAX_HIDDEN_PER_PROVIDER) throw hiddenError('TOO_MANY', `At most ${MAX_HIDDEN_PER_PROVIDER} ids per request.`);
234
540
  const provider = body.provider ? String(body.provider).trim().toLowerCase() : null;
235
541
  if (provider && !CATALOG_PROVIDERS.includes(provider)) throw hiddenError('PROVIDER_INVALID', `provider must be one of ${CATALOG_PROVIDERS.join(', ')}`);
542
+ // The rows and how each list was obtained, from one catalog build: what an alias names is
543
+ // only taken from a list the provider just gave (`live`), never from a remembered one.
236
544
  let rowsByProvider = {};
237
- try { rowsByProvider = (await catalog?.manageRows?.()) || {}; } catch {}
545
+ let sources = {};
546
+ try {
547
+ if (typeof catalog?.manageSnapshot === 'function') {
548
+ const snapshot = (await catalog.manageSnapshot()) || {};
549
+ rowsByProvider = snapshot.rows || {};
550
+ sources = snapshot.sources || {};
551
+ } else rowsByProvider = (await catalog?.manageRows?.()) || {};
552
+ } catch {}
238
553
  const hiddenByProvider = hiddenModels.all();
239
- const plan = new Map(); // provider → { hide: [], show: [] }
554
+ const plan = new Map(); // provider → { hide: [], show: [], pins: {} }
240
555
  const applied = [];
241
556
  const add = (action, raw) => {
242
557
  const hit = resolveHiddenModelId({ id: raw, provider, action, rowsByProvider, hiddenByProvider });
243
- if (!plan.has(hit.provider)) plan.set(hit.provider, { hide: [], show: [] });
558
+ if (!plan.has(hit.provider)) plan.set(hit.provider, { hide: [], show: [], pins: {} });
244
559
  plan.get(hit.provider)[action].push(hit.id);
245
- applied.push({ action, provider: hit.provider, id: hit.id, requested: String(raw) });
560
+ const entry = { action, provider: hit.provider, id: hit.id, requested: String(raw) };
561
+ if (action === 'hide') {
562
+ // The row being hidden: an alias is hidden as the model it names right now. While the list is
563
+ // not a verified one the entry is saved without a pin, and the next live build pins it to what
564
+ // the alias names then (reconcile): a pin read off a stale list would be undone by that build.
565
+ const row = sources[hit.provider] === 'live'
566
+ ? arr(rowsByProvider[hit.provider]).find((r) => String(r?.id || '').toLowerCase() === String(hit.id).toLowerCase())
567
+ : null;
568
+ const pin = row ? pinForRow({ ...row, provider: hit.provider }) : null;
569
+ if (pin) { plan.get(hit.provider).pins[String(hit.id).toLowerCase()] = pin; entry.pinned = pinnedModelId(pin); }
570
+ }
571
+ applied.push(entry);
246
572
  };
247
573
  for (const id of hide) add('hide', id);
248
574
  for (const id of show) add('show', id);
@@ -250,8 +576,9 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
250
576
  for (const [p, change] of plan) results.push(hiddenModels.update(p, change));
251
577
  const changed = results.filter((r) => r.changed);
252
578
  const providers = hiddenModels.all();
579
+ const pins = hiddenModels.allPins?.() || {};
253
580
  if (changed.length) {
254
- broadcastSync({ type: ASSISTANT_HIDDEN_SYNC_TYPE, providers, changed: changed.map((r) => r.provider) });
581
+ broadcastSync({ type: ASSISTANT_HIDDEN_SYNC_TYPE, providers, pins, changed: changed.map((r) => r.provider) });
255
582
  const oc = changed.find((r) => r.provider === 'opencode');
256
583
  if (oc) {
257
584
  let version = oc.version;
@@ -260,7 +587,7 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
260
587
  }
261
588
  }
262
589
  res.json({
263
- ok: true, providers, changed: changed.length > 0, applied,
590
+ ok: true, providers, pins, changed: changed.length > 0, applied,
264
591
  added: Object.fromEntries(changed.map((r) => [r.provider, r.added])),
265
592
  removed: Object.fromEntries(changed.map((r) => [r.provider, r.removed])),
266
593
  });
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Jev in the SynaBun Assistant: the prompt-urgency judgment of the per-turn
3
- * recall gate, and the worker-outcome / worker-claim judgments of dispatched
4
- * runs (docs/judgments.md). Everything is injected by server.js, which imports
3
+ * recall gate, the worker-outcome / worker-claim judgments of dispatched
4
+ * runs, and the budget-split judgment that divides a session's budget across
5
+ * the providers (docs/judgments.md). Everything is injected by server.js, which imports
5
6
  * it from mcp-server/dist in its own try/catch: a stale dist leaves the
6
7
  * Assistant working without Jev (this wrapper is then simply not created).
7
8
  *
@@ -18,6 +19,7 @@ const num = (value, fallback) => (value !== null && value !== undefined && value
18
19
  export function createAssistantJev({
19
20
  judgePrompt = null,
20
21
  judgeWorkerOutcome = null,
22
+ judgeBudgetSplit = null,
21
23
  surfaceConfig = () => null,
22
24
  enabled = () => true,
23
25
  retryAfterMs = () => 0,
@@ -107,11 +109,32 @@ export function createAssistantJev({
107
109
  return out;
108
110
  }
109
111
 
112
+ /**
113
+ * How much of a session's remaining spend lands on each provider (surface budget-split, one
114
+ * Score per provider in `input.ask`). Jev answers a share of the work, never an amount: the
115
+ * caller turns the scores into dollars (assistant-budget-split.js).
116
+ * → null (not asked, or no usable answer) | { scores: { [provider]: { score, confidence, probabilities } }, logId }
117
+ */
118
+ async function budgetSplit(input = {}, { sessionId = null, project = null } = {}) {
119
+ if (typeof judgeBudgetSplit !== 'function' || !available()) return null;
120
+ if (!settingsOf('budget-split')?.enabled) return null;
121
+ let logId = null;
122
+ let verdict = null;
123
+ try {
124
+ verdict = await judgeBudgetSplit(input, {
125
+ origin: 'assistant', sessionId: sessionId || null, project: project && project !== 'global' ? project : null,
126
+ onLogged: (id) => { logId = id ?? null; },
127
+ });
128
+ } catch { verdict = null; }
129
+ if (!verdict || typeof verdict !== 'object' || !verdict.scores || typeof verdict.scores !== 'object') return null;
130
+ return { scores: verdict.scores, logId };
131
+ }
132
+
110
133
  /** Record what the caller did with a judgment on its log row. Never throws. */
111
134
  function annotateLog(logId, outcome) {
112
135
  if (!logId) return;
113
136
  try { annotate(logId, outcome); } catch { /* diagnostics */ }
114
137
  }
115
138
 
116
- return { available, retryAfterMs: waitMs, promptUrgency, workerOutcome, annotate: annotateLog };
139
+ return { available, retryAfterMs: waitMs, promptUrgency, workerOutcome, budgetSplit, annotate: annotateLog };
117
140
  }