@vantran-se/openchamber-web 1.24.3 → 2.0.1

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-BjJHrzmU.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-D0C9nU7K.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-DyxZVVVR.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-CL2vsrIZ.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-Dh7WEsdM.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-Bsba8KFp.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-B8IcVQAA.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-COVQhWGd.js +0 -4
  316. package/dist/assets/FilesView-B1V5JxFX.js +0 -13
  317. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
  320. package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
  321. package/dist/assets/PlanView-LW2z_EAP.js +0 -5
  322. package/dist/assets/PluginPane-B4tKno2h.js +0 -3
  323. package/dist/assets/SettingsView-DEaMulN9.js +0 -29
  324. package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
  326. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
  328. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
  329. package/dist/assets/de-DgxgFyGG.js +0 -10
  330. package/dist/assets/es-BFsZj5F_.js +0 -16
  331. package/dist/assets/fr-jdZ5KKQi.js +0 -14
  332. package/dist/assets/index-CKHjPKbq.js +0 -1
  333. package/dist/assets/index-Cbci4i1W.js +0 -40
  334. package/dist/assets/index-DqaMWNrC.css +0 -1
  335. package/dist/assets/ja-C5kKrhO4.js +0 -16
  336. package/dist/assets/ko-DXDTT_0O.js +0 -16
  337. package/dist/assets/main-CZALtVek.js +0 -896
  338. package/dist/assets/main-CgXMBNvG.js +0 -2
  339. package/dist/assets/miniChat-CwTmDixi.js +0 -2
  340. package/dist/assets/mobile-CLf2vCtO.js +0 -2
  341. package/dist/assets/pl-D-7x4Y8Q.js +0 -16
  342. package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
  344. package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
  346. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
  347. package/dist/assets/tr-Dr-oJ22L.js +0 -7
  348. package/dist/assets/uk-9Nx83UcI.js +0 -16
  349. package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
  350. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-Bxy_Hj3r.js +0 -16
  354. package/dist/assets/zh-TW-0BB4gpHM.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -1,4 +1,4 @@
1
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
1
+ import { OpenCode } from '@opencode/client';
2
2
  import { DateTime } from 'luxon';
3
3
  import { CronExpressionParser } from 'cron-parser';
4
4
  import { expandSnippets } from '../opencode/snippets.js';
@@ -255,12 +255,27 @@ export const createScheduledTasksRuntime = (deps) => {
255
255
  emitTaskRunEvent,
256
256
  setSessionAutoAccept,
257
257
  sessionKnowledgeRuntime = null,
258
+ // The goal record lives in OpenChamber's metadata store (OpenCode 2.x takes
259
+ // session metadata only at create time); without it goal mode cannot run.
260
+ persistSessionGoal = null,
258
261
  logger = console,
259
262
  maxGlobalConcurrency = DEFAULT_GLOBAL_CONCURRENCY,
260
263
  maxProjectConcurrency = DEFAULT_PROJECT_CONCURRENCY,
261
264
  maxRunDurationMs = DEFAULT_MAX_RUN_MS,
262
265
  } = deps;
263
266
 
267
+ // Every OpenCode route lives under /api in v2 and the client appends it, so
268
+ // the client only wants the origin. Directory scoping is a request header.
269
+ const openCodeOrigin = () => new URL(buildOpenCodeUrl('/api/info', '')).origin;
270
+ const createScopedClient = (directory) => OpenCode.make({
271
+ baseUrl: openCodeOrigin(),
272
+ headers: {
273
+ ...getOpenCodeAuthHeaders(),
274
+ ...(directory ? { 'x-opencode-directory': encodeURIComponent(directory) } : {}),
275
+ },
276
+ fetch,
277
+ });
278
+
264
279
  let started = false;
265
280
  const tasksByProject = new Map();
266
281
  const projectPathByID = new Map();
@@ -461,61 +476,45 @@ export const createScheduledTasksRuntime = (deps) => {
461
476
  return projectRunning < maxProjectConcurrency;
462
477
  };
463
478
 
