@aiworker/web 1.22.0 → 1.23.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 (415) hide show
  1. package/README.md +4 -4
  2. package/bin/cli.js +14 -4
  3. package/bin/cli.test.js +165 -73
  4. package/bin/lib/cli-api-target.js +2 -2
  5. package/bin/lib/cli-args.js +119 -119
  6. package/bin/lib/cli-lifecycle.js +9 -9
  7. package/bin/lib/cli-network.js +3 -3
  8. package/bin/lib/cli-paths.js +3 -3
  9. package/bin/lib/cli-ports.js +2 -2
  10. package/bin/lib/cli-process.js +4 -4
  11. package/bin/lib/cli-startup.js +64 -15
  12. package/bin/lib/cli-tunnel-profiles.js +1 -1
  13. package/bin/lib/cli-tunnel-utils.js +2 -2
  14. package/bin/lib/commands-connect-url.js +19 -19
  15. package/bin/lib/commands-lifecycle.js +27 -27
  16. package/bin/lib/commands-logs.js +5 -5
  17. package/bin/lib/commands-models.js +1 -1
  18. package/bin/lib/commands-projects.js +1 -1
  19. package/bin/lib/commands-schedule.js +1 -1
  20. package/bin/lib/commands-serve.js +29 -29
  21. package/bin/lib/commands-session.js +1 -1
  22. package/bin/lib/commands-startup.js +45 -10
  23. package/bin/lib/commands-status.js +1 -1
  24. package/bin/lib/commands-tunnel.js +40 -40
  25. package/bin/lib/commands-update.js +1 -1
  26. package/bin/lib/commands-update.test.js +6 -6
  27. package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
  28. package/dist/assets/DiffView-DXjVsEOw.js +4 -0
  29. package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
  30. package/dist/assets/FilesView-CJdG0-wk.js +17 -0
  31. package/dist/assets/GitView-BXsS3jG6.js +6 -0
  32. package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
  33. package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
  34. package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
  35. package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
  36. package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
  37. package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
  38. package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
  39. package/dist/assets/PlanView-CJTmOF6I.js +5 -0
  40. package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
  41. package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
  42. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  43. package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
  44. package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
  45. package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
  46. package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
  47. package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
  48. package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
  49. package/dist/assets/de-DCrsQ5zS.js +10 -0
  50. package/dist/assets/es-Dwv4YfH6.js +16 -0
  51. package/dist/assets/fr-BCWg4Cnx.js +14 -0
  52. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  53. package/dist/assets/index-B68h5S5B.css +1 -0
  54. package/dist/assets/index-DVwMarHI.js +39 -0
  55. package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
  56. package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
  57. package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
  58. package/dist/assets/ja-Bruw3RYC.js +16 -0
  59. package/dist/assets/ko-idhXGF8j.js +16 -0
  60. package/dist/assets/main-BkyaQhJX.js +930 -0
  61. package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
  62. package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
  63. package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
  64. package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
  65. package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
  66. package/dist/assets/pl-CGZWUqZJ.js +16 -0
  67. package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
  68. package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
  69. package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
  70. package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
  71. package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
  72. package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
  73. package/dist/assets/surface-oMvhsog3.js +1 -0
  74. package/dist/assets/tr-BK_KiO2V.js +7 -0
  75. package/dist/assets/uk-CG74xV8N.js +16 -0
  76. package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
  77. package/dist/assets/useGitComparison-DH220WmO.js +1 -0
  78. package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
  79. package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
  80. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
  82. package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
  83. package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
  84. package/dist/assets/zh-CN-T4AXofZI.js +16 -0
  85. package/dist/assets/zh-TW-Dte40sxp.js +16 -0
  86. package/dist/index.html +21 -59
  87. package/dist/mini-chat.html +7 -7
  88. package/dist/mobile.html +5 -5
  89. package/package.json +3 -4
  90. package/server/index.d.ts +15 -0
  91. package/server/index.js +158 -89
  92. package/server/lib/agent-memory/actions.js +2 -2
  93. package/server/lib/agent-memory/feature-flag.js +1 -1
  94. package/server/lib/agent-memory/feature-flag.test.js +8 -8
  95. package/server/lib/agent-memory/routes.js +2 -2
  96. package/server/lib/agent-tool/runtime.js +37 -54
  97. package/server/lib/agent-tool/runtime.test.js +59 -59
  98. package/server/lib/browser-control/broker.js +3 -3
  99. package/server/lib/browser-control/broker.test.js +1 -1
  100. package/server/lib/changelog/update-notes.js +82 -0
  101. package/server/lib/changelog/update-notes.test.js +59 -0
  102. package/server/lib/client-auth/remote-clients.test.js +5 -5
  103. package/server/lib/context-obligatory/runtime.js +7 -7
  104. package/server/lib/context-obligatory/runtime.test.js +5 -5
  105. package/server/lib/data-dir-migration.js +40 -0
  106. package/server/lib/data-dir-migration.test.js +59 -0
  107. package/server/lib/dev-servers/parse.js +2 -2
  108. package/server/lib/dev-servers/parse.test.js +2 -2
  109. package/server/lib/dev-servers/routes.js +1 -1
  110. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  111. package/server/lib/dev-tunnel/client.js +1 -1
  112. package/server/lib/dev-tunnel/runtime.js +5 -5
  113. package/server/lib/dev-tunnel/tunnel.test.js +21 -1
  114. package/server/lib/dictation/local/model-catalog.js +1 -1
  115. package/server/lib/dictation/local/worker-process.js +2 -2
  116. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  117. package/server/lib/event-stream/global-ws-bridge.js +1 -1
  118. package/server/lib/event-stream/protocol.test.js +4 -4
  119. package/server/lib/event-stream/runtime.test.js +18 -18
  120. package/server/lib/fs/DOCUMENTATION.md +5 -0
  121. package/server/lib/fs/routes.js +68 -26
  122. package/server/lib/fs/routes.test.js +208 -22
  123. package/server/lib/git/DOCUMENTATION.md +32 -6
  124. package/server/lib/git/identity-storage.js +1 -1
  125. package/server/lib/git/issue-2746-longpaths.test.js +4 -4
  126. package/server/lib/git/routes.js +41 -5
  127. package/server/lib/git/service.js +300 -224
  128. package/server/lib/git/service.test.js +596 -28
  129. package/server/lib/github/auth.js +8 -8
  130. package/server/lib/github/pr-status.js +1 -1
  131. package/server/lib/github/routes.js +2 -2
  132. package/server/lib/inherited-env.js +17 -10
  133. package/server/lib/inherited-env.test.js +10 -11
  134. package/server/lib/linear/auth.js +8 -8
  135. package/server/lib/linear/auth.test.js +24 -24
  136. package/server/lib/linear/issues.test.js +6 -6
  137. package/server/lib/linear/mapping.test.js +4 -4
  138. package/server/lib/linear/oauth.js +2 -2
  139. package/server/lib/linear/oauth.test.js +14 -14
  140. package/server/lib/linear/routes.js +5 -5
  141. package/server/lib/linear/routes.test.js +16 -16
  142. package/server/lib/linear/status-runtime.test.js +9 -9
  143. package/server/lib/linear/status.js +1 -1
  144. package/server/lib/linear/status.test.js +15 -15
  145. package/server/lib/linear/teams.test.js +4 -4
  146. package/server/lib/magic-prompts/routes.js +2 -2
  147. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  148. package/server/lib/mcp-reconnect/runtime.js +122 -0
  149. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  150. package/server/lib/message-queue/DOCUMENTATION.md +161 -0
  151. package/server/lib/message-queue/runtime.js +870 -0
  152. package/server/lib/message-queue/runtime.test.js +482 -0
  153. package/server/lib/network-defaults.js +23 -0
  154. package/server/lib/network-defaults.test.js +52 -0
  155. package/server/lib/notifications/apns-runtime.js +17 -17
  156. package/server/lib/notifications/apns-runtime.test.js +11 -11
  157. package/server/lib/notifications/emitter-runtime.js +1 -1
  158. package/server/lib/notifications/emitter-runtime.test.js +1 -1
  159. package/server/lib/notifications/push-runtime.js +7 -7
  160. package/server/lib/notifications/routes.js +1 -1
  161. package/server/lib/notifications/runtime.js +1 -1
  162. package/server/lib/openchamber-control/action-resolution.test.js +10 -10
  163. package/server/lib/openchamber-control/actions.js +24 -24
  164. package/server/lib/openchamber-control/error.js +4 -4
  165. package/server/lib/openchamber-control/routes.js +2 -2
  166. package/server/lib/openchamber-control/routes.test.js +5 -5
  167. package/server/lib/openchamber-control/screenshots.js +1 -1
  168. package/server/lib/openchamber-control/screenshots.test.js +1 -1
  169. package/server/lib/openchamber-control/service.js +50 -50
  170. package/server/lib/openchamber-control/service.test.js +5 -5
  171. package/server/lib/openchamber-sessions/routes.js +124 -22
  172. package/server/lib/openchamber-sessions/routes.test.js +99 -11
  173. package/server/lib/opencode/DOCUMENTATION.md +30 -6
  174. package/server/lib/opencode/auth.js +1 -1
  175. package/server/lib/opencode/bootstrap-runtime.js +8 -6
  176. package/server/lib/opencode/cli-options.js +8 -8
  177. package/server/lib/opencode/config-paths.test.js +116 -0
  178. package/server/lib/opencode/core-routes.js +8 -7
  179. package/server/lib/opencode/core-routes.test.js +11 -11
  180. package/server/lib/opencode/env-config.js +5 -5
  181. package/server/lib/opencode/env-config.test.js +7 -7
  182. package/server/lib/opencode/env-runtime.js +10 -10
  183. package/server/lib/opencode/env-runtime.test.js +18 -18
  184. package/server/lib/opencode/feature-routes-runtime.js +19 -12
  185. package/server/lib/opencode/hmr-state-runtime.js +2 -2
  186. package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
  187. package/server/lib/opencode/input-history-scope.js +14 -0
  188. package/server/lib/opencode/lifecycle.js +8 -8
  189. package/server/lib/opencode/lifecycle.test.js +4 -4
  190. package/server/lib/opencode/managed-plugin-config.js +35 -0
  191. package/server/lib/opencode/managed-process-registry.js +3 -3
  192. package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
  193. package/server/lib/opencode/openchamber-routes.js +79 -19
  194. package/server/lib/opencode/openchamber-routes.test.js +139 -12
  195. package/server/lib/opencode/plugins.js +2 -2
  196. package/server/lib/opencode/plugins.test.js +1 -1
  197. package/server/lib/opencode/project-icon-routes.js +2 -2
  198. package/server/lib/opencode/project-icon-routes.test.js +1 -1
  199. package/server/lib/opencode/providers.js +44 -2
  200. package/server/lib/opencode/providers.test.js +131 -4
  201. package/server/lib/opencode/proxy.js +2 -2
  202. package/server/lib/opencode/pwa-manifest-routes.js +1 -1
  203. package/server/lib/opencode/restart-session-recovery.test.js +1 -1
  204. package/server/lib/opencode/routes-upgrade.test.js +4 -4
  205. package/server/lib/opencode/routes.js +19 -17
  206. package/server/lib/opencode/server-startup-runtime.js +6 -6
  207. package/server/lib/opencode/session-runtime.js +3 -3
  208. package/server/lib/opencode/session-runtime.test.js +6 -6
  209. package/server/lib/opencode/settings-files.js +214 -0
  210. package/server/lib/opencode/settings-helpers.js +116 -44
  211. package/server/lib/opencode/settings-helpers.test.js +354 -12
  212. package/server/lib/opencode/settings-registry.json +747 -0
  213. package/server/lib/opencode/settings-runtime.js +141 -20
  214. package/server/lib/opencode/settings-runtime.test.js +191 -4
  215. package/server/lib/opencode/shared.js +5 -2
  216. package/server/lib/opencode/shared.test.js +7 -7
  217. package/server/lib/opencode/shutdown-runtime.js +2 -0
  218. package/server/lib/opencode/skill-routes.js +2 -1
  219. package/server/lib/opencode/snippets.js +1 -2
  220. package/server/lib/opencode/startup-performance.js +1 -1
  221. package/server/lib/opencode/startup-performance.test.js +6 -6
  222. package/server/lib/opencode/static-routes-runtime.js +8 -8
  223. package/server/lib/opencode/static-routes-runtime.test.js +7 -7
  224. package/server/lib/opencode/upgrade-capability.js +2 -2
  225. package/server/lib/opencode/upgrade-capability.test.js +3 -3
  226. package/server/lib/package-manager.js +17 -42
  227. package/server/lib/package-manager.test.js +19 -19
  228. package/server/lib/permission-auto-accept/runtime.js +1 -1
  229. package/server/lib/project-context/DOCUMENTATION.md +25 -1
  230. package/server/lib/project-context/routes.http.test.js +18 -0
  231. package/server/lib/project-context/routes.js +16 -1
  232. package/server/lib/project-context/runtime.js +248 -23
  233. package/server/lib/project-context/runtime.test.js +106 -3
  234. package/server/lib/projects/DOCUMENTATION.md +76 -0
  235. package/server/lib/projects/project-config.js +144 -11
  236. package/server/lib/projects/project-id.js +17 -0
  237. package/server/lib/projects/project-setup.js +409 -0
  238. package/server/lib/projects/project-setup.test.js +557 -0
  239. package/server/lib/projects/routes.js +59 -0
  240. package/server/lib/quota/DOCUMENTATION.md +40 -2
  241. package/server/lib/quota/credentials/providers.js +4 -0
  242. package/server/lib/quota/credentials/store.js +4 -4
  243. package/server/lib/quota/credentials/store.test.js +8 -8
  244. package/server/lib/quota/index.js +1 -0
  245. package/server/lib/quota/providers/cline-pass.js +125 -0
  246. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  247. package/server/lib/quota/providers/exe-dev.js +75 -0
  248. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  249. package/server/lib/quota/providers/hyper.js +118 -0
  250. package/server/lib/quota/providers/hyper.test.js +126 -0
  251. package/server/lib/quota/providers/index.js +22 -0
  252. package/server/lib/quota/providers/neuralwatt.js +3 -6
  253. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  254. package/server/lib/quota/providers/ollama-cloud.js +32 -1
  255. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  256. package/server/lib/quota/providers/opencode-go.js +3 -2
  257. package/server/lib/quota/providers/opencode-go.test.js +35 -6
  258. package/server/lib/quota/providers/openrouter.js +119 -24
  259. package/server/lib/quota/providers/openrouter.test.js +330 -0
  260. package/server/lib/quota/providers/xai.js +1 -1
  261. package/server/lib/quota/routes.js +2 -0
  262. package/server/lib/quota/utils/auth.js +1 -1
  263. package/server/lib/relay/DOCUMENTATION.md +56 -1
  264. package/server/lib/relay/cross-compat.test.js +26 -0
  265. package/server/lib/relay/downstream-scheduler.js +154 -0
  266. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  267. package/server/lib/relay/e2ee.js +10 -8
  268. package/server/lib/relay/flow-control.test.js +177 -0
  269. package/server/lib/relay/host-client.js +38 -9
  270. package/server/lib/relay/host-client.test.js +9 -1
  271. package/server/lib/relay/host-lock.js +1 -1
  272. package/server/lib/relay/service.js +5 -5
  273. package/server/lib/relay/tunnel-codec.js +9 -0
  274. package/server/lib/relay/tunnel-host.js +69 -18
  275. package/server/lib/relay/tunnel-host.test.js +9 -1
  276. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
  277. package/server/lib/scheduled-tasks/routes.js +5 -5
  278. package/server/lib/scheduled-tasks/service.js +25 -25
  279. package/server/lib/scheduled-tasks/service.test.js +1 -1
  280. package/server/lib/security/bind-host.js +4 -4
  281. package/server/lib/security/bind-host.test.js +1 -1
  282. package/server/lib/security/request-security.js +11 -5
  283. package/server/lib/security/request-security.test.js +14 -14
  284. package/server/lib/session-assist/runtime.js +15 -18
  285. package/server/lib/session-folders/routes.js +2 -2
  286. package/server/lib/session-folders/routes.test.js +8 -8
  287. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  288. package/server/lib/session-goal/create.js +4 -2
  289. package/server/lib/session-goal/create.test.js +1 -1
  290. package/server/lib/session-goal/objectives.js +4 -4
  291. package/server/lib/session-goal/routes.js +1 -1
  292. package/server/lib/session-goal/runtime.js +91 -30
  293. package/server/lib/session-goal/runtime.test.js +428 -2
  294. package/server/lib/session-knowledge/runtime.js +11 -11
  295. package/server/lib/session-knowledge/runtime.test.js +5 -5
  296. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  297. package/server/lib/skills-catalog/cache.test.js +2 -2
  298. package/server/lib/skills-catalog/disk-cache.js +5 -5
  299. package/server/lib/skills-catalog/github-meta.test.js +2 -2
  300. package/server/lib/skills-catalog/install.js +3 -2
  301. package/server/lib/small-model/DOCUMENTATION.md +7 -2
  302. package/server/lib/small-model/call.js +18 -7
  303. package/server/lib/small-model/call.test.js +58 -6
  304. package/server/lib/small-model/index.js +12 -16
  305. package/server/lib/small-model/index.test.js +1 -1
  306. package/server/lib/small-model/resolve.js +1 -1
  307. package/server/lib/small-model/resolve.test.js +1 -1
  308. package/server/lib/small-model/routes.js +2 -1
  309. package/server/lib/small-model/runtime-providers.js +15 -7
  310. package/server/lib/small-model/runtime-providers.test.js +7 -1
  311. package/server/lib/system-prompt/runtime.js +3 -22
  312. package/server/lib/system-prompt/runtime.test.js +3 -3
  313. package/server/lib/terminal/DOCUMENTATION.md +14 -10
  314. package/server/lib/terminal/runtime.js +190 -31
  315. package/server/lib/terminal/runtime.test.js +572 -38
  316. package/server/lib/terminal/shells.js +20 -3
  317. package/server/lib/terminal/shells.test.js +33 -2
  318. package/server/lib/tts/base-url.js +3 -3
  319. package/server/lib/tts/routes.test.js +20 -20
  320. package/server/lib/ui-auth/ui-auth.js +12 -9
  321. package/server/lib/ui-auth/ui-auth.test.js +19 -3
  322. package/server/lib/ui-auth/ui-passkeys.js +7 -7
  323. package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
  324. package/server/lib/walkthrough/index.js +1 -0
  325. package/server/lib/walkthrough/jobs.test.js +1 -1
  326. package/server/lib/walkthrough/language.test.js +1 -1
  327. package/server/lib/walkthrough/model-settings.js +11 -15
  328. package/server/lib/walkthrough/model-settings.test.js +1 -1
  329. package/server/lib/walkthrough/prompt.js +3 -1
  330. package/server/lib/walkthrough/pull-request.test.js +5 -5
  331. package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
  332. package/server/lib/walkthrough/routes.test.js +26 -5
  333. package/server/lib/walkthrough/sources.js +23 -2
  334. package/server/lib/walkthrough/store.js +3 -3
  335. package/server/lib/walkthrough/store.test.js +1 -1
  336. package/server/opencode-proxy.test.js +2 -2
  337. package/server/proxy-headers.js +1 -1
  338. package/server/sse-routes.test.js +4 -4
  339. package/README.md.bak +0 -250
  340. package/bin/lib/DOCUMENTATION.md.bak +0 -163
  341. package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
  342. package/dist/assets/DiffView-DluiRSla.js +0 -4
  343. package/dist/assets/FilesView-Bdq8ppva.js +0 -17
  344. package/dist/assets/GitView-Znn7VJcP.js +0 -6
  345. package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
  346. package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
  347. package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
  348. package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
  349. package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
  350. package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
  351. package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
  352. package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
  353. package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
  354. package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
  355. package/dist/assets/de-C1P6X5Rs.js +0 -9
  356. package/dist/assets/es-CDgaF1_t.js +0 -15
  357. package/dist/assets/fr-Bgxqvvz_.js +0 -13
  358. package/dist/assets/index-C57jT0Sa.css +0 -1
  359. package/dist/assets/index-DW_E3rnL.js +0 -7
  360. package/dist/assets/ja-BDRrEcNP.js +0 -15
  361. package/dist/assets/ko-Ba8sQV79.js +0 -15
  362. package/dist/assets/main-Bv5Pkngh.js +0 -928
  363. package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
  364. package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
  365. package/dist/assets/pl-Dioz29BI.js +0 -15
  366. package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
  367. package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
  368. package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
  369. package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
  370. package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
  371. package/dist/assets/tr-DFnMlPxY.js +0 -6
  372. package/dist/assets/uk-ZrCNjM7Y.js +0 -15
  373. package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
  374. package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
  375. package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
  376. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  377. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
  378. package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
  379. package/dist/assets/zh-TW-CzEUk38j.js +0 -15
  380. package/server/index.d.ts.bak +0 -39
  381. package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
  382. package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
  383. package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
  384. package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
  385. package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
  386. package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
  387. package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
  388. package/server/lib/git/DOCUMENTATION.md.bak +0 -193
  389. package/server/lib/github/DOCUMENTATION.md.bak +0 -183
  390. package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
  391. package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
  392. package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
  393. package/server/lib/notifications/APNS.md.bak +0 -135
  394. package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
  395. package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
  396. package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
  397. package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
  398. package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
  399. package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
  400. package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
  401. package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
  402. package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
  403. package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
  404. package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
  405. package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
  406. package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
  407. package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
  408. package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
  409. package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
  410. package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
  411. package/server/lib/text/DOCUMENTATION.md.bak +0 -35
  412. package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
  413. package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
  414. package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
  415. package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createDownstreamScheduler, DOWNSTREAM_CHUNK_BYTES } from './downstream-scheduler.js';
