@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.10
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-CzTtVMnI.js} +1 -1
- package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-hOi72Wdx.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
- package/dist/assets/Icon-B8fSTLdP.js +1 -0
- package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
- package/dist/assets/MobileDetailNavigation-iX_hb_Xt.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-CAMUupwR.js} +2 -2
- package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-O4cGdz5p.js} +1 -1
- package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
- package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-B8d8qIQ7.js} +1 -1
- package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
- package/dist/assets/appThemeRegistry-BGGHio8T.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-CI7Q-Zw_.js → button-DIIrsMjW.js} +1 -1
- package/dist/assets/{collapsible-D5Phaykh.js → collapsible-CnvUeYst.js} +1 -1
- package/dist/assets/{context-menu-CubSDW2g.js → context-menu-BGBUpZEQ.js} +1 -1
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-rvn7_qBM.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-CQ63sc9E.js} +1 -1
- package/dist/assets/es-Dsmc0A_3.js +15 -0
- package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
- package/dist/assets/fr-C_ZlZDI2.js +13 -0
- package/dist/assets/gitApi-BF8vuoZc.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-BVu-iykl.js} +1 -1
- package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
- package/dist/assets/ja-BqqTaWTi.js +15 -0
- package/dist/assets/ko-DP2XPM37.js +15 -0
- package/dist/assets/main-B7t9lavz.js +2 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/main-DWP-prNB.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-D9-B1wdY.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
- package/dist/assets/miniChat-DgOrEQbN.js +2 -0
- package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
- package/dist/assets/multirun-DelyUO6k.js +4 -0
- package/dist/assets/persistence-D0y7226-.js +3 -0
- package/dist/assets/pl-BCf5uLlK.js +15 -0
- package/dist/assets/platform-CurqO28D.js +6 -0
- package/dist/assets/{projectMeta-S1hJ7oBp.js → projectMeta-CPElzJ0n.js} +2 -2
- package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-pOE93w86.js +2 -0
- package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-DSS0KxG3.js} +1 -1
- package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
- package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-Bss8M20i.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
- package/dist/assets/src-lS1keDVV.css +2 -0
- package/dist/assets/store-jaP79mqI.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-mDR2jXTo.js} +1 -1
- package/dist/assets/textarea-XD2oX_Ee.js +1 -0
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-DYeQ_Hvy.js} +1 -1
- package/dist/assets/uk-BgFzz8m2.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
- package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-Bjx8zkGM.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
- package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
- package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
- package/dist/index.html +12 -12
- package/dist/mini-chat.html +12 -12
- package/dist/mobile.html +12 -12
- package/package.json +3 -2
- package/server/index.js +423 -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/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/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/DOCUMENTATION.md +3 -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/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +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/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +133 -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 +111 -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 +117 -103
- package/server/lib/opencode/config-entity-routes.test.js +148 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -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 +90 -5
- package/server/lib/opencode/feature-routes-runtime.test.js +25 -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 +373 -28
- package/server/lib/opencode/proxy.test.js +353 -0
- package/server/lib/opencode/routes.js +333 -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/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +299 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -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 +96 -0
- 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 +197 -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 +38 -0
- package/server/lib/session-metadata/session-metadata-store.js +422 -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 +279 -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 +69 -44
- 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/Icon-BgdfPhNs.js +0 -1
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-D2dsW5Lg.js +0 -3
- 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/TerminalView-CDdzF4yi.js +0 -8
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fileMentionTouchSelection-DNeiqns-.js +0 -4
- 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/main-qSeeJRW1.css +0 -1
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
- 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/persistence-Ce3AnpEr.js +0 -3
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- 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/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-ChOxt-J7.js +0 -147
- package/dist/assets/sessionChangeSummary-NnxcqCTI.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/textarea-bHYxZtfH.js +0 -1
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createSessionTitleRuntime } from './runtime.js';
|
|
3
|
+
import { mergeMetadataPatch } from '../session-metadata/session-metadata-store.js';
|
|
4
|
+
import { createTitleSessionAccess } from './session-access.js';
|
|
5
|
+
import { configureServerOpenCodeFetchGate } from '../opencode/server-opencode-fetch.js';
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.useRealTimers();
|
|
9
|
+
vi.unstubAllGlobals();
|
|
10
|
+
configureServerOpenCodeFetchGate(null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it.each([false, true])('runs a Host smart-title request and clears loading (model failure: %s)', async (fail) => {
|
|
14
|
+
vi.useFakeTimers();
|
|
15
|
+
const metadata = { openchamber: { titleRefresh: { lastAutoTitle: 'Original', requestedAt: 1000 } } };
|
|
16
|
+
const session = { id: 'ses_title', title: 'Original', directory: '/repo', metadata };
|
|
17
|
+
const updates = [];
|
|
18
|
+
const sessionAccess = {
|
|
19
|
+
get: vi.fn(async () => structuredClone(session)),
|
|
20
|
+
update: vi.fn(async (_id, _directory, patch) => {
|
|
21
|
+
if (patch.title) session.title = patch.title;
|
|
22
|
+
if (patch.metadata) session.metadata = mergeMetadataPatch(session.metadata, patch.metadata);
|
|
23
|
+
updates.push(structuredClone(session));
|
|
24
|
+
}),
|
|
25
|
+
messages: vi.fn(async () => [
|
|
26
|
+
{ info: { id: 'u1', role: 'user' }, parts: [{ type: 'text', text: 'Implement a title refresh' }] },
|
|
27
|
+
{ info: { id: 'a1', role: 'assistant' }, parts: [{ type: 'text', text: 'Done' }] },
|
|
28
|
+
]),
|
|
29
|
+
};
|
|
30
|
+
const generate = vi.fn(async () => {
|
|
31
|
+
expect(session.metadata.openchamber.titleRefresh.isGenerating).toBe(true);
|
|
32
|
+
if (fail) throw new Error('title model unavailable');
|
|
33
|
+
return { text: 'Title refresh', providerID: 'test', modelID: 'test' };
|
|
34
|
+
});
|
|
35
|
+
const runtime = createSessionTitleRuntime({
|
|
36
|
+
sessionAccess,
|
|
37
|
+
buildOpenCodeUrl: (path) => `http://opencode${path}`,
|
|
38
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
39
|
+
getSmallModelService: async () => ({ generateSmallModelText: generate }),
|
|
40
|
+
now: () => 1000,
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
runtime.processPayload({ type: 'openchamber:session-metadata', properties: {
|
|
44
|
+
sessionID: session.id, directory: '/repo', metadata,
|
|
45
|
+
} });
|
|
46
|
+
await vi.runAllTimersAsync();
|
|
47
|
+
expect(generate).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(session.title).toBe(fail ? 'Original' : 'Title refresh');
|
|
49
|
+
if (fail) expect(session.metadata.openchamber.titleRefresh.lastError).toBe('title model unavailable');
|
|
50
|
+
expect(session.metadata.openchamber.titleRefresh.isGenerating).not.toBe(true);
|
|
51
|
+
expect(session.metadata.openchamber.titleRefresh.requestedAt).toBeUndefined();
|
|
52
|
+
expect(updates.some((row) => row.metadata.openchamber.titleRefresh.isGenerating)).toBe(true);
|
|
53
|
+
} finally {
|
|
54
|
+
runtime.stop();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses the real v2 client and publishes the generated title without another session click', async () => {
|
|
59
|
+
vi.useFakeTimers();
|
|
60
|
+
configureServerOpenCodeFetchGate(null, { allowMissingContract: true });
|
|
61
|
+
let metadata = { openchamber: { titleRefresh: { lastAutoTitle: 'Original', requestedAt: 1000 } } };
|
|
62
|
+
const session = { id: 'ses_title', title: 'Original', location: { directory: '/repo' }, time: { created: 1, updated: 2 } };
|
|
63
|
+
const requests = [];
|
|
64
|
+
vi.stubGlobal('fetch', vi.fn(async (input, init) => {
|
|
65
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
66
|
+
const url = new URL(request.url);
|
|
67
|
+
requests.push(`${request.method} ${url.pathname}`);
|
|
68
|
+
if (url.pathname.endsWith('/message')) {
|
|
69
|
+
expect(url.searchParams.get('order')).toBe('desc');
|
|
70
|
+
return Response.json({ data: [
|
|
71
|
+
{ id: 'a1', type: 'assistant', content: [{ type: 'text', text: 'Done' }] },
|
|
72
|
+
{ id: 'u1', type: 'user', text: 'Implement title refresh' },
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
if (request.method === 'PATCH') {
|
|
76
|
+
const body = await request.json();
|
|
77
|
+
expect(Object.keys(body)).toEqual(['title']);
|
|
78
|
+
session.title = body.title;
|
|
79
|
+
return new Response(null, { status: 204 });
|
|
80
|
+
}
|
|
81
|
+
return Response.json({ data: session });
|
|
82
|
+
}));
|
|
83
|
+
const events = [];
|
|
84
|
+
const access = createTitleSessionAccess({
|
|
85
|
+
buildOpenCodeUrl: (path) => `http://opencode${path}`,
|
|
86
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
87
|
+
readSessionMetadata: async () => metadata,
|
|
88
|
+
persistSessionMetadata: async (_id, patch) => {
|
|
89
|
+
metadata = mergeMetadataPatch(metadata, patch);
|
|
90
|
+
events.push({ type: 'metadata', metadata: structuredClone(metadata) });
|
|
91
|
+
},
|
|
92
|
+
publishSession: (row) => events.push({ type: 'session', row }),
|
|
93
|
+
});
|
|
94
|
+
const runtime = createSessionTitleRuntime({
|
|
95
|
+
sessionAccess: access,
|
|
96
|
+
getSmallModelService: async () => ({ generateSmallModelText: async () => ({ text: 'Title refresh', providerID: 'test', modelID: 'test' }) }),
|
|
97
|
+
now: () => 1000,
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
runtime.processPayload({ type: 'openchamber:session-metadata', properties: {
|
|
101
|
+
sessionID: session.id, metadata, directory: '/repo',
|
|
102
|
+
} });
|
|
103
|
+
await vi.runAllTimersAsync();
|
|
104
|
+
expect(events.some((event) => event.metadata?.openchamber?.titleRefresh?.isGenerating === true)).toBe(true);
|
|
105
|
+
expect(events.at(-1)).toMatchObject({ type: 'session', row: { title: 'Title refresh', directory: '/repo' } });
|
|
106
|
+
expect(metadata.openchamber.titleRefresh.isGenerating).toBeUndefined();
|
|
107
|
+
expect(requests).toContain('PATCH /api/session/ses_title');
|
|
108
|
+
} finally {
|
|
109
|
+
runtime.stop();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('starts the first title from admitted user text while the assistant is still running', async () => {
|
|
114
|
+
vi.useFakeTimers();
|
|
115
|
+
const session = {
|
|
116
|
+
id: 'ses_first', title: '', time: { created: 1, updated: 1 },
|
|
117
|
+
metadata: {},
|
|
118
|
+
};
|
|
119
|
+
let finishTitle;
|
|
120
|
+
const generation = new Promise((resolve) => { finishTitle = resolve; });
|
|
121
|
+
const generate = vi.fn(() => generation);
|
|
122
|
+
const access = {
|
|
123
|
+
get: vi.fn(async () => structuredClone(session)),
|
|
124
|
+
update: vi.fn(async (_id, _directory, patch) => {
|
|
125
|
+
if (patch.metadata) session.metadata = mergeMetadataPatch(session.metadata, patch.metadata);
|
|
126
|
+
if (patch.title) session.title = patch.title;
|
|
127
|
+
}),
|
|
128
|
+
messages: vi.fn(async () => []),
|
|
129
|
+
};
|
|
130
|
+
const runtime = createSessionTitleRuntime({
|
|
131
|
+
sessionAccess: access,
|
|
132
|
+
getSmallModelService: async () => ({ generateSmallModelText: generate }),
|
|
133
|
+
isTitleRefreshEnabled: () => true,
|
|
134
|
+
now: () => 1000,
|
|
135
|
+
});
|
|
136
|
+
const emit = (type, data) => runtime.processPayload({ type, data, created: 1000, location: { directory: '/repo' } });
|
|
137
|
+
try {
|
|
138
|
+
emit('session.created', { sessionID: session.id, location: { directory: '/repo' } });
|
|
139
|
+
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_synthetic', item: {
|
|
140
|
+
type: 'synthetic', delivery: 'steer', payload: { text: 'Internal instructions' },
|
|
141
|
+
} });
|
|
142
|
+
await vi.runAllTimersAsync();
|
|
143
|
+
expect(generate).not.toHaveBeenCalled();
|
|
144
|
+
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_first', item: {
|
|
145
|
+
type: 'user', delivery: 'steer', payload: { text: 'Implement a session title generator' },
|
|
146
|
+
} });
|
|
147
|
+
emit('session.execution.started', { sessionID: session.id });
|
|
148
|
+
emit('session.status', { sessionID: session.id, status: { type: 'busy' } });
|
|
149
|
+
await vi.runAllTimersAsync();
|
|
150
|
+
expect(generate).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(generate.mock.calls[0][0].prompt).toContain('Implement a session title generator');
|
|
152
|
+
expect(session.metadata.openchamber?.titleRefresh?.isGenerating).toBe(true);
|
|
153
|
+
expect(access.messages).not.toHaveBeenCalled();
|
|
154
|
+
finishTitle({ text: 'Session titles', providerID: 'test', modelID: 'test' });
|
|
155
|
+
await vi.runAllTimersAsync();
|
|
156
|
+
expect(session.title).toBe('Session titles');
|
|
157
|
+
expect(session.metadata.openchamber.titleRefresh.isGenerating).toBeUndefined();
|
|
158
|
+
emit('session.execution.succeeded', { sessionID: session.id });
|
|
159
|
+
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_second', item: {
|
|
160
|
+
type: 'user', delivery: 'steer', payload: { text: 'Follow up on the implementation' },
|
|
161
|
+
} });
|
|
162
|
+
await vi.runAllTimersAsync();
|
|
163
|
+
expect(generate).toHaveBeenCalledTimes(1);
|
|
164
|
+
} finally {
|
|
165
|
+
runtime.stop();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -122,8 +122,8 @@ const buildTitleSystemPrompt = () => [
|
|
|
122
122
|
'Name the main subject of the work.',
|
|
123
123
|
'Keep that subject across follow-ups, polish, commit, push, tidy, and review.',
|
|
124
124
|
'Switch only when the user clearly starts a different topic.',
|
|
125
|
-
'One line,
|
|
126
|
-
'
|
|
125
|
+
'One line, no explanation.',
|
|
126
|
+
'Hard limit: never exceed 6 CJK characters (or 6 words for English). No modifiers. No verbs.',
|
|
127
127
|
'Use the language of the user messages.',
|
|
128
128
|
'Keep technical terms, numbers, filenames, and HTTP codes exact.',
|
|
129
129
|
'Never include tool names, summarizing, or generating.',
|
|
@@ -131,14 +131,18 @@ const buildTitleSystemPrompt = () => [
|
|
|
131
131
|
].join('\n');
|
|
132
132
|
|
|
133
133
|
const extractSessionStatus = (payload) => {
|
|
134
|
-
if (!payload
|
|
135
|
-
const
|
|
134
|
+
if (!payload) return null;
|
|
135
|
+
const executionType = payload.type === 'session.execution.started' ? 'busy'
|
|
136
|
+
: payload.type === 'session.execution.succeeded' || payload.type === 'session.execution.failed' ? 'idle'
|
|
137
|
+
: null;
|
|
138
|
+
if (payload.type !== 'session.status' && !executionType) return null;
|
|
139
|
+
const properties = payload.properties ?? payload.data ?? {};
|
|
136
140
|
const status = properties.status && typeof properties.status === 'object' ? properties.status : {};
|
|
137
141
|
const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
|
|
138
142
|
const sessionId = typeof properties.sessionID === 'string' ? properties.sessionID.trim() : '';
|
|
139
|
-
const type = typeof status.type === 'string'
|
|
143
|
+
const type = executionType ?? (typeof status.type === 'string'
|
|
140
144
|
? status.type.trim()
|
|
141
|
-
: (typeof info.type === 'string' ? info.type.trim() : '');
|
|
145
|
+
: (typeof info.type === 'string' ? info.type.trim() : ''));
|
|
142
146
|
if (!sessionId || !type) return null;
|
|
143
147
|
const directory = typeof properties.directory === 'string' && properties.directory
|
|
144
148
|
? properties.directory
|
|
@@ -159,9 +163,11 @@ export const isSystemOwnedSession = (sessionOrInfo) => {
|
|
|
159
163
|
};
|
|
160
164
|
|
|
161
165
|
const extractTitleRefreshRequest = (payload) => {
|
|
162
|
-
if (!payload || payload.type !== 'session.updated') return null;
|
|
166
|
+
if (!payload || (payload.type !== 'session.updated' && payload.type !== 'openchamber:session-metadata')) return null;
|
|
163
167
|
const properties = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
|
|
164
|
-
const info =
|
|
168
|
+
const info = payload.type === 'openchamber:session-metadata'
|
|
169
|
+
? { id: properties.sessionID, metadata: properties.metadata }
|
|
170
|
+
: (properties.info && typeof properties.info === 'object' ? properties.info : {});
|
|
165
171
|
if (isSystemOwnedSession(info)) return null;
|
|
166
172
|
const sessionId = typeof info.id === 'string' ? info.id.trim() : '';
|
|
167
173
|
const titleRefresh = info.metadata?.openchamber?.titleRefresh;
|
|
@@ -176,20 +182,35 @@ const extractTitleRefreshRequest = (payload) => {
|
|
|
176
182
|
|
|
177
183
|
const extractCreatedSession = (payload) => {
|
|
178
184
|
if (!payload || payload.type !== 'session.created') return null;
|
|
179
|
-
const properties = payload.properties
|
|
180
|
-
const info = properties.info
|
|
185
|
+
const properties = payload.properties ?? payload.data ?? {};
|
|
186
|
+
const info = properties.info ?? { ...properties, id: properties.sessionID };
|
|
181
187
|
const sessionId = typeof info.id === 'string' ? info.id.trim() : '';
|
|
182
188
|
if (!sessionId || (typeof info.parentID === 'string' && info.parentID)) return null;
|
|
183
189
|
if (isSystemOwnedSession(info)) return null;
|
|
184
190
|
const directory = typeof properties.directory === 'string' && properties.directory
|
|
185
191
|
? properties.directory
|
|
186
|
-
: (typeof info.directory === 'string' ? info.directory : '');
|
|
187
|
-
const createdAt = typeof info.time?.created === 'number' ? info.time.created : Date.now();
|
|
192
|
+
: (typeof info.location?.directory === 'string' ? info.location.directory : (typeof info.directory === 'string' ? info.directory : ''));
|
|
193
|
+
const createdAt = typeof info.time?.created === 'number' ? info.time.created : (payload.created ?? Date.now());
|
|
188
194
|
const title = typeof info.title === 'string' ? info.title : '';
|
|
189
195
|
return { sessionId, directory, createdAt, title };
|
|
190
196
|
};
|
|
191
197
|
|
|
192
198
|
const extractUserMessage = (payload) => {
|
|
199
|
+
if (payload?.type === 'session.inbox.enqueued') {
|
|
200
|
+
const properties = payload.properties ?? payload.data ?? {};
|
|
201
|
+
const item = properties.item;
|
|
202
|
+
if (typeof properties.sessionID !== 'string' || item?.type !== 'user') return null;
|
|
203
|
+
const text = typeof item.payload?.text === 'string' ? item.payload.text.slice(0, TRANSCRIPT_USER_CHAR_LIMIT) : '';
|
|
204
|
+
return {
|
|
205
|
+
sessionId: properties.sessionID,
|
|
206
|
+
createdAt: payload.created ?? 0,
|
|
207
|
+
directory: payload.location?.directory ?? '',
|
|
208
|
+
initialRecord: text.trim() ? {
|
|
209
|
+
info: { id: properties.inboxID, role: 'user' },
|
|
210
|
+
parts: [{ type: 'text', text }],
|
|
211
|
+
} : null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
193
214
|
if (!payload || payload.type !== 'message.updated') return null;
|
|
194
215
|
const info = payload.properties?.info;
|
|
195
216
|
if (!info || typeof info !== 'object' || info.role !== 'user') return null;
|
|
@@ -351,6 +372,9 @@ export const buildLatestTitleTranscript = (
|
|
|
351
372
|
export const createSessionTitleRuntime = ({
|
|
352
373
|
buildOpenCodeUrl,
|
|
353
374
|
getOpenCodeAuthHeaders,
|
|
375
|
+
/** Ticket 11: Host write admission shared with proxy (optional). */
|
|
376
|
+
serverOpenCodeFetch = null,
|
|
377
|
+
sessionAccess = null,
|
|
354
378
|
getSmallModelService,
|
|
355
379
|
quietMs = TITLE_QUIET_MS,
|
|
356
380
|
throttleMs = TITLE_THROTTLE_MS,
|
|
@@ -362,6 +386,7 @@ export const createSessionTitleRuntime = ({
|
|
|
362
386
|
const forcedRefreshes = new Set();
|
|
363
387
|
const initialRefreshes = new Set();
|
|
364
388
|
const newSessions = new Set();
|
|
389
|
+
const initialUserMessages = new Map();
|
|
365
390
|
const forkFirstSendPending = new Map();
|
|
366
391
|
const forkFirstRefreshes = new Set();
|
|
367
392
|
/** In-memory throttle timestamps (metadata also persists across restarts). */
|
|
@@ -377,6 +402,20 @@ export const createSessionTitleRuntime = ({
|
|
|
377
402
|
};
|
|
378
403
|
|
|
379
404
|
const openCodeFetch = async (pathname, { directory, method = 'GET', body } = {}) => {
|
|
405
|
+
if (sessionAccess) {
|
|
406
|
+
const sessionId = decodeURIComponent(pathname.slice('/session/'.length));
|
|
407
|
+
return method === 'PATCH'
|
|
408
|
+
? sessionAccess.update(sessionId, directory, body)
|
|
409
|
+
: sessionAccess.get(sessionId, directory);
|
|
410
|
+
}
|
|
411
|
+
if (typeof serverOpenCodeFetch === 'function') {
|
|
412
|
+
return serverOpenCodeFetch(pathname, {
|
|
413
|
+
directory,
|
|
414
|
+
method,
|
|
415
|
+
body,
|
|
416
|
+
timeoutMs: FETCH_TIMEOUT_MS,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
380
419
|
const base = buildOpenCodeUrl(pathname, '');
|
|
381
420
|
const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
|
|
382
421
|
const response = await fetch(url, {
|
|
@@ -396,6 +435,7 @@ export const createSessionTitleRuntime = ({
|
|
|
396
435
|
};
|
|
397
436
|
|
|
398
437
|
const fetchRecentMessages = async (sessionId, directory) => {
|
|
438
|
+
if (sessionAccess) return sessionAccess.messages(sessionId, directory, TRANSCRIPT_MESSAGE_LIMIT);
|
|
399
439
|
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
|
|
400
440
|
const params = new URLSearchParams({ limit: String(TRANSCRIPT_MESSAGE_LIMIT) });
|
|
401
441
|
if (directory) params.set('directory', directory);
|
|
@@ -421,20 +461,13 @@ export const createSessionTitleRuntime = ({
|
|
|
421
461
|
|
|
422
462
|
const meta = readTitleRefreshMeta(session);
|
|
423
463
|
if (meta.titleRefresh.isGenerating !== true) return;
|
|
424
|
-
const {
|
|
425
|
-
isGenerating: _isGenerating,
|
|
426
|
-
requestedAt: _requestedAt,
|
|
427
|
-
...titleRefresh
|
|
428
|
-
} = meta.titleRefresh;
|
|
429
464
|
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
|
|
430
465
|
directory,
|
|
431
466
|
method: 'PATCH',
|
|
432
467
|
body: {
|
|
433
468
|
metadata: {
|
|
434
|
-
...meta.metadata,
|
|
435
469
|
openchamber: {
|
|
436
|
-
|
|
437
|
-
titleRefresh,
|
|
470
|
+
titleRefresh: { isGenerating: null, requestedAt: null },
|
|
438
471
|
},
|
|
439
472
|
},
|
|
440
473
|
},
|
|
@@ -446,17 +479,13 @@ export const createSessionTitleRuntime = ({
|
|
|
446
479
|
.catch(() => null);
|
|
447
480
|
if (!session || typeof session !== 'object') return;
|
|
448
481
|
|
|
449
|
-
const meta = readTitleRefreshMeta(session);
|
|
450
482
|
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
|
|
451
483
|
directory,
|
|
452
484
|
method: 'PATCH',
|
|
453
485
|
body: {
|
|
454
486
|
metadata: {
|
|
455
|
-
...meta.metadata,
|
|
456
487
|
openchamber: {
|
|
457
|
-
...meta.openchamber,
|
|
458
488
|
titleRefresh: {
|
|
459
|
-
...meta.titleRefresh,
|
|
460
489
|
lastError: error instanceof Error ? error.message : String(error),
|
|
461
490
|
failedAt: now(),
|
|
462
491
|
},
|
|
@@ -505,11 +534,8 @@ export const createSessionTitleRuntime = ({
|
|
|
505
534
|
method: 'PATCH',
|
|
506
535
|
body: {
|
|
507
536
|
metadata: {
|
|
508
|
-
...meta.metadata,
|
|
509
537
|
openchamber: {
|
|
510
|
-
...meta.openchamber,
|
|
511
538
|
titleRefresh: {
|
|
512
|
-
...meta.titleRefresh,
|
|
513
539
|
activityUpdatedAt,
|
|
514
540
|
},
|
|
515
541
|
},
|
|
@@ -524,6 +550,8 @@ export const createSessionTitleRuntime = ({
|
|
|
524
550
|
const generateTitle = async (sessionId, directory) => {
|
|
525
551
|
const forceRefresh = forcedRefreshes.delete(sessionId);
|
|
526
552
|
const initialRefresh = initialRefreshes.delete(sessionId);
|
|
553
|
+
const initialUserMessage = initialUserMessages.get(sessionId);
|
|
554
|
+
initialUserMessages.delete(sessionId);
|
|
527
555
|
const forkFirstRefresh = forkFirstRefreshes.delete(sessionId);
|
|
528
556
|
if (!forceRefresh && !isTitleRefreshEnabled()) return;
|
|
529
557
|
|
|
@@ -558,15 +586,14 @@ export const createSessionTitleRuntime = ({
|
|
|
558
586
|
return;
|
|
559
587
|
}
|
|
560
588
|
|
|
561
|
-
const messages = await fetchRecentMessages(sessionId, directory);
|
|
589
|
+
const messages = initialUserMessage ? [initialUserMessage] : await fetchRecentMessages(sessionId, directory);
|
|
562
590
|
if (!messages || messages.length === 0) return;
|
|
563
591
|
|
|
564
592
|
const { transcript, lastAssistantId, realUserCount, languageSample } = buildLatestTitleTranscript(messages);
|
|
565
593
|
if (!transcript) return;
|
|
566
594
|
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
// still the default placeholder.
|
|
595
|
+
// Initial titles use the admitted first user message immediately; ordinary
|
|
596
|
+
// later refreshes still require a moved-on conversation or placeholder.
|
|
570
597
|
if (!forceRefresh && !initialRefresh && !forkFirstRefresh && realUserCount < 2 && !isDefaultSessionTitle(currentTitle)) {
|
|
571
598
|
return;
|
|
572
599
|
}
|
|
@@ -597,14 +624,11 @@ export const createSessionTitleRuntime = ({
|
|
|
597
624
|
method: 'PATCH',
|
|
598
625
|
body: {
|
|
599
626
|
metadata: {
|
|
600
|
-
...meta.metadata,
|
|
601
627
|
openchamber: {
|
|
602
|
-
...meta.openchamber,
|
|
603
628
|
titleRefresh: {
|
|
604
|
-
...meta.titleRefresh,
|
|
605
629
|
isGenerating: true,
|
|
606
|
-
lastError:
|
|
607
|
-
failedAt:
|
|
630
|
+
lastError: null,
|
|
631
|
+
failedAt: null,
|
|
608
632
|
},
|
|
609
633
|
},
|
|
610
634
|
},
|
|
@@ -677,9 +701,6 @@ export const createSessionTitleRuntime = ({
|
|
|
677
701
|
const generatedAt = now();
|
|
678
702
|
lastGeneratedAtBySession.set(sessionId, generatedAt);
|
|
679
703
|
|
|
680
|
-
const currentMetadata = freshMeta.metadata;
|
|
681
|
-
const currentNamespace = freshMeta.openchamber;
|
|
682
|
-
|
|
683
704
|
console.log(`[session-title] refreshed ${sessionId} → "${nextTitle}" via ${generated.providerID}/${generated.modelID}`);
|
|
684
705
|
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
|
|
685
706
|
directory,
|
|
@@ -687,11 +708,8 @@ export const createSessionTitleRuntime = ({
|
|
|
687
708
|
body: {
|
|
688
709
|
title: nextTitle,
|
|
689
710
|
metadata: {
|
|
690
|
-
...currentMetadata,
|
|
691
711
|
openchamber: {
|
|
692
|
-
...currentNamespace,
|
|
693
712
|
titleRefresh: {
|
|
694
|
-
...freshMeta.titleRefresh,
|
|
695
713
|
lastAutoTitle: nextTitle,
|
|
696
714
|
forMessageID: lastAssistantId || latestAssistantId || '',
|
|
697
715
|
generatedAt,
|
|
@@ -723,6 +741,7 @@ export const createSessionTitleRuntime = ({
|
|
|
723
741
|
|
|
724
742
|
const processPayload = (payload, directoryHint = '') => {
|
|
725
743
|
if (stopped) return;
|
|
744
|
+
directoryHint = directoryHint || payload?.location?.directory || '';
|
|
726
745
|
const createdSession = extractCreatedSession(payload);
|
|
727
746
|
if (createdSession) {
|
|
728
747
|
if (isForkedSessionTitle(createdSession.title)) {
|
|
@@ -758,13 +777,18 @@ export const createSessionTitleRuntime = ({
|
|
|
758
777
|
initialRefreshes.add(status.sessionId);
|
|
759
778
|
armTimer(status.sessionId, status.directory || directoryHint, 0);
|
|
760
779
|
}
|
|
761
|
-
} else {
|
|
780
|
+
} else if (!initialRefreshes.has(status.sessionId)) {
|
|
762
781
|
clearTimer(status.sessionId);
|
|
763
782
|
}
|
|
764
783
|
return;
|
|
765
784
|
}
|
|
766
785
|
const userMessage = extractUserMessage(payload);
|
|
767
786
|
if (userMessage) {
|
|
787
|
+
if (userMessage.initialRecord && newSessions.delete(userMessage.sessionId)) {
|
|
788
|
+
initialRefreshes.add(userMessage.sessionId);
|
|
789
|
+
initialUserMessages.set(userMessage.sessionId, userMessage.initialRecord);
|
|
790
|
+
armTimer(userMessage.sessionId, userMessage.directory || directoryHint, 0);
|
|
791
|
+
}
|
|
768
792
|
const pendingFork = forkFirstSendPending.get(userMessage.sessionId);
|
|
769
793
|
if (pendingFork && userMessage.createdAt > pendingFork.createdAt) {
|
|
770
794
|
pendingFork.hasNewUserMessage = true;
|
|
@@ -780,7 +804,7 @@ export const createSessionTitleRuntime = ({
|
|
|
780
804
|
// session settles. Only a message created after the timer was armed
|
|
781
805
|
// means the user actually moved on.
|
|
782
806
|
const armed = timers.get(userMessage.sessionId);
|
|
783
|
-
if (armed && userMessage.createdAt >= armed.armedAt) {
|
|
807
|
+
if (armed && !initialRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
|
|
784
808
|
clearTimer(userMessage.sessionId);
|
|
785
809
|
}
|
|
786
810
|
}
|
|
@@ -792,6 +816,7 @@ export const createSessionTitleRuntime = ({
|
|
|
792
816
|
clearTimeout(timer);
|
|
793
817
|
}
|
|
794
818
|
timers.clear();
|
|
819
|
+
initialUserMessages.clear();
|
|
795
820
|
};
|
|
796
821
|
|
|
797
822
|
return { processPayload, stop, armTimer };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
2
|
+
import { projectSessionWithHostMetadata } from '../session-metadata/session-projection.js';
|
|
3
|
+
import { projectSessionMessageRecords } from '../session-turn-pages/session-message-projection.js';
|
|
4
|
+
|
|
5
|
+
/** Title reads use OpenCode; title-refresh metadata and its notifications belong to the Host. */
|
|
6
|
+
export function createTitleSessionAccess({
|
|
7
|
+
buildOpenCodeUrl,
|
|
8
|
+
getOpenCodeAuthHeaders,
|
|
9
|
+
readSessionMetadata,
|
|
10
|
+
persistSessionMetadata,
|
|
11
|
+
publishSession,
|
|
12
|
+
}) {
|
|
13
|
+
const client = () => makeOpenCodeV2Client({
|
|
14
|
+
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
15
|
+
authHeaders: getOpenCodeAuthHeaders(),
|
|
16
|
+
});
|
|
17
|
+
const options = () => ({ signal: AbortSignal.timeout(30_000) });
|
|
18
|
+
const get = async (sessionID) => {
|
|
19
|
+
const session = await client().session.get({ sessionID }, options());
|
|
20
|
+
return projectSessionWithHostMetadata(session, await readSessionMetadata(sessionID));
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
get,
|
|
24
|
+
async messages(sessionID, _directory, limit) {
|
|
25
|
+
const page = await client().message.list({ sessionID, limit, order: 'desc' }, options());
|
|
26
|
+
if (!Array.isArray(page?.data)) throw new Error('Session title: invalid message page');
|
|
27
|
+
return projectSessionMessageRecords([...page.data].reverse());
|
|
28
|
+
},
|
|
29
|
+
async update(sessionID, directory, patch) {
|
|
30
|
+
if (typeof patch.title === 'string') {
|
|
31
|
+
await client().session.update({ sessionID, title: patch.title }, options());
|
|
32
|
+
}
|
|
33
|
+
// Never write the complete metadata snapshot back: goal/archive/ownership
|
|
34
|
+
// may have changed while the title model was running.
|
|
35
|
+
const refresh = patch.metadata?.openchamber?.titleRefresh;
|
|
36
|
+
if (refresh) {
|
|
37
|
+
await persistSessionMetadata(sessionID, { openchamber: { titleRefresh: refresh } });
|
|
38
|
+
}
|
|
39
|
+
if (typeof patch.title === 'string' || (refresh && Object.hasOwn(refresh, 'isGenerating'))) {
|
|
40
|
+
const session = await get(sessionID);
|
|
41
|
+
await publishSession(session, directory);
|
|
42
|
+
return session;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { beforeEach, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mergeMetadataPatch } from '../session-metadata/session-metadata-store.js';
|
|
3
|
+
|
|
4
|
+
const { api } = vi.hoisted(() => ({ api: {
|
|
5
|
+
session: { get: vi.fn(), update: vi.fn() },
|
|
6
|
+
message: { list: vi.fn() },
|
|
7
|
+
} }));
|
|
8
|
+
vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: () => api }));
|
|
9
|
+
import { createTitleSessionAccess } from './session-access.js';
|
|
10
|
+
|
|
11
|
+
beforeEach(() => vi.resetAllMocks());
|
|
12
|
+
|
|
13
|
+
function fixture() {
|
|
14
|
+
let metadata = { openchamber: {
|
|
15
|
+
goal: { status: 'active' },
|
|
16
|
+
titleRefresh: { isGenerating: true, requestedAt: 1000, lastError: 'old failure', failedAt: 1 },
|
|
17
|
+
} };
|
|
18
|
+
api.session.get.mockResolvedValue({
|
|
19
|
+
id: 'ses_title', title: 'Updated title', location: { directory: '/repo' },
|
|
20
|
+
time: { created: 10, updated: 20 },
|
|
21
|
+
});
|
|
22
|
+
const persist = vi.fn(async (_id, patch) => { metadata = mergeMetadataPatch(metadata, patch); });
|
|
23
|
+
const publish = vi.fn();
|
|
24
|
+
const access = createTitleSessionAccess({
|
|
25
|
+
buildOpenCodeUrl: (path) => `http://opencode${path}`,
|
|
26
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
27
|
+
readSessionMetadata: async () => metadata,
|
|
28
|
+
persistSessionMetadata: persist,
|
|
29
|
+
publishSession: publish,
|
|
30
|
+
});
|
|
31
|
+
return { access, persist, publish, metadata: () => metadata };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('publishes the full authoritative title row and clears Host loading without overwriting unrelated state', async () => {
|
|
35
|
+
const f = fixture();
|
|
36
|
+
await f.access.update('ses_title', '/repo', {
|
|
37
|
+
title: 'Updated title',
|
|
38
|
+
metadata: { openchamber: { goal: { status: 'stale' }, titleRefresh: {
|
|
39
|
+
lastAutoTitle: 'Updated title', isGenerating: null, requestedAt: null, lastError: null, failedAt: null,
|
|
40
|
+
} } },
|
|
41
|
+
});
|
|
42
|
+
expect(api.session.update.mock.calls[0][0]).toEqual({ sessionID: 'ses_title', title: 'Updated title' });
|
|
43
|
+
expect(f.metadata()).toEqual({ openchamber: {
|
|
44
|
+
goal: { status: 'active' }, titleRefresh: { lastAutoTitle: 'Updated title' },
|
|
45
|
+
} });
|
|
46
|
+
expect(f.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
47
|
+
id: 'ses_title', title: 'Updated title', directory: '/repo', metadata: f.metadata(),
|
|
48
|
+
}), '/repo');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('projects only the bounded newest message page in conversation order', async () => {
|
|
52
|
+
const f = fixture();
|
|
53
|
+
api.message.list.mockResolvedValue({ data: [
|
|
54
|
+
{ id: 'a1', type: 'assistant', content: [{ type: 'text', text: 'Finished' }] },
|
|
55
|
+
{ id: 'u1', type: 'user', text: 'Implement title refresh' },
|
|
56
|
+
] });
|
|
57
|
+
const messages = await f.access.messages('ses_title', '/repo', 16);
|
|
58
|
+
expect(api.message.list.mock.calls[0][0]).toEqual({ sessionID: 'ses_title', limit: 16, order: 'desc' });
|
|
59
|
+
expect(messages.map((row) => row.info.id)).toEqual(['u1', 'a1']);
|
|
60
|
+
expect(messages[0].parts[0].text).toBe('Implement title refresh');
|
|
61
|
+
expect(messages[1].parts[0].text).toBe('Finished');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('publishes a full loading row before the new session exists in client catalogs', async () => {
|
|
65
|
+
const f = fixture();
|
|
66
|
+
await f.access.update('ses_title', '/repo', { metadata: { openchamber: { titleRefresh: { isGenerating: true } } } });
|
|
67
|
+
expect(f.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
68
|
+
id: 'ses_title', directory: '/repo', metadata: expect.objectContaining({
|
|
69
|
+
openchamber: expect.objectContaining({ titleRefresh: expect.objectContaining({ isGenerating: true }) }),
|
|
70
|
+
}),
|
|
71
|
+
}), '/repo');
|
|
72
|
+
expect(api.session.update).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('does not publish success on a failed title write', async () => {
|
|
76
|
+
const f = fixture();
|
|
77
|
+
api.session.update.mockRejectedValue(new Error('offline'));
|
|
78
|
+
await expect(f.access.update('ses_title', '/repo', {
|
|
79
|
+
title: 'Failed', metadata: { openchamber: { titleRefresh: { lastAutoTitle: 'Failed' } } },
|
|
80
|
+
})).rejects.toThrow('offline');
|
|
81
|
+
expect(f.persist).not.toHaveBeenCalled();
|
|
82
|
+
expect(f.publish).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not turn a failed or malformed message read into empty success', async () => {
|
|
86
|
+
const f = fixture();
|
|
87
|
+
api.message.list.mockRejectedValueOnce(new Error('offline'));
|
|
88
|
+
await expect(f.access.messages('ses_title', '/repo', 16)).rejects.toThrow('offline');
|
|
89
|
+
api.message.list.mockResolvedValueOnce({});
|
|
90
|
+
await expect(f.access.messages('ses_title', '/repo', 16)).rejects.toThrow('invalid message page');
|
|
91
|
+
});
|