@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.31
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 +14 -0
- package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
- package/dist/assets/AssistantView-D_vWSFzf.js +3 -0
- package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B_hiA-Tg.js} +1 -1
- package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
- package/dist/assets/Icon-BN87CyS4.js +1 -0
- package/dist/assets/MarkdownRenderer--4f1RVU1.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-Blh7LZm7.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CstRN3R1.js +3 -0
- package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DR6wUqeI.js} +1 -1
- package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-CTFAmi5i.js} +3 -3
- package/dist/assets/MobileSurface-B5XmnUTI.js +2 -0
- package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-ccv_K6Mt.js} +1 -1
- package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
- package/dist/assets/SettingsView-LYoifZgH.js +25 -0
- package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CnDiFgGl.js} +1 -1
- package/dist/assets/TerminalView-DRhXa_u9.js +8 -0
- package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-cQm6YPbu.js} +1 -1
- package/dist/assets/appThemeRegistry-DvJG4vb9.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
- package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
- package/dist/assets/collapsible-nU99prkD.js +1 -0
- package/dist/assets/context-menu-DMzZN1oi.js +1 -0
- package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
- package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
- package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
- package/dist/assets/dialog-ByGUkS3c.js +1 -0
- package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
- package/dist/assets/es-DCjl24dV.js +15 -0
- package/dist/assets/fileMentionTouchSelection-B4QTB6Tq.js +2 -0
- package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
- package/dist/assets/fr-DfJ5j0t5.js +13 -0
- package/dist/assets/{gitApi-CM42qUed.js → gitApi-ChE0ftqF.js} +3 -3
- package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
- package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
- package/dist/assets/insertionBoundaries-BP5YmZFb.js +1 -0
- package/dist/assets/ja-B_9_w2Xf.js +15 -0
- package/dist/assets/ko-DlGbo3kV.js +15 -0
- package/dist/assets/main-B0y8N4sq.js +2 -0
- package/dist/assets/main-D83L0SQU.js +169 -0
- package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-BtSRn19d.js} +1 -1
- package/dist/assets/miniChat-s8g2Otyu.js +2 -0
- package/dist/assets/mobile-B6toR_NR.js +2 -0
- package/dist/assets/multirun-BqZ3MZrK.js +3 -0
- package/dist/assets/persistence-Dnj1QOdX.js +3 -0
- package/dist/assets/pl-DjUsS8Ei.js +15 -0
- package/dist/assets/platform-X3Y3GyM5.js +6 -0
- package/dist/assets/projectMeta-DKjtarC_.js +1 -0
- package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-Bk98yyKC.js +2 -0
- package/dist/assets/renderMobileApp-B_qgjDlv.js +28 -0
- package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
- package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
- package/dist/assets/runtimeConfig-BMSsd_sn.js +2 -0
- package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
- package/dist/assets/runtimeEndpointReset-HsKwHFWr.js +148 -0
- package/dist/assets/sessionChangeSummary-C8OSNmpS.js +2 -0
- package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-DuNsPLgr.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
- package/dist/assets/src-BmSX_CVj.css +2 -0
- package/dist/assets/store-Cuktref-.js +15 -0
- package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
- package/dist/assets/textarea-biAqiqnJ.js +1 -0
- package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
- package/dist/assets/uk-Bh7HXyLp.js +15 -0
- package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
- package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-oEf92G32.js} +57 -52
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-CD-czj1j.js +1 -0
- package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-BmrftoaF.js} +1 -1
- package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
- package/dist/assets/useMobileNavigationStore-Bzu_5Aa7.js +415 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-CHvstjnp.js +2 -0
- package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
- package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-Cm51EGlD.js} +1 -1
- package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
- package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
- package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
- package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
- package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
- package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
- package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
- package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
- package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
- package/dist/index.html +15 -15
- package/dist/mini-chat.html +15 -15
- package/dist/mobile.html +15 -15
- package/package.json +3 -2
- package/server/index.js +46 -27
- package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/message-queue/service.js +1 -1
- package/server/lib/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +128 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +36 -19
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +16 -124
- package/server/lib/opencode/config-entity-routes.test.js +48 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +18 -1
- package/server/lib/opencode/feature-routes-runtime.js +19 -7
- package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugin-routes.test.js +4 -2
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/plugins.test.js +5 -3
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +13 -1
- package/server/lib/opencode/proxy.test.js +31 -3
- package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
- package/server/lib/opencode/routes.js +40 -8
- package/server/lib/opencode/routes.test.js +51 -10
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/update-discovery.js +39 -0
- package/server/lib/opencode/update-discovery.test.js +39 -0
- package/server/lib/opencode/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +292 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -0
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
- package/server/lib/question-auto-delegate/core.d.ts +5 -1
- package/server/lib/question-auto-delegate/core.js +21 -13
- package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
- package/server/lib/question-auto-delegate/form-io.js +39 -0
- package/server/lib/question-auto-delegate/runtime.js +4 -24
- package/server/lib/question-auto-delegate/runtime.test.js +138 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +3 -1
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
- package/server/lib/scheduled-tasks/runtime.js +12 -4
- package/server/lib/scheduled-tasks/runtime.test.js +81 -6
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/session-goal/runtime.js +4 -4
- package/server/lib/session-goal/runtime.test.js +32 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +5 -1
- package/server/lib/session-metadata/session-archive.js +1 -1
- package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
- package/server/lib/session-metadata/session-metadata-store.js +63 -16
- package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
- package/server/lib/session-metadata/session-projection.d.ts +1 -0
- package/server/lib/session-metadata/session-projection.js +49 -2
- package/server/lib/session-metadata/session-projection.test.js +35 -0
- package/server/lib/session-title/DOCUMENTATION.md +52 -14
- package/server/lib/session-title/native-runtime.test.js +59 -5
- package/server/lib/session-title/runtime.js +65 -19
- package/server/lib/session-title/session-access.js +108 -3
- package/server/lib/session-title/session-access.test.js +125 -7
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
- package/dist/assets/Icon-BiEQEcih.js +0 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
- package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
- package/dist/assets/SettingsView-BRxELjow.js +0 -25
- package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
- package/dist/assets/collapsible-DXbDynN4.js +0 -3
- package/dist/assets/context-menu-CqolsLCm.js +0 -1
- package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
- package/dist/assets/dialog-DQYl3W_o.js +0 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
- package/dist/assets/es-CN3E_zXj.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
- package/dist/assets/fr-9zZvQZD2.js +0 -13
- package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
- package/dist/assets/ja-Dnjk_82N.js +0 -15
- package/dist/assets/ko-BxCEE99C.js +0 -15
- package/dist/assets/main-BILgBI3B.js +0 -169
- package/dist/assets/main-BeveiW6F.js +0 -2
- package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
- package/dist/assets/miniChat-BgrzM3u7.js +0 -2
- package/dist/assets/mobile-DtYoQE8C.js +0 -2
- package/dist/assets/multirun-jy8dEI96.js +0 -4
- package/dist/assets/persistence-C9LQpiOS.js +0 -3
- package/dist/assets/pl-CNNsWnPM.js +0 -15
- package/dist/assets/platform-BQghHRfp.js +0 -6
- package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
- package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
- package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
- package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
- package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
- package/dist/assets/src-CLN7Z-zx.css +0 -2
- package/dist/assets/store-DLCiZaA6.js +0 -15
- package/dist/assets/textarea-CakKvqFw.js +0 -1
- package/dist/assets/uk-rwQrxeYM.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
- package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
- package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readOpenCodeCredentials } from '../../opencode/auth.js';
|
|
2
2
|
import {
|
|
3
3
|
getAuthEntry,
|
|
4
4
|
normalizeAuthEntry,
|
|
@@ -12,14 +12,14 @@ export const providerId = 'openrouter';
|
|
|
12
12
|
export const providerName = 'OpenRouter';
|
|
13
13
|
const aliases = ['openrouter'];
|
|
14
14
|
|
|
15
|
-
export const isConfigured = () => {
|
|
16
|
-
const auth =
|
|
15
|
+
export const isConfigured = async () => {
|
|
16
|
+
const auth = await readOpenCodeCredentials();
|
|
17
17
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
18
18
|
return Boolean(entry?.key || entry?.token);
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export const fetchQuota = async () => {
|
|
22
|
-
const auth =
|
|
22
|
+
const auth = await readOpenCodeCredentials();
|
|
23
23
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
24
24
|
const apiKey = entry?.key ?? entry?.token;
|
|
25
25
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readOpenCodeCredentials } from '../../opencode/auth.js';
|
|
2
2
|
import {
|
|
3
3
|
getAuthEntry,
|
|
4
4
|
normalizeAuthEntry,
|
|
@@ -17,14 +17,14 @@ const aliases = ['wafer', 'wafer-ai', 'wafer_ai', 'wafer.ai'];
|
|
|
17
17
|
const WAFER_QUOTA_URL = 'https://pass.wafer.ai/v1/inference/quota';
|
|
18
18
|
const WAFER_WINDOW_SECONDS = 5 * 3600;
|
|
19
19
|
|
|
20
|
-
export const isConfigured = () => {
|
|
21
|
-
const auth =
|
|
20
|
+
export const isConfigured = async () => {
|
|
21
|
+
const auth = await readOpenCodeCredentials();
|
|
22
22
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
23
23
|
return Boolean(entry?.key || entry?.token);
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const fetchQuota = async () => {
|
|
27
|
-
const auth =
|
|
27
|
+
const auth = await readOpenCodeCredentials();
|
|
28
28
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
29
29
|
const apiKey = entry?.key ?? entry?.token;
|
|
30
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readOpenCodeCredentials } from '../../opencode/auth.js';
|
|
2
2
|
import {
|
|
3
3
|
getAuthEntry,
|
|
4
4
|
normalizeAuthEntry,
|
|
@@ -15,14 +15,14 @@ export const providerId = 'zai-coding-plan';
|
|
|
15
15
|
export const providerName = 'z.ai';
|
|
16
16
|
const aliases = ['zai-coding-plan', 'zai', 'z.ai'];
|
|
17
17
|
|
|
18
|
-
export const isConfigured = () => {
|
|
19
|
-
const auth =
|
|
18
|
+
export const isConfigured = async () => {
|
|
19
|
+
const auth = await readOpenCodeCredentials();
|
|
20
20
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
21
21
|
return Boolean(entry?.key || entry?.token);
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export const fetchQuota = async () => {
|
|
25
|
-
const auth =
|
|
25
|
+
const auth = await readOpenCodeCredentials();
|
|
26
26
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
27
27
|
const apiKey = entry?.key ?? entry?.token;
|
|
28
28
|
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
* @property {number} [nextResetTime]
|
|
26
26
|
* @property {Array<{modelCode: string, usage: number}>} [usageDetails]
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
29
|
-
import { readConfigLayers } from '../../opencode/shared.js';
|
|
28
|
+
import { readOpenCodeCredentials } from '../../opencode/auth.js';
|
|
30
29
|
import {
|
|
31
30
|
getAuthEntry,
|
|
32
31
|
normalizeAuthEntry,
|
|
@@ -40,8 +39,8 @@ export const providerId = 'zhipuai-coding-plan';
|
|
|
40
39
|
export const providerName = 'Zhipu AI Coding Plan';
|
|
41
40
|
const aliases = ['zhipuai-coding-plan', 'zhipuai', 'zhipu'];
|
|
42
41
|
|
|
43
|
-
function getApiKey() {
|
|
44
|
-
const auth =
|
|
42
|
+
async function getApiKey() {
|
|
43
|
+
const auth = await readOpenCodeCredentials();
|
|
45
44
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
46
45
|
const apiKeyFromAuth = entry?.key ?? entry?.token;
|
|
47
46
|
|
|
@@ -49,28 +48,15 @@ function getApiKey() {
|
|
|
49
48
|
return apiKeyFromAuth;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
try {
|
|
53
|
-
const { mergedConfig } = readConfigLayers();
|
|
54
|
-
|
|
55
|
-
for (const alias of aliases) {
|
|
56
|
-
const providerConfig = mergedConfig?.provider?.[alias];
|
|
57
|
-
if (providerConfig?.options?.apiKey) {
|
|
58
|
-
return providerConfig.options.apiKey;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// Ignore config read errors; the provider will be treated as not configured.
|
|
63
|
-
}
|
|
64
|
-
|
|
65
51
|
return null;
|
|
66
52
|
}
|
|
67
53
|
|
|
68
|
-
export const isConfigured = () => {
|
|
69
|
-
return Boolean(getApiKey());
|
|
54
|
+
export const isConfigured = async () => {
|
|
55
|
+
return Boolean(await getApiKey());
|
|
70
56
|
};
|
|
71
57
|
|
|
72
58
|
export const fetchQuota = async () => {
|
|
73
|
-
const apiKey = getApiKey();
|
|
59
|
+
const apiKey = await getApiKey();
|
|
74
60
|
|
|
75
61
|
if (!apiKey) {
|
|
76
62
|
return buildResult({
|
|
@@ -28,7 +28,7 @@ export function registerQuotaRoutes(app, { getQuotaProviders }) {
|
|
|
28
28
|
app.get('/api/quota/providers', async (_req, res) => {
|
|
29
29
|
try {
|
|
30
30
|
const { listConfiguredQuotaProviders } = await getQuotaProviders();
|
|
31
|
-
res.json({ providers: listConfiguredQuotaProviders() });
|
|
31
|
+
res.json({ providers: await listConfiguredQuotaProviders() });
|
|
32
32
|
} catch (error) {
|
|
33
33
|
console.error('Failed to list quota providers:', error);
|
|
34
34
|
res.status(500).json({ error: error.message || 'Failed to list quota providers' });
|
|
@@ -39,7 +39,8 @@ Relay is not a separate link format: it is one transport candidate inside the un
|
|
|
39
39
|
## What travels the tunnel
|
|
40
40
|
|
|
41
41
|
Everything a client normally sends to the single OpenChamber origin:
|
|
42
|
-
- **HTTP** — REST endpoints and proxied OpenCode SDK calls under `/api/*`, plus `/auth/*` and `/health`.
|
|
42
|
+
- **HTTP** — REST endpoints and proxied OpenCode SDK calls under `/api/*`, plus `/auth/*` and `/health`. On a packaged desktop client the UI origin is `openchamber-ui://` (`location.origin` is `"null"`) while the SDK still addresses the injected loopback. Relay mode tunnels those shell-local absolute `/api` `/auth` `/health` URLs; they must not fall through to the local OpenCode process.
|
|
43
|
+
- **Events** — relay `auto` uses SSE (bearer), the same path mobile uses. Desktop WebSocket upgrades over the tunnel close and drop assistant deltas; an explicit `ws` setting is unchanged.
|
|
43
44
|
- **SSE** — streamed responses opened through `runtimeFetch`, including `/api/openchamber/events` and SDK global SSE. Relay carries these HTTP response body frames through the tunnel.
|
|
44
45
|
- **WebSocket** — the endpoints that use a real socket (the global event stream on platforms that support WS, terminal I/O, dictation, and Preview/Browser HMR under `/api/preview/proxy/<id>/...`).
|
|
45
46
|
|
|
@@ -117,6 +118,7 @@ Catalog loaders (`loadProviders` / `loadAgents`) and assistant Query keys gate w
|
|
|
117
118
|
- The tunnel is transparent to the app: adding relay support to a feature should not require the feature to know the relay exists — it goes through the shared runtime transport helpers.
|
|
118
119
|
- The two implementations stay byte-compatible and the wire format is versioned/negotiated so mixed client/host app versions degrade gracefully rather than break. The Layer 1 Relay does not inspect application versions; unsupported Host/Client features are omitted at the endpoints.
|
|
119
120
|
- Host-control reconnect storms are bounded at both ends: the Host does not reset backoff on a brief `open`, and the Relay rate-limits host-control upgrades per IP and per `serverId` (`4029`) without replacing a live control socket.
|
|
121
|
+
- Concurrent service starts recheck the endpoint's active Host after awaiting identity, so each endpoint has one managed Host and stopping the service closes every started Host.
|
|
120
122
|
- Tunneled HTTP/WS always dial `getLocalPort()` on loopback; there is no client-selected target-port override on the host dispatcher.
|
|
121
123
|
|
|
122
124
|
For the operational rules that keep future changes (new WebSocket endpoints, transport refactors, terminal/voice porting) from breaking this, load the `relay-transport` skill.
|
|
@@ -313,6 +313,7 @@ export const createRelayService = ({
|
|
|
313
313
|
}
|
|
314
314
|
const identity = await identityRuntime.getRelayIdentity();
|
|
315
315
|
for (const relayUrl of pending) {
|
|
316
|
+
if (hostClients.has(relayUrl)) continue;
|
|
316
317
|
const hostClient = startRelayHost({
|
|
317
318
|
relayUrl,
|
|
318
319
|
identity,
|
|
@@ -123,6 +123,19 @@ describe('relay service pairing endpoint', () => {
|
|
|
123
123
|
delete process.env.OPENCHAMBER_RELAY_URL;
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it('starts only one host per endpoint during concurrent startup and stops every started host', async () => {
|
|
127
|
+
const { service, settings } = createTestRelayService({ hasRelayDemand: async () => true });
|
|
128
|
+
await settings.write({ privateRelay: { enabled: true, relayUrl: DEFAULT_RELAY_URL } });
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await Promise.all([service.reconcile(), service.reconcile()]);
|
|
132
|
+
expect(relayMocks.starts).toEqual([DEFAULT_RELAY_URL]);
|
|
133
|
+
} finally {
|
|
134
|
+
service.stop();
|
|
135
|
+
}
|
|
136
|
+
expect(relayMocks.stops).toEqual(relayMocks.starts);
|
|
137
|
+
});
|
|
138
|
+
|
|
126
139
|
it('appends a new pairing endpoint (multi-relay) while keeping the primary', async () => {
|
|
127
140
|
const { service, settings } = createTestRelayService();
|
|
128
141
|
|
|
@@ -8,6 +8,7 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
|
|
8
8
|
- Runtime orchestration and execution is owned by this module.
|
|
9
9
|
- Durable run history is owned by an independent SQLite store in this module.
|
|
10
10
|
- This module is OpenChamber feature logic; it is intentionally separate from OpenCode proxy/runtime internals.
|
|
11
|
+
- Prompt and goal execution preserve hash-prefixed text literally; no snippet discovery or expansion occurs before dispatch.
|
|
11
12
|
|
|
12
13
|
## Files
|
|
13
14
|
|
|
@@ -215,7 +216,13 @@ Every actual run (timer or manual):
|
|
|
215
216
|
`message.list({ limit: 50, order: 'desc' })` (newest first) — never
|
|
216
217
|
`order: 'asc'` page-0 + `at(-1)`, which reads an old page once the
|
|
217
218
|
session exceeds the limit.
|
|
218
|
-
|
|
219
|
+
- OpenCode 2.0.16 emits a native `idle` tail whose `outcome` is authoritative:
|
|
220
|
+
`succeeded` → success; `failed` / `interrupted` → error. An unknown idle
|
|
221
|
+
outcome keeps polling. Read it only after `session.active` confirms the
|
|
222
|
+
session is inactive; an older terminal record cannot settle live work.
|
|
223
|
+
This also applies to post-run continuation snapshots. Earlier servers
|
|
224
|
+
without idle records use the assistant-tail rules below.
|
|
225
|
+
- Assistant-tail terminal success requires `time.completed`
|
|
219
226
|
and finish not `tool-calls` (prefer `finish: 'stop'`). `finish:
|
|
220
227
|
'tool-calls'`, incomplete assistants (no `time.completed`), and
|
|
221
228
|
non-assistant tails such as `model-switched` never succeed — including
|
|
@@ -2,7 +2,6 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
3
|
import parser from 'cron-parser';
|
|
4
4
|
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
5
|
-
import { expandSnippets } from '../opencode/snippets.js';
|
|
6
5
|
import {
|
|
7
6
|
getSessionGoalCapability,
|
|
8
7
|
isSessionGoalSupported,
|
|
@@ -304,7 +303,7 @@ const isUserMessage = (info) => (
|
|
|
304
303
|
* - finish tool-calls is mid-loop, never success
|
|
305
304
|
* - incomplete (no time.completed) is never success
|
|
306
305
|
* - model-switched / non-assistant tails are not success
|
|
307
|
-
* -
|
|
306
|
+
* - native idle outcomes settle the execution; older servers use assistant finish
|
|
308
307
|
*/
|
|
309
308
|
const classifyMessageTail = (messages) => {
|
|
310
309
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
@@ -317,6 +316,15 @@ const classifyMessageTail = (messages) => {
|
|
|
317
316
|
if (!latest) {
|
|
318
317
|
return { outcome: 'unknown' };
|
|
319
318
|
}
|
|
319
|
+
if (latest.type === 'idle') {
|
|
320
|
+
if (latest.outcome === 'succeeded') {
|
|
321
|
+
return { outcome: 'success' };
|
|
322
|
+
}
|
|
323
|
+
if (latest.outcome === 'failed' || latest.outcome === 'interrupted') {
|
|
324
|
+
return { outcome: 'error', error: `session execution ${latest.outcome}` };
|
|
325
|
+
}
|
|
326
|
+
return { outcome: 'unknown' };
|
|
327
|
+
}
|
|
320
328
|
if (isAssistantMessage(latest)) {
|
|
321
329
|
if (latest.error) {
|
|
322
330
|
return {
|
|
@@ -849,7 +857,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
849
857
|
};
|
|
850
858
|
|
|
851
859
|
const buildPromptText = (task, projectPath) => {
|
|
852
|
-
const promptText =
|
|
860
|
+
const promptText = task.execution.prompt;
|
|
853
861
|
if (!task.execution.goalEnabled) {
|
|
854
862
|
return promptText;
|
|
855
863
|
}
|
|
@@ -874,7 +882,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
874
882
|
// back to an inline objective so parseGoalMetadata still accepts the
|
|
875
883
|
// record. Oversized prompts are distilled into audit criteria by the
|
|
876
884
|
// small model; on distill failure a head+tail excerpt keeps intent.
|
|
877
|
-
let objectiveText =
|
|
885
|
+
let objectiveText = task.execution.prompt;
|
|
878
886
|
if (objectiveText.length > 5000) {
|
|
879
887
|
let distilled = null;
|
|
880
888
|
try {
|
|
@@ -1108,6 +1108,78 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1108
1108
|
expect(client.prompt).not.toHaveBeenCalled();
|
|
1109
1109
|
});
|
|
1110
1110
|
|
|
1111
|
+
it.each([
|
|
1112
|
+
['succeeded', 'success', null],
|
|
1113
|
+
['failed', 'error', 'session execution failed'],
|
|
1114
|
+
['interrupted', 'error', 'session execution interrupted'],
|
|
1115
|
+
])('settles native idle outcome %s instead of timing out', async (outcome, status, error) => {
|
|
1116
|
+
vi.useFakeTimers();
|
|
1117
|
+
try {
|
|
1118
|
+
const history = createHistoryStore();
|
|
1119
|
+
const client = createSuccessfulClient({
|
|
1120
|
+
messageListImpl: async () => ({
|
|
1121
|
+
data: [
|
|
1122
|
+
{ id: 'msg_idle', type: 'idle', outcome, time: { created: 1_200 } },
|
|
1123
|
+
outcome === 'succeeded' ? erroredAssistant() : completedAssistant(),
|
|
1124
|
+
],
|
|
1125
|
+
}),
|
|
1126
|
+
});
|
|
1127
|
+
const updateState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1128
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1129
|
+
}));
|
|
1130
|
+
const runtime = createRuntime(updateState, {
|
|
1131
|
+
runHistoryStore: history,
|
|
1132
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1133
|
+
maxRunDurationMs: 2_500,
|
|
1134
|
+
});
|
|
1135
|
+
await runtime.syncProject('project-1');
|
|
1136
|
+
const pending = runtime.runNow('project-1', 'task-1');
|
|
1137
|
+
await vi.advanceTimersByTimeAsync(2_500);
|
|
1138
|
+
const result = await pending;
|
|
1139
|
+
expect(result.status).toBe(status);
|
|
1140
|
+
expect(result.error ?? null).toBe(error);
|
|
1141
|
+
expect(history.finishRun).toHaveBeenCalledWith(
|
|
1142
|
+
expect.any(String), expect.objectContaining({ status }),
|
|
1143
|
+
);
|
|
1144
|
+
expect(client.interrupt).not.toHaveBeenCalled();
|
|
1145
|
+
expect(client.messageList).toHaveBeenCalledTimes(1);
|
|
1146
|
+
expect(history.finishRun.mock.calls[0][1].durationMs).toBeLessThan(2_500);
|
|
1147
|
+
runtime.stop();
|
|
1148
|
+
} finally {
|
|
1149
|
+
vi.useRealTimers();
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it('does not settle an older idle record while the session is active', async () => {
|
|
1154
|
+
vi.useFakeTimers();
|
|
1155
|
+
try {
|
|
1156
|
+
const history = createHistoryStore();
|
|
1157
|
+
let busy = true;
|
|
1158
|
+
const client = createSuccessfulClient({
|
|
1159
|
+
activeImpl: async () => busy ? { ses_1: { type: 'running' } } : {},
|
|
1160
|
+
messageListImpl: async () => ({
|
|
1161
|
+
data: [{ id: 'msg_idle', type: 'idle', outcome: 'succeeded', time: { created: 1_200 } }],
|
|
1162
|
+
}),
|
|
1163
|
+
});
|
|
1164
|
+
const runtime = createRuntime(vi.fn(async () => ({ task: scheduledTask })), {
|
|
1165
|
+
runHistoryStore: history,
|
|
1166
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1167
|
+
maxRunDurationMs: 5_000,
|
|
1168
|
+
});
|
|
1169
|
+
await runtime.syncProject('project-1');
|
|
1170
|
+
const pending = runtime.runNow('project-1', 'task-1');
|
|
1171
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1172
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1173
|
+
expect(client.messageList).not.toHaveBeenCalled();
|
|
1174
|
+
busy = false;
|
|
1175
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1176
|
+
expect((await pending).status).toBe('success');
|
|
1177
|
+
runtime.stop();
|
|
1178
|
+
} finally {
|
|
1179
|
+
vi.useRealTimers();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1111
1183
|
it('ordinary non-goal runs still create a session and settle on the assistant tail', async () => {
|
|
1112
1184
|
const history = createHistoryStore();
|
|
1113
1185
|
const client = createSuccessfulClient({ sessionID: 'ses_plain' });
|
|
@@ -1182,16 +1254,17 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1182
1254
|
);
|
|
1183
1255
|
});
|
|
1184
1256
|
|
|
1185
|
-
it(
|
|
1257
|
+
it.each([
|
|
1258
|
+
{ id: 'msg_switch', type: 'model-switched', model: { providerID: 'openai', id: 'gpt-4.1' } },
|
|
1259
|
+
{ id: 'msg_idle', type: 'idle', outcome: 'unknown' },
|
|
1260
|
+
])('does not succeed on an unrecognized terminal: $type', async (tail) => {
|
|
1186
1261
|
vi.useFakeTimers();
|
|
1187
1262
|
const history = createHistoryStore();
|
|
1188
1263
|
createSuccessfulClient({
|
|
1189
1264
|
messageListImpl: async () => ({
|
|
1190
1265
|
data: [{
|
|
1191
|
-
|
|
1192
|
-
type: 'model-switched',
|
|
1266
|
+
...tail,
|
|
1193
1267
|
time: { created: Date.now() },
|
|
1194
|
-
model: { providerID: 'openai', id: 'gpt-4.1' },
|
|
1195
1268
|
}],
|
|
1196
1269
|
}),
|
|
1197
1270
|
});
|
|
@@ -1442,7 +1515,7 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1442
1515
|
vi.unstubAllGlobals();
|
|
1443
1516
|
});
|
|
1444
1517
|
|
|
1445
|
-
it('corrects lastStatus error → success
|
|
1518
|
+
it.each(['assistant', 'idle'])('corrects lastStatus error → success from %s after a continuation, without rewriting history', async (tailType) => {
|
|
1446
1519
|
const history = createHistoryStore();
|
|
1447
1520
|
const client = createSuccessfulClient({ settlement: 'assistant-error' });
|
|
1448
1521
|
const fetchMock = vi.fn(async (url) => {
|
|
@@ -1468,7 +1541,9 @@ describe('scheduled-tasks run history and session lifecycle', () => {
|
|
|
1468
1541
|
expect(history.finishRun).toHaveBeenCalledTimes(1);
|
|
1469
1542
|
|
|
1470
1543
|
client.messageList.mockImplementation(async () => ({
|
|
1471
|
-
data: [
|
|
1544
|
+
data: [tailType === 'idle'
|
|
1545
|
+
? { id: 'msg_idle', type: 'idle', outcome: 'succeeded', time: { created: 1_200 } }
|
|
1546
|
+
: completedAssistant('msg_ok')],
|
|
1472
1547
|
}));
|
|
1473
1548
|
|
|
1474
1549
|
await runtime.observeSessionEvent({
|
|
@@ -186,6 +186,17 @@ before touching the filesystem). Rationale: metadata rides every
|
|
|
186
186
|
|
|
187
187
|
## Flow
|
|
188
188
|
|
|
189
|
+
Recent-message reads request the newest 40 records with V2 `order: desc`, then
|
|
190
|
+
reverse that page into chronological order for tail checks, audit selection and
|
|
191
|
+
accounting. The SDK and HTTP fallback use the same ordering contract; `asc`
|
|
192
|
+
without a cursor would read the oldest page and strand long sessions after
|
|
193
|
+
compaction. Message IDs are not used to reorder the fetched page.
|
|
194
|
+
|
|
195
|
+
UI goal removal sends the Merge Patch tombstone `openchamber.goal: null`.
|
|
196
|
+
Omitting the key preserves the stored goal. Objective-file cleanup and stopping
|
|
197
|
+
the active turn follow a successful metadata write; failed writes retain the
|
|
198
|
+
goal and its objective for retry.
|
|
199
|
+
|
|
189
200
|
1. `createSessionGoalRuntime` subscribes to the global SSE hub (it needs
|
|
190
201
|
the envelope's `directory`).
|
|
191
202
|
2. `session.status: idle` arms a 15s per-session timer; `busy`/`retry` clears
|
|
@@ -722,20 +722,20 @@ export const createSessionGoalRuntime = ({
|
|
|
722
722
|
const client = openCodeClient();
|
|
723
723
|
if (typeof client?.message?.list === 'function') {
|
|
724
724
|
const listed = await client.message.list(
|
|
725
|
-
{ sessionID: sessionId, limit: MESSAGE_FETCH_LIMIT, order: '
|
|
725
|
+
{ sessionID: sessionId, limit: MESSAGE_FETCH_LIMIT, order: 'desc' },
|
|
726
726
|
directoryRequestOptions(directory),
|
|
727
727
|
);
|
|
728
728
|
const projected = projectGoalMessages(listed, sessionId);
|
|
729
|
-
if (projected) return projected;
|
|
729
|
+
if (projected) return projected.reverse();
|
|
730
730
|
}
|
|
731
731
|
} catch {
|
|
732
732
|
// Fall through to controlled HTTP seam for tests / older fixtures.
|
|
733
733
|
}
|
|
734
734
|
const raw = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/message`, {
|
|
735
735
|
directory,
|
|
736
|
-
query: { limit: String(MESSAGE_FETCH_LIMIT) },
|
|
736
|
+
query: { limit: String(MESSAGE_FETCH_LIMIT), order: 'desc' },
|
|
737
737
|
}).catch(() => null);
|
|
738
|
-
return projectGoalMessages(raw, sessionId);
|
|
738
|
+
return projectGoalMessages(raw, sessionId)?.reverse() ?? null;
|
|
739
739
|
};
|
|
740
740
|
|
|
741
741
|
/**
|
|
@@ -136,7 +136,7 @@ const makeFetch = ({ messages, liveStatus }) => {
|
|
|
136
136
|
}
|
|
137
137
|
if (pathEndsWith(reqPath, '/session/ses-goal/message')) {
|
|
138
138
|
// Official v2 SessionMessagesResponse shape.
|
|
139
|
-
return jsonResponse({ data: messages, cursor: {} });
|
|
139
|
+
return jsonResponse({ data: new URL(url).searchParams.get('order') === 'desc' ? [...messages].reverse() : messages, cursor: {} });
|
|
140
140
|
}
|
|
141
141
|
if (pathEndsWith(reqPath, '/session/ses-goal') && !reqPath.includes('/message')) {
|
|
142
142
|
if (init.method === 'PATCH') {
|
|
@@ -263,6 +263,33 @@ describe('session-goal v2 message projection helpers', () => {
|
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
describe('session-goal continuation delivery', () => {
|
|
266
|
+
it.each([false, true])('continues after a completed compaction beyond the first message page (HTTP fallback: %s)', async (fallback) => {
|
|
267
|
+
const messages = Array.from({ length: 42 }, (_, index) => ({
|
|
268
|
+
id: `msg_${String(index).padStart(3, '0')}`,
|
|
269
|
+
type: index === 41 ? 'compaction' : index % 2 ? 'user' : 'assistant',
|
|
270
|
+
time: index === 41 ? { created: index + 1 } : { created: index + 1, completed: index + 2 },
|
|
271
|
+
model: { providerID: 'p', id: 'm' },
|
|
272
|
+
agent: 'build',
|
|
273
|
+
...(index === 41 ? { status: 'completed', summary: 'Work remains' } : { text: 'Working' }),
|
|
274
|
+
}));
|
|
275
|
+
const fixture = makeFetch({ messages, liveStatus: 'idle' });
|
|
276
|
+
const runtime = makeRuntime();
|
|
277
|
+
try {
|
|
278
|
+
await withFetch(async (input, init) => {
|
|
279
|
+
const url = new URL(String(input));
|
|
280
|
+
if (pathEndsWith(url.pathname, '/session/ses-goal/message')) {
|
|
281
|
+
if (fallback && url.pathname.startsWith('/api/')) return jsonResponse({ error: 'unavailable' }, 503);
|
|
282
|
+
const ordered = url.searchParams.get('order') === 'desc' ? [...messages].reverse() : messages;
|
|
283
|
+
return jsonResponse({ data: ordered.slice(0, Number(url.searchParams.get('limit') || 40)), cursor: {} });
|
|
284
|
+
}
|
|
285
|
+
return fixture.fetch(input, init);
|
|
286
|
+
}, async () => runtime.runTick('ses-goal', '/repo'));
|
|
287
|
+
expect(fixture.calls.prompt).toBe(1);
|
|
288
|
+
} finally {
|
|
289
|
+
runtime.stop();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
266
293
|
it('dispatches v2 session.prompt with stable continuation id and metadata', async () => {
|
|
267
294
|
const promptBodies = [];
|
|
268
295
|
const messages = [
|
|
@@ -291,7 +318,7 @@ describe('session-goal continuation delivery', () => {
|
|
|
291
318
|
return jsonResponse({ data: {} });
|
|
292
319
|
}
|
|
293
320
|
if (pathEndsWith(reqPath, '/session/ses-goal/message')) {
|
|
294
|
-
return jsonResponse({ data: messages, cursor: {} });
|
|
321
|
+
return jsonResponse({ data: new URL(url).searchParams.get('order') === 'desc' ? [...messages].reverse() : messages, cursor: {} });
|
|
295
322
|
}
|
|
296
323
|
if (pathEndsWith(reqPath, '/session/ses-goal') && !reqPath.includes('/message')) {
|
|
297
324
|
if (init.method === 'PATCH' && init.body) {
|
|
@@ -984,7 +1011,9 @@ describe('session-goal runtime — pause / audit / dispatch boundary', () => {
|
|
|
984
1011
|
const body = Array.isArray(messages)
|
|
985
1012
|
? { data: messages, cursor: {} }
|
|
986
1013
|
: messages;
|
|
987
|
-
return jsonResponse(
|
|
1014
|
+
return jsonResponse(new URL(String(input)).searchParams.get('order') === 'desc'
|
|
1015
|
+
? { ...body, data: [...body.data].reverse() }
|
|
1016
|
+
: body);
|
|
988
1017
|
}
|
|
989
1018
|
if (pathEndsWith(reqPath, '/session/ses-goal')) {
|
|
990
1019
|
if (init.method === 'PATCH') {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Session Index Runtime
|
|
2
2
|
|
|
3
|
+
Native `session.renamed` updates the existing runtime-scoped summary's title and
|
|
4
|
+
update clock directly, publishing through the normal index revision path. It
|
|
5
|
+
preserves activity ordering, pins and execution status; older/duplicate events
|
|
6
|
+
and unknown sessions are no-ops. Partial rename payloads never fabricate rows.
|
|
7
|
+
|
|
8
|
+
Native `session.moved` uses `data.location.directory`, not the old event-envelope directory. The transactional `move` operation retains summary activity/status and independent pin membership, maintains the newest-20 destination bound, and installs a runtime-scoped event-clock fence against delayed source-directory upserts/pages. Duplicate/older moves are no-ops. Unknown sessions are not fabricated from the partial move payload.
|
|
9
|
+
|
|
3
10
|
Every OpenChamber Web Server enables `session-index.sqlite` in its data
|
|
4
11
|
directory by default. Electron injects its user-data path explicitly.
|
|
5
12
|
`sessionIndexDbPath` and `OPENCHAMBER_SESSION_INDEX_DB_PATH` override that
|
|
@@ -36,6 +36,19 @@ export const applySessionIndexEvent = (service, event, observedAt = Date.now(),
|
|
|
36
36
|
|
|
37
37
|
const baseType = normalizeSessionEventType(payload.type);
|
|
38
38
|
|
|
39
|
+
if (baseType === 'session.renamed') {
|
|
40
|
+
if (isHostReady && !isHostReady()) return false;
|
|
41
|
+
const data = payload.data ?? properties;
|
|
42
|
+
return service.rename(data.sessionID, data.title, payload.created ?? properties.eventCreated ?? observedAt);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (baseType === 'session.moved') {
|
|
46
|
+
const data = payload.data ?? properties;
|
|
47
|
+
return typeof data.sessionID === 'string' && typeof data.location?.directory === 'string'
|
|
48
|
+
? service.move(data.sessionID, data.location.directory, typeof payload.created === 'number' ? payload.created : observedAt, payload.durable?.seq)
|
|
49
|
+
: false;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
if (baseType === 'session.created' || baseType === 'session.updated') {
|
|
40
53
|
// Until Host metadata is ready, do not write session lifecycle into the
|
|
41
54
|
// index (would bake unprojected upstream archive). Transcript paths use
|
|
@@ -3,6 +3,13 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { applySessionIndexEvent } from './event-ingest.js';
|
|
4
4
|
|
|
5
5
|
describe('session index event ingest', () => {
|
|
6
|
+
it('takes moved ownership from native data rather than the old event envelope directory', () => {
|
|
7
|
+
const service = { move: vi.fn(() => true) };
|
|
8
|
+
expect(applySessionIndexEvent(service, { directory: '/old', payload: {
|
|
9
|
+
type: 'session.moved', created: 20, data: { sessionID: 'ses_move', location: { directory: '/new' }, projectID: 'project' },
|
|
10
|
+
} }, 30)).toBe(true);
|
|
11
|
+
expect(service.move).toHaveBeenCalledWith('ses_move', '/new', 20, undefined);
|
|
12
|
+
});
|
|
6
13
|
it('writes session summaries, user activity, and status to the service', () => {
|
|
7
14
|
const service = {
|
|
8
15
|
upsertAndReportChange: vi.fn(() => true),
|
|
@@ -277,6 +277,30 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
|
|
|
277
277
|
WHERE runtime_key = ? AND session_id = ?
|
|
278
278
|
`);
|
|
279
279
|
|
|
280
|
+
const movedLocations = new Map();
|
|
281
|
+
const isStaleLocation = (session, directory) => {
|
|
282
|
+
const moved = movedLocations.get(`${runtimeKey()}:${session?.id}`);
|
|
283
|
+
return moved && moved.directory !== normalizeDirectory(directory) && toTimestamp(session?.time?.updated) <= moved.at;
|
|
284
|
+
};
|
|
285
|
+
const move = db.transaction((sessionID, directory, at = Date.now(), sequence) => {
|
|
286
|
+
directory = normalizeDirectory(directory);
|
|
287
|
+
if (!sessionID || !directory) return false;
|
|
288
|
+
const key = runtimeKey();
|
|
289
|
+
const fenceKey = `${key}:${sessionID}`;
|
|
290
|
+
const previous = movedLocations.get(fenceKey);
|
|
291
|
+
if (previous && (sequence !== undefined && previous.sequence !== undefined
|
|
292
|
+
? sequence <= previous.sequence
|
|
293
|
+
: at < previous.at || (at === previous.at && previous.directory === directory))) return false;
|
|
294
|
+
movedLocations.set(fenceKey, { directory, at, sequence });
|
|
295
|
+
touchExistingDirectory.run({ runtimeKey: key, directory, lastAccessedAt: at });
|
|
296
|
+
const roots = db.prepare('UPDATE OR REPLACE session_summary SET directory = ? WHERE runtime_key = ? AND session_id = ? AND directory != ?').run(directory, key, sessionID, directory);
|
|
297
|
+
const children = db.prepare('UPDATE OR REPLACE session_child SET directory = ? WHERE runtime_key = ? AND session_id = ? AND directory != ?').run(directory, key, sessionID, directory);
|
|
298
|
+
db.prepare(`DELETE FROM session_summary WHERE runtime_key = ? AND directory = ? AND session_id IN (
|
|
299
|
+
SELECT session_id FROM session_summary WHERE runtime_key = ? AND directory = ? ORDER BY activity_updated_at DESC, session_id DESC LIMIT -1 OFFSET ?
|
|
300
|
+
)`).run(key, directory, key, directory, MAX_ROOT_SESSIONS);
|
|
301
|
+
return roots.changes > 0 || children.changes > 0;
|
|
302
|
+
});
|
|
303
|
+
|
|
280
304
|
const replaceDirectoryRows = ({ directory, sessions, cursor, hasMore, fullSync = true, now = Date.now() }) => {
|
|
281
305
|
const normalizedDirectory = normalizeDirectory(directory);
|
|
282
306
|
if (!normalizedDirectory) throw new Error('directory is required');
|
|
@@ -289,6 +313,7 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
|
|
|
289
313
|
// stale unarchived cache cannot resurrect them.
|
|
290
314
|
const summaries = Array.isArray(sessions)
|
|
291
315
|
? sessions
|
|
316
|
+
.filter((session) => !isStaleLocation(session, normalizedDirectory))
|
|
292
317
|
.map((session) => toSummary(session, normalizedDirectory))
|
|
293
318
|
.filter((summary) => summary && !summary.archivedAt)
|
|
294
319
|
.slice(0, MAX_ROOT_SESSIONS)
|
|
@@ -328,6 +353,7 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
|
|
|
328
353
|
});
|
|
329
354
|
|
|
330
355
|
const upsertMutation = db.transaction((session, now = Date.now(), options = {}) => {
|
|
356
|
+
if (isStaleLocation(session, session?.directory ?? session?.location?.directory)) return { accepted: false, changed: false };
|
|
331
357
|
if (!isVisibleSession(session)) {
|
|
332
358
|
const changed = remove(session?.id);
|
|
333
359
|
return { accepted: changed, changed };
|
|
@@ -517,6 +543,23 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
|
|
|
517
543
|
return result.changes > 0;
|
|
518
544
|
};
|
|
519
545
|
|
|
546
|
+
const rename = (sessionID, title, observedAt) => {
|
|
547
|
+
if (typeof sessionID !== 'string' || !sessionID || typeof title !== 'string') return false;
|
|
548
|
+
const timestamp = toTimestamp(observedAt);
|
|
549
|
+
if (!timestamp) return false;
|
|
550
|
+
const key = runtimeKey();
|
|
551
|
+
if (HIDDEN_SESSION_TITLES.has(title)) {
|
|
552
|
+
const current = db.prepare('SELECT updated_at FROM session_summary WHERE runtime_key = ? AND session_id = ?').get(key, sessionID);
|
|
553
|
+
return current && current.updated_at <= timestamp ? remove(sessionID) : false;
|
|
554
|
+
}
|
|
555
|
+
const result = db.prepare(`
|
|
556
|
+
UPDATE session_summary SET title = ?, updated_at = ?
|
|
557
|
+
WHERE runtime_key = ? AND session_id = ? AND updated_at <= ?
|
|
558
|
+
AND (title != ? OR updated_at != ?)
|
|
559
|
+
`).run(title, timestamp, key, sessionID, timestamp, title, timestamp);
|
|
560
|
+
return result.changes > 0;
|
|
561
|
+
};
|
|
562
|
+
|
|
520
563
|
const updateStatus = (sessionID, status, observedAt) => {
|
|
521
564
|
if (typeof sessionID !== 'string' || !sessionID) return false;
|
|
522
565
|
if (status !== 'busy' && status !== 'retry' && status !== 'idle') return false;
|
|
@@ -570,6 +613,8 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
|
|
|
570
613
|
upsertAndReportChange,
|
|
571
614
|
touchActivity,
|
|
572
615
|
updateStatus,
|
|
616
|
+
rename,
|
|
617
|
+
move,
|
|
573
618
|
setPinned,
|
|
574
619
|
clearPinned,
|
|
575
620
|
remove,
|