3
+ import { encodeTunnelFrame, decodeTunnelFrame, TunnelFrameType } from './tunnel-codec.js';
4
+
5
+ const tick = () => new Promise(resolve => setTimeout(resolve, 0));
6
+ const body = (streamId, value = 0, size = DOWNSTREAM_CHUNK_BYTES) =>
7
+ encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, new Uint8Array(size).fill(value));
8
+
9
+ function fixture() {
10
+ const sent = [];
11
+ const errors = [];
12
+ let time = 0;
13
+ const scheduler = createDownstreamScheduler({
14
+ sendBatch: async frames => { sent.push(...frames); },
15
+ onError: error => errors.push(error.message),
16
+ now: () => time,
17
+ });
18
+ return { scheduler, sent, errors, advance: ms => { time += ms; } };
19
+ }
20
+
21
+ describe('relay downstream scheduling', () => {
22
+ test('stops without end-client ACKs even when the relay socket accepts everything', async () => {
23
+ const { scheduler, sent } = fixture();
24
+ const pending = Array.from({ length: 100 }, () => scheduler.send(body(1)));
25
+ await tick();
26
+ const bytes = sent.reduce((sum, frame) => sum + frame.length, 0);
27
+ expect(bytes).toBeLessThanOrEqual(64 * 1024);
28
+ expect(bytes).toBeGreaterThan(48 * 1024);
29
+ await tick();
30
+ expect(sent.reduce((sum, frame) => sum + frame.length, 0)).toBe(bytes);
31
+ scheduler.acknowledge(bytes);
32
+ await tick();
33
+ expect(sent.reduce((sum, frame) => sum + frame.length, 0)).toBeGreaterThan(bytes);
34
+ scheduler.close();
35
+ await Promise.all(pending);
36
+ });
37
+
38
+ test('rotates streams while preserving every delta and each stream end', async () => {
39
+ const { scheduler, sent } = fixture();
40
+ const pending = [];
41
+ for (let value = 0; value < 3; value++) pending.push(scheduler.send(body(1, value)));
42
+ pending.push(scheduler.send(encodeTunnelFrame(TunnelFrameType.StreamEnd, 1, new Uint8Array())));
43
+ pending.push(scheduler.send(body(3, 99, 10)));
44
+ await Promise.all(pending);
45
+ expect(sent.map(frame => decodeTunnelFrame(frame).streamId)).toEqual([1, 3, 1, 1, 1]);
46
+ expect(sent.filter(frame => decodeTunnelFrame(frame).streamId === 1).map(frame => decodeTunnelFrame(frame).payload[0])).toEqual([0, 1, 2, undefined]);
47
+ scheduler.close();
48
+ });
49
+
50
+ test('keepalives bypass credit; cancellation and close release blocked producers', async () => {
51
+ const { scheduler, sent } = fixture();
52
+ const pending = Array.from({ length: 20 }, () => scheduler.send(body(1)));
53
+ await tick();
54
+ const before = sent.length;
55
+ await scheduler.send(encodeTunnelFrame(TunnelFrameType.Pong, 0, new Uint8Array()));
56
+ expect(sent.length).toBe(before + 1);
57
+ scheduler.cancel(1);
58
+ await Promise.all(pending);
59
+ const blocked = scheduler.send(body(3));
60
+ scheduler.close();
61
+ await blocked;
62
+ // Teardown makes late ACKs harmless.
63
+ scheduler.acknowledge(123);
64
+ });
65
+
66
+ test('rejects forged, replayed, regressing and partial-frame ACKs', async () => {
67
+ const { scheduler, sent } = fixture();
68
+ await scheduler.send(body(1));
69
+ const bytes = sent[0].length;
70
+ for (const value of [0, -1, 1, bytes + 1, NaN, Infinity]) {
71
+ expect(() => scheduler.acknowledge(value)).toThrow();
72
+ }
73
+ scheduler.acknowledge(bytes);
74
+ expect(() => scheduler.acknowledge(bytes)).toThrow();
75
+ scheduler.close();
76
+ });
77
+
78
+ test('bounds pending memory and reports overflow instead of silently dropping deltas', async () => {
79
+ const { scheduler, errors } = fixture();
80
+ const pending = Array.from({ length: 600 }, () => scheduler.send(body(1)));
81
+ await Promise.all(pending);
82
+ expect(errors).toEqual(['relay downstream queue limit exceeded']);
83
+ });
84
+
85
+ test('send failure settles selected and queued producers', async () => {
86
+ const errors = [];
87
+ const scheduler = createDownstreamScheduler({
88
+ sendBatch: async () => { throw new Error('wire failed'); },
89
+ onError: error => errors.push(error.message),
90
+ });
91
+ await Promise.all([scheduler.send(body(1)), scheduler.send(body(3))]);
92
+ expect(errors).toEqual(['wire failed']);
93
+ });
94
+
95
+ test('teardown releases a producer even while encryption is still pending', async () => {
96
+ let release;
97
+ let started = false;
98
+ const scheduler = createDownstreamScheduler({
99
+ sendBatch: () => new Promise(resolve => { started = true; release = resolve; }),
100
+ onError: error => { throw error; },
101
+ });
102
+ const pending = scheduler.send(body(1));
103
+ await tick();
104
+ expect(started).toBe(true);
105
+ scheduler.close();
106
+ await pending;
107
+ release();
108
+ await tick();
109
+ });
110
+
111
+ test('sparse traffic cannot inflate the window for a later bulk burst', async () => {
112
+ const { scheduler, sent, advance } = fixture();
113
+ let acknowledged = 0;
114
+ for (let i = 0; i < 100; i++) {
115
+ const frame = body(1);
116
+ await scheduler.send(frame);
117
+ advance(50);
118
+ acknowledged += frame.length;
119
+ scheduler.acknowledge(acknowledged);
120
+ }
121
+ const before = sent.length;
122
+ const pending = Array.from({ length: 100 }, () => scheduler.send(body(1)));
123
+ await tick();
124
+ expect(sent.slice(before).reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(64 * 1024);
125
+ scheduler.close();
126
+ await Promise.all(pending);
127
+ });
128
+
129
+ test('adapts to a clear high-RTT path, then contracts when delivery backs up', async () => {
130
+ const { scheduler, sent, advance, errors } = fixture();
131
+ let acknowledged = 0;
132
+ let maxFlight = 0;
133
+ const pending = Array.from({ length: 400 }, () => scheduler.send(body(1)));
134
+ for (let round = 0; round < 12; round++) {
135
+ // Keep enough queued output to exercise growth all the way to the cap.
136
+ while (pending.length < sent.length + 400) pending.push(scheduler.send(body(1)));
137
+ await tick();
138
+ const delivered = sent.reduce((sum, frame) => sum + frame.length, 0);
139
+ maxFlight = Math.max(maxFlight, delivered - acknowledged);
140
+ expect(delivered - acknowledged).toBeLessThanOrEqual(1024 * 1024);
141
+ advance(200);
142
+ scheduler.acknowledge(delivered);
143
+ acknowledged = delivered;
144
+ }
145
+ // Reach the cap with a backlogged sender, allowing for whole-frame sizing.
146
+ expect(maxFlight).toBeGreaterThan(1024 * 1024 - body(1).length);
147
+ // Keep the sender backlogged while ACK latency jumps to a second.
148
+ for (let round = 0; round < 12; round++) {
149
+ for (let i = 0; i < 30; i++) pending.push(scheduler.send(body(1)));
150
+ await tick();
151
+ const delivered = sent.reduce((sum, frame) => sum + frame.length, 0);
152
+ advance(1000);
153
+ scheduler.acknowledge(delivered);
154
+ acknowledged = delivered;
155
+ }
156
+ await tick();
157
+ expect(sent.reduce((sum, frame) => sum + frame.length, 0) - acknowledged).toBeLessThanOrEqual(64 * 1024);
158
+ expect(errors).toEqual([]);
159
+ scheduler.close();
160
+ await Promise.all(pending);
161
+ });
162
+ });
@@ -252,11 +252,12 @@ const parseHandshakeMessage = (raw) => {
252
252
  if (parsed.v !== RELAY_PROTOCOL_VERSION) return null;
253
253
  // Unknown/missing capability flag = false = legacy behavior.
254
254
  const batch = parsed.batch === true;
255
+ const flowControl = parsed.flowControl === true;
255
256
  if (parsed.t === 'ready') {
256
- return { t: 'ready', v: RELAY_PROTOCOL_VERSION, batch };
257
+ return { t: 'ready', v: RELAY_PROTOCOL_VERSION, batch, flowControl };
257
258
  }
258
259
  if (parsed.t === 'hello' && typeof parsed.nonce === 'string' && typeof parsed.clientPubJwk === 'object' && parsed.clientPubJwk !== null) {
259
- return { t: 'hello', v: RELAY_PROTOCOL_VERSION, clientPubJwk: parsed.clientPubJwk, nonce: parsed.nonce, batch };
260
+ return { t: 'hello', v: RELAY_PROTOCOL_VERSION, clientPubJwk: parsed.clientPubJwk, nonce: parsed.nonce, batch, flowControl };
260
261
  }
261
262
  return null;
262
263
  };
@@ -275,7 +276,7 @@ const failClosed = (reason) => ({
275
276
  * { type: 'ignore' } — drop the frame
276
277
  * { type: 'fail', closeCode, reason } — close the socket with closeCode
277
278
  * @param {CryptoKey} hostEncPrivateKey long-lived ECDH private key
278
- * @param {{ batch?: boolean }} [options] `batch` defaults true; set false to force legacy behavior
279
+ * @param {{ batch?: boolean, flowControl?: boolean }} [options] Capabilities default true.
279
280
  */
280
281
  export const createHostHandshake = (hostEncPrivateKey, options = {}) => {
281
282
  const localBatch = options.batch !== false;
@@ -321,15 +322,16 @@ export const createHostHandshake = (hostEncPrivateKey, options = {}) => {
321
322
  acceptedClientKeyFingerprint = fingerprint;
322
323
  // Batching runs only if both peers advertised it.
323
324
  negotiatedBatch = localBatch && message.batch === true;
324
- readyText = JSON.stringify(
325
- negotiatedBatch
326
- ? { t: 'ready', v: RELAY_PROTOCOL_VERSION, batch: true }
327
- : { t: 'ready', v: RELAY_PROTOCOL_VERSION },
328
- );
325
+ const flowControl = options.flowControl !== false && message.flowControl === true;
326
+ const ready = { t: 'ready', v: RELAY_PROTOCOL_VERSION };
327
+ if (negotiatedBatch) ready.batch = true;
328
+ if (flowControl) ready.flowControl = true;
329
+ readyText = JSON.stringify(ready);
329
330
  established = true;
330
331
  return {
331
332
  type: 'established',
332
333
  batch: negotiatedBatch,
334
+ flowControl,
333
335
  replyText: readyText,
334
336
  channel: {
335
337
  encryptor: createFrameEncryptor(keys.hostToClient),
@@ -0,0 +1,177 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import http from 'node:http';
3
+ import { WebSocketServer } from 'ws';
4
+ import { startRelayHost } from './host-client.js';
5
+ import { generateEcdhKeyPair, exportPublicKeyJwk } from './e2ee.js';
6
+ import { createRelayTunnelClient } from '../../../../ui/src/lib/relay/tunnel-client.ts';
7
+
8
+ const waitFor = async predicate => {
9
+ for (let attempt = 0; attempt < 1000; attempt++) {
10
+ if (predicate()) return;
11
+ await new Promise(resolve => setTimeout(resolve, 5));
12
+ }
13
+ throw new Error('test condition timed out');
14
+ };
15
+
16
+ async function exercise({ clientFlow = true, hostFlow = true, batch = true, cancel = false, websocket = false } = {}) {
17
+ const total = 512 * 1024;
18
+ const content = Buffer.alloc(total, 'a');
19
+ content.fill('b', total / 2);
20
+ const upstream = http.createServer((req, res) => {
21
+ // HTTP bearer ownership is unchanged by flow control.
22
+ if (req.headers.authorization !== 'Bearer fixture-token') { res.writeHead(401); res.end(); return; }
23
+ res.end(req.url === '/health' ? 'ok' : content);
24
+ });
25
+ const upstreamWs = new WebSocketServer({ noServer: true });
26
+ upstream.on('upgrade', (req, socket, head) => {
27
+ // Mirror the real URL-token and loopback-origin gates for tunneled WS.
28
+ const query = new URL(req.url, 'http://localhost');
29
+ if (query.searchParams.get('oc_url_token') !== 'fixture-url-token' || req.headers.origin !== `http://127.0.0.1:${upstream.address().port}`) {
30
+ socket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
31
+ return;
32
+ }
33
+ upstreamWs.handleUpgrade(req, socket, head, ws => {
34
+ ws.send(content.subarray(0, total / 2));
35
+ ws.send(content.subarray(total / 2));
36
+ // Let Bun finish the local write turn, then close while the throttled
37
+ // tunnel still has most of the two messages queued.
38
+ setTimeout(() => ws.close(1000, 'complete'), 20);
39
+ });
40
+ });
41
+ await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve));
42
+ const relay = new WebSocketServer({ host: '127.0.0.1', port: 0 });
43
+ await new Promise(resolve => relay.on('listening', resolve));
44
+ let control;
45
+ let clientSocket;
46
+ let hostData;
47
+ const waiting = [];
48
+ const down = [];
49
+ // This fixture brokers an isolated room only; it does not replace production
50
+ // relay authentication. Neither WebSocket leg fails or drops a frame.
51
+ relay.on('connection', (socket, req) => {
52
+ const role = new URL(req.url, 'http://localhost').searchParams.get('role');
53
+ if (role === 'host-control') {
54
+ control = socket;
55
+ socket.send(JSON.stringify({ type: 'sync', connectionIds: clientSocket ? ['fixture'] : [] }));
56
+ } else if (role === 'client') {
57
+ clientSocket = socket;
58
+ control?.send(JSON.stringify({ type: 'connected', connectionId: 'fixture' }));
59
+ socket.on('message', (data, binary) => {
60
+ if (hostData) hostData.send(data, { binary });
61
+ else waiting.push({ data, binary });
62
+ });
63
+ } else {
64
+ hostData = socket;
65
+ for (const frame of waiting.splice(0)) socket.send(frame.data, { binary: frame.binary });
66
+ socket.on('message', (data, binary) => {
67
+ down.push({ data, binary });
68
+ });
69
+ }
70
+ });
71
+ // Drain at a byte budget, not a frame count: frame-size changes cannot make
72
+ // the test's effective bandwidth change. Preserve whole WS messages in order.
73
+ let credit = 0;
74
+ const drain = setInterval(() => {
75
+ credit = Math.min(128 * 1024, credit + 4096);
76
+ while (down.length && down[0].data.length <= credit) {
77
+ const frame = down.shift();
78
+ credit -= frame.data.length;
79
+ clientSocket.send(frame.data, { binary: frame.binary });
80
+ }
81
+ }, 5);
82
+ const keys = await generateEcdhKeyPair();
83
+ const relayUrl = `ws://127.0.0.1:${relay.address().port}/ws`;
84
+ const host = startRelayHost({
85
+ relayUrl, localPort: upstream.address().port, batch, flowControl: hostFlow,
86
+ identity: { serverId: 'fixture', hostEncPrivateKey: keys.privateKey, signRelayAuth: () => ({ ts: 0, sig: '', pk: '' }) },
87
+ });
88
+ const client = createRelayTunnelClient({ relayUrl, serverId: 'fixture', hostEncPubJwk: await exportPublicKeyJwk(keys.publicKey), batch, flowControl: clientFlow });
89
+ const states = [];
90
+ client.subscribeStatus(status => states.push(status.state));
91
+ const headers = { authorization: 'Bearer fixture-token' };
92
+ let received = 0;
93
+ let finished = false;
94
+ let wsMessages = 0;
95
+ let streamFailure;
96
+ const chunks = [];
97
+ const abort = new AbortController();
98
+ try {
99
+ await waitFor(() => client.getStatus().state === 'connected');
100
+ let stream;
101
+ if (websocket) {
102
+ const socket = client.openWebSocket('/api/terminal/ws?oc_url_token=fixture-url-token');
103
+ stream = new Promise((resolve, reject) => {
104
+ socket.onmessage = event => {
105
+ const bytes = new Uint8Array(event.data);
106
+ chunks.push(bytes);
107
+ received += bytes.length;
108
+ wsMessages++;
109
+ };
110
+ socket.onclose = event => {
111
+ finished = true;
112
+ if (event.code !== 1000) reject(new Error(event.reason));
113
+ else resolve();
114
+ };
115
+ });
116
+ } else {
117
+ const response = await client.fetch('/api/global/event', { headers, signal: abort.signal });
118
+ stream = (async () => {
119
+ try {
120
+ for await (const chunk of response.body) { chunks.push(chunk); received += chunk.length; }
121
+ } catch (error) {
122
+ if (!cancel || error.name !== 'AbortError') throw error;
123
+ } finally { finished = true; }
124
+ })();
125
+ }
126
+ stream = stream.catch(error => { streamFailure = error; });
127
+ await waitFor(() => received > 0 || streamFailure);
128
+ if (streamFailure) throw streamFailure;
129
+ if (cancel) abort.abort();
130
+ const response = await client.fetch('/health', { headers, signal: AbortSignal.timeout(5000) });
131
+ expect(await response.text()).toBe('ok');
132
+ const bytesAtProbe = received;
133
+ await stream;
134
+ expect(streamFailure).toBeUndefined();
135
+ expect(finished).toBe(true);
136
+ if (!cancel) expect(Buffer.concat(chunks).equals(content)).toBe(true);
137
+ if (websocket) expect(wsMessages).toBe(2);
138
+ expect(states).toEqual(['connected']);
139
+ return { bytesAtProbe, total };
140
+ } finally {
141
+ client.close();
142
+ host.stop();
143
+ clearInterval(drain);
144
+ for (const ws of relay.clients) ws.terminate();
145
+ relay.close();
146
+ for (const ws of upstreamWs.clients) ws.terminate();
147
+ upstreamWs.close();
148
+ upstream.closeAllConnections();
149
+ upstream.close();
150
+ }
151
+ }
152
+
153
+ describe('end-to-end downstream credit', () => {
154
+ test('small HTTP response overtakes bulk output without losing bytes or reconnecting', async () => {
155
+ const result = await exercise();
156
+ expect(result.bytesAtProbe).toBeLessThan(result.total / 2);
157
+ // Queue peaks depend on real ACK timing and include encryption overhead.
158
+ // downstream-scheduler.test.js checks credit bounds with a controlled clock.
159
+ });
160
+ test('works without batching', async () => {
161
+ const result = await exercise({ batch: false });
162
+ expect(result.bytesAtProbe).toBeLessThan(result.total / 2);
163
+ });
164
+ test('cancelling a blocked stream leaves unrelated requests usable', async () => {
165
+ const result = await exercise({ cancel: true });
166
+ expect(result.bytesAtProbe).toBeLessThan(result.total);
167
+ });
168
+ test('fragmented WS messages remain complete and precede the close', async () => {
169
+ await exercise({ websocket: true });
170
+ });
171
+ for (const legacy of [{ clientFlow: false }, { hostFlow: false }]) {
172
+ test(`legacy fallback ${JSON.stringify(legacy)} preserves delivery`, async () => {
173
+ const result = await exercise(legacy);
174
+ expect(result.bytesAtProbe).toBe(result.total);
175
+ });
176
+ }
177
+ });
@@ -6,8 +6,9 @@
6
6
  import { WebSocket } from 'ws';
7
7
 
8
8
  import { RELAY_PROTOCOL_VERSION, RelayCloseCode, createHostHandshake } from './e2ee.js';
9
- import { createOutboundFrameBatcher, decodeFrameBatch } from './tunnel-codec.js';
9
+ import { createOutboundFrameBatcher, decodeFrameBatch, decodeTunnelFrame, decodeDeliveryAck, encodeFrameBatch, TunnelFrameType } from './tunnel-codec.js';
10
10
  import { createTunnelHost } from './tunnel-host.js';
11
+ import { createDownstreamScheduler, DOWNSTREAM_CHUNK_BYTES } from './downstream-scheduler.js';
11
12
 
12
13
  const BACKOFF_BASE_MS = 1000;
13
14
  const BACKOFF_CAP_MS = 30000;
@@ -33,7 +34,7 @@ const DEFAULT_BATCH_WINDOW_MS = 150;
33
34
  // the 150 ms default. Only applies on directions where batching was negotiated.
34
35
  const resolveBatchWindowMs = (option) => {
35
36
  if (Number.isFinite(option) && option >= 0) return option;
36
- const envValue = Number.parseInt(process.env.OPENCHAMBER_RELAY_BATCH_WINDOW_MS ?? '', 10);
37
+ const envValue = Number.parseInt(process.env.AIWORKER_RELAY_BATCH_WINDOW_MS ?? '', 10);
37
38
  if (Number.isFinite(envValue) && envValue >= 0) return envValue;
38
39
  return DEFAULT_BATCH_WINDOW_MS;
39
40
  };
@@ -48,7 +49,7 @@ const resolveBatchWindowMs = (option) => {
48
49
  * logger?: Pick<Console, 'warn'>,
49
50
  * }} options
50
51
  */
51
- export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, onStatus, logger = console, batchWindowMs, batch }) => {
52
+ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, onStatus, logger = console, batchWindowMs, batch, flowControl }) => {
52
53
  const resolveLocalPort = typeof getLocalPort === 'function' ? getLocalPort : () => localPort;
53
54
  const localBatch = batch !== false;
54
55
  const resolvedBatchWindowMs = resolveBatchWindowMs(batchWindowMs);
@@ -95,6 +96,7 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
95
96
  dataSockets.delete(connectionId);
96
97
  if (entry.openTimer) clearTimeout(entry.openTimer);
97
98
  entry.batcher?.dispose();
99
+ entry.scheduler?.close();
98
100
  entry.tunnel?.close();
99
101
  try {
100
102
  if (entry.socket.readyState === WebSocket.OPEN || entry.socket.readyState === WebSocket.CONNECTING) {
@@ -118,14 +120,14 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
118
120
  return;
119
121
  }
120
122
 
121
- const entry = { socket, tunnel: null, openTimer: null, batcher: null, lastActivityAt: Date.now() };
123
+ const entry = { socket, tunnel: null, openTimer: null, batcher: null, scheduler: null, lastActivityAt: Date.now() };
122
124
  dataSockets.set(connectionId, entry);
123
125
  entry.openTimer = setTimeout(() => {
124
126
  logger.warn('[Relay] host-data socket open timeout');
125
127
  teardownDataSocket(connectionId);
126
128
  }, DATA_SOCKET_OPEN_TIMEOUT_MS);
127
129
 
128
- const handshake = createHostHandshake(identity.hostEncPrivateKey, { batch: localBatch });
130
+ const handshake = createHostHandshake(identity.hostEncPrivateKey, { batch: localBatch, flowControl });
129
131
  let channel = null;
130
132
  let batchNegotiated = false;
131
133
  // Serialize async message handling so encrypted frame order (and the
@@ -140,11 +142,14 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
140
142
  .then(async () => {
141
143
  if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN || !channel) return;
142
144
  const encrypted = await channel.encryptor.encrypt(plaintext);
145
+ if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN) return;
143
146
  socket.send(encrypted, { binary: true });
144
147
  })
145
148
  .catch((error) => {
146
149
  logger.warn(`[Relay] host-data send failed: ${error?.message ?? error}`);
150
+ failChannel(RelayCloseCode.ChannelFailure, 'send failed');
147
151
  });
152
+ return sendChain;
148
153
  };
