@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
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
vi.mock('
|
|
4
|
-
const {
|
|
3
|
+
vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: vi.fn() }));
|
|
4
|
+
const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
|
|
5
5
|
const { createOpenCodeMessageQueueAdapter } = await import('./opencode-adapter.js');
|
|
6
6
|
const { createSessionTurnGate } = await import('./session-turn-gate.js');
|
|
7
7
|
|
|
8
|
+
const notFound = (status = 404) => Object.assign(new Error('not_found'), { status, code: 'not_found' });
|
|
9
|
+
const v2Client = ({ active = {}, list = { data: [] }, inbox = [], message } = {}) => ({
|
|
10
|
+
session: {
|
|
11
|
+
active: vi.fn(async () => active),
|
|
12
|
+
inbox: { list: vi.fn(async () => inbox) },
|
|
13
|
+
message: vi.fn(async ({ messageID }) => {
|
|
14
|
+
if (typeof message === 'function') return message(messageID);
|
|
15
|
+
if (message && typeof message === 'object' && messageID in message) return message[messageID];
|
|
16
|
+
throw notFound();
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
message: { list: vi.fn(async () => list) },
|
|
20
|
+
});
|
|
21
|
+
|
|
8
22
|
describe('OpenCode message queue adapter', () => {
|
|
9
23
|
it('waits for readiness without forwarding worker options', async () => {
|
|
10
24
|
const waitForReady = vi.fn(); const adapter = createOpenCodeMessageQueueAdapter({ waitForReady });
|
|
11
25
|
await adapter.waitForReady({ signal: new AbortController().signal });
|
|
12
26
|
expect(waitForReady).toHaveBeenCalledWith();
|
|
13
27
|
});
|
|
14
|
-
it('captures runtime, materializes text and files, and
|
|
15
|
-
const
|
|
28
|
+
it('captures runtime, materializes text and files, and applies sendConfig before prompt', async () => {
|
|
29
|
+
const fetchMock = vi.fn(async () => ({ ok: true, status: 204, json: async () => ({}) }));
|
|
30
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
31
|
+
makeOpenCodeV2Client.mockReturnValue(v2Client());
|
|
16
32
|
let generation = 1;
|
|
17
33
|
const adapter = createOpenCodeMessageQueueAdapter({ waitForReady: vi.fn(), buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({ Authorization: 'secret' }), getRuntimeConfig: () => ({ apiBaseUrl: 'http://open.code' }), getRuntimeGeneration: () => generation, getSessionEligibility: () => ({ idle: true, settled: true }), getLatestMessageID: () => 'old', readAttachment: () => ({ type: 'file', url: 'file:///attachment' }) });
|
|
18
34
|
const runtime = adapter.captureRuntime(); const scope = { sessionID: 'session', directory: '/repo' }; expect(await adapter.checkEligibility(scope)).toMatchObject({ available: true, idle: true, settled: true, latestMessageID: 'old' });
|
|
19
35
|
const parts = await adapter.materializeAttachments({ content: 'text', attachments: [{}] }); expect(parts).toEqual([{ type: 'text', text: 'text' }, { type: 'file', url: 'file:///attachment' }]);
|
|
20
|
-
await adapter.send({ scope, messageID: 'message', runtime, sendConfig: { providerID: 'p', modelID: 'm', agent: 'a', variant: 'v' }, parts });
|
|
36
|
+
await adapter.send({ scope, messageID: 'message', runtime, sendConfig: { providerID: 'p', modelID: 'm', agent: 'a', variant: 'v' }, parts });
|
|
37
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
38
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('/api/session/session/agent');
|
|
39
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ agent: 'a' });
|
|
40
|
+
expect(String(fetchMock.mock.calls[1][0])).toContain('/api/session/session/model');
|
|
41
|
+
expect(JSON.parse(fetchMock.mock.calls[1][1].body)).toEqual({ model: { id: 'm', providerID: 'p', variant: 'v' } });
|
|
42
|
+
expect(String(fetchMock.mock.calls[2][0])).toContain('/api/session/session/prompt');
|
|
43
|
+
const promptBody = JSON.parse(fetchMock.mock.calls[2][1].body);
|
|
44
|
+
expect(promptBody).toEqual(expect.objectContaining({ id: 'message', delivery: 'steer', text: 'text' }));
|
|
45
|
+
expect(promptBody.model).toBeUndefined();
|
|
46
|
+
expect(promptBody.agent).toBeUndefined();
|
|
47
|
+
expect(promptBody.variant).toBeUndefined();
|
|
21
48
|
generation = 2; expect(await adapter.send({ scope, runtime, sendConfig: { providerID: 'p', modelID: 'm' } })).toMatchObject({ code: 'runtime_stale' });
|
|
49
|
+
vi.unstubAllGlobals();
|
|
22
50
|
});
|
|
23
51
|
it('materializes Assistant attachment IDs in delivery order', async () => {
|
|
24
52
|
const readAttachment = vi.fn((attachment) => ({ type: 'file', url: `file:///${attachment.attachmentID}`, filename: attachment.filename }));
|
|
@@ -30,110 +58,352 @@ describe('OpenCode message queue adapter', () => {
|
|
|
30
58
|
expect(readAttachment).toHaveBeenCalledWith({ attachmentID: 'image', filename: 'image.png' }, expect.any(Object), expect.any(Object));
|
|
31
59
|
});
|
|
32
60
|
it('uses the injected upstream runtime URL and detects its changes', async () => {
|
|
33
|
-
const
|
|
34
|
-
|
|
61
|
+
const fetchMock = vi.fn(async () => ({ ok: true, status: 204, json: async () => ({}) }));
|
|
62
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
63
|
+
makeOpenCodeV2Client.mockReturnValue(v2Client());
|
|
35
64
|
let upstreamUrl = 'http://opencode-upstream:4096/';
|
|
36
65
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => upstreamUrl, getOpenCodeAuthHeaders: () => ({}), getRuntimeConfig: () => ({ apiBaseUrl: upstreamUrl }), readAttachment: () => null });
|
|
37
66
|
const runtime = adapter.captureRuntime();
|
|
38
67
|
await adapter.send({ scope: { sessionID: 's', directory: '/repo' }, messageID: 'msg_1', runtime, sendConfig: { providerID: 'p', modelID: 'm' }, parts: [] });
|
|
39
|
-
|
|
68
|
+
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt'));
|
|
69
|
+
expect(String(promptCall[0])).toContain('http://opencode-upstream:4096/api/session/s/prompt');
|
|
40
70
|
upstreamUrl = 'http://opencode-upstream:4097/';
|
|
41
71
|
expect(adapter.isCurrent(runtime)).toBe(false);
|
|
72
|
+
vi.unstubAllGlobals();
|
|
42
73
|
});
|
|
43
74
|
it('classifies SDK results and exact reconciliation matches without exposing transport details', async () => {
|
|
44
|
-
const
|
|
75
|
+
const fetchMock = vi.fn(async () => ({ ok: false, status: 503, json: async () => ({}) }));
|
|
76
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
77
|
+
makeOpenCodeV2Client.mockReturnValue(v2Client({ inbox: [{ id: 'wanted', sessionID: 's' }] }));
|
|
45
78
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), getSessionEligibility: () => ({ idle: true, settled: true }), getLatestMessageID: () => null, readAttachment: () => null });
|
|
46
|
-
|
|
79
|
+
// Prompt-only path (no sendConfig) classifies transport status on the POST itself.
|
|
80
|
+
expect(await adapter.send({ scope: { sessionID: 's', directory: '/d' }, messageID: 'm', parts: [] })).toMatchObject({ kind: 'ambiguous', status: 503 });
|
|
81
|
+
expect(await adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).toEqual({ found: true });
|
|
82
|
+
vi.unstubAllGlobals();
|
|
47
83
|
});
|
|
48
84
|
it('treats explicit 2xx empty bodies as ok and never treats undefined/malformed results as success', async () => {
|
|
49
|
-
const
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
|
|
85
|
+
const fetchMock = vi.fn()
|
|
86
|
+
.mockResolvedValueOnce({ ok: true, status: 204, json: async () => undefined })
|
|
87
|
+
.mockResolvedValueOnce({ ok: true, status: 202, json: async () => undefined })
|
|
88
|
+
.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({}) })
|
|
89
|
+
.mockResolvedValueOnce(undefined)
|
|
90
|
+
.mockResolvedValueOnce(null)
|
|
91
|
+
.mockResolvedValueOnce({});
|
|
92
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
57
93
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), getRuntimeConfig: () => ({ apiBaseUrl: 'http://open.code' }), readAttachment: () => null });
|
|
58
|
-
const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm',
|
|
94
|
+
const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm', parts: [] };
|
|
59
95
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: true, status: 204 });
|
|
60
96
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: true, status: 202 });
|
|
61
97
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: true, status: 200 });
|
|
62
98
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', code: 'malformed_result' });
|
|
63
99
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', code: 'malformed_result' });
|
|
64
100
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', code: 'malformed_result' });
|
|
101
|
+
vi.unstubAllGlobals();
|
|
65
102
|
});
|
|
66
103
|
it('reads failure status from error.status and classifies definitive 4xx failures', async () => {
|
|
67
|
-
const
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
.
|
|
71
|
-
.
|
|
72
|
-
|
|
104
|
+
const fetchMock = vi.fn()
|
|
105
|
+
.mockResolvedValueOnce({ ok: false, status: 400, json: async () => ({}) })
|
|
106
|
+
.mockResolvedValueOnce({ ok: false, status: 422, json: async () => ({}) })
|
|
107
|
+
.mockResolvedValueOnce({ ok: false, status: 429, json: async () => ({}) })
|
|
108
|
+
.mockResolvedValueOnce({ ok: false, status: 408, json: async () => ({}) })
|
|
109
|
+
.mockResolvedValueOnce({ ok: false, status: 409, json: async () => ({ _tag: 'SessionBusyError' }) });
|
|
110
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
73
111
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
74
|
-
const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm',
|
|
112
|
+
const base = { scope: { sessionID: 's', directory: '/d' }, messageID: 'm', parts: [] };
|
|
75
113
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'failed', status: 400 });
|
|
76
114
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'failed', status: 422 });
|
|
77
115
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', status: 429 });
|
|
78
116
|
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'ambiguous', status: 408 });
|
|
117
|
+
await expect(adapter.send(base)).resolves.toMatchObject({ ok: false, kind: 'retry', code: 'session_busy' });
|
|
118
|
+
vi.unstubAllGlobals();
|
|
79
119
|
});
|
|
80
120
|
it('prefers client.v2.session.message exact lookup and treats 404 as found:false', async () => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
const api = v2Client({
|
|
122
|
+
inbox: [],
|
|
123
|
+
message: { wanted: { id: 'wanted', type: 'user' } },
|
|
124
|
+
});
|
|
125
|
+
makeOpenCodeV2Client.mockReturnValue(api);
|
|
86
126
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
87
127
|
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ found: true });
|
|
88
128
|
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'missing')).resolves.toEqual({ found: false });
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
91
|
-
});
|
|
92
|
-
it('
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.mockReturnValueOnce({ session: { message: legacyMessage, messages, promptAsync: vi.fn() } })
|
|
99
|
-
.mockReturnValueOnce({ session: { message: legacyMessage, messages, promptAsync: vi.fn() } })
|
|
100
|
-
.mockReturnValueOnce({ session: { messages, promptAsync: vi.fn() } });
|
|
129
|
+
expect(api.session.inbox.list).toHaveBeenCalled();
|
|
130
|
+
expect(api.session.message).toHaveBeenCalledWith({ sessionID: 's', messageID: 'wanted' }, expect.any(Object));
|
|
131
|
+
});
|
|
132
|
+
it('reconciles inbox then session.message without session.messages or 1.x fallback', async () => {
|
|
133
|
+
const api = v2Client({
|
|
134
|
+
inbox: [{ id: 'legacy-hit' }],
|
|
135
|
+
message: { 'bounded-hit': { id: 'bounded-hit', type: 'user' } },
|
|
136
|
+
});
|
|
137
|
+
makeOpenCodeV2Client.mockReturnValue(api);
|
|
101
138
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
102
139
|
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'legacy-hit')).resolves.toEqual({ found: true });
|
|
103
140
|
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'missing')).resolves.toEqual({ found: false });
|
|
104
141
|
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'bounded-hit')).resolves.toEqual({ found: true });
|
|
105
|
-
expect(
|
|
142
|
+
expect(api.session.status).toBeUndefined();
|
|
143
|
+
expect(api.session.messages).toBeUndefined();
|
|
144
|
+
expect(api.session.promptAsync).toBeUndefined();
|
|
106
145
|
});
|
|
107
|
-
it('
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
146
|
+
it('marks non-404 inbox/message failures unavailable instead of falling back', async () => {
|
|
147
|
+
const api = v2Client();
|
|
148
|
+
api.session.inbox.list.mockRejectedValue(Object.assign(new Error('unsupported'), { status: 405 }));
|
|
149
|
+
makeOpenCodeV2Client.mockReturnValue(api);
|
|
111
150
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
112
|
-
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({
|
|
113
|
-
expect(messages).toHaveBeenCalledTimes(1);
|
|
151
|
+
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ unavailable: true });
|
|
114
152
|
});
|
|
115
|
-
it('uses absent session
|
|
116
|
-
const
|
|
153
|
+
it('uses absent session.active membership as idle and derives settlement from type and time.completed', async () => {
|
|
154
|
+
const list = vi.fn(async () => ({ data: [] }));
|
|
155
|
+
const active = vi.fn(async () => ({}));
|
|
156
|
+
makeOpenCodeV2Client.mockReturnValue({ session: { active, inbox: { list: vi.fn() }, message: vi.fn() }, message: { list } });
|
|
117
157
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
118
158
|
expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ idle: true, settled: true });
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
list.mockResolvedValueOnce({ data: [{ id: 'user', type: 'user', time: { created: 1 } }] });
|
|
160
|
+
expect((await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).settled).toBe(false);
|
|
161
|
+
list.mockResolvedValueOnce({ data: [{ id: 'assistant', type: 'assistant', time: { completed: 1 } }] });
|
|
162
|
+
expect((await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).settled).toBe(true);
|
|
163
|
+
expect(list).toHaveBeenCalledWith({ sessionID: 's', limit: 1, order: 'desc' }, expect.any(Object));
|
|
164
|
+
});
|
|
165
|
+
it('treats a sessionID present in session.active as not idle', async () => {
|
|
166
|
+
makeOpenCodeV2Client.mockReturnValue(v2Client({
|
|
167
|
+
active: { s: { type: 'running' } },
|
|
168
|
+
list: { data: [{ id: 'assistant', type: 'assistant', time: { completed: 1 } }] },
|
|
169
|
+
}));
|
|
170
|
+
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
171
|
+
expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ available: true, idle: false });
|
|
121
172
|
});
|
|
122
173
|
it('recovers an idle incomplete assistant tail without treating fetch failure as empty', async () => {
|
|
123
174
|
let now = 0; const turnGate = createSessionTurnGate({ clock: () => now });
|
|
124
|
-
const
|
|
125
|
-
const
|
|
175
|
+
const list = vi.fn(async () => ({ data: [{ id: 'assistant', type: 'assistant', time: { created: 1 } }] }));
|
|
176
|
+
const active = vi.fn(async () => ({}));
|
|
177
|
+
makeOpenCodeV2Client.mockReturnValue({ session: { active, inbox: { list: vi.fn() }, message: vi.fn() }, message: { list } });
|
|
126
178
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null, turnGate });
|
|
127
179
|
expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ settled: false, settlementReason: 'tail_unsettled' });
|
|
128
|
-
|
|
180
|
+
active.mockRejectedValueOnce(Object.assign(new Error('upstream'), { status: 500 })); now = 3_000;
|
|
129
181
|
await expect(adapter.checkEligibility({ sessionID: 's', directory: '/d' })).resolves.toEqual({ available: false, idle: false, settled: false });
|
|
130
182
|
expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ settled: false, settlementReason: 'tail_unsettled' });
|
|
131
183
|
now = 6_000;
|
|
132
184
|
expect(await adapter.checkEligibility({ sessionID: 's', directory: '/d' })).toMatchObject({ settled: true, settlementReason: 'stopped_assistant' });
|
|
133
185
|
});
|
|
134
186
|
it('marks malformed or failed authoritative eligibility reads unavailable', async () => {
|
|
135
|
-
|
|
187
|
+
makeOpenCodeV2Client.mockReturnValue({
|
|
188
|
+
session: { active: vi.fn(async () => { throw Object.assign(new Error('upstream'), { status: 500 }); }), inbox: { list: vi.fn() }, message: vi.fn() },
|
|
189
|
+
message: { list: vi.fn(async () => ({ data: [] })) },
|
|
190
|
+
});
|
|
136
191
|
const adapter = createOpenCodeMessageQueueAdapter({ buildOpenCodeUrl: () => 'http://open.code/', getOpenCodeAuthHeaders: () => ({}), readAttachment: () => null });
|
|
137
192
|
await expect(adapter.checkEligibility({ sessionID: 's', directory: '/d' })).resolves.toEqual({ available: false, idle: false, settled: false });
|
|
138
193
|
});
|
|
139
194
|
});
|
|
195
|
+
|
|
196
|
+
describe('sendConfig serial boundary (v2 prompt contract)', () => {
|
|
197
|
+
it('applies different models in agent→model→prompt order per item', async () => {
|
|
198
|
+
const calls = [];
|
|
199
|
+
const fetchMock = vi.fn(async (url, init) => {
|
|
200
|
+
calls.push({ url: String(url), body: init?.body ? JSON.parse(init.body) : null });
|
|
201
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
202
|
+
});
|
|
203
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
204
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
205
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
206
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
207
|
+
readAttachment: () => null,
|
|
208
|
+
});
|
|
209
|
+
const scope = { sessionID: 's', directory: '/d' };
|
|
210
|
+
await adapter.send({
|
|
211
|
+
scope, messageID: 'msg_a', parts: [{ type: 'text', text: 'first' }],
|
|
212
|
+
sendConfig: { providerID: 'openai', modelID: 'gpt-a', agent: 'build', variant: 'fast' },
|
|
213
|
+
});
|
|
214
|
+
await adapter.send({
|
|
215
|
+
scope, messageID: 'msg_b', parts: [{ type: 'text', text: 'second' }],
|
|
216
|
+
sendConfig: { providerID: 'anthropic', modelID: 'claude-b', agent: 'plan' },
|
|
217
|
+
});
|
|
218
|
+
expect(calls.map((c) => c.url.replace(/^.*\/api\/session\/s\//, ''))).toEqual([
|
|
219
|
+
'agent?directory=%2Fd',
|
|
220
|
+
'model?directory=%2Fd',
|
|
221
|
+
'prompt?directory=%2Fd',
|
|
222
|
+
'agent?directory=%2Fd',
|
|
223
|
+
'model?directory=%2Fd',
|
|
224
|
+
'prompt?directory=%2Fd',
|
|
225
|
+
]);
|
|
226
|
+
expect(calls[0].body).toEqual({ agent: 'build' });
|
|
227
|
+
expect(calls[1].body).toEqual({ model: { id: 'gpt-a', providerID: 'openai', variant: 'fast' } });
|
|
228
|
+
expect(calls[2].body).toEqual(expect.objectContaining({ id: 'msg_a', text: 'first', delivery: 'steer' }));
|
|
229
|
+
expect(calls[2].body.model).toBeUndefined();
|
|
230
|
+
expect(calls[3].body).toEqual({ agent: 'plan' });
|
|
231
|
+
expect(calls[4].body).toEqual({ model: { id: 'claude-b', providerID: 'anthropic' } });
|
|
232
|
+
expect(calls[5].body).toEqual(expect.objectContaining({ id: 'msg_b', text: 'second' }));
|
|
233
|
+
vi.unstubAllGlobals();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('does not POST prompt when model switch fails', async () => {
|
|
237
|
+
const fetchMock = vi.fn(async (url) => {
|
|
238
|
+
if (String(url).includes('/model')) return { ok: false, status: 400, json: async () => ({ _tag: 'InvalidRequestError' }) };
|
|
239
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
240
|
+
});
|
|
241
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
242
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
243
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
244
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
245
|
+
readAttachment: () => null,
|
|
246
|
+
});
|
|
247
|
+
const result = await adapter.send({
|
|
248
|
+
scope: { sessionID: 's', directory: '/d' },
|
|
249
|
+
messageID: 'msg_fail',
|
|
250
|
+
parts: [{ type: 'text', text: 'nope' }],
|
|
251
|
+
sendConfig: { providerID: 'p', modelID: 'm', agent: 'a' },
|
|
252
|
+
});
|
|
253
|
+
expect(result).toMatchObject({ ok: false, kind: 'failed', code: 'config_switch_failed', status: 400 });
|
|
254
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/prompt'))).toBe(false);
|
|
255
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/agent'))).toBe(true);
|
|
256
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/model'))).toBe(true);
|
|
257
|
+
vi.unstubAllGlobals();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('treats session busy on switch as retry without prompt', async () => {
|
|
261
|
+
const fetchMock = vi.fn(async (url) => {
|
|
262
|
+
if (String(url).includes('/agent')) return { ok: false, status: 409, json: async () => ({ _tag: 'SessionBusyError' }) };
|
|
263
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
264
|
+
});
|
|
265
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
266
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
267
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
268
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
269
|
+
readAttachment: () => null,
|
|
270
|
+
});
|
|
271
|
+
const result = await adapter.send({
|
|
272
|
+
scope: { sessionID: 's', directory: '/d' },
|
|
273
|
+
messageID: 'msg_busy',
|
|
274
|
+
parts: [{ type: 'text', text: 'wait' }],
|
|
275
|
+
sendConfig: { providerID: 'p', modelID: 'm', agent: 'a' },
|
|
276
|
+
});
|
|
277
|
+
expect(result).toMatchObject({ ok: false, kind: 'retry', code: 'session_busy' });
|
|
278
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/prompt'))).toBe(false);
|
|
279
|
+
vi.unstubAllGlobals();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('maps MessageNotFoundError _tag from projection to found:false', async () => {
|
|
283
|
+
const api = v2Client({ inbox: [] });
|
|
284
|
+
api.session.message.mockRejectedValue({
|
|
285
|
+
_tag: 'MessageNotFoundError',
|
|
286
|
+
sessionID: 's',
|
|
287
|
+
messageID: 'missing',
|
|
288
|
+
message: 'not found',
|
|
289
|
+
});
|
|
290
|
+
makeOpenCodeV2Client.mockReturnValue(api);
|
|
291
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
292
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
293
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
294
|
+
readAttachment: () => null,
|
|
295
|
+
});
|
|
296
|
+
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'missing')).resolves.toEqual({ found: false });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('awaits projection rejections so non-404 stays unavailable instead of escaping', async () => {
|
|
300
|
+
const api = v2Client({ inbox: [] });
|
|
301
|
+
api.session.message.mockRejectedValue(Object.assign(new Error('boom'), { status: 500 }));
|
|
302
|
+
makeOpenCodeV2Client.mockReturnValue(api);
|
|
303
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
304
|
+
buildOpenCodeUrl: () => 'http://open.code/',
|
|
305
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
306
|
+
readAttachment: () => null,
|
|
307
|
+
});
|
|
308
|
+
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ unavailable: true });
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('intent queue prompt/inbox authority (ticket 12)', () => {
|
|
313
|
+
const jsonResponse = (status, body) => ({
|
|
314
|
+
ok: status >= 200 && status < 300,
|
|
315
|
+
status,
|
|
316
|
+
json: async () => body,
|
|
317
|
+
text: async () => JSON.stringify(body),
|
|
318
|
+
headers: { get: () => 'application/json' },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('sends via v2 prompt/inbox and does not treat local parts as the body of record', async () => {
|
|
322
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, {
|
|
323
|
+
id: 'msg_inbox',
|
|
324
|
+
sessionID: 's',
|
|
325
|
+
delivery: 'steer',
|
|
326
|
+
payload: { text: 'from-projection' },
|
|
327
|
+
}));
|
|
328
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
329
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
330
|
+
buildOpenCodeUrl: (pathname) => `http://open.code${pathname}`,
|
|
331
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic secret' }),
|
|
332
|
+
readAttachment: () => ({ type: 'file', url: 'file:///staged' }),
|
|
333
|
+
});
|
|
334
|
+
const localParts = [{ type: 'text', text: 'stale-local-parts' }];
|
|
335
|
+
const result = await adapter.send({
|
|
336
|
+
scope: { sessionID: 's', directory: '/d' },
|
|
337
|
+
messageID: 'msg_inbox',
|
|
338
|
+
sendConfig: { providerID: 'p', modelID: 'm' },
|
|
339
|
+
parts: localParts,
|
|
340
|
+
content: 'stale-local-parts',
|
|
341
|
+
});
|
|
342
|
+
expect(result).toMatchObject({ ok: true, status: 200 });
|
|
343
|
+
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt'));
|
|
344
|
+
expect(promptCall).toBeTruthy();
|
|
345
|
+
const [url, init] = promptCall;
|
|
346
|
+
expect(String(url)).toContain('/api/session/s/prompt');
|
|
347
|
+
const body = JSON.parse(init.body);
|
|
348
|
+
expect(body.delivery).toMatch(/steer|queue/);
|
|
349
|
+
expect(body.id).toBe('msg_inbox');
|
|
350
|
+
expect(body.model).toBeUndefined();
|
|
351
|
+
expect(init.method).toBe('POST');
|
|
352
|
+
vi.unstubAllGlobals();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('reconciles only against OpenCode inbox and projection, never a local parts scan', async () => {
|
|
356
|
+
const list = vi.fn(async () => ({ data: [{ id: 'wanted', type: 'user', text: 'local-scan' }] }));
|
|
357
|
+
const api = v2Client({
|
|
358
|
+
inbox: [{ id: 'wanted', sessionID: 's', delivery: 'steer', payload: { text: 'inbox' } }],
|
|
359
|
+
message: { wanted: { id: 'wanted', type: 'user' } },
|
|
360
|
+
});
|
|
361
|
+
api.message.list = list;
|
|
362
|
+
makeOpenCodeV2Client.mockReturnValue(api);
|
|
363
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
364
|
+
buildOpenCodeUrl: (pathname) => `http://open.code${pathname}`,
|
|
365
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
366
|
+
readAttachment: () => null,
|
|
367
|
+
});
|
|
368
|
+
await expect(adapter.findMessage({ sessionID: 's', directory: '/d' }, 'wanted')).resolves.toEqual({ found: true });
|
|
369
|
+
expect(list).not.toHaveBeenCalled();
|
|
370
|
+
expect(api.session.inbox.list).toHaveBeenCalledWith({ sessionID: 's' }, expect.any(Object));
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('keeps staged attachment files until prompt is accepted, then projection wins', async () => {
|
|
374
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, {
|
|
375
|
+
id: 'msg_att',
|
|
376
|
+
sessionID: 's',
|
|
377
|
+
delivery: 'steer',
|
|
378
|
+
payload: { text: 'accepted' },
|
|
379
|
+
}));
|
|
380
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
381
|
+
const readAttachment = vi.fn(() => ({ type: 'file', url: 'file:///staged-disk', filename: 'note.txt' }));
|
|
382
|
+
const adapter = createOpenCodeMessageQueueAdapter({
|
|
383
|
+
buildOpenCodeUrl: (pathname) => `http://open.code${pathname}`,
|
|
384
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
385
|
+
readAttachment,
|
|
386
|
+
});
|
|
387
|
+
const parts = await adapter.materializeAttachments({
|
|
388
|
+
content: 'caption',
|
|
389
|
+
attachments: [{ attachmentID: 'att-1', filename: 'note.txt' }],
|
|
390
|
+
});
|
|
391
|
+
expect(parts).toEqual([
|
|
392
|
+
{ type: 'text', text: 'caption' },
|
|
393
|
+
{ type: 'file', url: 'file:///staged-disk', filename: 'note.txt' },
|
|
394
|
+
]);
|
|
395
|
+
const result = await adapter.send({
|
|
396
|
+
scope: { sessionID: 's', directory: '/d' },
|
|
397
|
+
messageID: 'msg_att',
|
|
398
|
+
sendConfig: { providerID: 'p', modelID: 'm' },
|
|
399
|
+
parts,
|
|
400
|
+
});
|
|
401
|
+
expect(result.ok).toBe(true);
|
|
402
|
+
expect(readAttachment).toHaveBeenCalled();
|
|
403
|
+
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt'));
|
|
404
|
+
const body = JSON.parse(promptCall[1].body);
|
|
405
|
+
expect(body.files?.length || body.parts).toBeTruthy();
|
|
406
|
+
expect(body.model).toBeUndefined();
|
|
407
|
+
vi.unstubAllGlobals();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -35,13 +35,26 @@ export const createMessageQueueRuntime = ({ dbPath, attachmentRoot, service, att
|
|
|
35
35
|
const observeSessionEvent = (event) => {
|
|
36
36
|
if (!isCurrentRuntimeEvent(event)) return false;
|
|
37
37
|
const payload = event?.payload?.payload ?? event?.payload;
|
|
38
|
-
|
|
38
|
+
// Accept bridge `{ properties }` and native v2 `{ data }` envelopes.
|
|
39
|
+
const properties = (payload?.properties && typeof payload.properties === 'object')
|
|
40
|
+
? payload.properties
|
|
41
|
+
: ((payload?.data && typeof payload.data === 'object') ? payload.data : null);
|
|
39
42
|
const directory = typeof event?.directory === 'string' && event.directory !== 'global' ? event.directory : '';
|
|
40
43
|
const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
|
|
41
44
|
if (!directory || !sessionID) return false;
|
|
45
|
+
// Ticket 07: shutdown interrupt keeps the upstream claim — do not treat as
|
|
46
|
+
// idle/error settlement for queue wake; wait for authoritative terminal.
|
|
47
|
+
if (payload.type === 'session.execution.interrupted' && properties?.reason === 'shutdown') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
42
50
|
const phase = payload.type === 'session.status'
|
|
43
51
|
? properties?.status?.type ?? properties?.info?.type
|
|
44
|
-
: payload.type === 'session.idle' ? 'idle'
|
|
52
|
+
: payload.type === 'session.idle' ? 'idle'
|
|
53
|
+
: payload.type === 'session.error' ? 'error'
|
|
54
|
+
: payload.type === 'session.execution.succeeded' ? 'idle'
|
|
55
|
+
: payload.type === 'session.execution.failed' ? 'error'
|
|
56
|
+
: payload.type === 'session.execution.interrupted' ? 'idle'
|
|
57
|
+
: '';
|
|
45
58
|
if (typeof phase !== 'string' || !phase) return false;
|
|
46
59
|
openCodeAdapter.observeSessionEvent?.({ directory, sessionID }, phase);
|
|
47
60
|
return true;
|
|
@@ -4,24 +4,24 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
6
6
|
|
|
7
|
-
vi.mock('
|
|
8
|
-
const {
|
|
7
|
+
vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: vi.fn() }));
|
|
8
|
+
const { makeOpenCodeV2Client } = await import('../opencode/v2-client.js');
|
|
9
9
|
import { createMessageQueueRuntime } from './runtime.js';
|
|
10
10
|
|
|
11
11
|
describe('message queue runtime ownership', () => {
|
|
12
12
|
it('keeps the durable queue identity separate from the upstream OpenCode adapter URL', async () => {
|
|
13
13
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'message-queue-runtime-'));
|
|
14
14
|
const uiUrl = 'http://127.0.0.1:57123'; const upstreamUrl = 'http://127.0.0.1:4096/';
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const active = vi.fn(() => ({})); const list = vi.fn(() => ({ data: [] }));
|
|
16
|
+
makeOpenCodeV2Client.mockReturnValue({ session: { active, prompt: vi.fn() }, message: { list } });
|
|
17
17
|
const runtime = createMessageQueueRuntime({ dbPath: path.join(root, 'queue.sqlite'), getRuntimeConfig: () => ({ apiBaseUrl: uiUrl }), getOpenCodeRuntimeConfig: () => ({ apiBaseUrl: upstreamUrl }), buildOpenCodeUrl: () => upstreamUrl, getOpenCodeAuthHeaders: () => ({}) });
|
|
18
18
|
try {
|
|
19
19
|
const adapterRuntime = runtime.adapter.captureRuntime();
|
|
20
20
|
await runtime.adapter.checkEligibility({ directory: '/repo', sessionID: 'session' }, adapterRuntime);
|
|
21
21
|
expect(runtime.service.getRuntimeKey()).toBe(crypto.createHash('sha256').update(`url:${uiUrl}`).digest('hex'));
|
|
22
22
|
expect(adapterRuntime.config.apiBaseUrl).toBe(upstreamUrl);
|
|
23
|
-
expect(
|
|
24
|
-
expect(
|
|
23
|
+
expect(makeOpenCodeV2Client).toHaveBeenCalledWith(expect.objectContaining({ baseUrl: 'http://127.0.0.1:4096' }));
|
|
24
|
+
expect(makeOpenCodeV2Client.mock.calls.map(([options]) => options.baseUrl)).not.toContain(uiUrl);
|
|
25
25
|
} finally {
|
|
26
26
|
await runtime.stop(); fs.rmSync(root, { recursive: true, force: true });
|
|
27
27
|
}
|
|
@@ -109,7 +109,13 @@ export const createMessageQueueWorker = ({ service, adapter, workerID, concurren
|
|
|
109
109
|
};
|
|
110
110
|
const run = async () => {
|
|
111
111
|
if (paused || stopping) return status();
|
|
112
|
-
const runtimeKey = service.getRuntimeKey();
|
|
112
|
+
const runtimeKey = service.getRuntimeKey();
|
|
113
|
+
// The managed OpenCode service may still be booting when the worker starts;
|
|
114
|
+
// captureRuntime throws until its port is detected. Leave queued work
|
|
115
|
+
// pending and retry on the next tick instead of failing the flight.
|
|
116
|
+
let runtime;
|
|
117
|
+
try { runtime = adapter.captureRuntime(); } catch { return status(); }
|
|
118
|
+
const authority = service.getAuthority({ runtimeKey });
|
|
113
119
|
if (authority?.authority !== 'active') return status();
|
|
114
120
|
service.recoverExpiredSending?.({ runtimeKey });
|
|
115
121
|
// Start dispatch probes before awaiting reconciling work so a hung findMessage
|
|
@@ -412,6 +412,11 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
+
// Ticket 07: shutdown interrupt is not a completion terminal — no ready push.
|
|
416
|
+
if (payload.type === 'session.execution.interrupted') {
|
|
417
|
+
const reason = payload.properties?.reason ?? payload.data?.reason;
|
|
418
|
+
if (reason === 'shutdown') return;
|
|
419
|
+
}
|
|
415
420
|
if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
|
|
416
421
|
const error = payload.properties?.error;
|
|
417
422
|
const errorText = typeof error?.message === 'string'
|