464
- const buildPromptAsyncPayload = (task, projectPath, knowledgeText = '') => ({
465
- model: {
466
- providerID: task.execution.providerID,
467
- modelID: task.execution.modelID,
468
- },
469
- ...(task.execution.agent ? { agent: task.execution.agent } : {}),
470
- ...(task.execution.variant ? { variant: task.execution.variant } : {}),
471
- parts: [
472
- // Standing project context first, so the prompt reads against it. A
473
- // scheduled run has no UI to attach this, which is why it is asked for
474
- // here rather than assembled by whoever is sending.
475
- ...(knowledgeText ? [{ type: 'text', text: knowledgeText, synthetic: true }] : []),
476
- {
477
- type: 'text',
478
- text: expandSnippets(task.execution.prompt, projectPath),
479
- },
480
- ...(task.execution.goalEnabled
481
- ? [{ type: 'text', text: buildGoalIntroText(task.execution.goalTokenBudget), synthetic: true }]
482
- : []),
483
- ],
484
- });
485
-
486
- const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, projectPath, task }) => {
487
- // Never allowed to fail the run: a task that executes without its
488
- // background is a lesser loss than a task that does not execute.
489
- const knowledge = sessionKnowledgeRuntime
490
- ? await sessionKnowledgeRuntime.resolvePendingForSession(sessionID, projectPath)
491
- .catch(() => ({ text: '', signature: '' }))
492
- : { text: '', signature: '' };
493
-
494
- const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`);
495
- promptUrl.searchParams.set('directory', projectPath);
496
- const response = await fetch(promptUrl.toString(), {
497
- method: 'POST',
498
- headers: {
499
- ...authHeaders,
500
- 'content-type': 'application/json',
501
- accept: 'application/json',
502
- },
503
- body: JSON.stringify(buildPromptAsyncPayload(task, projectPath, knowledge.text)),
504
- });
505
-
506
- if (!response.ok) {
507
- const body = await response.text().catch(() => '');
508
- throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`);
509
- }
479
+ // Never allowed to fail the run: a task that executes without its
480
+ // background is a lesser loss than a task that does not execute.
481
+ const resolveKnowledge = (sessionID, projectPath) => (sessionKnowledgeRuntime
482
+ ? sessionKnowledgeRuntime.resolvePendingForSession(sessionID, projectPath)
483
+ .catch(() => ({ text: '', signature: '' }))
484
+ : Promise.resolve({ text: '', signature: '' }));
510
485
 
511
- // Recorded only after the prompt is accepted, so a failed dispatch carries
512
- // the context again on the next run.
486
+ // Recorded only after the send is accepted, so a failed dispatch carries
487
+ // the context again on the next run.
488
+ const recordKnowledge = async (sessionID, projectPath, knowledge) => {
513
489
  if (knowledge.text && sessionKnowledgeRuntime) {
514
490
  await sessionKnowledgeRuntime.recordDelivered(sessionID, projectPath, knowledge.signature)
515
491
  .catch(() => undefined);
516
492
  }
517
493
  };
518
494
 