149
154
 
150
155
  const failChannel = (closeCode, reason) => {
@@ -167,17 +172,25 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
167
172
  } else if (action.type === 'established') {
168
173
  channel = action.channel;
169
174
  batchNegotiated = action.batch === true;
170
- entry.batcher = batchNegotiated
175
+ entry.scheduler = action.flowControl ? createDownstreamScheduler({
176
+ sendBatch: frames => sendEncryptedPlaintext(batchNegotiated ? encodeFrameBatch(frames) : frames[0]),
177
+ maxBatchFrames: batchNegotiated ? 32 : 1,
178
+ onError: () => failChannel(RelayCloseCode.ChannelFailure, 'downstream queue failed'),
179
+ }) : null;
180
+ entry.batcher = batchNegotiated && !entry.scheduler
171
181
  ? createOutboundFrameBatcher({ windowMs: resolvedBatchWindowMs, sendBatch: sendEncryptedPlaintext })
172
182
  : null;
173
183
  entry.tunnel = createTunnelHost({
174
184
  connectionId,
175
185
  getLocalPort: resolveLocalPort,
176
186
  getBufferedAmount: () => socket.bufferedAmount,
187
+ responseChunkBytes: entry.scheduler ? DOWNSTREAM_CHUNK_BYTES : undefined,
188
+ cancelPendingFrames: streamId => entry.scheduler?.cancel(streamId),
177
189
  sendFrame: (plaintextFrame) => {
178
190
  if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN) return;
191
+ if (entry.scheduler) return entry.scheduler.send(plaintextFrame);
179
192
  if (entry.batcher) entry.batcher.enqueue(plaintextFrame);
180
- else sendEncryptedPlaintext(plaintextFrame);
193
+ else return sendEncryptedPlaintext(plaintextFrame);
181
194
  },
182
195
  });
