@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3
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.
- package/README.md +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
- package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
- package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
- package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
- package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
- package/dist/assets/SettingsView-BRxELjow.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
- package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
- package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
- package/dist/assets/collapsible-DXbDynN4.js +3 -0
- package/dist/assets/context-menu-CqolsLCm.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-CM42qUed.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
- package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/main-BILgBI3B.js +169 -0
- package/dist/assets/main-BeveiW6F.js +2 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
- package/dist/assets/miniChat-BgrzM3u7.js +2 -0
- package/dist/assets/mobile-DtYoQE8C.js +2 -0
- package/dist/assets/multirun-jy8dEI96.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-BQghHRfp.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
- package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
- package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
- package/dist/assets/src-CLN7Z-zx.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
- package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
- package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +2 -2
- package/server/index.js +392 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/runtime.js +5 -0
- package/server/lib/opencode/DOCUMENTATION.md +103 -16
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +111 -102
- package/server/lib/opencode/config-entity-routes.test.js +123 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +80 -5
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +368 -28
- package/server/lib/opencode/proxy.test.js +338 -0
- package/server/lib/opencode/routes.js +303 -76
- package/server/lib/opencode/routes.test.js +315 -50
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +35 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
- package/server/lib/session-metadata/routes.js +193 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +375 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +125 -241
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/collapsible-D5Phaykh.js +0 -1
- package/dist/assets/context-menu-CubSDW2g.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { createAssistantsService } from './service.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const root = () => fs.mkdtempSync(path.join(os.tmpdir(), 'assistants-proj-'));
|
|
10
|
+
|
|
11
|
+
const createV2Client = (overrides = {}) => {
|
|
12
|
+
const create = overrides.create ?? (async () => ({ id: crypto.randomUUID() }));
|
|
13
|
+
const get = overrides.get ?? (async () => ({ id: 'present' }));
|
|
14
|
+
const prompt = overrides.prompt ?? overrides.promptAsync ?? (async () => ({ id: 'inbox_1', type: 'user' }));
|
|
15
|
+
const compact = overrides.compact ?? overrides.summarize ?? (async () => ({ id: 'inbox_compact', type: 'compaction' }));
|
|
16
|
+
const interrupt = overrides.interrupt ?? overrides.abort ?? (async () => {});
|
|
17
|
+
const rename = overrides.rename ?? (async () => {});
|
|
18
|
+
const remove = overrides.remove ?? (async () => {});
|
|
19
|
+
const list = overrides.messages ?? overrides.list ?? (async () => ({ data: [], cursor: {} }));
|
|
20
|
+
const switchAgent = overrides.switchAgent ?? (async () => {});
|
|
21
|
+
const switchModel = overrides.switchModel ?? (async () => {});
|
|
22
|
+
const putInstruction = overrides.putInstruction ?? (async () => {});
|
|
23
|
+
return {
|
|
24
|
+
session: {
|
|
25
|
+
create: async (input) => create(input),
|
|
26
|
+
get: async (input) => get(input),
|
|
27
|
+
rename,
|
|
28
|
+
remove,
|
|
29
|
+
prompt: async (input) => prompt(input),
|
|
30
|
+
compact: async (input) => compact(input),
|
|
31
|
+
interrupt: async (input) => interrupt(input),
|
|
32
|
+
switchAgent,
|
|
33
|
+
switchModel,
|
|
34
|
+
instructions: { entry: { put: putInstruction } },
|
|
35
|
+
},
|
|
36
|
+
message: {
|
|
37
|
+
list: async (input) => {
|
|
38
|
+
// Official contract fixture: refuse order+cursor combinations.
|
|
39
|
+
if (input?.cursor && input?.order != null) {
|
|
40
|
+
const error = new Error('InvalidRequestError');
|
|
41
|
+
error.status = 400;
|
|
42
|
+
error._tag = 'InvalidRequestError';
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
const result = await list(input);
|
|
46
|
+
if (result?.error) return result;
|
|
47
|
+
if (Array.isArray(result?.data) || result?.cursor || result?.response) return result;
|
|
48
|
+
if (Array.isArray(result)) return { data: result, cursor: {} };
|
|
49
|
+
return result;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const setup = (directory = root(), client = {}, options = {}) => {
|
|
55
|
+
const { enabled = true, fetchImpl, ...serviceOptions } = options;
|
|
56
|
+
const service = createAssistantsService({
|
|
57
|
+
dbPath: path.join(directory, 'assistants.sqlite'),
|
|
58
|
+
dataDir: directory,
|
|
59
|
+
getAllowedRoots: () => [directory],
|
|
60
|
+
buildOpenCodeUrl: () => 'http://opencode.test',
|
|
61
|
+
getOpenCodeAuthHeaders: () => ({ authorization: 'Bearer test' }),
|
|
62
|
+
fetchImpl,
|
|
63
|
+
clientFactory: () => createV2Client(client),
|
|
64
|
+
// Contact composer send needs a harness stub; body authority is OpenCode projection.
|
|
65
|
+
runContactTurn: serviceOptions.runContactTurn ?? (async ({ userText }) => ({ text: `reply:${userText}`, bubbles: [`reply:${userText}`] })),
|
|
66
|
+
...serviceOptions,
|
|
67
|
+
});
|
|
68
|
+
if (enabled) {
|
|
69
|
+
const snapshot = service.snapshot();
|
|
70
|
+
if (!snapshot.enabled) service.setEnabled({ enabled: true, expectedRevision: snapshot.revision });
|
|
71
|
+
}
|
|
72
|
+
return service;
|
|
73
|
+
};
|
|
74
|
+
const assistantInput = { name: 'A', providerID: 'p', modelID: 'm' };
|
|
75
|
+
|
|
76
|
+
const projectionEntry = (sessionID, id, created, text = id, role = 'assistant') => ({
|
|
77
|
+
info: { id, sessionID, role, time: { created } },
|
|
78
|
+
parts: [{ id: `prt_${id}`, sessionID, messageID: id, type: 'text', text }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** Official v2 SessionMessageInfo-shaped user/assistant with content (not only parts). */
|
|
82
|
+
const syntheticContentEntry = (sessionID, id, created, role, text) => ({
|
|
83
|
+
id,
|
|
84
|
+
sessionID,
|
|
85
|
+
type: role,
|
|
86
|
+
time: { created },
|
|
87
|
+
content: [{ type: 'text', text }],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Official-shaped desc pager: non-empty pages always expose cursor.next (even the
|
|
92
|
+
* logical last page); an empty follow-up page is the finite end.
|
|
93
|
+
*/
|
|
94
|
+
const createOfficialDescPager = (rowsBySession, pageLimitDefault = null) => {
|
|
95
|
+
const state = new Map();
|
|
96
|
+
for (const [sessionID, rows] of Object.entries(rowsBySession)) {
|
|
97
|
+
state.set(sessionID, rows);
|
|
98
|
+
}
|
|
99
|
+
const calls = [];
|
|
100
|
+
const list = async (input) => {
|
|
101
|
+
const sessionID = input.sessionID;
|
|
102
|
+
const limit = Number(input.limit) || pageLimitDefault || 50;
|
|
103
|
+
const rows = state.get(sessionID) || [];
|
|
104
|
+
calls.push({
|
|
105
|
+
sessionID,
|
|
106
|
+
limit,
|
|
107
|
+
order: input.order ?? null,
|
|
108
|
+
cursor: input.cursor ?? null,
|
|
109
|
+
hasOrder: input.order != null,
|
|
110
|
+
});
|
|
111
|
+
let start = 0;
|
|
112
|
+
if (input.cursor) {
|
|
113
|
+
const marker = String(input.cursor).replace(/^next:/, '');
|
|
114
|
+
const idx = rows.findIndex((row) => (row.info?.id ?? row.id) === marker);
|
|
115
|
+
start = idx >= 0 ? idx + 1 : rows.length;
|
|
116
|
+
}
|
|
117
|
+
const page = rows.slice(start, start + limit);
|
|
118
|
+
// Non-empty ⇒ always next (even past the last real row marker).
|
|
119
|
+
const next = page.length > 0
|
|
120
|
+
? `next:${page[page.length - 1].info?.id ?? page[page.length - 1].id}`
|
|
121
|
+
: null;
|
|
122
|
+
return {
|
|
123
|
+
data: page,
|
|
124
|
+
cursor: next ? { previous: null, next } : { previous: null, next: null },
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
return { list, calls };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
describe('ticket 11 assistants read from OpenCode projections', () => {
|
|
131
|
+
it('does not persist message bodies after send, share, or live events', async () => {
|
|
132
|
+
const directory = root();
|
|
133
|
+
const service = setup(directory, {
|
|
134
|
+
create: async () => ({ data: { id: 'ses_1' } }),
|
|
135
|
+
prompt: async () => ({ id: 'inbox_1', type: 'user' }),
|
|
136
|
+
});
|
|
137
|
+
const assistant = service.createAssistant(assistantInput);
|
|
138
|
+
const binding = await service.ensure(assistant.id);
|
|
139
|
+
await service.send(assistant.id, { ...binding, messageID: 'msg_send', parts: [{ type: 'text', text: 'hello' }] });
|
|
140
|
+
await service.share(assistant.id, { operationID: 'share_1', payload: { messageID: 'msg_share', parts: [{ type: 'text', text: 'shared' }] } });
|
|
141
|
+
service.processEvent({ type: 'message.updated', properties: { info: { id: 'msg_event', sessionID: binding.sessionID, role: 'assistant', time: { created: 1 } } } });
|
|
142
|
+
service.processEvent({ type: 'message.part.updated', properties: { sessionID: binding.sessionID, part: { id: 'prt_1', messageID: 'msg_event', type: 'text', text: 'live' } } });
|
|
143
|
+
const db = new (require('better-sqlite3'))(path.join(directory, 'assistants.sqlite'));
|
|
144
|
+
expect(db.prepare('SELECT COUNT(*) AS count FROM assistant_message_mirror').get().count).toBe(0);
|
|
145
|
+
expect(db.prepare('SELECT COUNT(*) AS count FROM assistant_message_part_mirror').get().count).toBe(0);
|
|
146
|
+
expect(db.prepare('SELECT COUNT(*) AS count FROM assistant_share_operation').get().count).toBe(1);
|
|
147
|
+
expect(db.prepare('SELECT session_id, message_id FROM assistant_share_operation').get()).toMatchObject({ session_id: binding.sessionID, message_id: 'msg_share' });
|
|
148
|
+
db.close();
|
|
149
|
+
service.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('opens history from OpenCode projections per binding and ignores leftover mirrors', async () => {
|
|
153
|
+
const directory = root();
|
|
154
|
+
let creates = 0;
|
|
155
|
+
const calls = [];
|
|
156
|
+
const service = setup(directory, {
|
|
157
|
+
create: async () => ({ data: { id: `ses_${++creates}` } }),
|
|
158
|
+
messages: async ({ sessionID, limit, order, cursor }) => {
|
|
159
|
+
calls.push({ sessionID, limit, order, cursor });
|
|
160
|
+
if (sessionID === 'ses_1') return { data: [projectionEntry('ses_1', 'msg_old', 1, 'from-projection')] };
|
|
161
|
+
return { data: [projectionEntry(sessionID, 'msg_live', 2, 'current')] };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
|
|
165
|
+
const first = await service.ensure(assistant.id);
|
|
166
|
+
await service.createNew(assistant.id);
|
|
167
|
+
const Database = require('better-sqlite3');
|
|
168
|
+
const seed = new Database(path.join(directory, 'assistants.sqlite'));
|
|
169
|
+
seed.prepare('INSERT INTO assistant_message_mirror(assistant_id,session_id,message_id,info_json,ordinal,covered,updated_at) VALUES (?,?,?,?,?,?,?)').run(
|
|
170
|
+
assistant.id,
|
|
171
|
+
first.sessionID,
|
|
172
|
+
'msg_mirror',
|
|
173
|
+
JSON.stringify({ id: 'msg_mirror', sessionID: first.sessionID, role: 'user', time: { created: 99 }, openchamberAssistantAdmission: true }),
|
|
174
|
+
99,
|
|
175
|
+
1,
|
|
176
|
+
1,
|
|
177
|
+
);
|
|
178
|
+
seed.close();
|
|
179
|
+
const page = await service.historicalMessages(assistant.id, { limit: 10 });
|
|
180
|
+
expect(page.entries.map((entry) => entry.info.id)).toEqual(['msg_old', 'msg_live']);
|
|
181
|
+
expect(page.entries[0]?.parts[0]?.text).toBe('from-projection');
|
|
182
|
+
expect(page.entries.some((entry) => entry.info.id === 'msg_mirror')).toBe(false);
|
|
183
|
+
// First page of each binding may use order=desc; never with a cursor.
|
|
184
|
+
expect(calls.every((call) => call.cursor == null || call.order == null)).toBe(true);
|
|
185
|
+
expect(calls.some((call) => call.order === 'desc' && call.cursor == null)).toBe(true);
|
|
186
|
+
expect(page.complete).toBe(true);
|
|
187
|
+
expect(page.partial).toBe(false);
|
|
188
|
+
service.close();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('traverses source seq CBA across bindings with limit 1 (not time.created sort)', async () => {
|
|
192
|
+
// ses_old (ordinal lower): source desc page [B created20, C created10]
|
|
193
|
+
// ses_new: [A created30]
|
|
194
|
+
// Public ascending walk with limit 1 must be A → B → C (source seq), not time sort tricks.
|
|
195
|
+
let creates = 0;
|
|
196
|
+
const pager = createOfficialDescPager({
|
|
197
|
+
ses_1: [
|
|
198
|
+
projectionEntry('ses_1', 'msg_B', 20, 'B'),
|
|
199
|
+
projectionEntry('ses_1', 'msg_C', 10, 'C'),
|
|
200
|
+
],
|
|
201
|
+
ses_2: [
|
|
202
|
+
projectionEntry('ses_2', 'msg_A', 30, 'A'),
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
const service = setup(root(), {
|
|
206
|
+
create: async () => ({ data: { id: `ses_${++creates}` } }),
|
|
207
|
+
messages: pager.list,
|
|
208
|
+
});
|
|
209
|
+
const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
|
|
210
|
+
await service.ensure(assistant.id); // ses_1
|
|
211
|
+
await service.createNew(assistant.id); // ses_2 live
|
|
212
|
+
const seen = [];
|
|
213
|
+
let before;
|
|
214
|
+
for (let step = 0; step < 5; step++) {
|
|
215
|
+
const page = await service.historicalMessages(assistant.id, { before, limit: 1 });
|
|
216
|
+
seen.push(...page.entries.map((entry) => entry.info.id));
|
|
217
|
+
if (page.complete) break;
|
|
218
|
+
before = page.nextCursor;
|
|
219
|
+
expect(before).toEqual(expect.any(String));
|
|
220
|
+
}
|
|
221
|
+
expect(seen).toEqual(['msg_A', 'msg_B', 'msg_C']);
|
|
222
|
+
// Continuations must never send order with cursor.
|
|
223
|
+
expect(pager.calls.filter((call) => call.cursor).every((call) => call.hasOrder === false)).toBe(true);
|
|
224
|
+
expect(pager.calls.filter((call) => !call.cursor).every((call) => call.order === 'desc')).toBe(true);
|
|
225
|
+
service.close();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('pages 8 rows with limit 2 and 3-page budget once each id, finite end (no tip loop)', async () => {
|
|
229
|
+
const rows = Array.from({ length: 8 }, (_, index) => {
|
|
230
|
+
const n = 8 - index;
|
|
231
|
+
return projectionEntry('ses_1', `msg_${n}`, n * 10, `t${n}`);
|
|
232
|
+
});
|
|
233
|
+
const pager = createOfficialDescPager({ ses_1: rows });
|
|
234
|
+
const service = setup(root(), {
|
|
235
|
+
create: async () => ({ data: { id: 'ses_1' } }),
|
|
236
|
+
messages: pager.list,
|
|
237
|
+
});
|
|
238
|
+
const assistant = service.createAssistant(assistantInput);
|
|
239
|
+
await service.ensure(assistant.id);
|
|
240
|
+
const pages = [];
|
|
241
|
+
let before;
|
|
242
|
+
let rounds = 0;
|
|
243
|
+
do {
|
|
244
|
+
const page = await service.historicalMessages(assistant.id, { before, limit: 2 });
|
|
245
|
+
rounds += 1;
|
|
246
|
+
pages.push(page);
|
|
247
|
+
// Each demand page is ascending within itself.
|
|
248
|
+
const ids = page.entries.map((entry) => entry.info.id);
|
|
249
|
+
expect(ids).toEqual([...ids].sort((a, b) => Number(a.slice(4)) - Number(b.slice(4))));
|
|
250
|
+
before = page.complete ? null : page.nextCursor;
|
|
251
|
+
expect(rounds).toBeLessThanOrEqual(8);
|
|
252
|
+
} while (before);
|
|
253
|
+
// Infinite-query flatten (oldest page first) yields full chronological seq once.
|
|
254
|
+
const seen = pages.slice().reverse().flatMap((page) => page.entries.map((entry) => entry.info.id));
|
|
255
|
+
expect(seen).toEqual(['msg_1', 'msg_2', 'msg_3', 'msg_4', 'msg_5', 'msg_6', 'msg_7', 'msg_8']);
|
|
256
|
+
expect(new Set(seen).size).toBe(8);
|
|
257
|
+
// Tip (no cursor) is opened once; later calls only continue via opaque cursor
|
|
258
|
+
// (page-local skip may re-hit the same upstream page, but never restarts the tip).
|
|
259
|
+
const tipCalls = pager.calls.filter((call) => call.cursor == null);
|
|
260
|
+
expect(tipCalls).toHaveLength(1);
|
|
261
|
+
expect(pages.at(-1)?.complete).toBe(true);
|
|
262
|
+
service.close();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('marks middle binding 503 as partial with retryable cursor; recovery fills the gap', async () => {
|
|
266
|
+
let creates = 0;
|
|
267
|
+
let ses2Fail = true;
|
|
268
|
+
const service = setup(root(), {
|
|
269
|
+
create: async () => ({ data: { id: `ses_${++creates}` } }),
|
|
270
|
+
messages: async ({ sessionID }) => {
|
|
271
|
+
if (sessionID === 'ses_2' && ses2Fail) return { error: { status: 503 } };
|
|
272
|
+
return { data: [projectionEntry(sessionID, `msg_${sessionID}`, 1)] };
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
|
|
276
|
+
await service.ensure(assistant.id); // ses_1
|
|
277
|
+
await service.createNew(assistant.id); // ses_2
|
|
278
|
+
await service.createNew(assistant.id); // ses_3 live
|
|
279
|
+
const first = await service.historicalMessages(assistant.id, { limit: 10 });
|
|
280
|
+
// Newest success delivered; failed middle is not skipped permanently.
|
|
281
|
+
expect(first.entries.map((entry) => [entry.sessionID, entry.info.id])).toEqual([
|
|
282
|
+
['ses_3', 'msg_ses_3'],
|
|
283
|
+
]);
|
|
284
|
+
expect(first.complete).toBe(false);
|
|
285
|
+
expect(first.partial).toBe(true);
|
|
286
|
+
expect(first.failed).toEqual([
|
|
287
|
+
expect.objectContaining({ sessionID: 'ses_2', status: 503 }),
|
|
288
|
+
]);
|
|
289
|
+
expect(first.nextCursor).toEqual(expect.any(String));
|
|
290
|
+
|
|
291
|
+
ses2Fail = false;
|
|
292
|
+
const recovered = [];
|
|
293
|
+
let before = first.nextCursor;
|
|
294
|
+
for (let step = 0; step < 5; step++) {
|
|
295
|
+
const page = await service.historicalMessages(assistant.id, { before, limit: 10 });
|
|
296
|
+
recovered.push(...page.entries.map((entry) => [entry.sessionID, entry.info.id]));
|
|
297
|
+
if (page.complete) {
|
|
298
|
+
expect(page.partial).toBe(false);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
before = page.nextCursor;
|
|
302
|
+
}
|
|
303
|
+
// Public pages stay ascending by binding ordinal (older session first).
|
|
304
|
+
expect(recovered).toEqual([
|
|
305
|
+
['ses_1', 'msg_ses_1'],
|
|
306
|
+
['ses_2', 'msg_ses_2'],
|
|
307
|
+
]);
|
|
308
|
+
service.close();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('keeps complete sessions when one projection page fails and does not treat total failure as empty success', async () => {
|
|
312
|
+
const directory = root();
|
|
313
|
+
let creates = 0;
|
|
314
|
+
const service = setup(directory, {
|
|
315
|
+
create: async () => ({ data: { id: `ses_${++creates}` } }),
|
|
316
|
+
messages: async ({ sessionID }) => {
|
|
317
|
+
if (sessionID === 'ses_2') return { error: { status: 503 } };
|
|
318
|
+
return { data: [projectionEntry(sessionID, `msg_${sessionID}`, 1)] };
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
|
|
322
|
+
await service.ensure(assistant.id);
|
|
323
|
+
await service.createNew(assistant.id);
|
|
324
|
+
await service.createNew(assistant.id);
|
|
325
|
+
const page = await service.historicalMessages(assistant.id, { limit: 10 });
|
|
326
|
+
expect(page.entries.map((entry) => [entry.sessionID, entry.info.id])).toEqual([
|
|
327
|
+
['ses_3', 'msg_ses_3'],
|
|
328
|
+
]);
|
|
329
|
+
expect(page.entries.some((entry) => entry.sessionID === 'ses_2')).toBe(false);
|
|
330
|
+
expect(page.partial).toBe(true);
|
|
331
|
+
expect(page.complete).toBe(false);
|
|
332
|
+
|
|
333
|
+
const isolated = setup(root(), {
|
|
334
|
+
create: async () => ({ data: { id: 'ses_only' } }),
|
|
335
|
+
messages: async () => ({ error: { status: 500 } }),
|
|
336
|
+
});
|
|
337
|
+
const lonely = isolated.createAssistant(assistantInput);
|
|
338
|
+
await isolated.ensure(lonely.id);
|
|
339
|
+
await expect(isolated.historicalMessages(lonely.id, { limit: 10 })).rejects.toMatchObject({ code: 'upstream_error' });
|
|
340
|
+
isolated.close();
|
|
341
|
+
service.close();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('projects real user/assistant content synthetic rows from official shape', async () => {
|
|
345
|
+
const pager = createOfficialDescPager({
|
|
346
|
+
ses_1: [
|
|
347
|
+
syntheticContentEntry('ses_1', 'msg_asst', 20, 'assistant', 'assistant-body'),
|
|
348
|
+
syntheticContentEntry('ses_1', 'msg_user', 10, 'user', 'user-body'),
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
const service = setup(root(), {
|
|
352
|
+
create: async () => ({ data: { id: 'ses_1' } }),
|
|
353
|
+
messages: pager.list,
|
|
354
|
+
});
|
|
355
|
+
const assistant = service.createAssistant(assistantInput);
|
|
356
|
+
await service.ensure(assistant.id);
|
|
357
|
+
const page = await service.historicalMessages(assistant.id, { limit: 10 });
|
|
358
|
+
expect(page.entries.map((entry) => [entry.info.id, entry.info.role, entry.parts[0]?.text])).toEqual([
|
|
359
|
+
['msg_user', 'user', 'user-body'],
|
|
360
|
+
['msg_asst', 'assistant', 'assistant-body'],
|
|
361
|
+
]);
|
|
362
|
+
expect(page.complete).toBe(true);
|
|
363
|
+
service.close();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('uses official GET /api/session/:id/message when fetchImpl is provided', async () => {
|
|
367
|
+
const directory = root();
|
|
368
|
+
const urls = [];
|
|
369
|
+
const service = setup(directory, {
|
|
370
|
+
create: async () => ({ data: { id: 'ses_proj' } }),
|
|
371
|
+
}, {
|
|
372
|
+
fetchImpl: async (input) => {
|
|
373
|
+
const url = new URL(String(input));
|
|
374
|
+
urls.push(url);
|
|
375
|
+
return new Response(JSON.stringify({
|
|
376
|
+
data: [projectionEntry('ses_proj', 'msg_fetch', 1)],
|
|
377
|
+
cursor: { previous: null, next: null },
|
|
378
|
+
}), {
|
|
379
|
+
status: 200,
|
|
380
|
+
headers: { 'content-type': 'application/json' },
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const assistant = service.createAssistant(assistantInput);
|
|
385
|
+
await service.ensure(assistant.id);
|
|
386
|
+
const page = await service.historicalMessages(assistant.id, { limit: 20 });
|
|
387
|
+
expect(page.entries.map((entry) => entry.info.id)).toEqual(['msg_fetch']);
|
|
388
|
+
expect(urls).toHaveLength(1);
|
|
389
|
+
expect(urls[0].pathname).toBe('/api/session/ses_proj/message');
|
|
390
|
+
expect(urls[0].searchParams.get('limit')).toBe('20');
|
|
391
|
+
expect(urls[0].searchParams.get('order')).toBe('desc');
|
|
392
|
+
expect(urls[0].searchParams.has('cursor')).toBe(false);
|
|
393
|
+
expect(urls[0].searchParams.get('directory')).toEqual(expect.any(String));
|
|
394
|
+
service.close();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('fetchImpl continuation omits order and only sends cursor', async () => {
|
|
398
|
+
const rows = [
|
|
399
|
+
projectionEntry('ses_1', 'msg_2', 2),
|
|
400
|
+
projectionEntry('ses_1', 'msg_1', 1),
|
|
401
|
+
];
|
|
402
|
+
const urls = [];
|
|
403
|
+
const service = setup(root(), {
|
|
404
|
+
create: async () => ({ data: { id: 'ses_1' } }),
|
|
405
|
+
}, {
|
|
406
|
+
fetchImpl: async (input) => {
|
|
407
|
+
const url = new URL(String(input));
|
|
408
|
+
urls.push(url);
|
|
409
|
+
const cursor = url.searchParams.get('cursor');
|
|
410
|
+
if (!cursor) {
|
|
411
|
+
return new Response(JSON.stringify({
|
|
412
|
+
data: [rows[0]],
|
|
413
|
+
cursor: { next: 'opaque_next' },
|
|
414
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
415
|
+
}
|
|
416
|
+
if (cursor === 'opaque_next') {
|
|
417
|
+
return new Response(JSON.stringify({
|
|
418
|
+
data: [rows[1]],
|
|
419
|
+
// Non-empty last page still exposes next; empty follow-up ends.
|
|
420
|
+
cursor: { next: 'opaque_end' },
|
|
421
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
422
|
+
}
|
|
423
|
+
return new Response(JSON.stringify({
|
|
424
|
+
data: [],
|
|
425
|
+
cursor: { next: null },
|
|
426
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
const assistant = service.createAssistant(assistantInput);
|
|
430
|
+
await service.ensure(assistant.id);
|
|
431
|
+
const first = await service.historicalMessages(assistant.id, { limit: 1 });
|
|
432
|
+
expect(first.entries.map((e) => e.info.id)).toEqual(['msg_2']);
|
|
433
|
+
expect(first.complete).toBe(false);
|
|
434
|
+
const second = await service.historicalMessages(assistant.id, { before: first.nextCursor, limit: 1 });
|
|
435
|
+
expect(second.entries.map((e) => e.info.id)).toEqual(['msg_1']);
|
|
436
|
+
expect(urls[0].searchParams.get('order')).toBe('desc');
|
|
437
|
+
expect(urls[0].searchParams.has('cursor')).toBe(false);
|
|
438
|
+
const cont = urls.filter((url) => url.searchParams.has('cursor'));
|
|
439
|
+
expect(cont.length).toBeGreaterThanOrEqual(1);
|
|
440
|
+
expect(cont.every((url) => !url.searchParams.has('order'))).toBe(true);
|
|
441
|
+
service.close();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('exposes V2 sharing as unavailable and never reads mirrors to fake a share', async () => {
|
|
445
|
+
const service = setup();
|
|
446
|
+
expect(await service.capability()).toMatchObject({ sharingAvailable: false, archiveMetadataAvailable: false, sessionMetadataAvailable: false });
|
|
447
|
+
const source = fs.readFileSync(new URL('./service.js', import.meta.url), 'utf8');
|
|
448
|
+
expect(source).toContain('sharingAvailable: false');
|
|
449
|
+
expect(/historicalMessages[\s\S]*JOIN assistant_message_mirror/.test(source)).toBe(false);
|
|
450
|
+
expect(/sendWithConfig[\s\S]*mirrorAdmittedUserMessage\(/.test(source)).toBe(false);
|
|
451
|
+
expect(/submitClaim[\s\S]*mirrorAdmittedUserMessage\(/.test(source)).toBe(false);
|
|
452
|
+
service.close();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -3,9 +3,9 @@ import { isSafeInlineImageMime } from './contact-attachments.js';
|
|
|
3
3
|
import { createChatCompletion } from '../llm/completions.js';
|
|
4
4
|
import { ensureLlmTempDirectory } from '../llm/temp-directory.js';
|
|
5
5
|
import { setAssignedSessionSettleHandler } from '../session-goal/runtime.js';
|
|
6
|
-
import {
|
|
6
|
+
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
7
7
|
|
|
8
|
-
const respond = (res, work, success = 200) => Promise.resolve().then(work).then((body) => res.status(success).json(body)).catch((error) => { const code = error instanceof AssistantError ? error.code : error?.code || 'internal_error'; const status = code === 'not_found' ? 404 : ['revision_conflict', 'idempotency_conflict', 'contact_generation_conflict'].includes(code) ? 409 : code === 'assistant_disabled' ? 403 : code === 'no_provider' ? 400 : code === 'upstream_error' ? 502 : 400; res.status(status).json({ ok: false, error: code, message: typeof error?.message === 'string' && error.message.trim() ? error.message : code }); });
|
|
8
|
+
const respond = (res, work, success = 200) => Promise.resolve().then(work).then((body) => res.status(success).json(body)).catch((error) => { const code = error instanceof AssistantError ? error.code : error?.code || 'internal_error'; const status = code === 'not_found' ? 404 : ['revision_conflict', 'idempotency_conflict', 'contact_generation_conflict'].includes(code) ? 409 : code === 'assistant_disabled' ? 403 : code === 'no_provider' ? 400 : code === 'upstream_error' || code === 'llm_attachment_generation_unavailable' ? 502 : 400; res.status(status).json({ ok: false, error: code, message: typeof error?.message === 'string' && error.message.trim() ? error.message : code }); });
|
|
9
9
|
const gone = (_req, res) => res.status(410).json({ ok: false, error: 'assistant_topics_retired' });
|
|
10
10
|
|
|
11
11
|
export const registerAssistantRoutes = (app, dependencies) => {
|
|
@@ -13,13 +13,15 @@ export const registerAssistantRoutes = (app, dependencies) => {
|
|
|
13
13
|
...input,
|
|
14
14
|
buildOpenCodeUrl: dependencies.buildOpenCodeUrl,
|
|
15
15
|
getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders,
|
|
16
|
-
clientFactory: () =>
|
|
16
|
+
clientFactory: () => makeOpenCodeV2Client({
|
|
17
17
|
baseUrl: dependencies.buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
18
|
-
|
|
18
|
+
authHeaders: dependencies.getOpenCodeAuthHeaders(),
|
|
19
19
|
}),
|
|
20
20
|
ensureTempDirectory: ensureLlmTempDirectory,
|
|
21
|
-
//
|
|
21
|
+
// Attachment-session generate may subscribe for session.text.delta (contact harness).
|
|
22
22
|
globalEventHub: input?.globalEventHub ?? dependencies.globalEventHub,
|
|
23
|
+
persistSessionMetadata: dependencies.persistSessionMetadata,
|
|
24
|
+
onSystemSessionPersisted: dependencies.onSystemSessionPersisted,
|
|
23
25
|
});
|
|
24
26
|
const service = createAssistantsService({
|
|
25
27
|
dbPath: dependencies.dbPath,
|
|
@@ -38,6 +40,10 @@ export const registerAssistantRoutes = (app, dependencies) => {
|
|
|
38
40
|
onRevisionTip: dependencies.onRevisionTip,
|
|
39
41
|
onContactTurnEvent: dependencies.onContactTurnEvent,
|
|
40
42
|
onContactTurnComplete: dependencies.onContactTurnComplete,
|
|
43
|
+
archiveSessionHost: dependencies.archiveSessionHost,
|
|
44
|
+
forgetSessionHost: dependencies.forgetSessionHost,
|
|
45
|
+
persistSessionMetadata: dependencies.persistSessionMetadata,
|
|
46
|
+
onSystemSessionPersisted: dependencies.onSystemSessionPersisted,
|
|
41
47
|
createChatCompletion: boundCompletion,
|
|
42
48
|
});
|
|
43
49
|
setAssignedSessionSettleHandler(({ sessionId, status }) => service.reportAssignedSessionSettle(sessionId, status));
|