@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,36 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OpenCode } from '@opencode/client';
|
|
2
|
+
import { buildLlmSessionMetadata } from '../session-metadata/system-session.js';
|
|
3
|
+
import { ensureLlmTempDirectory } from './temp-directory.js';
|
|
2
4
|
|
|
3
5
|
const LLM_AGENT_NAME = 'openchamber-llm';
|
|
6
|
+
const TEXT_AGENT_NAME = 'openchamber-text';
|
|
4
7
|
const GENERATE_TIMEOUT_MS = 90_000;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
+
const AGENT_READY_WAIT_MS = 5_000;
|
|
9
|
+
const AGENT_READY_POLL_MS = 250;
|
|
10
|
+
/** Single attachment payload cap (decoded bytes). Composer UI allows larger drafts; gateway enforces this. */
|
|
11
|
+
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
12
|
+
const MAX_INLINE_FILE_CHARS = 100_000;
|
|
13
|
+
const TEXT_FILE_MIME = /^(text\/|application\/(json|javascript|xml|sql|yaml|x-yaml|toml))/i;
|
|
8
14
|
|
|
9
15
|
const positiveMs = (value, fallback) => (
|
|
10
16
|
Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback
|
|
11
17
|
);
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 1.18.23/1.18.29: create returns the same rule on the session object.
|
|
17
|
-
*/
|
|
18
|
-
export const LLM_SESSION_DENY_PERMISSION = Object.freeze([
|
|
19
|
-
Object.freeze({ permission: '*', pattern: '*', action: 'deny' }),
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Agent frontmatter for OpenCode 1.18. Runtime /agent metadata uses
|
|
24
|
-
* permission:[{permission,pattern,action}]. Markdown config accepts the
|
|
25
|
-
* PermissionActionConfig map (`"*": deny`) / scalar `deny` / tools:"*":false —
|
|
26
|
-
* not the legacy action/resource/effect list (that loads but does not add
|
|
27
|
-
* a terminal * deny, so build-like allows and skill guidance stay visible).
|
|
20
|
+
* Agent frontmatter for official client agent permissions (action/resource/effect).
|
|
21
|
+
* The final rule must be deny-all; assertLlmAgentDenyAll verifies that before prompt.
|
|
28
22
|
*/
|
|
29
23
|
const AGENT_MARKDOWN = `---
|
|
30
24
|
mode: primary
|
|
31
25
|
hidden: true
|
|
32
|
-
|
|
33
|
-
"*"
|
|
26
|
+
permissions:
|
|
27
|
+
- action: "*"
|
|
28
|
+
resource: "*"
|
|
29
|
+
effect: deny
|
|
34
30
|
---
|
|
35
31
|
|
|
36
32
|
You generate responses for an application-owned assistant. Follow the supplied system instructions and response format.
|
|
@@ -40,51 +36,46 @@ Report execution and failures from supplied tool results only.
|
|
|
40
36
|
Do not call native OpenCode tools, MCP tools, or skill tools. Native permissions are denied for this generator session.
|
|
41
37
|
`;
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|| result?.status === 204
|
|
53
|
-
|| result?.data !== undefined
|
|
54
|
-
|| result?.response?.ok === true);
|
|
55
|
-
|
|
56
|
-
const sdkErrorMessage = (result, fallback) => {
|
|
57
|
-
const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status;
|
|
58
|
-
const message = result?.error?.message || result?.error?.data?.message || fallback;
|
|
59
|
-
return status ? `${message} (${status})` : message;
|
|
60
|
-
};
|
|
39
|
+
/** Neutral deny-all agent for text-only `session.generate`; the caller's instructions travel in the prompt. */
|
|
40
|
+
const TEXT_AGENT_MARKDOWN = `---
|
|
41
|
+
mode: primary
|
|
42
|
+
hidden: true
|
|
43
|
+
permissions:
|
|
44
|
+
- action: "*"
|
|
45
|
+
resource: "*"
|
|
46
|
+
effect: deny
|
|
47
|
+
---
|
|
61
48
|
|
|
62
|
-
|
|
49
|
+
Follow the instructions in the request and respond in the requested format.
|
|
50
|
+
Native OpenCode, MCP, and skill tools are denied for this generator session.
|
|
51
|
+
When the request defines an application tool-call format (such as fenced JSON), write it as plain text in your reply; the application executes it.
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const failGenerate = (message, code = 'upstream_error') => {
|
|
63
55
|
const error = new Error(message);
|
|
64
|
-
error.code =
|
|
56
|
+
error.code = code;
|
|
65
57
|
throw error;
|
|
66
58
|
};
|
|
67
59
|
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
82
|
-
});
|
|
60
|
+
const clientErrorMessage = (error, fallback) => {
|
|
61
|
+
if (typeof error?.message === 'string' && error.message.trim()) return error.message;
|
|
62
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
63
|
+
return fallback;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const isAbortLike = (error, signal) => (
|
|
67
|
+
signal?.aborted
|
|
68
|
+
|| error?.name === 'AbortError'
|
|
69
|
+
|| error?.code === 'ABORT_ERR'
|
|
70
|
+
|| (typeof error?.message === 'string' && /aborted|This operation was aborted/i.test(error.message))
|
|
71
|
+
);
|
|
83
72
|
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
if (
|
|
87
|
-
|
|
73
|
+
const rethrowIfAborted = (error, signal) => {
|
|
74
|
+
if (!isAbortLike(error, signal)) return;
|
|
75
|
+
if (error instanceof Error) throw error;
|
|
76
|
+
const next = new Error(clientErrorMessage(error, 'aborted'));
|
|
77
|
+
next.name = 'AbortError';
|
|
78
|
+
throw next;
|
|
88
79
|
};
|
|
89
80
|
|
|
90
81
|
const assistantErrorDetail = (error) => {
|
|
@@ -100,179 +91,6 @@ const assistantErrorDetail = (error) => {
|
|
|
100
91
|
return '';
|
|
101
92
|
};
|
|
102
93
|
|
|
103
|
-
const assistantTextFromPrompt = (data) => {
|
|
104
|
-
const parts = Array.isArray(data?.parts) ? data.parts : Array.isArray(data?.data?.parts) ? data.data.parts : [];
|
|
105
|
-
const text = parts
|
|
106
|
-
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
107
|
-
.map((part) => part.text)
|
|
108
|
-
.join('');
|
|
109
|
-
if (text.trim()) return text;
|
|
110
|
-
const info = data?.info ?? data?.data?.info;
|
|
111
|
-
if (info?.error) {
|
|
112
|
-
const detail = assistantErrorDetail(info.error);
|
|
113
|
-
if (detail) failGenerate(detail);
|
|
114
|
-
}
|
|
115
|
-
return '';
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const assistantTextFromMessages = (messages) => {
|
|
119
|
-
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
120
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
121
|
-
const message = messages[i];
|
|
122
|
-
const info = readMessageInfo(message);
|
|
123
|
-
if (info?.role !== 'assistant') continue;
|
|
124
|
-
if (info.error) {
|
|
125
|
-
const detail = assistantErrorDetail(info.error);
|
|
126
|
-
failGenerate(detail || 'OpenCode assistant error');
|
|
127
|
-
}
|
|
128
|
-
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
129
|
-
return parts
|
|
130
|
-
.map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.join('');
|
|
133
|
-
}
|
|
134
|
-
return '';
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const deniedTools = (ids) => {
|
|
138
|
-
const tools = Object.create(null);
|
|
139
|
-
for (const id of ids) {
|
|
140
|
-
if (typeof id === 'string' && id.trim()) tools[id.trim()] = false;
|
|
141
|
-
}
|
|
142
|
-
return tools;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const waitForIdleAssistant = async ({
|
|
146
|
-
client,
|
|
147
|
-
sessionID,
|
|
148
|
-
directory,
|
|
149
|
-
signal,
|
|
150
|
-
onProgress = null,
|
|
151
|
-
settlePollMs = SETTLE_POLL_MS,
|
|
152
|
-
}) => {
|
|
153
|
-
let incompleteAssistantProbes = 0;
|
|
154
|
-
let emptyIdleProbes = 0;
|
|
155
|
-
const pollMs = positiveMs(settlePollMs, SETTLE_POLL_MS);
|
|
156
|
-
|
|
157
|
-
for (;;) {
|
|
158
|
-
signal?.throwIfAborted?.();
|
|
159
|
-
|
|
160
|
-
let sessionBusy = false;
|
|
161
|
-
try {
|
|
162
|
-
const statusResult = await client.session.status({ directory }, { signal });
|
|
163
|
-
if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
|
|
164
|
-
const statusValue = statusResult.data[sessionID];
|
|
165
|
-
const type = statusValue?.type ?? statusValue?.status;
|
|
166
|
-
sessionBusy = type === 'busy' || type === 'retry';
|
|
167
|
-
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (signal?.aborted) throw error;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (sessionBusy) {
|
|
173
|
-
incompleteAssistantProbes = 0;
|
|
174
|
-
emptyIdleProbes = 0;
|
|
175
|
-
onProgress?.();
|
|
176
|
-
await sleep(pollMs, signal);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
const messagesResult = await client.session.messages({
|
|
182
|
-
sessionID,
|
|
183
|
-
directory,
|
|
184
|
-
limit: 20,
|
|
185
|
-
}, { signal });
|
|
186
|
-
if (messagesResult?.error) {
|
|
187
|
-
// Deterministic upstream failures (e.g. 400) must not look like "still settling".
|
|
188
|
-
failGenerate(`OpenCode LLM session.messages failed: ${sdkErrorMessage(messagesResult, 'messages failed')}`);
|
|
189
|
-
}
|
|
190
|
-
if (Array.isArray(messagesResult?.data)) {
|
|
191
|
-
const lastInfo = readMessageInfo(messagesResult.data.at(-1));
|
|
192
|
-
if (lastInfo?.role === 'assistant') {
|
|
193
|
-
emptyIdleProbes = 0;
|
|
194
|
-
if (lastInfo.error) {
|
|
195
|
-
const detail = assistantErrorDetail(lastInfo.error);
|
|
196
|
-
failGenerate(detail || 'OpenCode assistant error');
|
|
197
|
-
}
|
|
198
|
-
if (lastInfo.time?.completed) {
|
|
199
|
-
return messagesResult.data;
|
|
200
|
-
}
|
|
201
|
-
incompleteAssistantProbes += 1;
|
|
202
|
-
onProgress?.();
|
|
203
|
-
if (incompleteAssistantProbes >= INCOMPLETE_ASSISTANT_SETTLE_PROBES) {
|
|
204
|
-
return messagesResult.data;
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
incompleteAssistantProbes = 0;
|
|
208
|
-
emptyIdleProbes += 1;
|
|
209
|
-
if (emptyIdleProbes >= EMPTY_IDLE_PROBES && lastInfo?.role === 'user') {
|
|
210
|
-
failGenerate('OpenCode LLM generator ended without assistant text');
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
} catch (error) {
|
|
215
|
-
if (signal?.aborted) throw error;
|
|
216
|
-
if (error?.code === 'upstream_error') throw error;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
await sleep(pollMs, signal);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const isJsonContentType = (response) => /json/i.test(response?.headers?.get?.('content-type') || '');
|
|
224
|
-
|
|
225
|
-
const looksLikeJsonObject = (text) => {
|
|
226
|
-
const trimmed = String(text || '').trim();
|
|
227
|
-
if (!trimmed || trimmed.startsWith('<')) return false;
|
|
228
|
-
try {
|
|
229
|
-
const parsed = JSON.parse(trimmed);
|
|
230
|
-
return parsed !== null && typeof parsed === 'object';
|
|
231
|
-
} catch {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Detect a sessionless generate endpoint on the running OpenCode.
|
|
238
|
-
* Bundled 1.18.4 does not expose POST /generate; keep the probe so a later
|
|
239
|
-
* OpenCode that does can be used without changing the public completions API.
|
|
240
|
-
*
|
|
241
|
-
* SPA / OpenCode HTML often answers 200 text/html `<!doctype html>` for
|
|
242
|
-
* unknown paths. That is not generate — only JSON (Content-Type or body)
|
|
243
|
-
* counts as available.
|
|
244
|
-
*/
|
|
245
|
-
export async function detectSessionlessGenerate({ fetchImpl, baseUrl, headers }) {
|
|
246
|
-
const clientShape = typeof arguments[0]?.client?.generate === 'function'
|
|
247
|
-
|| typeof arguments[0]?.client?.v2?.generate === 'function';
|
|
248
|
-
if (clientShape) {
|
|
249
|
-
return { available: true, mode: 'sdk' };
|
|
250
|
-
}
|
|
251
|
-
const root = String(baseUrl || '').replace(/\/$/, '');
|
|
252
|
-
const candidates = [`${root}/generate`, `${root}/api/generate`];
|
|
253
|
-
for (const url of candidates) {
|
|
254
|
-
try {
|
|
255
|
-
const response = await fetchImpl(url, {
|
|
256
|
-
method: 'POST',
|
|
257
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
|
|
258
|
-
body: JSON.stringify({ probe: true }),
|
|
259
|
-
signal: AbortSignal.timeout(4_000),
|
|
260
|
-
});
|
|
261
|
-
if (response.status === 404 || response.status === 405) continue;
|
|
262
|
-
const body = await response.text().catch(() => '');
|
|
263
|
-
if (isJsonContentType(response) || looksLikeJsonObject(body)) {
|
|
264
|
-
return { available: true, mode: 'http', url };
|
|
265
|
-
}
|
|
266
|
-
} catch {
|
|
267
|
-
// Probe failure is not a generate capability.
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return { available: false, mode: 'throwaway-session' };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const TEXT_FILE_MIME = /^(text\/|application\/(json|javascript|xml|sql|yaml|x-yaml|toml))/i;
|
|
274
|
-
const MAX_INLINE_FILE_CHARS = 100_000;
|
|
275
|
-
|
|
276
94
|
const messageText = (message) => {
|
|
277
95
|
if (typeof message?.content === 'string') return message.content;
|
|
278
96
|
if (Array.isArray(message?.content)) {
|
|
@@ -294,21 +112,51 @@ const messageFileParts = (message) => {
|
|
|
294
112
|
}));
|
|
295
113
|
};
|
|
296
114
|
|
|
297
|
-
|
|
298
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Strict data-URL parse. Returns null when the URI is not a well-formed data URL.
|
|
117
|
+
* Decoded size is checked against MAX_ATTACHMENT_BYTES.
|
|
118
|
+
*/
|
|
119
|
+
export function parseDataUrl(url) {
|
|
120
|
+
if (typeof url !== 'string' || !url.startsWith('data:')) return null;
|
|
299
121
|
const comma = url.indexOf(',');
|
|
300
|
-
if (comma <
|
|
122
|
+
if (comma < 5) return null;
|
|
301
123
|
const meta = url.slice(5, comma);
|
|
302
124
|
const payload = url.slice(comma + 1);
|
|
125
|
+
if (!meta || payload.length === 0) return null;
|
|
126
|
+
const parts = meta.split(';');
|
|
127
|
+
const mime = parts[0] && /^[\w.+-]+\/[\w.+-]+$/i.test(parts[0]) ? parts[0] : null;
|
|
128
|
+
if (!mime) return null;
|
|
129
|
+
const isBase64 = parts.some((part) => part.toLowerCase() === 'base64');
|
|
130
|
+
let bytes;
|
|
303
131
|
try {
|
|
304
|
-
|
|
132
|
+
bytes = isBase64
|
|
305
133
|
? Buffer.from(payload, 'base64')
|
|
306
134
|
: Buffer.from(decodeURIComponent(payload), 'utf8');
|
|
307
|
-
const text = bytes.toString('utf8');
|
|
308
|
-
return text.length > MAX_INLINE_FILE_CHARS ? `${text.slice(0, MAX_INLINE_FILE_CHARS)}\n…` : text;
|
|
309
135
|
} catch {
|
|
310
136
|
return null;
|
|
311
137
|
}
|
|
138
|
+
// Reject corrupt base64 that decodes to empty while payload was non-empty.
|
|
139
|
+
if (isBase64 && bytes.length === 0 && payload.replace(/\s/g, '').length > 0) return null;
|
|
140
|
+
if (bytes.length > MAX_ATTACHMENT_BYTES) {
|
|
141
|
+
const error = new Error(`Attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
|
|
142
|
+
error.code = 'validation_error';
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
return { mime, bytes, isBase64, byteLength: bytes.length };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const decodeDataUrlText = (url, mime) => {
|
|
149
|
+
if (!TEXT_FILE_MIME.test(mime)) return null;
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = parseDataUrl(url);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error?.code === 'validation_error') throw error;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!parsed) return null;
|
|
158
|
+
const text = parsed.bytes.toString('utf8');
|
|
159
|
+
return text.length > MAX_INLINE_FILE_CHARS ? `${text.slice(0, MAX_INLINE_FILE_CHARS)}\n…` : text;
|
|
312
160
|
};
|
|
313
161
|
|
|
314
162
|
const describeContactFilePart = (part) => {
|
|
@@ -320,6 +168,25 @@ const describeContactFilePart = (part) => {
|
|
|
320
168
|
return `[file: ${name} (${mime})]`;
|
|
321
169
|
};
|
|
322
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Validate every file part URL before generate. Non-data URLs are rejected
|
|
173
|
+
* (gateway only accepts contact data-URL attachments).
|
|
174
|
+
*/
|
|
175
|
+
export function assertValidAttachmentParts(files) {
|
|
176
|
+
for (const part of files) {
|
|
177
|
+
if (!part || typeof part.url !== 'string') {
|
|
178
|
+
failGenerate('Attachment is missing a data URL', 'validation_error');
|
|
179
|
+
}
|
|
180
|
+
if (!part.url.startsWith('data:')) {
|
|
181
|
+
failGenerate('Attachment URL must be a data URL', 'validation_error');
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseDataUrl(part.url);
|
|
184
|
+
if (!parsed) {
|
|
185
|
+
failGenerate('Attachment data URL is malformed', 'validation_error');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
323
190
|
const flattenMessages = (messages) => {
|
|
324
191
|
const system = [];
|
|
325
192
|
const turns = [];
|
|
@@ -345,50 +212,36 @@ const flattenMessages = (messages) => {
|
|
|
345
212
|
};
|
|
346
213
|
};
|
|
347
214
|
|
|
215
|
+
/** Image bytes only when the catalog model is vision-capable. */
|
|
348
216
|
const filesForPrompt = (files, forwardImageParts) => {
|
|
349
217
|
if (forwardImageParts) return files;
|
|
350
218
|
return files.filter((part) => !String(part?.mime || '').startsWith('image/'));
|
|
351
219
|
};
|
|
352
220
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
});
|
|
364
|
-
if (!response.ok) {
|
|
365
|
-
const body = await response.text().catch(() => '');
|
|
366
|
-
failGenerate(`OpenCode generate failed (${response.status})${body ? `: ${body.slice(0, 200)}` : ''}`);
|
|
367
|
-
}
|
|
368
|
-
const payload = await response.json();
|
|
369
|
-
const text = typeof payload?.text === 'string'
|
|
370
|
-
? payload.text
|
|
371
|
-
: typeof payload?.message === 'string'
|
|
372
|
-
? payload.message
|
|
373
|
-
: '';
|
|
374
|
-
if (!text.trim()) failGenerate('OpenCode generate returned no text');
|
|
375
|
-
return { text: text.trim(), source: 'generate' };
|
|
376
|
-
}
|
|
221
|
+
const imageFilesForSession = (files, forwardImageParts) => {
|
|
222
|
+
if (!forwardImageParts) return [];
|
|
223
|
+
return files.filter((part) => String(part?.mime || '').startsWith('image/'));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const modelRef = (providerID, modelID, variant) => ({
|
|
227
|
+
id: modelID,
|
|
228
|
+
providerID,
|
|
229
|
+
...(variant ? { variant } : {}),
|
|
230
|
+
});
|
|
377
231
|
|
|
378
232
|
const eventPayload = (event) => event?.payload?.payload ?? event?.payload ?? event;
|
|
379
233
|
|
|
380
|
-
const
|
|
234
|
+
const eventDeltaData = (payload) => {
|
|
381
235
|
if (!payload || typeof payload !== 'object') return null;
|
|
382
|
-
if (payload.properties && typeof payload.properties === 'object') return payload.properties;
|
|
383
236
|
if (payload.data && typeof payload.data === 'object') return payload.data;
|
|
237
|
+
if (payload.properties && typeof payload.properties === 'object') return payload.properties;
|
|
384
238
|
return null;
|
|
385
239
|
};
|
|
386
240
|
|
|
387
241
|
/**
|
|
388
|
-
* Forward real OpenCode `
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* typewriter chunks from a completed string.
|
|
242
|
+
* Forward real OpenCode `session.text.delta` tokens for one throwaway session.
|
|
243
|
+
* Consumes data.sessionID / assistantMessageID / ordinal / delta.
|
|
244
|
+
* Returns unsubscribe, or null when deltas cannot be observed.
|
|
392
245
|
*
|
|
393
246
|
* @param {{ globalEventHub?: { subscribeEvent?: Function } | null, sessionID: string, onTextDelta?: ((text: string) => void) | null }} args
|
|
394
247
|
* @returns {(() => void) | null}
|
|
@@ -398,22 +251,19 @@ export function subscribeThrowawayTextDeltas({ globalEventHub, sessionID, onText
|
|
|
398
251
|
if (typeof sessionID !== 'string' || !sessionID) return null;
|
|
399
252
|
if (typeof globalEventHub?.subscribeEvent !== 'function') return null;
|
|
400
253
|
|
|
401
|
-
// Lock to the first assistant messageID seen for this throwaway session so
|
|
402
|
-
// concurrent sessions on the shared hub cannot leak tokens into this generate.
|
|
403
254
|
let assistantMessageID = null;
|
|
255
|
+
let lastOrdinal = -1;
|
|
404
256
|
|
|
405
257
|
const unsubscribe = globalEventHub.subscribeEvent((event) => {
|
|
406
258
|
const payload = eventPayload(event);
|
|
407
|
-
if (payload?.type !== '
|
|
408
|
-
const
|
|
409
|
-
if (!
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const messageID = typeof props.messageID === 'string' && props.messageID
|
|
416
|
-
? props.messageID
|
|
259
|
+
if (payload?.type !== 'session.text.delta') return;
|
|
260
|
+
const data = eventDeltaData(payload);
|
|
261
|
+
if (!data) return;
|
|
262
|
+
if (data.sessionID !== sessionID) return;
|
|
263
|
+
if (typeof data.delta !== 'string' || data.delta.length === 0) return;
|
|
264
|
+
|
|
265
|
+
const messageID = typeof data.assistantMessageID === 'string' && data.assistantMessageID
|
|
266
|
+
? data.assistantMessageID
|
|
417
267
|
: null;
|
|
418
268
|
if (assistantMessageID) {
|
|
419
269
|
if (messageID && messageID !== assistantMessageID) return;
|
|
@@ -421,8 +271,13 @@ export function subscribeThrowawayTextDeltas({ globalEventHub, sessionID, onText
|
|
|
421
271
|
assistantMessageID = messageID;
|
|
422
272
|
}
|
|
423
273
|
|
|
274
|
+
if (typeof data.ordinal === 'number' && Number.isFinite(data.ordinal)) {
|
|
275
|
+
if (data.ordinal <= lastOrdinal) return;
|
|
276
|
+
lastOrdinal = data.ordinal;
|
|
277
|
+
}
|
|
278
|
+
|
|
424
279
|
try {
|
|
425
|
-
onTextDelta(
|
|
280
|
+
onTextDelta(data.delta);
|
|
426
281
|
} catch {
|
|
427
282
|
// Caller callback failures must not abort generate or leave the hub broken.
|
|
428
283
|
}
|
|
@@ -432,19 +287,323 @@ export function subscribeThrowawayTextDeltas({ globalEventHub, sessionID, onText
|
|
|
432
287
|
}
|
|
433
288
|
|
|
434
289
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
290
|
+
* OpenCode loads a new location's config lazily: the first requests against a
|
|
291
|
+
* fresh temp directory can report no agents at all. Poll `agent.get` within a
|
|
292
|
+
* bounded window instead of failing on that first read.
|
|
293
|
+
*/
|
|
294
|
+
const readGeneratorAgent = async ({ client, location, signal, agentID, waitMs, pollMs }) => {
|
|
295
|
+
const deadline = Date.now() + waitMs;
|
|
296
|
+
for (;;) {
|
|
297
|
+
try {
|
|
298
|
+
return await client.agent.get({
|
|
299
|
+
agentID,
|
|
300
|
+
...(location ? { location } : {}),
|
|
301
|
+
}, { signal });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
rethrowIfAborted(error, signal);
|
|
304
|
+
if (Date.now() + pollMs > deadline) throw error;
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Verify the generator agent ends with deny-all permissions.
|
|
312
|
+
* Title alone is never treated as isolation. Failure must happen before prompt.
|
|
313
|
+
*/
|
|
314
|
+
export async function assertLlmAgentDenyAll({
|
|
315
|
+
client,
|
|
316
|
+
location,
|
|
317
|
+
signal,
|
|
318
|
+
agentID = LLM_AGENT_NAME,
|
|
319
|
+
waitMs = AGENT_READY_WAIT_MS,
|
|
320
|
+
pollMs = AGENT_READY_POLL_MS,
|
|
321
|
+
}) {
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
result = await readGeneratorAgent({ client, location, signal, agentID, waitMs, pollMs });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
rethrowIfAborted(error, signal);
|
|
327
|
+
failGenerate(
|
|
328
|
+
`OpenCode LLM agent is unavailable (${clientErrorMessage(error, 'agent.get failed')}); generation blocked`,
|
|
329
|
+
'llm_attachment_generation_unavailable',
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const permissions = result?.data?.permissions;
|
|
333
|
+
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
334
|
+
failGenerate(
|
|
335
|
+
'OpenCode LLM agent permissions are missing; generation blocked',
|
|
336
|
+
'llm_attachment_generation_unavailable',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const last = permissions[permissions.length - 1];
|
|
340
|
+
if (
|
|
341
|
+
!last
|
|
342
|
+
|| last.action !== '*'
|
|
343
|
+
|| last.resource !== '*'
|
|
344
|
+
|| last.effect !== 'deny'
|
|
345
|
+
) {
|
|
346
|
+
failGenerate(
|
|
347
|
+
'OpenCode LLM agent final permission is not deny-all; generation blocked',
|
|
348
|
+
'llm_attachment_generation_unavailable',
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return result.data;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const assistantTextFromMessages = (messages) => {
|
|
355
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
356
|
+
for (const message of messages) {
|
|
357
|
+
// message.list order:desc — first assistant is the newest.
|
|
358
|
+
const role = message?.type ?? message?.role ?? message?.info?.role;
|
|
359
|
+
if (role !== 'assistant') continue;
|
|
360
|
+
const error = message?.error ?? message?.info?.error;
|
|
361
|
+
if (error) {
|
|
362
|
+
const detail = assistantErrorDetail(error);
|
|
363
|
+
failGenerate(detail || 'OpenCode assistant error');
|
|
364
|
+
}
|
|
365
|
+
const finish = message?.finish ?? message?.info?.finish;
|
|
366
|
+
if (finish === 'error') {
|
|
367
|
+
failGenerate(assistantErrorDetail(message?.error) || 'OpenCode assistant finish=error');
|
|
368
|
+
}
|
|
369
|
+
const content = Array.isArray(message?.content)
|
|
370
|
+
? message.content
|
|
371
|
+
: Array.isArray(message?.parts)
|
|
372
|
+
? message.parts
|
|
373
|
+
: [];
|
|
374
|
+
const text = content
|
|
375
|
+
.map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
|
|
376
|
+
.filter(Boolean)
|
|
377
|
+
.join('');
|
|
378
|
+
return text;
|
|
379
|
+
}
|
|
380
|
+
return '';
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const makeClient = ({ baseUrl, headers, clientFactory }) => {
|
|
384
|
+
if (typeof clientFactory === 'function') return clientFactory();
|
|
385
|
+
return OpenCode.make({ baseUrl, headers });
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const prepareGeneratorDirectory = async ({ ensureTempDirectory, agentName, agentMarkdown }) => {
|
|
389
|
+
let workingDirectory;
|
|
390
|
+
try {
|
|
391
|
+
workingDirectory = await ensureTempDirectory({ agentName, agentMarkdown });
|
|
392
|
+
} catch (error) {
|
|
393
|
+
failGenerate(
|
|
394
|
+
`LLM temp directory failed (${clientErrorMessage(error, 'ensure failed')})`,
|
|
395
|
+
'llm_attachment_generation_unavailable',
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (typeof workingDirectory !== 'string' || !workingDirectory.trim()) {
|
|
399
|
+
failGenerate(
|
|
400
|
+
'LLM temp directory is empty; generation blocked',
|
|
401
|
+
'llm_attachment_generation_unavailable',
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return workingDirectory.trim();
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Run `work(sessionID)` inside a throwaway OpenCode session owned by a
|
|
409
|
+
* verified deny-all agent. The session is always removed afterwards;
|
|
410
|
+
* cancellation interrupts it first.
|
|
411
|
+
*/
|
|
412
|
+
async function withThrowawaySession({
|
|
413
|
+
client,
|
|
414
|
+
workingDirectory,
|
|
415
|
+
agentName,
|
|
416
|
+
providerID,
|
|
417
|
+
modelID,
|
|
418
|
+
variant,
|
|
419
|
+
signal,
|
|
420
|
+
persistSessionMetadata,
|
|
421
|
+
onSystemSessionPersisted,
|
|
422
|
+
}, work) {
|
|
423
|
+
const location = { directory: workingDirectory };
|
|
424
|
+
|
|
425
|
+
// Permission isolation must be verified before any prompt. Never use title as isolation.
|
|
426
|
+
await assertLlmAgentDenyAll({ client, location, signal, agentID: agentName });
|
|
427
|
+
|
|
428
|
+
let sessionID = null;
|
|
429
|
+
try {
|
|
430
|
+
let created;
|
|
431
|
+
const isolationMetadata = buildLlmSessionMetadata();
|
|
432
|
+
try {
|
|
433
|
+
created = await client.session.create({
|
|
434
|
+
title: '[openchamber-llm] generate',
|
|
435
|
+
agent: agentName,
|
|
436
|
+
model: modelRef(providerID, modelID, variant),
|
|
437
|
+
location,
|
|
438
|
+
metadata: isolationMetadata,
|
|
439
|
+
}, { signal });
|
|
440
|
+
} catch (error) {
|
|
441
|
+
// Temp workspace invisible to OpenCode must fail explicitly.
|
|
442
|
+
failGenerate(
|
|
443
|
+
`OpenCode LLM session create failed (${clientErrorMessage(error, 'create failed')})`,
|
|
444
|
+
'llm_attachment_generation_unavailable',
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
sessionID = created?.id ?? created?.data?.id;
|
|
448
|
+
if (!sessionID) {
|
|
449
|
+
failGenerate('OpenCode LLM session create returned no id', 'llm_attachment_generation_unavailable');
|
|
450
|
+
}
|
|
451
|
+
if (typeof persistSessionMetadata === 'function') {
|
|
452
|
+
try {
|
|
453
|
+
await persistSessionMetadata(sessionID, isolationMetadata);
|
|
454
|
+
onSystemSessionPersisted?.({
|
|
455
|
+
sessionID,
|
|
456
|
+
directory: workingDirectory,
|
|
457
|
+
metadata: isolationMetadata,
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.warn('[llm] failed to persist system session metadata:', error?.message || error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return await work(sessionID);
|
|
464
|
+
} finally {
|
|
465
|
+
if (sessionID) {
|
|
466
|
+
if (signal?.aborted) {
|
|
467
|
+
try {
|
|
468
|
+
await client.session.interrupt({ sessionID, continue: false });
|
|
469
|
+
} catch {
|
|
470
|
+
// Best-effort interrupt on timeout/abort.
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
await client.session.remove({ sessionID });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.warn('[llm] failed to remove throwaway OpenCode session:', error?.message || error);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Text-only generation through `session.generate`: one model call with the
|
|
484
|
+
* session's provider context (so session-scoped providers such as OpenCode Go
|
|
485
|
+
* work), no tool loop, and no messages written to the session.
|
|
486
|
+
*/
|
|
487
|
+
async function generateViaSessionGenerate({ client, prompt, signal, ...sessionOptions }) {
|
|
488
|
+
return withThrowawaySession({ client, signal, ...sessionOptions }, async (sessionID) => {
|
|
489
|
+
let result;
|
|
490
|
+
try {
|
|
491
|
+
result = await client.session.generate({ sessionID, prompt }, { signal });
|
|
492
|
+
} catch (error) {
|
|
493
|
+
rethrowIfAborted(error, signal);
|
|
494
|
+
failGenerate(clientErrorMessage(error, 'OpenCode session.generate failed'));
|
|
495
|
+
}
|
|
496
|
+
const text = typeof result?.text === 'string'
|
|
497
|
+
? result.text.trim()
|
|
498
|
+
: typeof result?.data?.text === 'string'
|
|
499
|
+
? result.data.text.trim()
|
|
500
|
+
: '';
|
|
501
|
+
if (!text) failGenerate('OpenCode session.generate returned no text');
|
|
502
|
+
return { text, source: 'session.generate' };
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function generateViaAttachmentSession({
|
|
507
|
+
client,
|
|
508
|
+
system,
|
|
509
|
+
prompt,
|
|
510
|
+
imageFiles,
|
|
511
|
+
signal,
|
|
512
|
+
onTextDelta,
|
|
513
|
+
globalEventHub,
|
|
514
|
+
...sessionOptions
|
|
515
|
+
}) {
|
|
516
|
+
return withThrowawaySession({ client, signal, ...sessionOptions }, async (sessionID) => {
|
|
517
|
+
if (system) {
|
|
518
|
+
try {
|
|
519
|
+
await client.session.instructions.entry.put({
|
|
520
|
+
sessionID,
|
|
521
|
+
key: 'system',
|
|
522
|
+
value: system,
|
|
523
|
+
}, { signal });
|
|
524
|
+
} catch (error) {
|
|
525
|
+
failGenerate(clientErrorMessage(error, 'instructions.entry.put failed'));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const unsubscribeDeltas = subscribeThrowawayTextDeltas({
|
|
530
|
+
globalEventHub,
|
|
531
|
+
sessionID,
|
|
532
|
+
onTextDelta,
|
|
533
|
+
});
|
|
534
|
+
try {
|
|
535
|
+
return await promptAttachmentSession({ client, sessionID, prompt, imageFiles, signal });
|
|
536
|
+
} finally {
|
|
537
|
+
try {
|
|
538
|
+
unsubscribeDeltas?.();
|
|
539
|
+
} catch {
|
|
540
|
+
// Unsubscribe must not mask generate errors or block session remove.
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function promptAttachmentSession({ client, sessionID, prompt, imageFiles, signal }) {
|
|
547
|
+
const files = imageFiles.map((part) => ({
|
|
548
|
+
uri: part.url,
|
|
549
|
+
...(part.filename ? { name: part.filename } : {}),
|
|
550
|
+
}));
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
await client.session.prompt({
|
|
554
|
+
sessionID,
|
|
555
|
+
text: prompt || '[attachment]',
|
|
556
|
+
...(files.length > 0 ? { files } : {}),
|
|
557
|
+
delivery: 'steer',
|
|
558
|
+
}, { signal });
|
|
559
|
+
} catch (error) {
|
|
560
|
+
rethrowIfAborted(error, signal);
|
|
561
|
+
failGenerate(clientErrorMessage(error, 'session.prompt failed'));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
await client.session.wait({ sessionID }, { signal });
|
|
566
|
+
} catch (error) {
|
|
567
|
+
rethrowIfAborted(error, signal);
|
|
568
|
+
failGenerate(clientErrorMessage(error, 'session.wait failed'));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let listed;
|
|
572
|
+
try {
|
|
573
|
+
listed = await client.message.list({
|
|
574
|
+
sessionID,
|
|
575
|
+
limit: 20,
|
|
576
|
+
order: 'desc',
|
|
577
|
+
}, { signal });
|
|
578
|
+
} catch (error) {
|
|
579
|
+
rethrowIfAborted(error, signal);
|
|
580
|
+
failGenerate(clientErrorMessage(error, 'message.list failed'));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const messages = Array.isArray(listed?.data) ? listed.data : [];
|
|
584
|
+
const text = assistantTextFromMessages(messages);
|
|
585
|
+
if (!text.trim()) {
|
|
586
|
+
failGenerate('OpenCode LLM generator returned no assistant text');
|
|
587
|
+
}
|
|
588
|
+
return { text: text.trim(), source: 'attachment-session' };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Generate assistant text through OpenCode's connected providers via
|
|
593
|
+
* `@opencode/client`.
|
|
594
|
+
*
|
|
595
|
+
* Both paths run in a throwaway session owned by a verified deny-all agent in
|
|
596
|
+
* the LLM temp directory, and the session is removed afterwards.
|
|
597
|
+
* - Pure text: `session.generate` (single model call, nothing written).
|
|
598
|
+
* - Vision image attachments: `session.prompt` + wait + message list.
|
|
599
|
+
* - Non-vision: keep `[image: …]` descriptions; never forward image bytes.
|
|
438
600
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
601
|
+
* Optional internal streaming: `onTextDelta` + `globalEventHub` forwards real
|
|
602
|
+
* `session.text.delta` tokens on the attachment-session path only.
|
|
603
|
+
* `session.generate` cannot emit live deltas (no fake typewriter).
|
|
442
604
|
*
|
|
443
|
-
* Optional
|
|
444
|
-
*
|
|
445
|
-
* `message.part.delta` tokens for this session are forwarded. Sessionless
|
|
446
|
-
* `/generate` JSON cannot emit deltas — onTextDelta is skipped (no fake
|
|
447
|
-
* typewriter). Public HTTP completions stay non-streaming.
|
|
605
|
+
* Optional `signal` from the contact continuation is combined with the generator
|
|
606
|
+
* deadline. Cancellation still runs throwaway-session cleanup.
|
|
448
607
|
*/
|
|
449
608
|
export async function generateOpenCodeText({
|
|
450
609
|
buildOpenCodeUrl,
|
|
@@ -453,26 +612,29 @@ export async function generateOpenCodeText({
|
|
|
453
612
|
modelID,
|
|
454
613
|
messages,
|
|
455
614
|
variant,
|
|
456
|
-
fetchImpl = globalThis.fetch.bind(globalThis),
|
|
457
615
|
clientFactory,
|
|
458
|
-
ensureTempDirectory,
|
|
459
|
-
detect = detectSessionlessGenerate,
|
|
616
|
+
ensureTempDirectory = ensureLlmTempDirectory,
|
|
460
617
|
forwardImageParts = false,
|
|
461
618
|
onTextDelta = null,
|
|
462
619
|
globalEventHub = null,
|
|
620
|
+
persistSessionMetadata = null,
|
|
621
|
+
onSystemSessionPersisted = null,
|
|
463
622
|
signal: parentSignal = null,
|
|
464
623
|
timeoutMs = GENERATE_TIMEOUT_MS,
|
|
465
|
-
settlePollMs = SETTLE_POLL_MS,
|
|
466
624
|
}) {
|
|
467
|
-
parentSignal?.throwIfAborted();
|
|
625
|
+
parentSignal?.throwIfAborted?.();
|
|
468
626
|
if (!providerID || !modelID) {
|
|
469
627
|
const error = new Error('providerID and modelID are required');
|
|
470
628
|
error.code = 'validation_error';
|
|
471
629
|
throw error;
|
|
472
630
|
}
|
|
473
631
|
const flattened = flattenMessages(messages);
|
|
474
|
-
const
|
|
475
|
-
|
|
632
|
+
const allFiles = flattened.files;
|
|
633
|
+
assertValidAttachmentParts(allFiles);
|
|
634
|
+
|
|
635
|
+
const imageFiles = imageFilesForSession(allFiles, forwardImageParts);
|
|
636
|
+
// Non-image files stay as prompt descriptions (and inlined text when possible).
|
|
637
|
+
if (!flattened.prompt && imageFiles.length === 0 && allFiles.length === 0) {
|
|
476
638
|
const error = new Error('messages must include a user turn');
|
|
477
639
|
error.code = 'validation_error';
|
|
478
640
|
throw error;
|
|
@@ -480,180 +642,81 @@ export async function generateOpenCodeText({
|
|
|
480
642
|
|
|
481
643
|
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
482
644
|
const headers = getOpenCodeAuthHeaders() || {};
|
|
483
|
-
const
|
|
484
|
-
|
|
645
|
+
const client = makeClient({ baseUrl, headers, clientFactory });
|
|
646
|
+
|
|
485
647
|
const controller = new AbortController();
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
648
|
+
const deadlineMs = positiveMs(timeoutMs, GENERATE_TIMEOUT_MS);
|
|
649
|
+
const requestSignal = parentSignal
|
|
650
|
+
? AbortSignal.any([parentSignal, controller.signal])
|
|
651
|
+
: controller.signal;
|
|
652
|
+
const timeout = setTimeout(() => {
|
|
653
|
+
controller.abort(new Error(`OpenCode LLM generate timed out after ${deadlineMs}ms`));
|
|
654
|
+
}, deadlineMs);
|
|
655
|
+
timeout.unref?.();
|
|
656
|
+
|
|
657
|
+
const sessionOptions = {
|
|
658
|
+
client,
|
|
659
|
+
providerID,
|
|
660
|
+
modelID,
|
|
661
|
+
variant,
|
|
662
|
+
signal: requestSignal,
|
|
663
|
+
persistSessionMetadata,
|
|
664
|
+
onSystemSessionPersisted,
|
|
503
665
|
};
|
|
504
666
|
|
|
505
667
|
try {
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
providerID,
|
|
512
|
-
modelID,
|
|
513
|
-
variant,
|
|
514
|
-
messages: forwardImageParts
|
|
515
|
-
? messages
|
|
516
|
-
: messages.map((message) => {
|
|
517
|
-
if (!Array.isArray(message?.parts)) return message;
|
|
518
|
-
const parts = message.parts.filter((part) => part?.type !== 'file' || !String(part.mime || '').startsWith('image/'));
|
|
519
|
-
return parts.length === message.parts.length ? message : { ...message, parts };
|
|
520
|
-
}),
|
|
521
|
-
signal: requestSignal,
|
|
668
|
+
if (imageFiles.length > 0) {
|
|
669
|
+
const workingDirectory = await prepareGeneratorDirectory({
|
|
670
|
+
ensureTempDirectory,
|
|
671
|
+
agentName: LLM_AGENT_NAME,
|
|
672
|
+
agentMarkdown: AGENT_MARKDOWN,
|
|
522
673
|
});
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
: createOpencodeClient({ baseUrl, directory: workingDirectory, headers });
|
|
532
|
-
|
|
533
|
-
let toolIds;
|
|
534
|
-
try {
|
|
535
|
-
toolIds = await client.tool.ids({ directory: workingDirectory });
|
|
536
|
-
} catch (error) {
|
|
537
|
-
failGenerate(`OpenCode tool.ids failed: ${error?.message || 'tool.ids failed'}`);
|
|
538
|
-
}
|
|
539
|
-
if (toolIds?.error) {
|
|
540
|
-
failGenerate(`OpenCode tool.ids failed: ${sdkErrorMessage(toolIds, 'tool.ids failed')}`);
|
|
541
|
-
}
|
|
542
|
-
const tools = deniedTools(Array.isArray(toolIds?.data) ? toolIds.data : []);
|
|
543
|
-
|
|
544
|
-
const created = await client.session.create({
|
|
545
|
-
directory: workingDirectory,
|
|
546
|
-
title: '[openchamber-llm] generate',
|
|
547
|
-
agent: LLM_AGENT_NAME,
|
|
548
|
-
// Session fence: deny every native OpenCode/MCP/skill tool for this throwaway.
|
|
549
|
-
permission: LLM_SESSION_DENY_PERMISSION,
|
|
550
|
-
metadata: { openchamber: { llm: { purpose: 'chat-completions' } } },
|
|
551
|
-
}, { signal: requestSignal });
|
|
552
|
-
const sessionID = created?.data?.id;
|
|
553
|
-
if (created?.error || !sessionID) {
|
|
554
|
-
failGenerate(`OpenCode LLM session create failed: ${sdkErrorMessage(created, 'create failed')}`);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
let unsubscribeDeltas = null;
|
|
558
|
-
try {
|
|
559
|
-
const archiveAt = Date.now();
|
|
560
|
-
let archived = await client.session.update({
|
|
561
|
-
sessionID,
|
|
562
|
-
directory: workingDirectory,
|
|
563
|
-
time: { archived: archiveAt },
|
|
564
|
-
});
|
|
565
|
-
if (isMissing(archived)) {
|
|
566
|
-
archived = await client.session.update({
|
|
567
|
-
sessionID,
|
|
568
|
-
directory: workingDirectory,
|
|
569
|
-
time: { archived: archiveAt },
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
if (archived?.error) {
|
|
573
|
-
failGenerate(`OpenCode LLM session archive failed: ${sdkErrorMessage(archived, 'archive failed')}`);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Subscribe before promptAsync so early message.part.delta tokens are not missed.
|
|
577
|
-
unsubscribeDeltas = subscribeThrowawayTextDeltas({
|
|
674
|
+
return await generateViaAttachmentSession({
|
|
675
|
+
...sessionOptions,
|
|
676
|
+
workingDirectory,
|
|
677
|
+
agentName: LLM_AGENT_NAME,
|
|
678
|
+
system: flattened.system,
|
|
679
|
+
prompt: flattened.prompt,
|
|
680
|
+
imageFiles,
|
|
681
|
+
onTextDelta,
|
|
578
682
|
globalEventHub,
|
|
579
|
-
sessionID,
|
|
580
|
-
onTextDelta: (text) => {
|
|
581
|
-
bumpTimeout();
|
|
582
|
-
if (typeof onTextDelta === 'function') onTextDelta(text);
|
|
583
|
-
},
|
|
584
683
|
});
|
|
585
|
-
|
|
586
|
-
// promptAsync still forwards model/parts/tools. v2 session.prompt does not.
|
|
587
|
-
const prompted = await client.session.promptAsync({
|
|
588
|
-
sessionID,
|
|
589
|
-
directory: workingDirectory,
|
|
590
|
-
agent: LLM_AGENT_NAME,
|
|
591
|
-
model: { providerID, modelID },
|
|
592
|
-
...(variant ? { variant } : {}),
|
|
593
|
-
...(flattened.system ? { system: flattened.system } : {}),
|
|
594
|
-
tools,
|
|
595
|
-
parts: [
|
|
596
|
-
{ type: 'text', text: flattened.prompt, synthetic: false },
|
|
597
|
-
...promptFiles,
|
|
598
|
-
],
|
|
599
|
-
}, { signal: requestSignal });
|
|
600
|
-
if (!promptAdmitted(prompted)) {
|
|
601
|
-
failGenerate(`OpenCode LLM promptAsync failed: ${sdkErrorMessage(prompted, 'promptAsync failed')}`);
|
|
602
|
-
}
|
|
603
|
-
bumpTimeout();
|
|
604
|
-
|
|
605
|
-
let text = assistantTextFromPrompt(prompted?.data ?? prompted);
|
|
606
|
-
if (!text.trim()) {
|
|
607
|
-
const settled = await waitForIdleAssistant({
|
|
608
|
-
client,
|
|
609
|
-
sessionID,
|
|
610
|
-
directory: workingDirectory,
|
|
611
|
-
signal: requestSignal,
|
|
612
|
-
onProgress: bumpTimeout,
|
|
613
|
-
settlePollMs,
|
|
614
|
-
});
|
|
615
|
-
text = assistantTextFromMessages(settled);
|
|
616
|
-
}
|
|
617
|
-
if (!text.trim()) {
|
|
618
|
-
failGenerate('OpenCode LLM generator returned no assistant text');
|
|
619
|
-
}
|
|
620
|
-
return { text: text.trim(), source: 'throwaway-session' };
|
|
621
|
-
} finally {
|
|
622
|
-
try {
|
|
623
|
-
unsubscribeDeltas?.();
|
|
624
|
-
} catch {
|
|
625
|
-
// Unsubscribe must not mask generate errors or block session delete.
|
|
626
|
-
}
|
|
627
|
-
unsubscribeDeltas = null;
|
|
628
|
-
try {
|
|
629
|
-
const deleted = await client.session.delete({ sessionID, directory: workingDirectory });
|
|
630
|
-
if (deleted?.error) {
|
|
631
|
-
console.warn(
|
|
632
|
-
'[llm] failed to delete throwaway OpenCode session:',
|
|
633
|
-
sdkErrorMessage(deleted, 'delete failed'),
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
} catch (error) {
|
|
637
|
-
console.warn('[llm] failed to delete throwaway OpenCode session:', error?.message || error);
|
|
638
|
-
}
|
|
639
684
|
}
|
|
685
|
+
|
|
686
|
+
// Text path: the system prompt is prepended into the single prompt string.
|
|
687
|
+
const workingDirectory = await prepareGeneratorDirectory({
|
|
688
|
+
ensureTempDirectory,
|
|
689
|
+
agentName: TEXT_AGENT_NAME,
|
|
690
|
+
agentMarkdown: TEXT_AGENT_MARKDOWN,
|
|
691
|
+
});
|
|
692
|
+
const prompt = [flattened.system, flattened.prompt].filter((part) => String(part || '').trim()).join('\n\n');
|
|
693
|
+
return await generateViaSessionGenerate({
|
|
694
|
+
...sessionOptions,
|
|
695
|
+
workingDirectory,
|
|
696
|
+
agentName: TEXT_AGENT_NAME,
|
|
697
|
+
prompt,
|
|
698
|
+
});
|
|
640
699
|
} finally {
|
|
641
|
-
clearTimeout(
|
|
700
|
+
clearTimeout(timeout);
|
|
642
701
|
}
|
|
643
702
|
}
|
|
644
703
|
|
|
645
704
|
export const _test = {
|
|
646
705
|
flattenMessages,
|
|
647
706
|
filesForPrompt,
|
|
707
|
+
imageFilesForSession,
|
|
648
708
|
describeContactFilePart,
|
|
649
|
-
deniedTools,
|
|
650
|
-
assistantTextFromPrompt,
|
|
651
709
|
assistantTextFromMessages,
|
|
652
710
|
eventPayload,
|
|
653
|
-
|
|
711
|
+
eventDeltaData,
|
|
654
712
|
subscribeThrowawayTextDeltas,
|
|
713
|
+
assertLlmAgentDenyAll,
|
|
714
|
+
parseDataUrl,
|
|
715
|
+
assertValidAttachmentParts,
|
|
655
716
|
LLM_AGENT_NAME,
|
|
656
717
|
AGENT_MARKDOWN,
|
|
657
|
-
|
|
718
|
+
TEXT_AGENT_NAME,
|
|
719
|
+
TEXT_AGENT_MARKDOWN,
|
|
720
|
+
MAX_ATTACHMENT_BYTES,
|
|
658
721
|
GENERATE_TIMEOUT_MS,
|
|
659
722
|
};
|