183
196
  if (action.replyText) socket.send(action.replyText);
@@ -205,16 +218,32 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
205
218
  // in order through the same per-frame handling as legacy.
206
219
  for (const frame of decodeFrameBatch(plaintext)) {
207
220
  if (dataSockets.get(connectionId) !== entry) return;
208
- await entry.tunnel.handleFrame(frame);
221
+ await dispatchFrame(frame);
209
222
  }
210
223
  } else {
211
- await entry.tunnel.handleFrame(plaintext);
224
+ await dispatchFrame(plaintext);
212
225
  }
213
226
  } catch (error) {
214
227
  logger.warn(`[Relay] tunnel frame handling failed: ${error?.message ?? error}`);
228
+ failChannel(RelayCloseCode.ChannelFailure, 'invalid tunnel frame');
215
229
  }
216
230
  };
217
231
 
232
+ const dispatchFrame = (plaintext) => {
233
+ const frame = decodeTunnelFrame(plaintext);
234
+ if (frame.frameType === TunnelFrameType.DeliveryAck) {
235
+ if (!entry.scheduler || frame.streamId !== 0 || frame.hasMoreFragments) {
236
+ throw new Error('unexpected delivery acknowledgement');
237
+ }
238
+ entry.scheduler.acknowledge(decodeDeliveryAck(frame.payload));
239
+ return;
240
+ }
241
+ // Never await outbound credit on the receive chain: ACKs use this chain too.
242
+ void entry.tunnel.handleFrame(plaintext).catch(() => {
243
+ failChannel(RelayCloseCode.ChannelFailure, 'invalid tunnel frame');
244
+ });
245
+ };
246
+
218
247
  socket.on('open', () => {
219
248
  if (entry.openTimer) {
220
249
  clearTimeout(entry.openTimer);
@@ -98,6 +98,9 @@ const startFakeRelay = () => {
98
98
  wsUrl: `ws://127.0.0.1:${port}`,
99
99
  state,
100
100
  stop: () => new Promise((r) => {
101
+ // A socket a failed test left open would hold server.close() until
102
+ // the hook timeout; drop them so a failure is reported once.
103
+ for (const client of wss.clients) client.terminate();
101
104
  wss.close();
102
105
  server.close(() => r());
103
106
  }),
@@ -162,7 +165,6 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
162
165
  url.searchParams.set('role', 'client');
163
166
  url.searchParams.set('serverId', serverId);
164
167
  url.searchParams.set('connectionId', connectionId);
165
- const ws = new WebSocket(url.toString());
166
168
 
167
169
  const hostPub = await globalThis.crypto.subtle.importKey(
168
170
  'jwk',
@@ -182,6 +184,12 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
182
184
  resolveDone = resolve;
183
185
  });
184
186
 
187
+ // Dialed only now, with the key material ready and the listeners attached
188
+ // in the same tick. Dialing before the WebCrypto awaits above let a loopback
189
+ // socket open while the key generation was still queued on the threadpool,
190
+ // and an `open` event with no listener means no hello, no ready, and a
191
+ // client that waits forever. Loaded CI runners hit exactly that.
192
+ const ws = new WebSocket(url.toString());
185
193
  ws.on('open', async () => {
186
194
  ws.send(JSON.stringify({
187
195
  t: 'hello',
@@ -1,4 +1,4 @@
1
- // Per-machine relay-host claim. Every OpenChamber instance on a machine shares
1
+ // Per-machine relay-host claim. Every AiWorker instance on a machine shares
2
2
  // the same data dir and therefore the same relay signing key / serverId, so if
3
3
  // two processes run a relay host at once they fight over the single host slot
4
4
  // at the relay worker (each new connection closes the previous one with
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Config lives in the server settings file as `settings.privateRelay =
5
5
  // { enabled, relayUrl }` (same storage precedent as tunnels/notifications).
6
- // Routes are registered with the other OpenChamber feature routes, before the
6
+ // Routes are registered with the other AiWorker feature routes, before the
7
7
  // generic OpenCode proxy, and are covered by the same global UI auth gate.
8
8
  //
9
9
  // Cross-runtime parity note: relay host mode intentionally targets the web
@@ -16,7 +16,7 @@ import express from 'express';
16
16
  import { createRelayIdentityRuntime } from './identity.js';
17
17
  import { startRelayHost } from './host-client.js';
18
18
 
19
- export const DEFAULT_RELAY_URL = 'wss://relay.openchamber.dev/ws';
19
+ export const DEFAULT_RELAY_URL = 'wss://relay.aiworker.dev/ws';
20
20
 
21
21
  const isValidRelayUrl = (value) => {
22
22
  if (typeof value !== 'string') return false;
@@ -40,7 +40,7 @@ const normalizeRelayUrl = (value) => {
40
40
  // stored setting entirely, so the host connection, the pairing offer, and the
41
41
  // status all point at it — clients then inherit it from the offer automatically.
42
42
  const envRelayUrlOverride = () => {
43
- const raw = process.env.OPENCHAMBER_RELAY_URL;
43
+ const raw = process.env.AIWORKER_RELAY_URL;
44
44
  if (typeof raw !== 'string' || !raw.trim() || !isValidRelayUrl(raw)) return null;
45
45
  return raw.trim();
46
46
  };
@@ -100,7 +100,7 @@ export const createRelayService = ({
100
100
  return {
101
101
  enabled: stored?.enabled === true,
102
102
  relayUrl: override ?? normalizeRelayUrl(stored?.relayUrl),
103
- // True when the endpoint is pinned by OPENCHAMBER_RELAY_URL (a self-hosted
103
+ // True when the endpoint is pinned by AIWORKER_RELAY_URL (a self-hosted
104
104
  // relay); the stored setting is ignored while it is set.
105
105
  relayUrlLocked: override !== null,
106
106
  };
@@ -122,7 +122,7 @@ export const createRelayService = ({
122
122
 
123
123
  const standbyStatus = (holderPid) => ({
124
124
  state: 'standby',
125
- lastError: `relay host is owned by another local OpenChamber process (pid ${holderPid})`,
125
+ lastError: `relay host is owned by another local AiWorker process (pid ${holderPid})`,
126
126
  connectedClients: 0,
127
127
  });
128
128
 
@@ -34,6 +34,7 @@ export const TunnelFrameType = {
34
34
  WsClose: 10,
35
35
  Ping: 11,
36
36
  Pong: 12,
37
+ DeliveryAck: 13,
37
38
  };
38
39
 
39
40
  const TUNNEL_FRAME_TYPE_VALUES = new Set(Object.values(TunnelFrameType));
@@ -43,6 +44,14 @@ export const isTunnelFrameType = (value) => TUNNEL_FRAME_TYPE_VALUES.has(value);
43
44
 
44
45
  const MAX_STREAM_ID = 0xffffffff;
45
46
 
47
+ /** Decode the client's cumulative raw tunnel-frame byte count. */
48
+ export const decodeDeliveryAck = (payload) => {
49
+ if (payload.length !== 8) throw new Error('invalid delivery acknowledgement');
50
+ const bytes = Number(new DataView(payload.buffer, payload.byteOffset, payload.byteLength).getBigUint64(0));
51
+ if (!Number.isSafeInteger(bytes)) throw new Error('invalid delivery acknowledgement');
52
+ return bytes;
53
+ };
54
+
46
55
  export class TunnelCodecError extends Error {
47
56
  constructor(message) {
48
57
  super(message);