@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-pLaqWWmB.js} +1 -1
- package/dist/assets/AssistantView-C4GkABnI.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-ByYLIfWo.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-MuJAzJq_.js} +1 -1
- package/dist/assets/Icon-Dz2kO9RN.js +1 -0
- package/dist/assets/MarkdownRenderer-CPU6kSHd.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-2hw5cdqQ.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-DXZQkLTa.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-C9BzieQY.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-2_iOfzqO.js} +2 -2
- package/dist/assets/MobileSurface-DNVA7K_1.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-QA6pMrJ9.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-XWkr7MjT.js} +1 -1
- package/dist/assets/OverlayScrollbar-DCWyGseO.js +2 -0
- package/dist/assets/SettingsView-dWuu0VIb.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CetgiJMX.js} +1 -1
- package/dist/assets/TerminalView-BuXuo7gm.js +8 -0
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-DNSSOaCt.js} +2 -2
- package/dist/assets/appThemeRegistry-Cb7qFBsc.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-Doq9FI1I.js} +1 -1
- package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CYUeu3_N.js} +1 -1
- package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-C8kOMVtC.js} +1 -1
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-DbxnIQdS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-dvxF4V2p.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-B1p9Yn1h.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-D08-Lg-e.js} +1 -1
- package/dist/assets/es-Dsmc0A_3.js +15 -0
- package/dist/assets/fileMentionTouchSelection-BCAKb-NS.js +3 -0
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-X773xxvX.js} +1 -1
- package/dist/assets/fr-C_ZlZDI2.js +13 -0
- package/dist/assets/gitApi-3VBj3fbR.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-DZZuQ1IL.js} +1 -1
- package/dist/assets/insertionBoundaries-BMuCcvPy.js +1 -0
- package/dist/assets/ja-BqqTaWTi.js +15 -0
- package/dist/assets/ko-DP2XPM37.js +15 -0
- package/dist/assets/main-COEILjRF.js +169 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/{main-CCE_4y5F.js → main-DsUewWPt.js} +2 -2
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-DBgXGze8.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/mcpQueries-B05QR67V.js +1 -0
- package/dist/assets/miniChat-D7IPMm-k.js +2 -0
- package/dist/assets/mobile-BnCoDxnv.js +2 -0
- package/dist/assets/multirun-BDmFjX9N.js +4 -0
- package/dist/assets/persistence-DznVIKwz.js +3 -0
- package/dist/assets/pl-BCf5uLlK.js +15 -0
- package/dist/assets/platform-DSTtx-DI.js +6 -0
- package/dist/assets/projectMeta-C5gcAd5f.js +3 -0
- 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-DsAaWCss.js +2 -0
- package/dist/assets/renderMobileApp-DOaoohlp.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-CzPK1uMj.js} +1 -1
- package/dist/assets/runtimeEndpointReset-DKjVJCdt.js +149 -0
- package/dist/assets/sessionChangeSummary-CktSWasa.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-CDA1aAcI.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-Q3bb6MVj.js} +1 -1
- package/dist/assets/src-lS1keDVV.css +2 -0
- package/dist/assets/store-jaP79mqI.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-Da_DO_Uh.js} +1 -1
- package/dist/assets/textarea--CYehp07.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CGLzBwEK.js} +1 -1
- package/dist/assets/uk-BgFzz8m2.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-Byz3kd29.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-2zzPMQF2.js} +64 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-B-2YooUd.js +1 -0
- package/dist/assets/useEffectiveDirectory-K3U4eL5F.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-D9OIZs1p.js} +1 -1
- package/dist/assets/useMobileNavigationStore-B0zSdCwP.js +414 -0
- package/dist/assets/useSessionAutoCleanup-BCXHAcbX.js +2 -0
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CsoMQNik.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/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 +359 -63
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/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 +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/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 +89 -17
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +53 -4
- 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 +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +254 -46
- package/server/lib/opencode/proxy.test.js +238 -5
- package/server/lib/opencode/routes.js +326 -62
- package/server/lib/opencode/routes.test.js +278 -12
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +33 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/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/v2-client.js +14 -3
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +73 -18
- 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 +112 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
- package/server/lib/session-metadata/session-metadata-store.js +270 -83
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +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 +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +124 -240
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/Icon-DD-BuMfZ.js +0 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/TerminalView-Dh9MWCpS.js +0 -8
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BvrNCroG.js +0 -4
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/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-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/persistence-Ce3AnpEr.js +0 -3
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
LLM_SESSION_PURPOSE,
|
|
4
|
+
buildAssistantSessionMetadata,
|
|
5
|
+
buildLlmSessionMetadata,
|
|
6
|
+
buildScheduledTaskMetadata,
|
|
7
|
+
} from './system-session.js';
|
|
8
|
+
|
|
9
|
+
describe('system session metadata builders', () => {
|
|
10
|
+
it('builds scheduled-task isolation from a non-empty taskID', () => {
|
|
11
|
+
expect(buildScheduledTaskMetadata({
|
|
12
|
+
projectID: 'project-1',
|
|
13
|
+
taskID: 'task-1',
|
|
14
|
+
runID: 'run-1',
|
|
15
|
+
name: 'Morning Sync',
|
|
16
|
+
})).toEqual({
|
|
17
|
+
openchamber: {
|
|
18
|
+
scheduledTask: {
|
|
19
|
+
taskID: 'task-1',
|
|
20
|
+
projectID: 'project-1',
|
|
21
|
+
runID: 'run-1',
|
|
22
|
+
name: 'Morning Sync',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('refuses scheduled-task metadata without taskID', () => {
|
|
29
|
+
expect(() => buildScheduledTaskMetadata({ projectID: 'project-1' })).toThrow(/taskID/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('builds llm isolation with the chat-completions purpose', () => {
|
|
33
|
+
expect(buildLlmSessionMetadata()).toEqual({
|
|
34
|
+
openchamber: { llm: { purpose: LLM_SESSION_PURPOSE } },
|
|
35
|
+
});
|
|
36
|
+
expect(buildLlmSessionMetadata(' ')).toEqual({
|
|
37
|
+
openchamber: { llm: { purpose: LLM_SESSION_PURPOSE } },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('builds assistant isolation from a non-empty assistantID', () => {
|
|
42
|
+
expect(buildAssistantSessionMetadata({
|
|
43
|
+
assistantID: 'assistant_1',
|
|
44
|
+
name: 'Ops',
|
|
45
|
+
})).toEqual({
|
|
46
|
+
openchamber: {
|
|
47
|
+
assistant: { assistantID: 'assistant_1', name: 'Ops' },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('refuses assistant metadata without assistantID', () => {
|
|
53
|
+
expect(() => buildAssistantSessionMetadata({ name: 'Ops' })).toThrow(/assistantID/);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -5,20 +5,53 @@ conversation's **main subject** (overall feature / goal) with the small model
|
|
|
5
5
|
(`lib/small-model`), then PATCHes `title` plus `metadata.openchamber.titleRefresh`.
|
|
6
6
|
|
|
7
7
|
OpenCode only auto-titles once from the first user message
|
|
8
|
-
(`SessionPrompt.ensureTitle`). This module auto-refreshes sparsely (first
|
|
9
|
-
of a new session, first newly-sent reply on a fork) and on explicit smart-title
|
|
8
|
+
(`SessionPrompt.ensureTitle`). This module auto-refreshes sparsely (first user
|
|
9
|
+
admission of a new session, first newly-sent reply on a fork) and on explicit smart-title
|
|
10
10
|
requests, naming the durable work being done — not the last wrap-up utterance
|
|
11
11
|
like "commit and push". Background auto refreshes still respect a 5-minute
|
|
12
12
|
throttle (`TITLE_THROTTLE_MS`) when a refresh is armed.
|
|
13
13
|
|
|
14
14
|
## Flow
|
|
15
15
|
|
|
16
|
+
### OpenCode 2 smart-title boundary
|
|
17
|
+
|
|
18
|
+
Manual requests arrive as Host `openchamber:session-metadata` writes. The
|
|
19
|
+
metadata route's committed-write callback explicitly notifies the title runtime;
|
|
20
|
+
it must not rely on OpenCode echoing Host metadata as `session.updated`.
|
|
21
|
+
`session-access.js` owns the production boundary: official client session reads
|
|
22
|
+
and title updates, a bounded `message.list({ order: 'desc' })` page projected into
|
|
23
|
+
chronological transcript records, and Host metadata reads/merge patches.
|
|
24
|
+
Only changed `titleRefresh` fields are persisted. Transient fields are removed
|
|
25
|
+
with RFC 7386 `null`, so `isGenerating` and `requestedAt` cannot survive a clear.
|
|
26
|
+
Unrelated goal, archive, and ownership metadata is never replaced by a title read.
|
|
27
|
+
|
|
28
|
+
After a successful title write or generation-state change, an authoritative full session row is published
|
|
29
|
+
as `session.updated` and applied to the session index. Connected clients receive
|
|
30
|
+
both loading metadata and the new title without reselecting the session. Neither
|
|
31
|
+
event represents an execution-status transition: the original session's busy /
|
|
32
|
+
retry status and subsequent execution events remain independently authoritative.
|
|
33
|
+
Publishing the loading row also covers first-send admission arriving before the
|
|
34
|
+
renderer inserts the new session: a metadata-only event would otherwise be ignored
|
|
35
|
+
for that still-unknown row.
|
|
36
|
+
This Host path serves web, Electron, and connected hosted/native mobile clients;
|
|
37
|
+
extension-only VS Code retains the limitation below.
|
|
38
|
+
|
|
16
39
|
1. `createSessionTitleRuntime` is a consumer of the server's global SSE
|
|
17
40
|
fan-out (`index.js` → `globalMessageStreamHub.subscribeEvent`). Purely event-driven — dormant sessions never
|
|
18
41
|
generate anything.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
2. Auto title refresh is intentionally sparse (title stability first). A newly
|
|
43
|
+
observed root `session.created` generates its first title immediately on
|
|
44
|
+
the first native `session.inbox.enqueued` user item. Native `data` envelopes
|
|
45
|
+
are accepted alongside legacy `properties`. The admitted text is bounded
|
|
46
|
+
to the user-message limit and retained only until the generation starts;
|
|
47
|
+
it avoids a race with the not-yet-materialized message projection. Synthetic,
|
|
48
|
+
compaction, and move items cannot start this title. Busy/execution-start
|
|
49
|
+
events do not cancel this first-title timer: title generation and the main
|
|
50
|
+
assistant reply run concurrently. Generation publishes `isGenerating` via
|
|
51
|
+
the existing Host metadata path, then the final title through `session.updated`.
|
|
52
|
+
Later user admissions/idle events do not regenerate the initial title.
|
|
53
|
+
The legacy first-idle edge remains a fallback when no admission was observed.
|
|
54
|
+
A fork title (`(fork #n)`) waits for its first
|
|
22
55
|
newly-created user message; the matching assistant completion triggers an
|
|
23
56
|
immediate title refresh that bypasses inherited title metadata and throttle.
|
|
24
57
|
If the fork's `session.created` was lost (SSE reconnect gap, server or
|
|
@@ -27,8 +60,8 @@ throttle (`TITLE_THROTTLE_MS`) when a refresh is armed.
|
|
|
27
60
|
title + message created after the fork + activity timestamp not yet past
|
|
28
61
|
the fork time), so the first-reply refresh still fires.
|
|
29
62
|
Ordinary later idle transitions do **not** arm another refresh. Any
|
|
30
|
-
|
|
31
|
-
|
|
63
|
+
`busy`/`retry` status or a fresh user `message.updated` still clears an
|
|
64
|
+
ordinary already-armed timer; an admitted first-title timer is exempt.
|
|
32
65
|
A sidebar smart-title request sets `titleRefresh.requestedAt`; its
|
|
33
66
|
`session.updated` event arms the same flow immediately (forced / manual
|
|
34
67
|
refresh is unaffected by the background gate).
|
|
@@ -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
|
+
}
|