@aiworker/web 1.22.0 → 1.22.2

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 (354) hide show
  1. package/README.md +4 -4
  2. package/bin/cli.js +7 -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-CSICpWQD.js} +1 -1
  28. package/dist/assets/{DiffView-DluiRSla.js → DiffView-hDBKesA_.js} +3 -3
  29. package/dist/assets/DirtyBranchSwitchDialog-C22HdWj4.js +1 -0
  30. package/dist/assets/FilesView-CdWzeOp9.js +17 -0
  31. package/dist/assets/GitView-VQQkYuUw.js +6 -0
  32. package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-6lrhwD3q.js} +1 -1
  33. package/dist/assets/LinearIssuesView-Ba6Q9569.js +1 -0
  34. package/dist/assets/{MarkdownImageGallery-ClOvuZxC.js → MarkdownImageGallery-Czv_mhvS.js} +1 -1
  35. package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +12 -0
  36. package/dist/assets/{OnboardingScreen-D2DG7K6b.js → OnboardingScreen-ChxQoAkx.js} +1 -1
  37. package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-BzcZPlOq.js} +1 -1
  38. package/dist/assets/{PierreDiffViewer-B7m3f0d7.js → PierreDiffViewer-BchBtZlp.js} +2 -2
  39. package/dist/assets/PlanView-DyDt_nZ_.js +5 -0
  40. package/dist/assets/SettingsView-BKf5xBsP.js +28 -0
  41. package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-DDQYaKWQ.js} +1 -1
  42. package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-iYF8Y2bz.js} +6 -6
  43. package/dist/assets/ToolPartDiffPreview-CdjbOVMM.js +8 -0
  44. package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-fFFm2QgH.js} +1 -1
  45. package/dist/assets/{WalkthroughView-egQgxel3.js → WalkthroughView-DOeCsMju.js} +2 -2
  46. package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-CzgLum_K.js} +2 -2
  47. package/dist/assets/{contextPanelEmbeddedChat-C3fjDbdg.js → contextPanelEmbeddedChat-C0ZLAepL.js} +1 -1
  48. package/dist/assets/{de-C1P6X5Rs.js → de-B5JaA3SA.js} +6 -5
  49. package/dist/assets/{es-CDgaF1_t.js → es-EPIH88ii.js} +6 -5
  50. package/dist/assets/{fr-Bgxqvvz_.js → fr-CAMfxUdZ.js} +6 -5
  51. package/dist/assets/{index-CODwDsU_.js → index-Bl3LSbjn.js} +1 -1
  52. package/dist/assets/index-DF5kMP-i.js +39 -0
  53. package/dist/assets/index-Dl3xbAB_.css +1 -0
  54. package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-Dx-dB_8l.js} +1 -1
  55. package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-CWFPwmnU.js} +1 -1
  56. package/dist/assets/{ja-BDRrEcNP.js → ja-CifOE1VN.js} +7 -6
  57. package/dist/assets/{ko-Ba8sQV79.js → ko-BBDyAyYQ.js} +7 -6
  58. package/dist/assets/{main-C5DVOxc6.js → main-DnBAoktx.js} +2 -2
  59. package/dist/assets/main-MhSaOZoB.js +930 -0
  60. package/dist/assets/{markdownCore-CZwzz5gw.js → markdownCore-BrBwOOMn.js} +2 -2
  61. package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-BKyvLseN.js} +2 -2
  62. package/dist/assets/{mobile-CcaxB0n4.js → mobile-DKjeFgMN.js} +2 -2
  63. package/dist/assets/{patchFileDiff-yNfWXN4E.js → patchFileDiff-o8ms1me8.js} +1 -1
  64. package/dist/assets/pl-DgEHYfFM.js +16 -0
  65. package/dist/assets/{pt-BR-DIs6ElwV.js → pt-BR-DuWHojUw.js} +6 -5
  66. package/dist/assets/{renderElectronMiniChatApp-BcKLHXKe.js → renderElectronMiniChatApp-CDgBwwCS.js} +2 -2
  67. package/dist/assets/renderMobileApp-DJIKIK1r.js +25 -0
  68. package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +17 -0
  69. package/dist/assets/{scheduledTasksApi-DLW0mkQK.js → scheduledTasksApi-D8yamh1b.js} +1 -1
  70. package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-TEQCmzW8.js} +1 -1
  71. package/dist/assets/tr-D5m2yIWL.js +7 -0
  72. package/dist/assets/{uk-ZrCNjM7Y.js → uk-D_q60oog.js} +6 -5
  73. package/dist/assets/{useAppFontEffects-CjrntdL0.js → useAppFontEffects-D7sNpuL2.js} +161 -190
  74. package/dist/assets/useInlineCommentController-Brz_cLcQ.js +2 -0
  75. package/dist/assets/{useRootScrollLock-BHYPkrLv.js → useRootScrollLock-lXEnWlaU.js} +1 -1
  76. package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Dp3KMRQG.js} +2 -2
  77. package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
  78. package/dist/assets/{web-components-Bhg83tjf.js → web-components-_mNEMjFY.js} +1 -1
  79. package/dist/assets/{zh-CN-iv_AV4EC.js → zh-CN-D8QvsVeg.js} +6 -5
  80. package/dist/assets/{zh-TW-CzEUk38j.js → zh-TW-B9t7rQQO.js} +6 -5
  81. package/dist/index.html +22 -22
  82. package/dist/mini-chat.html +7 -7
  83. package/dist/mobile.html +5 -5
  84. package/package.json +3 -3
  85. package/server/index.js +118 -85
  86. package/server/lib/agent-memory/actions.js +2 -2
  87. package/server/lib/agent-memory/feature-flag.js +1 -1
  88. package/server/lib/agent-memory/feature-flag.test.js +8 -8
  89. package/server/lib/agent-memory/routes.js +2 -2
  90. package/server/lib/agent-tool/runtime.js +37 -54
  91. package/server/lib/agent-tool/runtime.test.js +59 -59
  92. package/server/lib/browser-control/broker.js +3 -3
  93. package/server/lib/browser-control/broker.test.js +1 -1
  94. package/server/lib/changelog/update-notes.js +82 -0
  95. package/server/lib/changelog/update-notes.test.js +59 -0
  96. package/server/lib/client-auth/remote-clients.test.js +5 -5
  97. package/server/lib/context-obligatory/runtime.js +7 -7
  98. package/server/lib/context-obligatory/runtime.test.js +5 -5
  99. package/server/lib/dev-servers/parse.js +2 -2
  100. package/server/lib/dev-servers/parse.test.js +2 -2
  101. package/server/lib/dev-servers/routes.js +1 -1
  102. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  103. package/server/lib/dev-tunnel/client.js +1 -1
  104. package/server/lib/dev-tunnel/runtime.js +5 -5
  105. package/server/lib/dev-tunnel/tunnel.test.js +21 -1
  106. package/server/lib/dictation/local/model-catalog.js +1 -1
  107. package/server/lib/dictation/local/worker-process.js +2 -2
  108. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  109. package/server/lib/event-stream/global-ws-bridge.js +1 -1
  110. package/server/lib/event-stream/protocol.test.js +4 -4
  111. package/server/lib/event-stream/runtime.test.js +6 -6
  112. package/server/lib/fs/DOCUMENTATION.md +2 -0
  113. package/server/lib/fs/routes.js +37 -26
  114. package/server/lib/fs/routes.test.js +98 -21
  115. package/server/lib/git/DOCUMENTATION.md +3 -1
  116. package/server/lib/git/identity-storage.js +1 -1
  117. package/server/lib/git/issue-2746-longpaths.test.js +4 -4
  118. package/server/lib/git/routes.js +17 -1
  119. package/server/lib/git/service.js +116 -26
  120. package/server/lib/git/service.test.js +174 -3
  121. package/server/lib/github/auth.js +8 -8
  122. package/server/lib/github/pr-status.js +1 -1
  123. package/server/lib/github/routes.js +2 -2
  124. package/server/lib/inherited-env.js +1 -1
  125. package/server/lib/inherited-env.test.js +2 -2
  126. package/server/lib/linear/auth.js +8 -8
  127. package/server/lib/linear/auth.test.js +24 -24
  128. package/server/lib/linear/issues.test.js +6 -6
  129. package/server/lib/linear/mapping.test.js +4 -4
  130. package/server/lib/linear/oauth.js +2 -2
  131. package/server/lib/linear/oauth.test.js +14 -14
  132. package/server/lib/linear/routes.js +5 -5
  133. package/server/lib/linear/routes.test.js +16 -16
  134. package/server/lib/linear/status-runtime.test.js +9 -9
  135. package/server/lib/linear/status.js +1 -1
  136. package/server/lib/linear/status.test.js +15 -15
  137. package/server/lib/linear/teams.test.js +4 -4
  138. package/server/lib/magic-prompts/routes.js +2 -2
  139. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  140. package/server/lib/mcp-reconnect/runtime.js +122 -0
  141. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  142. package/server/lib/message-queue/DOCUMENTATION.md +158 -0
  143. package/server/lib/message-queue/runtime.js +870 -0
  144. package/server/lib/message-queue/runtime.test.js +482 -0
  145. package/server/lib/notifications/apns-runtime.js +17 -17
  146. package/server/lib/notifications/apns-runtime.test.js +11 -11
  147. package/server/lib/notifications/emitter-runtime.js +1 -1
  148. package/server/lib/notifications/emitter-runtime.test.js +1 -1
  149. package/server/lib/notifications/push-runtime.js +7 -7
  150. package/server/lib/notifications/routes.js +1 -1
  151. package/server/lib/notifications/runtime.js +1 -1
  152. package/server/lib/openchamber-control/action-resolution.test.js +10 -10
  153. package/server/lib/openchamber-control/actions.js +24 -24
  154. package/server/lib/openchamber-control/error.js +4 -4
  155. package/server/lib/openchamber-control/routes.js +2 -2
  156. package/server/lib/openchamber-control/routes.test.js +5 -5
  157. package/server/lib/openchamber-control/screenshots.js +1 -1
  158. package/server/lib/openchamber-control/screenshots.test.js +1 -1
  159. package/server/lib/openchamber-control/service.js +50 -50
  160. package/server/lib/openchamber-control/service.test.js +5 -5
  161. package/server/lib/openchamber-sessions/routes.js +124 -22
  162. package/server/lib/openchamber-sessions/routes.test.js +99 -11
  163. package/server/lib/opencode/DOCUMENTATION.md +13 -5
  164. package/server/lib/opencode/auth.js +1 -1
  165. package/server/lib/opencode/bootstrap-runtime.js +6 -6
  166. package/server/lib/opencode/cli-options.js +8 -8
  167. package/server/lib/opencode/config-paths.test.js +116 -0
  168. package/server/lib/opencode/core-routes.js +8 -7
  169. package/server/lib/opencode/core-routes.test.js +11 -11
  170. package/server/lib/opencode/env-config.js +5 -5
  171. package/server/lib/opencode/env-config.test.js +7 -7
  172. package/server/lib/opencode/env-runtime.js +10 -10
  173. package/server/lib/opencode/env-runtime.test.js +18 -18
  174. package/server/lib/opencode/feature-routes-runtime.js +17 -12
  175. package/server/lib/opencode/hmr-state-runtime.js +2 -2
  176. package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
  177. package/server/lib/opencode/input-history-scope.js +14 -0
  178. package/server/lib/opencode/lifecycle.js +8 -8
  179. package/server/lib/opencode/lifecycle.test.js +4 -4
  180. package/server/lib/opencode/managed-plugin-config.js +35 -0
  181. package/server/lib/opencode/managed-process-registry.js +3 -3
  182. package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
  183. package/server/lib/opencode/openchamber-routes.js +15 -15
  184. package/server/lib/opencode/openchamber-routes.test.js +11 -11
  185. package/server/lib/opencode/plugins.js +2 -2
  186. package/server/lib/opencode/plugins.test.js +1 -1
  187. package/server/lib/opencode/project-icon-routes.js +2 -2
  188. package/server/lib/opencode/project-icon-routes.test.js +1 -1
  189. package/server/lib/opencode/providers.js +44 -2
  190. package/server/lib/opencode/providers.test.js +131 -4
  191. package/server/lib/opencode/proxy.js +2 -2
  192. package/server/lib/opencode/pwa-manifest-routes.js +1 -1
  193. package/server/lib/opencode/restart-session-recovery.test.js +1 -1
  194. package/server/lib/opencode/routes-upgrade.test.js +4 -4
  195. package/server/lib/opencode/routes.js +14 -14
  196. package/server/lib/opencode/server-startup-runtime.js +6 -6
  197. package/server/lib/opencode/session-runtime.js +3 -3
  198. package/server/lib/opencode/session-runtime.test.js +6 -6
  199. package/server/lib/opencode/settings-helpers.js +31 -3
  200. package/server/lib/opencode/settings-helpers.test.js +189 -12
  201. package/server/lib/opencode/settings-runtime.js +2 -2
  202. package/server/lib/opencode/settings-runtime.test.js +29 -0
  203. package/server/lib/opencode/shared.js +5 -2
  204. package/server/lib/opencode/shared.test.js +7 -7
  205. package/server/lib/opencode/shutdown-runtime.js +2 -0
  206. package/server/lib/opencode/skill-routes.js +2 -1
  207. package/server/lib/opencode/snippets.js +1 -2
  208. package/server/lib/opencode/startup-performance.js +1 -1
  209. package/server/lib/opencode/startup-performance.test.js +6 -6
  210. package/server/lib/opencode/static-routes-runtime.js +8 -8
  211. package/server/lib/opencode/static-routes-runtime.test.js +7 -7
  212. package/server/lib/opencode/upgrade-capability.js +2 -2
  213. package/server/lib/opencode/upgrade-capability.test.js +3 -3
  214. package/server/lib/package-manager.js +17 -42
  215. package/server/lib/package-manager.test.js +19 -19
  216. package/server/lib/permission-auto-accept/runtime.js +1 -1
  217. package/server/lib/project-context/routes.js +1 -1
  218. package/server/lib/projects/project-config.js +2 -2
  219. package/server/lib/quota/DOCUMENTATION.md +9 -1
  220. package/server/lib/quota/credentials/providers.js +4 -0
  221. package/server/lib/quota/credentials/store.js +4 -4
  222. package/server/lib/quota/credentials/store.test.js +8 -8
  223. package/server/lib/quota/index.js +1 -0
  224. package/server/lib/quota/providers/exe-dev.js +75 -0
  225. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  226. package/server/lib/quota/providers/hyper.js +118 -0
  227. package/server/lib/quota/providers/hyper.test.js +126 -0
  228. package/server/lib/quota/providers/index.js +15 -0
  229. package/server/lib/quota/providers/ollama-cloud.js +1 -1
  230. package/server/lib/quota/providers/opencode-go.js +2 -1
  231. package/server/lib/quota/providers/opencode-go.test.js +9 -5
  232. package/server/lib/quota/providers/xai.js +1 -1
  233. package/server/lib/quota/routes.js +2 -0
  234. package/server/lib/quota/utils/auth.js +1 -1
  235. package/server/lib/relay/DOCUMENTATION.md +1 -1
  236. package/server/lib/relay/host-client.js +1 -1
  237. package/server/lib/relay/host-client.test.js +9 -1
  238. package/server/lib/relay/host-lock.js +1 -1
  239. package/server/lib/relay/service.js +5 -5
  240. package/server/lib/relay/tunnel-host.js +3 -1
  241. package/server/lib/relay/tunnel-host.test.js +9 -1
  242. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
  243. package/server/lib/scheduled-tasks/routes.js +5 -5
  244. package/server/lib/scheduled-tasks/service.js +25 -25
  245. package/server/lib/scheduled-tasks/service.test.js +1 -1
  246. package/server/lib/security/bind-host.js +4 -4
  247. package/server/lib/security/bind-host.test.js +1 -1
  248. package/server/lib/security/request-security.js +11 -5
  249. package/server/lib/security/request-security.test.js +14 -14
  250. package/server/lib/session-assist/runtime.js +10 -9
  251. package/server/lib/session-folders/routes.js +2 -2
  252. package/server/lib/session-folders/routes.test.js +8 -8
  253. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  254. package/server/lib/session-goal/create.js +4 -2
  255. package/server/lib/session-goal/create.test.js +1 -1
  256. package/server/lib/session-goal/objectives.js +4 -4
  257. package/server/lib/session-goal/routes.js +1 -1
  258. package/server/lib/session-goal/runtime.js +88 -22
  259. package/server/lib/session-goal/runtime.test.js +428 -2
  260. package/server/lib/session-knowledge/runtime.js +11 -11
  261. package/server/lib/session-knowledge/runtime.test.js +5 -5
  262. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  263. package/server/lib/skills-catalog/cache.test.js +2 -2
  264. package/server/lib/skills-catalog/disk-cache.js +5 -5
  265. package/server/lib/skills-catalog/github-meta.test.js +2 -2
  266. package/server/lib/skills-catalog/install.js +3 -2
  267. package/server/lib/small-model/DOCUMENTATION.md +4 -0
  268. package/server/lib/small-model/call.js +9 -3
  269. package/server/lib/small-model/call.test.js +29 -6
  270. package/server/lib/small-model/index.js +8 -7
  271. package/server/lib/small-model/index.test.js +1 -1
  272. package/server/lib/small-model/resolve.js +1 -1
  273. package/server/lib/small-model/resolve.test.js +1 -1
  274. package/server/lib/small-model/routes.js +2 -1
  275. package/server/lib/system-prompt/runtime.js +3 -22
  276. package/server/lib/system-prompt/runtime.test.js +3 -3
  277. package/server/lib/terminal/DOCUMENTATION.md +11 -7
  278. package/server/lib/terminal/runtime.js +180 -26
  279. package/server/lib/terminal/runtime.test.js +554 -27
  280. package/server/lib/terminal/shells.js +20 -3
  281. package/server/lib/terminal/shells.test.js +33 -2
  282. package/server/lib/tts/base-url.js +3 -3
  283. package/server/lib/tts/routes.test.js +20 -20
  284. package/server/lib/ui-auth/ui-auth.js +12 -9
  285. package/server/lib/ui-auth/ui-auth.test.js +19 -3
  286. package/server/lib/ui-auth/ui-passkeys.js +7 -7
  287. package/server/lib/walkthrough/index.js +1 -0
  288. package/server/lib/walkthrough/jobs.test.js +1 -1
  289. package/server/lib/walkthrough/language.test.js +1 -1
  290. package/server/lib/walkthrough/model-settings.js +3 -3
  291. package/server/lib/walkthrough/model-settings.test.js +1 -1
  292. package/server/lib/walkthrough/pull-request.test.js +5 -5
  293. package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
  294. package/server/lib/walkthrough/routes.test.js +26 -5
  295. package/server/lib/walkthrough/store.js +3 -3
  296. package/server/lib/walkthrough/store.test.js +1 -1
  297. package/server/opencode-proxy.test.js +2 -2
  298. package/server/proxy-headers.js +1 -1
  299. package/server/sse-routes.test.js +4 -4
  300. package/README.md.bak +0 -250
  301. package/bin/lib/DOCUMENTATION.md.bak +0 -163
  302. package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
  303. package/dist/assets/FilesView-Bdq8ppva.js +0 -17
  304. package/dist/assets/GitView-Znn7VJcP.js +0 -6
  305. package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
  306. package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
  307. package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
  308. package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
  309. package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
  310. package/dist/assets/index-C57jT0Sa.css +0 -1
  311. package/dist/assets/index-DW_E3rnL.js +0 -7
  312. package/dist/assets/main-Bv5Pkngh.js +0 -928
  313. package/dist/assets/pl-Dioz29BI.js +0 -15
  314. package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
  315. package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
  316. package/dist/assets/tr-DFnMlPxY.js +0 -6
  317. package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
  318. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
  319. package/server/index.d.ts.bak +0 -39
  320. package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
  321. package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
  322. package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
  323. package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
  324. package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
  325. package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
  326. package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
  327. package/server/lib/git/DOCUMENTATION.md.bak +0 -193
  328. package/server/lib/github/DOCUMENTATION.md.bak +0 -183
  329. package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
  330. package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
  331. package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
  332. package/server/lib/notifications/APNS.md.bak +0 -135
  333. package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
  334. package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
  335. package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
  336. package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
  337. package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
  338. package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
  339. package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
  340. package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
  341. package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
  342. package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
  343. package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
  344. package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
  345. package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
  346. package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
  347. package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
  348. package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
  349. package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
  350. package/server/lib/text/DOCUMENTATION.md.bak +0 -35
  351. package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
  352. package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
  353. package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
  354. package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