495
+ const runPrompt = async ({ client, sessionID, projectPath, task }) => {
496
+ const knowledge = await resolveKnowledge(sessionID, projectPath);
497
+
498
+ // A v2 prompt carries a single authored text. Standing project context and
499
+ // the goal briefing therefore travel as synthetic messages sent first, so
500
+ // the model still reads the prompt against them exactly as before. A
501
+ // synthetic message schedules execution unless `resume: false`: without
502
+ // it the model would start on the briefing alone, before the task arrived.
503
+ if (knowledge.text) {
504
+ await client.session.synthetic({ sessionID, text: knowledge.text, resume: false });
505
+ }
506
+ if (task.execution.goalEnabled) {
507
+ await client.session.synthetic({ sessionID, text: buildGoalIntroText(task.execution.goalTokenBudget), resume: false });
508
+ }
509
+
510
+ await client.session.prompt({
511
+ sessionID,
512
+ text: expandSnippets(task.execution.prompt, projectPath),
513
+ });
514
+
515
+ await recordKnowledge(sessionID, projectPath, knowledge);
516
+ };
517
+
519
518
  const resolveScheduledCommand = async ({ client, projectPath, task }) => {
520
519
  const parsed = parseScheduledCommandPrompt(task?.execution?.prompt);
521
520
  if (!parsed) {
@@ -524,27 +523,35 @@ export const createScheduledTasksRuntime = (deps) => {
524
523
 
525
524
  let commands = [];
526
525
  try {
527
- const response = await client.command.list({ directory: projectPath });
526
+ const response = await client.command.list({ location: { directory: projectPath } });
528
527
  commands = Array.isArray(response?.data) ? response.data : [];
529
528
  } catch {
530
529
  return null;
531
530
  }
532
531
 
532
+ // v2 CommandInfo carries no template, so a goal objective distilled from
533
+ // the command body is no longer available; the goal falls back to the
534
+ // prompt text, which expandCommandGoalObjective already handles.
533
535
  const command = commands.find((candidate) => candidate?.name === parsed.command);
534
536
  return command ? { ...parsed, template: command.template } : null;
535
537
  };
536
538
 
537
- const runScheduledCommand = async ({ client, projectPath, sessionID, task, command }) => {
539
+ const runScheduledCommand = async ({ client, sessionID, projectPath, command }) => {
540
+ // The command route takes no extra parts, so standing context goes in
541
+ // first as a synthetic message that does not start execution.
542
+ const knowledge = await resolveKnowledge(sessionID, projectPath);
543
+ if (knowledge.text) {
544
+ await client.session.synthetic({ sessionID, text: knowledge.text, resume: false });
545
+ }
546
+ // Agent, model and variant are session properties in v2 and were already
547
+ // set when the run created the session; the command body only carries text.
538
548
  await client.session.command({
539
549
  sessionID,
540
- directory: projectPath,
541
- command: command.command,
542
- arguments: command.arguments,
543
- ...(task.execution.agent ? { agent: task.execution.agent } : {}),
544
- model: `${task.execution.providerID}/${task.execution.modelID}`,
545
- ...(task.execution.variant ? { variant: task.execution.variant } : {}),
550
+ // OpenCode 2.0.8 renamed the command body field `command` to `name`.
551
+ name: command.command,
552
+ text: command.arguments,
546
553
  });
547
-
554
+ await recordKnowledge(sessionID, projectPath, knowledge);
548
555
  };
549
556
 
550
557
  const runTaskWithWatchdog = async (projectID, task, reason) => {
@@ -559,18 +566,23 @@ export const createScheduledTasksRuntime = (deps) => {
559
566
  await waitForOpenCodeReady(10_000, 250);
560
567
  }
561
568
 
562
- const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
569
+ const baseUrl = openCodeOrigin();
563
570
  const authHeaders = getOpenCodeAuthHeaders();
564
- const client = createOpencodeClient({
565
- baseUrl,
566
- headers: authHeaders,
567
- });
571
+ const client = createScopedClient(projectPath);
568
572
 
569
- const sessionResponse = await client.session.create({
570
- directory: projectPath,
573
+ // Agent, model and variant belong to the session in v2: a scheduled run
574
+ // fixes them here instead of repeating them on every prompt.
575
+ const session = await client.session.create({
571
576
  title,
577
+ location: { directory: projectPath },
578
+ model: {
579
+ providerID: task.execution.providerID,
580
+ id: task.execution.modelID,
581
+ ...(task.execution.variant ? { variant: task.execution.variant } : {}),
582
+ },
583
+ ...(task.execution.agent ? { agent: task.execution.agent } : {}),
572
584
  });
573
- const sessionID = sessionResponse?.data?.id;
585
+ const sessionID = session?.id;
574
586
  if (!sessionID) {
575
587
  throw new Error('failed to create session');
576
588
  }
@@ -606,6 +618,7 @@ export const createScheduledTasksRuntime = (deps) => {
606
618
  await createSessionGoal({
607
619
  baseUrl,
608
620
  authHeaders,
621
+ persistSessionGoal,
609
622
  sessionID,
610
623
  directory: projectPath,
611
624
  objective: commandObjective ?? expandSnippets(task.execution.prompt, projectPath),
@@ -617,15 +630,9 @@ export const createScheduledTasksRuntime = (deps) => {
617
630
  }
618
631
 
619
632
  if (scheduledCommand) {
620
- await runScheduledCommand({ client, projectPath, sessionID, task, command: scheduledCommand });
633
+ await runScheduledCommand({ client, sessionID, projectPath, command: scheduledCommand });
621
634
  } else {
622
- await runPromptAsync({
623
- baseUrl,
624
- authHeaders,
625
- sessionID,
626
- projectPath,
627
- task,
628
- });
635
+ await runPrompt({ client, sessionID, projectPath, task });
629
636
  }
630
637
 
631
638
  const finishedAt = Date.now();
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
4
  import { mkdtemp, rm, mkdir, writeFile } from 'fs/promises';
@@ -273,3 +273,55 @@ Run daily.
273
273
  }
274
274
  });
275
275
  });
276
+
277
+ describe('scheduled-tasks runtime prompt dispatch', () => {
278
+ afterEach(() => {
279
+ vi.unstubAllGlobals();
280
+ });
281
+
282
+ it('parks the briefing with resume: false so execution starts on the task prompt', async () => {
283
+ const posts = [];
284
+ vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
285
+ const { pathname } = new URL(String(input));
286
+ if (init.method === 'POST') posts.push({ pathname, body: JSON.parse(init.body) });
287
+ const data = pathname === '/api/session' ? { id: 'ses_run' } : pathname === '/api/command' ? [] : {};
288
+ return new Response(JSON.stringify({ location: { directory: '/repo' }, data }), { status: 200, headers: { 'content-type': 'application/json' } });
289
+ }));
290
+ const task = {
291
+ id: 'task-1',
292
+ name: 'Nightly',
293
+ enabled: true,
294
+ schedule: { kind: 'daily', times: ['03:00'], timezone: 'UTC' },
295
+ execution: { prompt: 'Review open issues', providerID: 'openai', modelID: 'gpt-5', goalEnabled: true, goalTokenBudget: 50_000 },
296
+ state: { createdAt: 1, updatedAt: 1 },
297
+ };
298
+ const runtime = createScheduledTasksRuntime({
299
+ projectConfigRuntime: {
300
+ listScheduledTasks: async () => [task],
301
+ reconcileLoopTasks: async () => [task],
302
+ updateScheduledTaskState: async () => ({ task, updated: true }),
303
+ updateScheduledTaskStateIf: async () => ({ task, updated: true }),
304
+ },
305
+ listProjects: async () => [{ id: 'proj', path: '/repo' }],
306
+ buildOpenCodeUrl: () => 'http://127.0.0.1:1/',
307
+ getOpenCodeAuthHeaders: () => ({}),
308
+ waitForOpenCodeReady: async () => {},
309
+ persistSessionGoal: async () => undefined,
310
+ sessionKnowledgeRuntime: {
311
+ resolvePendingForSession: async () => ({ text: 'Project background', signature: 'sig' }),
312
+ recordDelivered: async () => undefined,
313
+ },
314
+ logger: { info: () => {}, warn: () => {}, error: () => {} },
315
+ });
316
+ await runtime.start();
317
+ await runtime.runNow('proj', 'task-1');
318
+ runtime.stop();
319
+
320
+ const dispatch = posts.filter((post) => post.pathname.startsWith('/api/session/ses_run/'));
321
+ expect(dispatch.map((post) => post.pathname.split('/').at(-1))).toEqual(['synthetic', 'synthetic', 'prompt']);
322
+ expect(dispatch[0].body).toMatchObject({ text: 'Project background', resume: false });
323
+ expect(dispatch[1].body.resume).toBe(false);
324
+ expect(dispatch[2].body).toMatchObject({ text: 'Review open issues' });
325
+ expect(dispatch[2].body.resume).toBeUndefined();
326
+ });
327
+ });
@@ -28,9 +28,14 @@ bringing an entire old task back into the prompt. This was compared against
28
28
  one, five, ten, and full-history contexts on long maintainer sessions. There
29
29
  is no full-history cache and no assumed provider prefix-cache behavior.
30
30
 
31
- The latest record must be a completed, successful, non-summary assistant answer
32
- with visible text. Child, archived, and reverted sessions are skipped. A new
33
- prompt clears the revert boundary before its next idle event.
31
+ The latest content record must be a completed, successful, non-summary
32
+ assistant answer with visible text. OpenCode closes every turn with an `idle`
33
+ record and appends agent/model/location switches as records of their own;
34
+ `newestContentId` looks past those, both here and in the re-check before the
35
+ write, so an ordinary v2 transcript still ends in its answer. An `idle` whose
36
+ outcome is `failed` or `interrupted` is not skipped: it disqualifies the turn.
37
+ Child, archived, and reverted sessions are skipped. A new prompt clears the
38
+ revert boundary before its next idle event.
34
39
 
35
40
  Human turns follow chronological message intervals. OpenCode can insert
36
41
  synthetic continuation users during compaction, so a final answer's `parentID`
@@ -23,8 +23,8 @@ export function excerpt(text, limit) {
23
23
  return text.slice(0, head) + marker + (tail > 0 ? text.slice(-tail) : '');
24
24
  }
25
25
 
26
- function attachedText(part) {
27
- const context = part.metadata?.openchamberContext;
26
+ function attachedText(message) {
27
+ const context = message.metadata?.openchamberContext;
28
28
  if (QUOTE_FIELDS.has(context?.kind)) {
29
29
  const source = (context[QUOTE_FIELDS.get(context.kind)] ?? '').trim();
30
30
  const authored = (context.text ?? '').trim();
@@ -33,8 +33,8 @@ function attachedText(part) {
33
33
  const quote = excerpt(source, 4_000).split('\n').map((line) => `> ${line}`).join('\n');
34
34
  return { text: `Attached ${context.kind}${label ? ` (${label}${location})` : ''}:\n${quote}\n\nUser comment:\n${excerpt(authored, USER_CHAR_LIMIT)}`, authored };
35
35
  }
36
- if (LINK_KINDS.has(context?.kind)) return { text: excerpt(part.text, USER_CHAR_LIMIT), authored: '' };
37
- const comment = part.metadata?.opencodeComment;
36
+ if (LINK_KINDS.has(context?.kind)) return { text: excerpt(message.text ?? '', USER_CHAR_LIMIT), authored: '' };
37
+ const comment = message.metadata?.opencodeComment;
38
38
  if (comment) {
39
39
  const authored = comment.comment.trim();
40
40
  const source = (comment.preview ?? '').trim();
@@ -44,51 +44,119 @@ function attachedText(part) {
44
44
  return null;
45
45
  }
46
46
 
47
- /** SDK message records are chronological; tool payloads never enter this view. */
48
- function readMessage({ info, parts }) {
47
+ // Records OpenCode appends around a turn that carry no conversation content:
48
+ // the `idle` marker that closes every turn and the agent/model/location
49
+ // switches. They are invisible to turn boundaries and to "what is the newest
50
+ // message". An `idle` whose outcome is not `succeeded` is not transparent: it
51
+ // is the evidence that the turn failed or was interrupted.
52
+ const TRANSPARENT_TYPES = new Set(['agent-switched', 'model-switched', 'location-switched']);
53
+
54
+ function isTransparent(record) {
55
+ if (record?.type === 'idle') return record.outcome === 'succeeded';
56
+ return TRANSPARENT_TYPES.has(record?.type);
57
+ }
58
+
59
+ /**
60
+ * The id of the newest record that is conversation content, given a page in
61
+ * v2's newest-first order; null when the page holds only service records.
62
+ * Both the assist reader and the pre-write re-check use it so they agree on
63
+ * what "the last message" is.
64
+ */
65
+ export function newestContentId(records) {
66
+ for (const record of Array.isArray(records) ? records : []) {
67
+ if (!record?.id || isTransparent(record)) continue;
68
+ return record.id;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * v2 message records are flat: an assistant message carries `content[]`, a user
75
+ * message a single `text` plus its attachments, and context items that used to
76
+ * ride along as `synthetic` text parts are their own `synthetic` messages now.
77
+ * Tool payloads never enter this view.
78
+ */
79
+ function readMessage(message) {
80
+ const role = message.type;
49
81
  const blocks = [];
50
82
  const authored = [];
51
- for (const part of parts) {
52
- if (part.type !== 'text' || part.ignored) continue;
53
- const attached = info.role === 'user' ? attachedText(part) : null;
83
+
84
+ if (role === 'user') {
85
+ const attached = attachedText(message);
54
86
  if (attached) {
55
87
  blocks.push(attached.text);
56
88
  authored.push(attached.authored);
57
- } else if (!part.synthetic) {
58
- blocks.push(part.text);
59
- if (info.role === 'user') {
60
- // Legacy terminal selections are source material, not a language sample.
61
- authored.push(part.text.replace(/\n*<terminal_context>\n[\s\S]*?\n<\/terminal_context>\s*$/, ''));
62
- }
89
+ } else if (typeof message.text === 'string') {
90
+ blocks.push(message.text);
91
+ // Legacy terminal selections are source material, not a language sample.
92
+ authored.push(message.text.replace(/\n*<terminal_context>\n[\s\S]*?\n<\/terminal_context>\s*$/, ''));
93
+ }
94
+ } else if (role === 'assistant') {
95
+ for (const part of Array.isArray(message.content) ? message.content : []) {
96
+ if (part?.type === 'text' && typeof part.text === 'string') blocks.push(part.text);
97
+ }
98
+ } else if (role === 'synthetic') {
99
+ // Context the user attached to the message that follows: in v1 it was a
100
+ // `synthetic` text part inside the user message, in v2 it is its own turn.
101
+ const attached = attachedText(message);
102
+ if (attached) {
103
+ blocks.push(attached.text);
104
+ authored.push(attached.authored);
105
+ } else if (typeof message.text === 'string') {
106
+ blocks.push(message.text);
63
107
  }
64
108
  }
109
+
65
110
  return {
66
- id: info.id, role: info.role, parentID: info.parentID,
67
- providerID: info.providerID, modelID: info.modelID,
68
- complete: info.role === 'assistant' && info.finish === 'stop' && Boolean(info.time?.completed) && !info.error && !info.summary,
69
- summary: Boolean(info.summary),
70
- text: excerpt(blocks.join('\n\n').trim(), info.role === 'user' ? USER_CHAR_LIMIT : ANSWER_CHAR_LIMIT),
111
+ id: message.id,
112
+ role,
113
+ transparent: isTransparent(message),
114
+ // v2 has no `parentID` on a message: a turn is the run of messages between
115
+ // one user message and the assistant reply that follows it, which is what
116
+ // `collectTurns` walks.
117
+ providerID: message.model?.providerID,
118
+ modelID: message.model?.id,
119
+ complete: role === 'assistant'
120
+ && message.finish === 'stop'
121
+ && Boolean(message.time?.completed)
122
+ && !message.error,
123
+ text: excerpt(blocks.join('\n\n').trim(), role === 'user' ? USER_CHAR_LIMIT : ANSWER_CHAR_LIMIT),
71
124
  authored: excerpt(authored.filter(Boolean).join('\n\n').trim(), USER_CHAR_LIMIT),
72
125
  };
73
126
  }
74
127
 
128
+ /**
129
+ * A turn is one user message and the assistant reply that closes it. v2 dropped
130
+ * `parentID`, so the boundary is positional: everything between two user
131
+ * messages belongs to the turn the earlier one opened.
132
+ *
133
+ * Synthetic messages are the context the user attached to the message they are
134
+ * about to send — v1 carried them as parts of that user message — so they are
135
+ * folded into the next user message instead of opening a turn of their own.
136
+ */
75
137
  function collectTurns(messages) {
76
138
  const turns = [];
77
139
  let active = null;
78
- let parents = new Set();
140
+ let attached = [];
79
141
  for (const message of messages) {
80
- if (message.role === 'user' && message.text) {
81
- active = { user: message, assistant: null, complete: false };
82
- parents = new Set([message.id]);
142
+ if (message.transparent) continue;
143
+ if (message.role === 'synthetic') {
144
+ if (message.text) attached.push(message);
145
+ continue;
146
+ }
147
+ if (message.role === 'user') {
148
+ const text = [...attached.map((entry) => entry.text), message.text].filter(Boolean).join('\n\n');
149
+ const authored = [...attached.map((entry) => entry.authored), message.authored].filter(Boolean).join('\n\n');
150
+ attached = [];
151
+ if (!text) continue;
152
+ active = { user: { ...message, text, authored }, assistant: null, complete: false };
83
153
  turns.push(active);
84
- } else if (active && message.role === 'user') {
85
- parents.add(message.id);
86
- } else if (active && message.role === 'assistant' && parents.has(message.parentID) && !message.summary) {
87
- // Compaction's synthetic continuation users belong to the same human
88
- // turn. Parent IDs alone would lose its eventual final answer.
89
- if (message.text) active.assistant = message;
90
- active.complete = message.complete && Boolean(message.text);
154
+ continue;
91
155
  }
156
+ attached = [];
157
+ if (!active || message.role !== 'assistant') continue;
158
+ if (message.text) active.assistant = message;
159
+ active.complete = message.complete && Boolean(message.text);
92
160
  }
93
161
  return turns.slice(-TURN_LIMIT);
94
162
  }
@@ -96,32 +164,40 @@ function collectTurns(messages) {
96
164
  /** Failure is thrown; null means no eligible final answer within bounded history. */
97
165
  export async function loadAssistContext({ readPage, signal }) {
98
166
  let messages = [];
99
- let before;
167
+ let cursor;
100
168
  const cursors = new Set();
101
169
  const ids = new Set();
102
170
  for (let pageNumber = 0; pageNumber < MAX_PAGES; pageNumber++) {
103
171
  signal.throwIfAborted();
104
- const page = await readPage({ limit: PAGE_SIZE, before });
172
+ // v2 pages messages newest first and returns `{ data, cursor }`.
173
+ const page = await readPage({ limit: PAGE_SIZE, cursor });
105
174
  signal.throwIfAborted();
106
- if (!Array.isArray(page.data)) throw new Error('Session message page is unavailable');
175
+ if (!Array.isArray(page?.data)) throw new Error('Session message page is unavailable');
107
176
  const older = [];
108
177
  for (const record of page.data) {
109
- if (ids.has(record.info.id)) continue;
110
- ids.add(record.info.id);
178
+ if (!record?.id || ids.has(record.id)) continue;
179
+ ids.add(record.id);
111
180
  older.push(readMessage(record));
112
181
  }
182
+ older.reverse();
113
183
  messages = older.concat(messages);
114
- const last = messages.at(-1);
115
- if (!last?.complete || !last.text) return null;
116
- const turns = collectTurns(messages);
117
- const cursor = page.response.headers.get('x-next-cursor');
118
- if (turns.length === TURN_LIMIT || !cursor || pageNumber === MAX_PAGES - 1) {
119
- if (!turns.at(-1)?.complete || turns.at(-1).assistant.id !== last.id) return null;
120
- return { turns, last };
184
+ const next = typeof page.cursor?.next === 'string' ? page.cursor.next : null;
185
+ // A successful turn ends with an `idle` marker after the answer; look past
186
+ // it and its siblings to the newest content record.
187
+ const last = messages.findLast((message) => !message.transparent);
188
+ if (!last) {
189
+ if (!next || pageNumber === MAX_PAGES - 1) return null;
190
+ } else {
191
+ if (!last.complete || !last.text) return null;
192
+ const turns = collectTurns(messages);
193
+ if (turns.length === TURN_LIMIT || !next || pageNumber === MAX_PAGES - 1) {
194
+ if (!turns.at(-1)?.complete || turns.at(-1).assistant.id !== last.id) return null;
195
+ return { turns, last };
196
+ }
121
197
  }
122
- if (cursors.has(cursor)) throw new Error('Session message pagination made no progress');
123
- cursors.add(cursor);
124
- before = cursor;
198
+ if (cursors.has(next)) throw new Error('Session message pagination made no progress');
199
+ cursors.add(next);
200
+ cursor = next;
125
201
  }
126
202
  return null;
127
203
  }