@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4
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 +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
- package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
- package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
- package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
- package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
- package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
- package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
- package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
- package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
- package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
- package/dist/assets/context-menu-B2LXJgXL.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-yuWg9BN4.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
- package/dist/assets/main-DiL6w6KF.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
- package/dist/assets/miniChat-CwtUaElU.js +2 -0
- package/dist/assets/mobile-DB-6--ws.js +2 -0
- package/dist/assets/multirun-TAsxFI8e.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-C13fMVEs.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
- package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
- package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
- package/dist/assets/src-ubCWXZhR.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
- package/dist/assets/textarea-BBOlRI9O.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
- package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.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-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.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-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-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 +1 -1
- package/server/index.js +327 -62
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/conversations/DOCUMENTATION.md +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- 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 +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/runtime.js +23 -5
- package/server/lib/notifications/runtime.test.js +11 -0
- package/server/lib/opencode/DOCUMENTATION.md +85 -17
- 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 +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +43 -4
- 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 +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- 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/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- 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-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- 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 +252 -46
- package/server/lib/opencode/proxy.test.js +235 -2
- package/server/lib/opencode/routes.js +296 -62
- package/server/lib/opencode/routes.test.js +278 -12
- 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 +33 -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 +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- 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/v2-client.js +14 -3
- 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/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 +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- 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 +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
- package/server/lib/session-metadata/routes.js +108 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- 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 +212 -72
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- 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 +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -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 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- 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 +124 -240
- 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 +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
- package/dist/assets/context-menu-jGtH6fR9.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.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 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -33,7 +33,7 @@ The service calls `waitForOpenCodeReady(6000, 75)` (6s, matching proxy gate `REA
|
|
|
33
33
|
- `packages/web/server/lib/conversations/validation.js`
|
|
34
34
|
- Request validation and sanitization with strict whitelist
|
|
35
35
|
- Required: `input.type='prompt'`, `directory`, `messageID`, `model` (providerID/modelID), `parts` (non-empty array)
|
|
36
|
-
|
|
36
|
+
- Optional: `title`, `parentID`, `agent`, `variant`, `metadata`, `skills` (array of `{ id }`, sanitized before forwarding as native V2 skill attachments)
|
|
37
37
|
- **Rejected**: `delivery`, `format`, unknown top-level keys (whitelist enforcement)
|
|
38
38
|
- Part types: `text` (requires non-empty `text`), `file` (requires `mime`+`url`), `agent` (requires `name`)
|
|
39
39
|
- At least one content-carrying part (text/file/agent) required
|
|
@@ -55,6 +55,7 @@ const toV2PromptInput = (sanitizedInput, sessionID) => {
|
|
|
55
55
|
text,
|
|
56
56
|
...(files.length > 0 ? { files } : {}),
|
|
57
57
|
...(agents.length > 0 ? { agents } : {}),
|
|
58
|
+
...(sanitizedInput.skills?.length ? { skills: sanitizedInput.skills.map(({ id }) => ({ id, name: id })) } : {}),
|
|
58
59
|
...(sanitizedInput.metadata ? { metadata: sanitizedInput.metadata } : {}),
|
|
59
60
|
delivery: 'steer',
|
|
60
61
|
};
|
|
@@ -393,6 +393,22 @@ describe('conversations service', () => {
|
|
|
393
393
|
|
|
394
394
|
// --- success ---
|
|
395
395
|
|
|
396
|
+
it('validates and forwards skill ids with the first prompt', async () => {
|
|
397
|
+
const validated = validateConversationInput({
|
|
398
|
+
input: { type: 'prompt' },
|
|
399
|
+
...baseInput({ parts: [{ type: 'text', text: '[skill:release] prepare notes' }], skills: [{ id: 'release', text: 'untrusted body' }] }),
|
|
400
|
+
});
|
|
401
|
+
expect(validated.valid).toBe(true);
|
|
402
|
+
expect(validated.sanitized.skills).toEqual([{ id: 'release' }]);
|
|
403
|
+
const prompt = vi.fn(async () => undefined);
|
|
404
|
+
mockMake.mockReturnValue({ session: { create: async () => ({ id: 'ses_skill' }), prompt } });
|
|
405
|
+
const result = await createService().createAndPrompt({ sanitizedInput: validated.sanitized });
|
|
406
|
+
expect(result.ok).toBe(true);
|
|
407
|
+
expect(prompt.mock.calls[0][0]).toMatchObject({
|
|
408
|
+
text: '[skill:release] prepare notes', skills: [{ id: 'release', name: 'release' }], delivery: 'steer',
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
396
412
|
it('returns success when create + prompt succeed', async () => {
|
|
397
413
|
const sessionData = { id: 'ses_created', title: 'Test' };
|
|
398
414
|
mockMake.mockReturnValue({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const ALLOWED_TOP_LEVEL_KEYS = new Set([
|
|
2
2
|
'input', 'directory', 'messageID', 'model', 'parts',
|
|
3
|
-
'title', 'parentID', 'agent', 'variant', 'metadata',
|
|
3
|
+
'title', 'parentID', 'agent', 'variant', 'metadata', 'skills',
|
|
4
4
|
]);
|
|
5
5
|
const ALLOWED_PART_TYPES = new Set(['text', 'file', 'agent']);
|
|
6
6
|
|
|
@@ -139,6 +139,15 @@ export const validateConversationInput = (body) => {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// Optional fields
|
|
142
|
+
let skills;
|
|
143
|
+
if (body.skills !== undefined) {
|
|
144
|
+
if (!Array.isArray(body.skills) || body.skills.some((skill) =>
|
|
145
|
+
!skill || typeof skill.id !== 'string' || !skill.id.trim())) {
|
|
146
|
+
errors.push('skills must be an array of non-empty skill ids');
|
|
147
|
+
} else {
|
|
148
|
+
skills = body.skills.map((skill) => ({ id: skill.id }));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
142
151
|
const title = asOptionalNonEmptyString(body.title);
|
|
143
152
|
if (title === null) errors.push('title must be a non-empty string if provided');
|
|
144
153
|
|
|
@@ -168,6 +177,7 @@ export const validateConversationInput = (body) => {
|
|
|
168
177
|
modelID: body.model.modelID.trim(),
|
|
169
178
|
},
|
|
170
179
|
parts: sanitizedParts,
|
|
180
|
+
...(skills?.length ? { skills } : {}),
|
|
171
181
|
title,
|
|
172
182
|
parentID,
|
|
173
183
|
agent,
|
|
@@ -33,7 +33,7 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
|
|
|
33
33
|
### Reasoning projection helpers (`reasoning-projection.js`)
|
|
34
34
|
Import from the module file (not the event-stream barrel). Public surface:
|
|
35
35
|
- `shouldIncludeReasoning` / `readIncludeReasoningQuery` / `readIncludeReasoningFromUrl` / `stripIncludeReasoningParam`
|
|
36
|
-
- `projectMessagesPayloadForReasoning` / `projectMessagesPayloadWithoutReasoning` — array / `{ records }` / `{ record }` / `{ parts }`
|
|
36
|
+
- `projectMessagesPayloadForReasoning` / `projectMessagesPayloadWithoutReasoning` — array / `{ records }` / `{ record }` / `{ parts }`, plus native v2 `{ data }` list/context envelopes and assistant `content[]`. Filtering preserves message identities, cursor, text/tool content and token totals.
|
|
37
37
|
- `createReasoningOutboundFilter` — per-connection stream filter (version-suffix-aware type match; preserves original `type` on kept events)
|
|
38
38
|
- `filterSseBlock` / `createSseBlockSplitter` — SSE disabled path (CR-safe chunk boundaries)
|
|
39
39
|
|
|
@@ -49,6 +49,8 @@ Import from the module file (not the event-stream barrel). Public surface:
|
|
|
49
49
|
- `createUpstreamSseReader(...)`: creates a start/stop reader for OpenCode SSE streams. The reader parses SSE blocks, tracks the latest `Last-Event-ID`, reconnects after closed or stalled upstream streams, and reports events through callbacks.
|
|
50
50
|
|
|
51
51
|
## Runtime behavior
|
|
52
|
+
|
|
53
|
+
Native v2 `session.reasoning.*` events are filtered at the same outbound boundary as legacy `session.next.reasoning.*`. Both `/api/session/:id/message` and `/api/session/:id/context` use the snapshot projection before relay encryption.
|
|
52
54
|
- Browser clients connect to the WS endpoints above.
|
|
53
55
|
- OpenChamber still fetches OpenCode upstream event streams over SSE.
|
|
54
56
|
- The web server creates one shared global message-stream hub. OpenCode watcher side effects and global WS clients subscribe to that hub, so there is one upstream `/global/event` SSE reader for both server-side processing and browser fan-out.
|
|
@@ -21,6 +21,8 @@ export function acceptDirectoryMessageStreamWsConnection({
|
|
|
21
21
|
buildOpenCodeUrl,
|
|
22
22
|
getOpenCodeAuthHeaders,
|
|
23
23
|
processForwardedEventPayload,
|
|
24
|
+
/** Optional Host archive/metadata projection for session lifecycle events. */
|
|
25
|
+
projectOutboundSessionPayload = null,
|
|
24
26
|
wsClients,
|
|
25
27
|
triggerHealthCheck,
|
|
26
28
|
heartbeatIntervalMs,
|
|
@@ -78,14 +80,23 @@ export function acceptDirectoryMessageStreamWsConnection({
|
|
|
78
80
|
const run = async () => {
|
|
79
81
|
const forwardEvent = ({ envelope, payload }) => {
|
|
80
82
|
const directory = requestedDirectory || envelope?.directory || 'global';
|
|
83
|
+
let outbound = payload;
|
|
84
|
+
if (typeof projectOutboundSessionPayload === 'function') {
|
|
85
|
+
try {
|
|
86
|
+
const projected = projectOutboundSessionPayload(payload);
|
|
87
|
+
if (projected != null) outbound = projected;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn('[event-stream] directory session projection failed:', error?.message ?? error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
81
92
|
|
|
82
|
-
sendMessageStreamWsEvent(socket,
|
|
93
|
+
sendMessageStreamWsEvent(socket, outbound, {
|
|
83
94
|
directory,
|
|
84
95
|
eventId: typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined,
|
|
85
96
|
reasoningFilter,
|
|
86
97
|
});
|
|
87
98
|
|
|
88
|
-
processForwardedEventPayload(
|
|
99
|
+
processForwardedEventPayload(outbound, (syntheticPayload) => {
|
|
89
100
|
sendMessageStreamWsEvent(socket, syntheticPayload, {
|
|
90
101
|
directory: 'global',
|
|
91
102
|
reasoningFilter,
|
|
@@ -64,6 +64,13 @@ export function createGlobalMessageStreamHub({
|
|
|
64
64
|
upstreamReconnectDelayMs,
|
|
65
65
|
replayLimit = MESSAGE_STREAM_GLOBAL_REPLAY_LIMIT,
|
|
66
66
|
getRuntimeIdentity = null,
|
|
67
|
+
/**
|
|
68
|
+
* Optional Host authority projection for session.created / session.updated
|
|
69
|
+
* (and GlobalEvent wrap). Applied before fan-out and replay so browser and
|
|
70
|
+
* server subscribers share the same archive view. Must be sync and cheap.
|
|
71
|
+
* @type {((payload: unknown) => unknown) | null}
|
|
72
|
+
*/
|
|
73
|
+
projectOutboundSessionPayload = null,
|
|
67
74
|
}) {
|
|
68
75
|
const eventSubscribers = new Set();
|
|
69
76
|
const statusSubscribers = new Set();
|
|
@@ -97,12 +104,19 @@ export function createGlobalMessageStreamHub({
|
|
|
97
104
|
}
|
|
98
105
|
};
|
|
99
106
|
|
|
107
|
+
let sessionPayloadProjector = typeof projectOutboundSessionPayload === 'function'
|
|
108
|
+
? projectOutboundSessionPayload
|
|
109
|
+
: null;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Diff-summarize only. Host archive projection is applied at fan-out and
|
|
113
|
+
* replayAfter time against the *current* committed store — never baked into
|
|
114
|
+
* the replay buffer (stale archive stamps must not stick after Host writes).
|
|
115
|
+
*/
|
|
100
116
|
const normalizeEvent = ({ envelope, payload, connectionContext }) => {
|
|
101
117
|
const directory =
|
|
102
118
|
typeof envelope?.directory === 'string' && envelope.directory.length > 0 ? envelope.directory : 'global';
|
|
103
119
|
const eventId = typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined;
|
|
104
|
-
// Summarize FileDiff bodies before fan-out and replay buffering so reconnect
|
|
105
|
-
// replay cannot re-amplify full patch frames into >64KB WS payloads.
|
|
106
120
|
const outboundPayload = summarizeOutboundEventPayload(payload);
|
|
107
121
|
return {
|
|
108
122
|
envelope,
|
|
@@ -113,6 +127,19 @@ export function createGlobalMessageStreamHub({
|
|
|
113
127
|
};
|
|
114
128
|
};
|
|
115
129
|
|
|
130
|
+
const projectForOutbound = (payload) => {
|
|
131
|
+
if (typeof sessionPayloadProjector !== 'function') return payload;
|
|
132
|
+
try {
|
|
133
|
+
const projected = sessionPayloadProjector(payload);
|
|
134
|
+
// null = Host not ready; suppress this lifecycle frame.
|
|
135
|
+
if (projected == null) return null;
|
|
136
|
+
return projected;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn('[event-stream] session payload projection failed:', error?.message ?? error);
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
116
143
|
const start = () => {
|
|
117
144
|
if (reader) {
|
|
118
145
|
return;
|
|
@@ -161,6 +188,7 @@ export function createGlobalMessageStreamHub({
|
|
|
161
188
|
...event,
|
|
162
189
|
connectionContext: event?.connectionContext ?? activeConnectionRuntimeIdentity,
|
|
163
190
|
});
|
|
191
|
+
// Buffer the unprojected payload so reconnect replay uses current Host authority.
|
|
164
192
|
if (normalized.eventId) {
|
|
165
193
|
replay.push(normalized);
|
|
166
194
|
if (replay.length > replayLimit) {
|
|
@@ -168,8 +196,14 @@ export function createGlobalMessageStreamHub({
|
|
|
168
196
|
}
|
|
169
197
|
}
|
|
170
198
|
|
|
199
|
+
const projectedPayload = projectForOutbound(normalized.payload);
|
|
200
|
+
if (projectedPayload == null) return;
|
|
201
|
+
const outbound = projectedPayload === normalized.payload
|
|
202
|
+
? normalized
|
|
203
|
+
: { ...normalized, payload: projectedPayload };
|
|
204
|
+
|
|
171
205
|
for (const subscriber of Array.from(eventSubscribers)) {
|
|
172
|
-
notifySubscriber('event', subscriber,
|
|
206
|
+
notifySubscriber('event', subscriber, outbound);
|
|
173
207
|
}
|
|
174
208
|
},
|
|
175
209
|
onError(error) {
|
|
@@ -227,10 +261,21 @@ export function createGlobalMessageStreamHub({
|
|
|
227
261
|
}
|
|
228
262
|
|
|
229
263
|
const index = replay.findIndex((entry) => entry.eventId === eventId);
|
|
230
|
-
|
|
264
|
+
if (index === -1) return [];
|
|
265
|
+
// Re-project with current committed Host authority at replay time.
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const entry of replay.slice(index + 1)) {
|
|
268
|
+
const projectedPayload = projectForOutbound(entry.payload);
|
|
269
|
+
if (projectedPayload == null) continue;
|
|
270
|
+
out.push(projectedPayload === entry.payload ? entry : { ...entry, payload: projectedPayload });
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
231
273
|
},
|
|
232
274
|
setRuntimeIdentityProvider(provider) {
|
|
233
275
|
runtimeIdentityProvider = typeof provider === 'function' ? provider : null;
|
|
234
276
|
},
|
|
277
|
+
setSessionPayloadProjector(projector) {
|
|
278
|
+
sessionPayloadProjector = typeof projector === 'function' ? projector : null;
|
|
279
|
+
},
|
|
235
280
|
};
|
|
236
281
|
}
|
|
@@ -96,6 +96,10 @@ function projectParts(parts) {
|
|
|
96
96
|
* @param {unknown} record
|
|
97
97
|
*/
|
|
98
98
|
function projectRecord(record) {
|
|
99
|
+
if (isPlainObject(record) && record.type === 'assistant' && Array.isArray(record.content)) {
|
|
100
|
+
const content = projectParts(record.content);
|
|
101
|
+
return content === record.content ? record : { ...record, content };
|
|
102
|
+
}
|
|
99
103
|
if (!isPlainObject(record) || !Array.isArray(record.parts)) return record;
|
|
100
104
|
const parts = projectParts(record.parts);
|
|
101
105
|
return parts === record.parts ? record : { ...record, parts };
|
|
@@ -126,6 +130,17 @@ export function projectMessagesPayloadWithoutReasoning(payload) {
|
|
|
126
130
|
let next = payload;
|
|
127
131
|
let changed = false;
|
|
128
132
|
|
|
133
|
+
if (Array.isArray(payload.data)) {
|
|
134
|
+
const data = projectMessagesPayloadWithoutReasoning(payload.data);
|
|
135
|
+
if (data !== payload.data) {
|
|
136
|
+
next = { ...next, data };
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (payload.type === 'assistant' && Array.isArray(payload.content)) {
|
|
141
|
+
return projectRecord(payload);
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
if (Array.isArray(payload.records)) {
|
|
130
145
|
const records = projectMessagesPayloadWithoutReasoning(payload.records);
|
|
131
146
|
if (records !== payload.records) {
|
|
@@ -222,7 +237,7 @@ export function createReasoningOutboundFilter() {
|
|
|
222
237
|
const type = baseEventType(rawType);
|
|
223
238
|
|
|
224
239
|
// session.next.reasoning.* (incl. versioned …reasoning.delta.1)
|
|
225
|
-
if (type.startsWith('session.next.reasoning.')) return null;
|
|
240
|
+
if (type.startsWith('session.next.reasoning.') || type.startsWith('session.reasoning.')) return null;
|
|
226
241
|
|
|
227
242
|
if (type === 'message.part.updated') {
|
|
228
243
|
const body = eventBodyOf(payload);
|
|
@@ -13,6 +13,21 @@ import {
|
|
|
13
13
|
} from './reasoning-projection.js';
|
|
14
14
|
|
|
15
15
|
describe('shouldIncludeReasoning / query helpers', () => {
|
|
16
|
+
it('projects v2 list/context and exact snapshots without dropping rows or cursor', () => {
|
|
17
|
+
const row = { id: 'm', type: 'assistant', tokens: { reasoning: 12 }, content: [
|
|
18
|
+
{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
|
|
19
|
+
] };
|
|
20
|
+
const payload = { data: [row], cursor: { next: 'older' } };
|
|
21
|
+
const projected = projectMessagesPayloadForReasoning(payload, false);
|
|
22
|
+
expect(projected).toEqual({ data: [{ ...row, content: [row.content[1]] }], cursor: payload.cursor });
|
|
23
|
+
expect(projectMessagesPayloadForReasoning(row, false)).toEqual(projected.data[0]);
|
|
24
|
+
expect(projectMessagesPayloadForReasoning(payload, true)).toBe(payload);
|
|
25
|
+
expect(row.content).toHaveLength(2);
|
|
26
|
+
const filter = createReasoningOutboundFilter();
|
|
27
|
+
expect(filter.projectEvent({ type: 'session.reasoning.delta.1', data: { delta: 'hidden' } })).toBeNull();
|
|
28
|
+
const text = { type: 'session.text.delta', data: { delta: 'visible' } };
|
|
29
|
+
expect(filter.projectEvent(text)).toBe(text);
|
|
30
|
+
});
|
|
16
31
|
it('only treats the strict string false as disabled', () => {
|
|
17
32
|
expect(shouldIncludeReasoning(undefined)).toBe(true);
|
|
18
33
|
expect(shouldIncludeReasoning(null)).toBe(true);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
MESSAGE_STREAM_GLOBAL_WS_PATH,
|
|
7
7
|
MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
|
|
8
8
|
sendMessageStreamWsEvent,
|
|
9
|
+
sendMessageStreamWsFrame,
|
|
9
10
|
} from './protocol.js';
|
|
10
11
|
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
11
12
|
import { createGlobalMessageStreamWsBridge } from './global-ws-bridge.js';
|
|
@@ -62,6 +63,9 @@ export function createMessageStreamWsRuntime({
|
|
|
62
63
|
buildOpenCodeUrl,
|
|
63
64
|
getOpenCodeAuthHeaders,
|
|
64
65
|
processForwardedEventPayload,
|
|
66
|
+
projectOutboundSessionPayload = null,
|
|
67
|
+
/** Await Host metadata ready before directory WS attaches upstream. */
|
|
68
|
+
ensureSessionMetadataReady = null,
|
|
65
69
|
wsClients,
|
|
66
70
|
triggerHealthCheck,
|
|
67
71
|
heartbeatIntervalMs = MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
|
|
@@ -104,29 +108,62 @@ export function createMessageStreamWsRuntime({
|
|
|
104
108
|
const includeReasoning = shouldIncludeReasoning(requestUrl.searchParams.get('includeReasoning'));
|
|
105
109
|
const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
// Host metadata readiness gates *both* global and directory streams before
|
|
112
|
+
// accept/hub.start — otherwise globalBridge.accept → hub.start bypasses the
|
|
113
|
+
// gate and lifecycle frames can fan out while the store is still unknown.
|
|
114
|
+
void (async () => {
|
|
115
|
+
if (typeof ensureSessionMetadataReady === 'function') {
|
|
116
|
+
try {
|
|
117
|
+
const ready = await ensureSessionMetadataReady();
|
|
118
|
+
if (!ready) {
|
|
119
|
+
sendMessageStreamWsFrame(socket, {
|
|
120
|
+
type: 'error',
|
|
121
|
+
message: 'session metadata is unavailable',
|
|
122
|
+
code: 'session_metadata_unavailable',
|
|
123
|
+
retryable: true,
|
|
124
|
+
});
|
|
125
|
+
socket.close(1013, 'session metadata unavailable');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
sendMessageStreamWsFrame(socket, {
|
|
130
|
+
type: 'error',
|
|
131
|
+
message: 'session metadata is unavailable',
|
|
132
|
+
code: 'session_metadata_unavailable',
|
|
133
|
+
retryable: true,
|
|
134
|
+
});
|
|
135
|
+
socket.close(1013, 'session metadata unavailable');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (socket.readyState !== 1) return;
|
|
141
|
+
|
|
142
|
+
if (isGlobalStream) {
|
|
143
|
+
globalBridge.accept(socket, {
|
|
144
|
+
requestedLastEventId,
|
|
145
|
+
reasoningFilter,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
acceptDirectoryMessageStreamWsConnection({
|
|
151
|
+
socket,
|
|
109
152
|
requestedLastEventId,
|
|
153
|
+
requestedDirectory,
|
|
110
154
|
reasoningFilter,
|
|
155
|
+
buildOpenCodeUrl,
|
|
156
|
+
getOpenCodeAuthHeaders,
|
|
157
|
+
processForwardedEventPayload,
|
|
158
|
+
projectOutboundSessionPayload,
|
|
159
|
+
wsClients,
|
|
160
|
+
triggerHealthCheck,
|
|
161
|
+
heartbeatIntervalMs,
|
|
162
|
+
upstreamStallTimeoutMs,
|
|
163
|
+
upstreamReconnectDelayMs,
|
|
164
|
+
fetchImpl,
|
|
111
165
|
});
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
acceptDirectoryMessageStreamWsConnection({
|
|
116
|
-
socket,
|
|
117
|
-
requestedLastEventId,
|
|
118
|
-
requestedDirectory,
|
|
119
|
-
reasoningFilter,
|
|
120
|
-
buildOpenCodeUrl,
|
|
121
|
-
getOpenCodeAuthHeaders,
|
|
122
|
-
processForwardedEventPayload,
|
|
123
|
-
wsClients,
|
|
124
|
-
triggerHealthCheck,
|
|
125
|
-
heartbeatIntervalMs,
|
|
126
|
-
upstreamStallTimeoutMs,
|
|
127
|
-
upstreamReconnectDelayMs,
|
|
128
|
-
fetchImpl,
|
|
129
|
-
});
|
|
166
|
+
})();
|
|
130
167
|
});
|
|
131
168
|
|
|
132
169
|
const upgradeHandler = (req, socket, head) => {
|
|
@@ -126,6 +126,118 @@ describe('event stream broadcaster', () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
describe('message stream websocket runtime Host metadata readiness', () => {
|
|
130
|
+
it('does not accept/start global hub while metadata load is pending', async () => {
|
|
131
|
+
const server = new EventEmitter();
|
|
132
|
+
const wsClients = new Set();
|
|
133
|
+
let fetchCalls = 0;
|
|
134
|
+
let releaseReady;
|
|
135
|
+
const readyGate = new Promise((resolve) => { releaseReady = resolve; });
|
|
136
|
+
|
|
137
|
+
const runtime = createMessageStreamWsRuntime({
|
|
138
|
+
server,
|
|
139
|
+
uiAuthController: null,
|
|
140
|
+
isRequestOriginAllowed: async () => true,
|
|
141
|
+
rejectWebSocketUpgrade() {},
|
|
142
|
+
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
143
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
144
|
+
processForwardedEventPayload() {},
|
|
145
|
+
ensureSessionMetadataReady: async () => {
|
|
146
|
+
await readyGate;
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
149
|
+
wsClients,
|
|
150
|
+
upstreamReconnectDelayMs: 0,
|
|
151
|
+
fetchImpl: async (_url, options) => {
|
|
152
|
+
fetchCalls += 1;
|
|
153
|
+
return createSseResponse({
|
|
154
|
+
signal: options.signal,
|
|
155
|
+
holdOpen: true,
|
|
156
|
+
blocks: [
|
|
157
|
+
'id: evt-1\ndata: {"type":"session.updated","properties":{"info":{"id":"ses_1","time":{}}}}\n\n',
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const socket = new FakeSocket();
|
|
164
|
+
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
166
|
+
|
|
167
|
+
// Still pending → no hub.start → no upstream fetch, no accept frames yet.
|
|
168
|
+
expect(fetchCalls).toBe(0);
|
|
169
|
+
expect(socket.closeCalls).toEqual([]);
|
|
170
|
+
expect(socket.sent.some((f) => f.type === 'event')).toBe(false);
|
|
171
|
+
|
|
172
|
+
releaseReady(true);
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
174
|
+
|
|
175
|
+
expect(fetchCalls).toBe(1);
|
|
176
|
+
expect(socket.sent.some((f) => f.type === 'event'
|
|
177
|
+
&& f.payload?.type === 'session.updated')).toBe(true);
|
|
178
|
+
|
|
179
|
+
socket.close();
|
|
180
|
+
await runtime.close();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('rejects global connection when metadata ready fails', async () => {
|
|
184
|
+
const server = new EventEmitter();
|
|
185
|
+
const runtime = createMessageStreamWsRuntime({
|
|
186
|
+
server,
|
|
187
|
+
uiAuthController: null,
|
|
188
|
+
isRequestOriginAllowed: async () => true,
|
|
189
|
+
rejectWebSocketUpgrade() {},
|
|
190
|
+
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
191
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
192
|
+
processForwardedEventPayload() {},
|
|
193
|
+
ensureSessionMetadataReady: async () => false,
|
|
194
|
+
wsClients: new Set(),
|
|
195
|
+
upstreamReconnectDelayMs: 0,
|
|
196
|
+
fetchImpl: async () => {
|
|
197
|
+
throw new Error('upstream must not start');
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const socket = new FakeSocket();
|
|
202
|
+
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
204
|
+
|
|
205
|
+
expect(socket.sent.some((f) => f.type === 'error'
|
|
206
|
+
&& f.code === 'session_metadata_unavailable'
|
|
207
|
+
&& f.retryable === true)).toBe(true);
|
|
208
|
+
expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
|
|
209
|
+
|
|
210
|
+
await runtime.close();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('gates directory stream the same way as global', async () => {
|
|
214
|
+
const server = new EventEmitter();
|
|
215
|
+
const runtime = createMessageStreamWsRuntime({
|
|
216
|
+
server,
|
|
217
|
+
uiAuthController: null,
|
|
218
|
+
isRequestOriginAllowed: async () => true,
|
|
219
|
+
rejectWebSocketUpgrade() {},
|
|
220
|
+
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
221
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
222
|
+
processForwardedEventPayload() {},
|
|
223
|
+
ensureSessionMetadataReady: async () => false,
|
|
224
|
+
wsClients: new Set(),
|
|
225
|
+
upstreamReconnectDelayMs: 0,
|
|
226
|
+
fetchImpl: async () => {
|
|
227
|
+
throw new Error('upstream must not start');
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const socket = new FakeSocket();
|
|
232
|
+
runtime.wsServer.emit('connection', socket, { url: '/api/event/ws?directory=/repo' });
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
234
|
+
|
|
235
|
+
expect(socket.sent.some((f) => f.code === 'session_metadata_unavailable')).toBe(true);
|
|
236
|
+
expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
|
|
237
|
+
await runtime.close();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
129
241
|
describe('message stream websocket runtime', () => {
|
|
130
242
|
it('filters reasoning events per connection when includeReasoning=false', async () => {
|
|
131
243
|
const server = new EventEmitter();
|
|
@@ -23,8 +23,8 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
|
|
|
23
23
|
|
|
24
24
|
`variant` is an optional OpenCode model variant string. Null, omitted, or empty
|
|
25
25
|
values preserve the provider default. Other types fail validation before a model
|
|
26
|
-
call. A selected variant is forwarded unchanged on
|
|
27
|
-
|
|
26
|
+
call. A selected variant is forwarded unchanged on the throwaway session's
|
|
27
|
+
`model` ref (both paths). OpenCode owns provider-specific variant
|
|
28
28
|
semantics. Variant selection changes inference configuration only; completion
|
|
29
29
|
output continues to include text parts and keeps reasoning parts private.
|
|
30
30
|
|
|
@@ -35,10 +35,26 @@ call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
|
|
|
35
35
|
|
|
36
36
|
### Text path (no vision image bytes)
|
|
37
37
|
|
|
38
|
-
`
|
|
39
|
-
|
|
38
|
+
`session.generate` inside a throwaway session owned by the hidden deny-all
|
|
39
|
+
`openchamber-text` agent (same temp root, same deny-all verification and
|
|
40
|
+
`session.create` / `session.remove` lifecycle as the attachment path below):
|
|
41
|
+
|
|
42
|
+
1. Ensure `.opencode/agent/openchamber-text.md` and verify its final
|
|
43
|
+
permission rule is deny-all before creating the session.
|
|
44
|
+
2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
|
|
45
|
+
3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
|
|
46
|
+
the session's provider context, no tool loop, no messages written. System
|
|
47
|
+
text is prepended into `prompt`.
|
|
48
|
+
4. Always `session.remove` in `finally` (interrupt first on abort/timeout).
|
|
49
|
+
|
|
50
|
+
`generate.text` (sessionless) is not used: session-scoped providers such as
|
|
51
|
+
OpenCode Go reject calls without a session (`x-opencode-session`).
|
|
40
52
|
`onTextDelta` is skipped honestly (no fake typewriter).
|
|
41
53
|
|
|
54
|
+
OpenCode loads a fresh location's config lazily, so `agent.get` can report the
|
|
55
|
+
agent as missing on the first read. Deny-all verification polls within a
|
|
56
|
+
bounded window (5s) before failing.
|
|
57
|
+
|
|
42
58
|
### Attachment / vision path
|
|
43
59
|
|
|
44
60
|
When the catalog marks the model vision-capable and the request includes image
|
|
@@ -49,8 +65,11 @@ data-URL file parts:
|
|
|
49
65
|
2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
|
|
50
66
|
permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
|
|
51
67
|
**before** any `session.prompt`. Title is never treated as permission isolation.
|
|
52
|
-
3. `session.create({ location, agent, model: { id, providerID, variant? }
|
|
53
|
-
|
|
68
|
+
3. `session.create({ location, agent, model: { id, providerID, variant? },
|
|
69
|
+
metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
|
|
70
|
+
(bare session id). Host `persistSessionMetadata` then commits the same
|
|
71
|
+
isolation patch; `onSystemSessionPersisted` drops the throwaway session
|
|
72
|
+
from the sidebar index. Create-time `metadata` is a hint only.
|
|
54
73
|
4. Optional `session.instructions.entry.put` for the system prompt.
|
|
55
74
|
5. Subscribe for `session.text.delta` (`data.sessionID` / `assistantMessageID` /
|
|
56
75
|
`ordinal` / `delta`) before prompt; unsubscribe in `finally`.
|
|
@@ -81,7 +100,7 @@ There is no `config.providers` catalog path.
|
|
|
81
100
|
`onTextDelta(text: string)` and `globalEventHub`.
|
|
82
101
|
|
|
83
102
|
- **Attachment session path:** real `session.text.delta` tokens for this session.
|
|
84
|
-
- **`generate
|
|
103
|
+
- **`session.generate` text path:** cannot emit live deltas — skip `onTextDelta`.
|
|
85
104
|
- **HTTP `POST .../chat/completions`:** does not pass these options and still
|
|
86
105
|
rejects `stream: true`.
|
|
87
106
|
|
|
@@ -99,8 +118,13 @@ application tool catalog and actual results.
|
|
|
99
118
|
The Assistant contact harness owns system prompt, OpenChamber transcript,
|
|
100
119
|
bubble splitting, and OpenChamber API tools (`assign_session`). Those tools
|
|
101
120
|
deliver through contact **cards**, not this completions payload. The gateway
|
|
102
|
-
stays a text generator: OpenCode coding tools stay denied on
|
|
103
|
-
|
|
121
|
+
stays a text generator: OpenCode coding tools stay denied on both paths via the
|
|
122
|
+
verified agent permissions ruleset.
|
|
123
|
+
|
|
124
|
+
The gateway is also the single OpenCode-backed path for server utility calls
|
|
125
|
+
(`small-model`: commit messages, session titles, goal audits, scheduled-task
|
|
126
|
+
distill, TTS summaries, PR descriptions). No server module calls provider APIs
|
|
127
|
+
directly with OpenCode credentials.
|
|
104
128
|
|
|
105
129
|
Internal completion/generate calls accept an optional AbortSignal from the
|
|
106
130
|
contact continuation. It is combined with the generator deadline and passed to
|
|
@@ -57,7 +57,7 @@ export function projectConnectedModels(source) {
|
|
|
57
57
|
for (const model of modelsIn) {
|
|
58
58
|
if (!isRecord(model)) continue;
|
|
59
59
|
const providerID = stringID(model.providerID);
|
|
60
|
-
// External model id for
|
|
60
|
+
// External model id for session model refs.
|
|
61
61
|
const modelID = stringID(model.id);
|
|
62
62
|
if (!providerID || !modelID) continue;
|
|
63
63
|
if (providerMeta.size > 0 && !providerMeta.has(providerID)) continue;
|
|
@@ -96,11 +96,13 @@ export async function createChatCompletion({
|
|
|
96
96
|
ensureTempDirectory,
|
|
97
97
|
onTextDelta = null,
|
|
98
98
|
globalEventHub = null,
|
|
99
|
+
persistSessionMetadata = null,
|
|
100
|
+
onSystemSessionPersisted = null,
|
|
99
101
|
signal = null,
|
|
100
102
|
}) {
|
|
101
103
|
signal?.throwIfAborted();
|
|
102
104
|
if (!isRecord(body)) throw new LlmError('validation_error', 400, 'JSON body is required');
|
|
103
|
-
// generate
|
|
105
|
+
// session.generate / attachment-session return a full turn. This HTTP gateway
|
|
104
106
|
// does not token-stream. Do not emit fake SSE after the fact.
|
|
105
107
|
// In-process onTextDelta (optional) is a separate internal callback path.
|
|
106
108
|
if (body.stream === true) {
|
|
@@ -148,6 +150,8 @@ export async function createChatCompletion({
|
|
|
148
150
|
))?.acceptsImages),
|
|
149
151
|
onTextDelta,
|
|
150
152
|
globalEventHub,
|
|
153
|
+
persistSessionMetadata,
|
|
154
|
+
onSystemSessionPersisted,
|
|
151
155
|
signal,
|
|
152
156
|
});
|
|
153
157
|
} catch (error) {
|
|
@@ -52,7 +52,7 @@ describe('createChatCompletion', () => {
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
it('returns a non-stream completion from generateText', async () => {
|
|
55
|
-
const generateText = vi.fn(async () => ({ text: 'done', source: 'generate
|
|
55
|
+
const generateText = vi.fn(async () => ({ text: 'done', source: 'session.generate' }))
|
|
56
56
|
const result = await createChatCompletion({
|
|
57
57
|
generateText,
|
|
58
58
|
loadCatalog: async () => ({
|
|
@@ -64,7 +64,7 @@ describe('createChatCompletion', () => {
|
|
|
64
64
|
expect(result.completion.choices[0].message.content).toBe('done')
|
|
65
65
|
expect(result.providerID).toBe('openai')
|
|
66
66
|
expect(result.modelID).toBe('gpt-5.2')
|
|
67
|
-
expect(result.source).toBe('generate
|
|
67
|
+
expect(result.source).toBe('session.generate')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
it('forwards in-process onTextDelta and globalEventHub to generateText', async () => {
|
|
@@ -109,7 +109,7 @@ describe('createChatCompletion', () => {
|
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
it('asks generate to skip image bytes when the catalog model is not vision-capable', async () => {
|
|
112
|
-
const generateText = vi.fn(async () => ({ text: 'ok', source: 'generate
|
|
112
|
+
const generateText = vi.fn(async () => ({ text: 'ok', source: 'session.generate' }))
|
|
113
113
|
await createChatCompletion({
|
|
114
114
|
generateText,
|
|
115
115
|
loadCatalog: async () => ({
|