@@ -1,5 +1,5 @@
1
1
  // Session goal: a persisted, self-continuing objective attached to a session
2
- // (metadata.openchamber.goal). While the goal is active, the server keeps the
2
+ // (metadata.aiworker.goal). While the goal is active, the server keeps the
3
3
  // session working toward it: after each busy→idle transition it accounts token
4
4
  // usage, asks the small model to audit progress (continue / complete /
5
5
  // blocked), and either re-prompts the session's own model with a continuation
@@ -20,16 +20,16 @@ import path from 'path';
20
20
 
21
21
  import { GOAL_OBJECTIVE_CHAR_LIMIT, readObjective } from './objectives.js';
22
22
 
23
- const OPENCHAMBER_SETTINGS_FILE = path.join(
24
- process.env.OPENCHAMBER_DATA_DIR
25
- ? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
26
- : path.join(os.homedir(), '.config', 'openchamber'),
23
+ const AIWORKER_SETTINGS_FILE = path.join(
24
+ process.env.AIWORKER_DATA_DIR
25
+ ? path.resolve(process.env.AIWORKER_DATA_DIR)
26
+ : path.join(os.homedir(), '.config', 'aiworker'),
27
27
  'settings.json',
28
28
  );
29
29
 
30
30
  const isSessionGoalEnabled = () => {
31
31
  try {
32
- const raw = fs.readFileSync(OPENCHAMBER_SETTINGS_FILE, 'utf8');
32
+ const raw = fs.readFileSync(AIWORKER_SETTINGS_FILE, 'utf8');
33
33
  const settings = JSON.parse(raw);
34
34
  return settings?.sessionGoalEnabled !== false;
35
35
  } catch {
@@ -188,7 +188,7 @@ const extractSessionUpdate = (payload) => {
188
188
  const parseGoalMetadata = (session) => {
189
189
  const metadata = session?.metadata;
190
190
  if (!metadata || typeof metadata !== 'object') return null;
191
- const namespace = metadata.openchamber;
191
+ const namespace = metadata.aiworker;
192
192
  if (!namespace || typeof namespace !== 'object') return null;
193
193
  const goal = namespace.goal;
194
194
  if (!goal || typeof goal !== 'object') return null;
@@ -244,6 +244,57 @@ const messageTokenTotal = (info) => {
244
244
  return input + cachedRead + output;
245
245
  };
246
246
 
247
+ const getErrorName = (error) => error?.name?.trim?.() ?? '';
248
+
249
+ const isLengthTruncated = (info, errorName = getErrorName(info?.error)) => {
250
+ const error = info?.error;
251
+ const hasError = error !== null && error !== undefined;
252
+ return errorName === 'MessageOutputLengthError' || (!hasError && info?.finish === 'length');
253
+ };
254
+
255
+ // Summary messages are assistant-shaped, but they are compaction turns rather
256
+ // than agent turns. They must not break or satisfy the consecutive truncation
257
+ // check; only completed, non-summary assistant turns participate. Chronology
258
+ // comes from `time.created`, never from message IDs; array position is only a
259
+ // tie-breaker for equal timestamps.
260
+ const hasRepeatedLengthTail = (messages, latestAssistant, goalCreatedAt) => {
261
+ const latestInfo = latestAssistant?.info;
262
+ if (latestInfo?.summary === true) return false;
263
+ const latestIndex = messages.indexOf(latestAssistant);
264
+ const latestCreated = latestInfo?.time?.created;
265
+ if (
266
+ latestIndex < 0
267
+ || !(latestInfo?.time?.completed > 0)
268
+ || !(Number.isFinite(latestCreated) && latestCreated > 0)
269
+ || !isLengthTruncated(latestInfo)
270
+ ) return false;
271
+
272
+ let previous = null;
273
+ for (let i = 0; i < messages.length; i += 1) {
274
+ const info = messages[i]?.info;
275
+ if (info?.role !== 'assistant' || info.summary === true || !(info.time?.completed > 0)) continue;
276
+ const created = info.time?.created;
277
+ // An unknown timestamp cannot safely participate in chronology. Ignore it
278
+ // rather than letting an unrelated older message hide known chronology.
279
+ if (!(Number.isFinite(created) && created > 0)) continue;
280
+ if (i === latestIndex) continue;
281
+ if (created > latestCreated || (created === latestCreated && i > latestIndex)) continue;
282
+ if (
283
+ !previous
284
+ || created > previous.created
285
+ || (created === previous.created && i > previous.index)
286
+ ) {
287
+ previous = { info, created, index: i };
288
+ }
289
+ }
290
+
291
+ return Boolean(
292
+ previous
293
+ && previous.created > goalCreatedAt
294
+ && isLengthTruncated(previous.info),
295
+ );
296
+ };
297
+
247
298
  export const createSessionGoalRuntime = ({
248
299
  buildOpenCodeUrl,
249
300
  getOpenCodeAuthHeaders,
@@ -319,8 +370,8 @@ export const createSessionGoalRuntime = ({
319
370
  if (!currentGoal || currentGoal.id !== expectedGoalId) return null;
320
371
  const nextGoal = { ...currentGoal, ...mutate(currentGoal), updatedAt: Date.now() };
321
372
  const currentMetadata = session?.metadata && typeof session.metadata === 'object' ? session.metadata : {};
322
- const currentNamespace = currentMetadata.openchamber && typeof currentMetadata.openchamber === 'object'
323
- ? currentMetadata.openchamber
373
+ const currentNamespace = currentMetadata.aiworker && typeof currentMetadata.aiworker === 'object'
374
+ ? currentMetadata.aiworker
324
375
  : {};
325
376
  await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
326
377
  directory,
@@ -328,7 +379,7 @@ export const createSessionGoalRuntime = ({
328
379
  body: {
329
380
  metadata: {
330
381
  ...currentMetadata,
331
- openchamber: { ...currentNamespace, goal: nextGoal },
382
+ aiworker: { ...currentNamespace, goal: nextGoal },
332
383
  },
333
384
  },
334
385
  });
@@ -378,6 +429,7 @@ export const createSessionGoalRuntime = ({
378
429
  prompt: `The goal objective:\n\n<objective>\n${goal.objective}\n</objective>\n\nThe agent's latest turn:\n\n${assistantText}\n\nReturn the verdict JSON. Write the note in the SAME language as this sample from the objective: "${goal.objective.slice(0, 200).replace(/\s+/g, ' ').trim()}"`,
379
430
  system: buildAuditSystemPrompt(),
380
431
  directory,
432
+ sessionID: typeof lastAssistantInfo?.sessionID === 'string' ? lastAssistantInfo.sessionID : undefined,
381
433
  preferredProviderID: typeof lastAssistantInfo?.providerID === 'string' ? lastAssistantInfo.providerID : undefined,
382
434
  preferredModelID: typeof lastAssistantInfo?.modelID === 'string' ? lastAssistantInfo.modelID : undefined,
383
435
  });
@@ -460,7 +512,7 @@ export const createSessionGoalRuntime = ({
460
512
  if (!goal || goal.status !== 'active') return;
461
513
 
462
514
  // File-backed objectives: the metadata carries only a flag; the objective
463
- // TEXT lives under the OpenChamber data dir keyed by session id and is
515
+ // TEXT lives under the AiWorker data dir keyed by session id and is
464
516
  // read fresh on every tick (live-editable). A missing file falls back to
465
517
  // whatever inline objective the metadata still has — the goal must never
466
518
  // die just because a file went away.
@@ -608,7 +660,11 @@ export const createSessionGoalRuntime = ({
608
660
  // resumed over an aborted tail: that is an explicit "keep going", so it
609
661
  // falls through to the continuation below (skipping the audit — an
610
662
  // aborted reply is not evidence of anything).
611
- const abortedTail = lastAssistantInfo.error?.name === 'MessageAbortedError';
663
+ const error = lastAssistantInfo.error;
664
+ const errorName = getErrorName(error);
665
+ const hasError = error !== null && error !== undefined;
666
+ const abortedTail = errorName === 'MessageAbortedError';
667
+ const lengthTail = isLengthTruncated(lastAssistantInfo, errorName);
612
668
  if (abortedTail && goal.statusReason !== 'resumed') {
613
669
  await writeGoal(sessionId, directory, goal.id, () => ({
614
670
  status: 'paused',
@@ -622,13 +678,12 @@ export const createSessionGoalRuntime = ({
622
678
  return;
623
679
  }
624
680
 
625
- // Turn error → blocked (prevents runaway auto-continuation into failures).
626
- if (!abortedTail && lastAssistantInfo.error && typeof lastAssistantInfo.error === 'object') {
627
- const reason = typeof lastAssistantInfo.error.name === 'string' && lastAssistantInfo.error.name
628
- ? lastAssistantInfo.error.name
629
- : 'assistant turn failed';
681
+ // Non-length turn error → blocked (prevents runaway auto-continuation into
682
+ // failures). Recognized length cutoffs are in-progress continuations, not
683
+ // hard failures.
684
+ if (!abortedTail && !lengthTail && hasError) {
630
685
  await settleGoal({
631
- sessionId, directory, goal, status: 'blocked', statusReason: reason, tokensUsed, tokensBaseline, tokensCommitted, lastAccountedMessageID,
686
+ sessionId, directory, goal, status: 'blocked', statusReason: errorName || 'assistant turn failed', tokensUsed, tokensBaseline, tokensCommitted, lastAccountedMessageID,
632
687
  });
633
688
  return;
634
689
  }
@@ -649,17 +704,28 @@ export const createSessionGoalRuntime = ({
649
704
  return;
650
705
  }
651
706
 
707
+ // A second consecutive completed, non-summary length-truncated turn is a
708
+ // bounded recovery failure. Derive this from the loaded transcript rather
709
+ // than persisting another goal counter.
710
+ if (lengthTail && goal.statusReason !== 'resumed' && hasRepeatedLengthTail(messages, lastAssistant, goal.createdAt)) {
711
+ await settleGoal({
712
+ sessionId, directory, goal, status: 'blocked', statusReason: 'repeated output truncation', tokensUsed, tokensBaseline, tokensCommitted, lastAccountedMessageID,
713
+ });
714
+ return;
715
+ }
716
+
652
717
  // --- Small-model audit: the sole termination authority besides the hard
653
718
  // stops above (turn error, budget, continuation cap). The working agent
654
719
  // has no channel to settle its own goal.
655
720
  //
656
- // Exception: when the latest message is a compaction summary, the agent
657
- // by definition ran into the context window mid-work that IS
658
- // "in progress, not finished". No audit call; continue unconditionally.
721
+ // Exception: when the latest message is a compaction summary or was cut off
722
+ // by the output token limit (length stop), the agent by definition ran into
723
+ // the context/output limit mid-work — that IS "in progress, not finished".
724
+ // No audit call; continue unconditionally.
659
725
  let audit = null;
660
726
  let blockedStreak = 0;
661
727
  let auditFailStreak = goal.auditFailStreak;
662
- if (lastAssistantInfo.summary === true || abortedTail) {
728
+ if (lastAssistantInfo.summary === true || abortedTail || lengthTail) {
663
729
  blockedStreak = goal.blockedStreak;
664
730
  } else {
665
731
  audit = await runAudit({ goal: { ...goal, objective: effectiveObjective }, assistantText, directory, lastAssistantInfo: executionInfo ?? lastAssistantInfo });
@@ -18,7 +18,7 @@ const goal = {
18
18
  const session = {
19
19
  id: SESSION_ID,
20
20
  directory: DIRECTORY,
21
- metadata: { openchamber: { goal } },
21
+ metadata: { aiworker: { goal } },
22
22
  };
23
23
 
24
24
  const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
@@ -46,6 +46,78 @@ const startIdleTick = async (fetchImpl) => {
46
46
  return { runtime, getSmallModelService };
47
47
  };
48
48
 
49
+ const assistantMessage = (id, infoOverrides = {}) => ({
50
+ info: {
51
+ id,
52
+ sessionID: SESSION_ID,
53
+ role: 'assistant',
54
+ providerID: 'provider',
55
+ modelID: 'model',
56
+ time: { created: 2, completed: 2 },
57
+ tokens: { input: 1, output: 1, cache: { read: 0 } },
58
+ ...infoOverrides,
59
+ },
60
+ parts: [{ type: 'text', text: 'The agent made progress.' }],
61
+ });
62
+
63
+ const createRuntimeHarness = ({ messages, messageFactory, goalOverrides = {}, maxAutoTurns = 20 }) => {
64
+ const requests = [];
65
+ let messageFetchCount = 0;
66
+ const activeSession = {
67
+ ...session,
68
+ metadata: { aiworker: { goal: { ...goal, ...goalOverrides } } },
69
+ };
70
+ const service = {
71
+ generateSmallModelText: vi.fn(async () => ({
72
+ text: '{"verdict":"continue","note":"More work remains"}',
73
+ providerID: 'provider',
74
+ modelID: 'model',
75
+ })),
76
+ };
77
+ const fetchImpl = vi.fn(async (input, init = {}) => {
78
+ const pathname = requestPath(input);
79
+ requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
80
+ if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') {
81
+ activeSession.metadata = JSON.parse(init.body).metadata;
82
+ return jsonResponse(activeSession);
83
+ }
84
+ if (pathname === `/session/${SESSION_ID}`) return jsonResponse(activeSession);
85
+ if (pathname === '/session/status') return jsonResponse({});
86
+ if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
87
+ if (pathname === `/session/${SESSION_ID}/message`) {
88
+ const nextMessages = messageFactory ? messageFactory(messageFetchCount) : messages;
89
+ messageFetchCount += 1;
90
+ return jsonResponse(nextMessages);
91
+ }
92
+ if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
93
+ throw new Error(`Unexpected request: ${pathname}`);
94
+ });
95
+ vi.stubGlobal('fetch', fetchImpl);
96
+ const runtime = createSessionGoalRuntime({
97
+ buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
98
+ getOpenCodeAuthHeaders: () => ({}),
99
+ getSmallModelService: async () => service,
100
+ isEnabled: () => true,
101
+ idleQuietMs: 10,
102
+ maxAutoTurns,
103
+ });
104
+ return { runtime, requests, service, activeSession };
105
+ };
106
+
107
+ const runIdleTick = async (runtime) => {
108
+ runtime.processPayload({
109
+ type: 'session.status',
110
+ properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
111
+ });
112
+ await vi.runOnlyPendingTimersAsync();
113
+ };
114
+
115
+ const lastPatchedGoal = (requests) => {
116
+ const patches = requests.filter((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
117
+ expect(patches.length).toBeGreaterThan(0);
118
+ return JSON.parse(patches.at(-1).body).metadata.aiworker.goal;
119
+ };
120
+
49
121
  describe('session goal live activity gate', () => {
50
122
  beforeEach(() => {
51
123
  vi.useFakeTimers();
@@ -170,7 +242,7 @@ describe('session goal live activity gate', () => {
170
242
  expect(service.generateSmallModelText).toHaveBeenCalledOnce();
171
243
  const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
172
244
  expect(patch).toBeDefined();
173
- const writtenGoal = JSON.parse(patch.body).metadata.openchamber.goal;
245
+ const writtenGoal = JSON.parse(patch.body).metadata.aiworker.goal;
174
246
  expect(writtenGoal).toMatchObject({
175
247
  status: 'complete',
176
248
  evaluationProviderID: 'provider',
@@ -178,4 +250,358 @@ describe('session goal live activity gate', () => {
178
250
  });
179
251
  runtime.stop();
180
252
  });
253
+
254
+ it('skips audit and sends continuation prompt when assistant message finishes with length stop', async () => {
255
+ const requests = [];
256
+ const fetchImpl = vi.fn(async (input, init = {}) => {
257
+ const pathname = requestPath(input);
258
+ requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
259
+ if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') return jsonResponse(session);
260
+ if (pathname === `/session/${SESSION_ID}`) return jsonResponse(session);
261
+ if (pathname === '/session/status') return jsonResponse({});
262
+ if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
263
+ if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
264
+ if (pathname === `/session/${SESSION_ID}/message`) {
265
+ return jsonResponse([{
266
+ info: {
267
+ id: 'msg_assistant_len',
268
+ sessionID: SESSION_ID,
269
+ role: 'assistant',
270
+ providerID: 'provider',
271
+ modelID: 'model',
272
+ finish: 'length',
273
+ time: { completed: 2 },
274
+ tokens: { input: 100, output: 4096, reasoning: 4096, cache: { read: 0 } },
275
+ },
276
+ parts: [{ type: 'reasoning', text: 'Drafting extensive implementation...' }],
277
+ }]);
278
+ }
279
+ throw new Error(`Unexpected request: ${pathname}`);
280
+ });
281
+ const service = {
282
+ generateSmallModelText: vi.fn(),
283
+ };
284
+ vi.stubGlobal('fetch', fetchImpl);
285
+ const runtime = createSessionGoalRuntime({
286
+ buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
287
+ getOpenCodeAuthHeaders: () => ({}),
288
+ getSmallModelService: async () => service,
289
+ isEnabled: () => true,
290
+ idleQuietMs: 10,
291
+ });
292
+
293
+ runtime.processPayload({
294
+ type: 'session.status',
295
+ properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
296
+ });
297
+ await vi.runOnlyPendingTimersAsync();
298
+
299
+ // Audit must be skipped on length-truncated turn
300
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
301
+
302
+ // Goal accounting is persisted and turnsUsed incremented
303
+ const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
304
+ expect(patch).toBeDefined();
305
+ const writtenGoal = JSON.parse(patch.body).metadata.aiworker.goal;
306
+ expect(writtenGoal).toMatchObject({
307
+ status: 'active',
308
+ turnsUsed: 2,
309
+ });
310
+
311
+ // Continuation prompt is dispatched
312
+ const promptAsync = requests.find((request) => request.pathname === `/session/${SESSION_ID}/prompt_async` && request.method === 'POST');
313
+ expect(promptAsync).toBeDefined();
314
+ const promptBody = JSON.parse(promptAsync.body);
315
+ expect(promptBody.parts[0].text).toContain('Continue working toward the active session goal.');
316
+ runtime.stop();
317
+ });
318
+
319
+ it('skips audit and sends continuation prompt when assistant message carries MessageOutputLengthError', async () => {
320
+ const requests = [];
321
+ const fetchImpl = vi.fn(async (input, init = {}) => {
322
+ const pathname = requestPath(input);
323
+ requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
324
+ if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') return jsonResponse(session);
325
+ if (pathname === `/session/${SESSION_ID}`) return jsonResponse(session);
326
+ if (pathname === '/session/status') return jsonResponse({});
327
+ if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
328
+ if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
329
+ if (pathname === `/session/${SESSION_ID}/message`) {
330
+ return jsonResponse([{
331
+ info: {
332
+ id: 'msg_assistant_len_err',
333
+ sessionID: SESSION_ID,
334
+ role: 'assistant',
335
+ providerID: 'provider',
336
+ modelID: 'model',
337
+ error: { name: 'MessageOutputLengthError', message: 'Maximum token limit reached' },
338
+ time: { completed: 2 },
339
+ tokens: { input: 100, output: 4096, reasoning: 4096, cache: { read: 0 } },
340
+ },
341
+ parts: [{ type: 'reasoning', text: 'Drafting extensive implementation...' }],
342
+ }]);
343
+ }
344
+ throw new Error(`Unexpected request: ${pathname}`);
345
+ });
346
+ const service = {
347
+ generateSmallModelText: vi.fn(),
348
+ };
349
+ vi.stubGlobal('fetch', fetchImpl);
350
+ const runtime = createSessionGoalRuntime({
351
+ buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
352
+ getOpenCodeAuthHeaders: () => ({}),
353
+ getSmallModelService: async () => service,
354
+ isEnabled: () => true,
355
+ idleQuietMs: 10,
356
+ });
357
+
358
+ runtime.processPayload({
359
+ type: 'session.status',
360
+ properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
361
+ });
362
+ await vi.runOnlyPendingTimersAsync();
363
+
364
+ // Audit must be skipped on MessageOutputLengthError
365
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
366
+
367
+ // Goal remains active and turnsUsed incremented
368
+ const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
369
+ expect(patch).toBeDefined();
370
+ const writtenGoal = JSON.parse(patch.body).metadata.aiworker.goal;
371
+ expect(writtenGoal).toMatchObject({
372
+ status: 'active',
373
+ turnsUsed: 2,
374
+ });
375
+
376
+ // Continuation prompt is dispatched
377
+ const promptAsync = requests.find((request) => request.pathname === `/session/${SESSION_ID}/prompt_async` && request.method === 'POST');
378
+ expect(promptAsync).toBeDefined();
379
+ runtime.stop();
380
+ });
381
+
382
+ it.each([
383
+ { name: 'APIError', error: { name: 'APIError', message: 'provider failed' } },
384
+ { name: 'StructuredOutputError', error: { name: 'StructuredOutputError', message: 'invalid output' } },
385
+ { name: 'unnamed errors', error: {} },
386
+ ])('blocks a length finish when the assistant also has a non-length $name', async ({ error }) => {
387
+ const { runtime, requests, service } = createRuntimeHarness({
388
+ messages: [assistantMessage('msg_assistant_error', { finish: 'length', error })],
389
+ });
390
+
391
+ await runIdleTick(runtime);
392
+
393
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
394
+ expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
395
+ expect(lastPatchedGoal(requests)).toMatchObject({
396
+ status: 'blocked',
397
+ statusReason: error.name || 'assistant turn failed',
398
+ });
399
+ runtime.stop();
400
+ });
401
+
402
+ it('blocks after two consecutive length-truncated agent turns without persisting a counter, regardless of message IDs', async () => {
403
+ const firstLength = assistantMessage('z', { finish: 'length', time: { created: 10, completed: 11 } });
404
+ const summary = assistantMessage('summary', { summary: true, time: { created: 15, completed: 16 } });
405
+ const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
406
+ const { runtime, requests, service } = createRuntimeHarness({
407
+ messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, summary, secondLength],
408
+ });
409
+
410
+ await runIdleTick(runtime);
411
+ runtime.processPayload({
412
+ type: 'session.status',
413
+ properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
414
+ });
415
+ await vi.runOnlyPendingTimersAsync();
416
+
417
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
418
+ expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(1);
419
+ expect(lastPatchedGoal(requests)).toMatchObject({
420
+ status: 'blocked',
421
+ statusReason: 'repeated output truncation',
422
+ });
423
+ runtime.stop();
424
+ });
425
+
426
+ it('allows one explicit Resume after repeated truncation, then blocks another cutoff', async () => {
427
+ const first = assistantMessage('first', { finish: 'length', time: { created: 10, completed: 11 } });
428
+ const second = assistantMessage('second', { finish: 'length', time: { created: 20, completed: 21 } });
429
+ const messages = [first, second];
430
+ const { runtime, requests, activeSession } = createRuntimeHarness({ messages });
431
+ try {
432
+ await runIdleTick(runtime);
433
+ expect(lastPatchedGoal(requests).status).toBe('blocked');
434
+ expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
435
+ Object.assign(activeSession.metadata.aiworker.goal, { status: 'active', statusReason: 'resumed', turnsUsed: 0 });
436
+ await runIdleTick(runtime);
437
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active', statusReason: '', turnsUsed: 1 });
438
+ expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
439
+ messages.push(assistantMessage('third', { finish: 'length', time: { created: 30, completed: 31 } }));
440
+ await runIdleTick(runtime);
441
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'blocked', statusReason: 'repeated output truncation' });
442
+ expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
443
+ } finally {
444
+ runtime.stop();
445
+ }
446
+ });
447
+
448
+ it.each([
449
+ { tokenBudget: 1, error: undefined, expectedStatus: 'budgetLimited' },
450
+ { tokenBudget: null, error: { name: 'APIError' }, expectedStatus: 'blocked' },
451
+ ])('keeps $expectedStatus protection on explicit Resume', async ({ tokenBudget, error, expectedStatus }) => {
452
+ const { runtime, requests } = createRuntimeHarness({
453
+ goalOverrides: { statusReason: 'resumed', tokenBudget },
454
+ messages: [assistantMessage('resumed', { finish: 'length', error })],
455
+ });
456
+ try {
457
+ await runIdleTick(runtime);
458
+ expect(lastPatchedGoal(requests).status).toBe(expectedStatus);
459
+ expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
460
+ } finally {
461
+ runtime.stop();
462
+ }
463
+ });
464
+
465
+ it('continues after a truncated agent turn followed by a length-finished summary', async () => {
466
+ const firstLength = assistantMessage('agent-length', { finish: 'length', time: { created: 10, completed: 11 } });
467
+ const summary = assistantMessage('summary', {
468
+ summary: true,
469
+ finish: 'length',
470
+ time: { created: 15, completed: 16 },
471
+ });
472
+ const { runtime, requests, service } = createRuntimeHarness({
473
+ messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, summary],
474
+ });
475
+
476
+ await runIdleTick(runtime);
477
+ await runIdleTick(runtime);
478
+
479
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
480
+ expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
481
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
482
+ runtime.stop();
483
+ });
484
+
485
+ it('does not infer a repeated streak when a completed assistant timestamp is missing', async () => {
486
+ const firstLength = assistantMessage('z', { finish: 'length', time: { completed: 11 } });
487
+ const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
488
+ const { runtime, requests, service } = createRuntimeHarness({
489
+ messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, secondLength],
490
+ });
491
+
492
+ await runIdleTick(runtime);
493
+ await runIdleTick(runtime);
494
+
495
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
496
+ expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
497
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
498
+ runtime.stop();
499
+ });
500
+
501
+ it('ignores a missing timestamp on an older assistant when known post-goal turns establish a streak', async () => {
502
+ const olderLength = assistantMessage('legacy', { finish: 'length', time: { completed: 5 } });
503
+ const firstLength = assistantMessage('z', { finish: 'length', time: { created: 10, completed: 11 } });
504
+ const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
505
+ const { runtime, requests, service } = createRuntimeHarness({
506
+ goalOverrides: { createdAt: 6 },
507
+ messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [olderLength, firstLength, secondLength],
508
+ });
509
+
510
+ await runIdleTick(runtime);
511
+ await runIdleTick(runtime);
512
+
513
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
514
+ expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(1);
515
+ expect(lastPatchedGoal(requests)).toMatchObject({
516
+ status: 'blocked',
517
+ statusReason: 'repeated output truncation',
518
+ });
519
+ runtime.stop();
520
+ });
521
+
522
+ it('does not count a previous truncated turn created at the goal boundary', async () => {
523
+ const firstLength = assistantMessage('before-boundary', { finish: 'length', time: { created: 10, completed: 11 } });
524
+ const secondLength = assistantMessage('after-boundary', { finish: 'length', time: { created: 20, completed: 21 } });
525
+ const { runtime, requests, service } = createRuntimeHarness({
526
+ goalOverrides: { createdAt: 10 },
527
+ messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, secondLength],
528
+ });
529
+
530
+ await runIdleTick(runtime);
531
+ await runIdleTick(runtime);
532
+
533
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
534
+ expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
535
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
536
+ runtime.stop();
537
+ });
538
+
539
+ it('allows length recovery after an ordinary assistant turn breaks the streak', async () => {
540
+ const firstLength = assistantMessage('msg_length_before_normal', { finish: 'length' });
541
+ const normal = assistantMessage('msg_normal');
542
+ const secondLength = assistantMessage('msg_length_after_normal', { finish: 'length' });
543
+ const { runtime, requests, service } = createRuntimeHarness({
544
+ messageFactory: (fetchCount) => fetchCount < 2
545
+ ? [firstLength]
546
+ : [firstLength, normal, secondLength],
547
+ });
548
+
549
+ await runIdleTick(runtime);
550
+ await runIdleTick(runtime);
551
+
552
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
553
+ expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
554
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
555
+ runtime.stop();
556
+ });
557
+
558
+ it('keeps MessageAbortedError pause behavior instead of continuing', async () => {
559
+ const { runtime, requests, service } = createRuntimeHarness({
560
+ messages: [assistantMessage('msg_aborted', { error: { name: 'MessageAbortedError' } })],
561
+ });
562
+
563
+ await runIdleTick(runtime);
564
+
565
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
566
+ expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
567
+ expect(lastPatchedGoal(requests)).toMatchObject({
568
+ status: 'paused',
569
+ statusReason: 'paused after abort',
570
+ });
571
+ runtime.stop();
572
+ });
573
+
574
+ it('checks the token budget before allowing length recovery', async () => {
575
+ const { runtime, requests, service } = createRuntimeHarness({
576
+ goalOverrides: { tokenBudget: 5 },
577
+ messages: [assistantMessage('msg_budget_length', {
578
+ finish: 'length',
579
+ tokens: { input: 3, output: 3, cache: { read: 0 } },
580
+ })],
581
+ });
582
+
583
+ await runIdleTick(runtime);
584
+
585
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
586
+ expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
587
+ expect(lastPatchedGoal(requests)).toMatchObject({ status: 'budgetLimited' });
588
+ runtime.stop();
589
+ });
590
+
591
+ it('checks the auto-continuation cap before allowing length recovery', async () => {
592
+ const { runtime, requests, service } = createRuntimeHarness({
593
+ maxAutoTurns: 1,
594
+ messages: [assistantMessage('msg_cap_length', { finish: 'length' })],
595
+ });
596
+
597
+ await runIdleTick(runtime);
598
+
599
+ expect(service.generateSmallModelText).not.toHaveBeenCalled();
600
+ expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
601
+ expect(lastPatchedGoal(requests)).toMatchObject({
602
+ status: 'blocked',
603
+ statusReason: 'auto-continuation limit reached',
604
+ });
605
+ runtime.stop();
606
+ });
181
607
  });