@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
- package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
- package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
- package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
- package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
- package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
- package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
- package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
- package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
- package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
- package/dist/assets/context-menu-B2LXJgXL.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-yuWg9BN4.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
- package/dist/assets/main-DiL6w6KF.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
- package/dist/assets/miniChat-CwtUaElU.js +2 -0
- package/dist/assets/mobile-DB-6--ws.js +2 -0
- package/dist/assets/multirun-TAsxFI8e.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-C13fMVEs.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
- package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
- package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
- package/dist/assets/src-ubCWXZhR.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
- package/dist/assets/textarea-BBOlRI9O.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
- package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +1 -1
- package/server/index.js +327 -62
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/conversations/DOCUMENTATION.md +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/runtime.js +23 -5
- package/server/lib/notifications/runtime.test.js +11 -0
- package/server/lib/opencode/DOCUMENTATION.md +85 -17
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +43 -4
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +252 -46
- package/server/lib/opencode/proxy.test.js +235 -2
- package/server/lib/opencode/routes.js +296 -62
- package/server/lib/opencode/routes.test.js +278 -12
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +33 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v2-client.js +14 -3
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
- package/server/lib/session-metadata/routes.js +108 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +212 -72
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +124 -240
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
- package/dist/assets/context-menu-jGtH6fR9.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type surface for session-projection.js (consumed by VS Code Extension Host facades).
|
|
3
|
+
* Runtime implementation stays in the .js module.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const HOST_ARCHIVE_METADATA_KEY: string;
|
|
7
|
+
|
|
8
|
+
export function normalizeSessionEventType(type: unknown): string;
|
|
9
|
+
|
|
10
|
+
export function isSessionLifecycleEventType(type: unknown): boolean;
|
|
11
|
+
|
|
12
|
+
export function extractSessionInfoFromPayload(
|
|
13
|
+
payload: unknown,
|
|
14
|
+
): { id: string; [key: string]: unknown } | null;
|
|
15
|
+
|
|
16
|
+
export function resolveSessionDirectory(session: unknown): string | null;
|
|
17
|
+
|
|
18
|
+
export function readHostArchivedAt(metadata: unknown): number | null;
|
|
19
|
+
|
|
20
|
+
export function resolveProjectedArchivedAt(
|
|
21
|
+
upstreamArchived: unknown,
|
|
22
|
+
hostArchivedAt: unknown,
|
|
23
|
+
): number | null | undefined;
|
|
24
|
+
|
|
25
|
+
export function isSessionArchived(session: unknown): boolean;
|
|
26
|
+
|
|
27
|
+
export function projectSessionWithHostMetadata(
|
|
28
|
+
session: unknown,
|
|
29
|
+
hostMetadata: unknown,
|
|
30
|
+
): unknown;
|
|
31
|
+
|
|
32
|
+
export function projectSessionWithStoredMap(
|
|
33
|
+
session: unknown,
|
|
34
|
+
storedBySessionId: unknown,
|
|
35
|
+
): unknown;
|
|
36
|
+
|
|
37
|
+
export function projectSessionLifecyclePayload(
|
|
38
|
+
payload: unknown,
|
|
39
|
+
readHostMetadata: (sessionId: string) => unknown,
|
|
40
|
+
options?: { hostReady?: boolean },
|
|
41
|
+
): unknown;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host authority projection for OpenCode sessions.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode 2.x has no durable archive write. OpenChamber stores archive state
|
|
5
|
+
* under `metadata.openchamber.archive.archivedAt` and folds it onto session
|
|
6
|
+
* list/detail/event payloads so clients keep reading `time.archived`.
|
|
7
|
+
*
|
|
8
|
+
* Authority:
|
|
9
|
+
* - positive archivedAt → Host archive wins
|
|
10
|
+
* - 0 → explicit unarchive (clears time.archived even if upstream still has history)
|
|
11
|
+
* - missing Host archive key → fall back to upstream time.archived
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
|
|
16
|
+
/** RFC 7386 merge (local copy — avoid circular import with the metadata store). */
|
|
17
|
+
const mergeMetadataPatch = (current, patch) => {
|
|
18
|
+
const base = isPlainObject(current) ? { ...current } : {};
|
|
19
|
+
if (!isPlainObject(patch)) return base;
|
|
20
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
21
|
+
if (value === null) {
|
|
22
|
+
delete base[key];
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
base[key] = isPlainObject(value) ? mergeMetadataPatch(base[key], value) : value;
|
|
26
|
+
}
|
|
27
|
+
return base;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const HOST_ARCHIVE_METADATA_KEY = 'archive';
|
|
31
|
+
|
|
32
|
+
const SESSION_LIFECYCLE_BASE = new Set([
|
|
33
|
+
'session.created',
|
|
34
|
+
'session.updated',
|
|
35
|
+
'session.deleted',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Strip optional version suffixes: `session.updated.v2` → `session.updated`.
|
|
40
|
+
* @param {unknown} type
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export const normalizeSessionEventType = (type) => {
|
|
44
|
+
if (typeof type !== 'string' || !type) return '';
|
|
45
|
+
const match = type.match(/^(session\.(?:created|updated|deleted|status|idle|error))(?:\.|$)/);
|
|
46
|
+
return match ? match[1] : type;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const isSessionLifecycleEventType = (type) => SESSION_LIFECYCLE_BASE.has(normalizeSessionEventType(type));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve session info from legacy `properties.info`, native v2 `data.info`,
|
|
53
|
+
* or a bare session object under properties/data.
|
|
54
|
+
* @param {object | null | undefined} payload
|
|
55
|
+
*/
|
|
56
|
+
export const extractSessionInfoFromPayload = (payload) => {
|
|
57
|
+
if (!isPlainObject(payload)) return null;
|
|
58
|
+
const properties = isPlainObject(payload.properties) ? payload.properties : null;
|
|
59
|
+
const data = isPlainObject(payload.data) ? payload.data : null;
|
|
60
|
+
|
|
61
|
+
const candidates = [
|
|
62
|
+
properties?.info,
|
|
63
|
+
data?.info,
|
|
64
|
+
// Bare session on properties/data when shape is already a Session.
|
|
65
|
+
properties && typeof properties.id === 'string' ? properties : null,
|
|
66
|
+
data && typeof data.id === 'string' ? data : null,
|
|
67
|
+
];
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (isPlainObject(candidate) && typeof candidate.id === 'string' && candidate.id) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Authoritative worktree directory: v2 `location.directory`, then legacy fields.
|
|
78
|
+
* @param {object | null | undefined} session
|
|
79
|
+
*/
|
|
80
|
+
export const resolveSessionDirectory = (session) => {
|
|
81
|
+
if (!isPlainObject(session)) return null;
|
|
82
|
+
const candidates = [
|
|
83
|
+
session.location?.directory,
|
|
84
|
+
session.directory,
|
|
85
|
+
session.project?.worktree,
|
|
86
|
+
];
|
|
87
|
+
for (const value of candidates) {
|
|
88
|
+
if (typeof value !== 'string') continue;
|
|
89
|
+
const normalized = value.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
90
|
+
if (normalized) return normalized;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {unknown} value
|
|
97
|
+
* @returns {number | null} finite number including 0, else null when absent/invalid
|
|
98
|
+
*/
|
|
99
|
+
export const readHostArchivedAt = (metadata) => {
|
|
100
|
+
if (!isPlainObject(metadata)) return null;
|
|
101
|
+
const openchamber = metadata.openchamber;
|
|
102
|
+
if (!isPlainObject(openchamber)) return null;
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(openchamber, HOST_ARCHIVE_METADATA_KEY)) return null;
|
|
104
|
+
const archive = openchamber[HOST_ARCHIVE_METADATA_KEY];
|
|
105
|
+
if (!isPlainObject(archive) || !Object.prototype.hasOwnProperty.call(archive, 'archivedAt')) return null;
|
|
106
|
+
const value = archive.archivedAt;
|
|
107
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
|
108
|
+
return value;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {unknown} upstreamArchived
|
|
113
|
+
* @param {number | null} hostArchivedAt
|
|
114
|
+
* @returns {number | undefined}
|
|
115
|
+
*/
|
|
116
|
+
export const resolveProjectedArchivedAt = (upstreamArchived, hostArchivedAt) => {
|
|
117
|
+
if (hostArchivedAt === 0) return undefined;
|
|
118
|
+
if (typeof hostArchivedAt === 'number' && hostArchivedAt > 0) return Math.trunc(hostArchivedAt);
|
|
119
|
+
if (typeof upstreamArchived === 'number' && Number.isFinite(upstreamArchived) && upstreamArchived > 0) {
|
|
120
|
+
return Math.trunc(upstreamArchived);
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const isSessionArchived = (session) => {
|
|
126
|
+
const archived = session?.time?.archived;
|
|
127
|
+
return typeof archived === 'number' && Number.isFinite(archived) && archived > 0;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Deep-merge Host metadata onto an upstream session and apply archive authority
|
|
132
|
+
* to `time.archived`. Does not mutate created/updated.
|
|
133
|
+
*
|
|
134
|
+
* @param {object | null | undefined} session
|
|
135
|
+
* @param {object | null | undefined} hostMetadata single session metadata object
|
|
136
|
+
* @returns {object | null | undefined}
|
|
137
|
+
*/
|
|
138
|
+
export const projectSessionWithHostMetadata = (session, hostMetadata) => {
|
|
139
|
+
if (!session || typeof session !== 'object' || Array.isArray(session)) return session;
|
|
140
|
+
if (typeof session.id !== 'string' || !session.id) return session;
|
|
141
|
+
|
|
142
|
+
const theirs = isPlainObject(session.metadata) ? session.metadata : {};
|
|
143
|
+
const ours = isPlainObject(hostMetadata) ? hostMetadata : null;
|
|
144
|
+
const metadata = ours ? mergeMetadataPatch(theirs, ours) : theirs;
|
|
145
|
+
|
|
146
|
+
const hostArchivedAt = readHostArchivedAt(ours ?? metadata);
|
|
147
|
+
const upstreamArchived = session.time && typeof session.time === 'object'
|
|
148
|
+
? session.time.archived
|
|
149
|
+
: undefined;
|
|
150
|
+
const projectedArchived = resolveProjectedArchivedAt(upstreamArchived, hostArchivedAt);
|
|
151
|
+
|
|
152
|
+
const timeBase = session.time && typeof session.time === 'object' && !Array.isArray(session.time)
|
|
153
|
+
? { ...session.time }
|
|
154
|
+
: {};
|
|
155
|
+
if (projectedArchived === undefined) delete timeBase.archived;
|
|
156
|
+
else timeBase.archived = projectedArchived;
|
|
157
|
+
|
|
158
|
+
const directory = resolveSessionDirectory(session);
|
|
159
|
+
const next = {
|
|
160
|
+
...session,
|
|
161
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : (session.metadata ?? metadata),
|
|
162
|
+
time: timeBase,
|
|
163
|
+
...(directory ? { directory } : {}),
|
|
164
|
+
};
|
|
165
|
+
if (Object.keys(metadata).length === 0 && session.metadata === undefined) {
|
|
166
|
+
delete next.metadata;
|
|
167
|
+
}
|
|
168
|
+
return next;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {object | null | undefined} session
|
|
173
|
+
* @param {Record<string, object> | null | undefined} storedBySessionId
|
|
174
|
+
*/
|
|
175
|
+
export const projectSessionWithStoredMap = (session, storedBySessionId) => {
|
|
176
|
+
if (!session || typeof session !== 'object' || typeof session.id !== 'string') return session;
|
|
177
|
+
if (!storedBySessionId || typeof storedBySessionId !== 'object') {
|
|
178
|
+
return projectSessionWithHostMetadata(session, null);
|
|
179
|
+
}
|
|
180
|
+
const host = storedBySessionId[session.id];
|
|
181
|
+
return projectSessionWithHostMetadata(session, isPlainObject(host) ? host : null);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const projectLifecycleInner = (payload, readHostMetadata) => {
|
|
185
|
+
const baseType = normalizeSessionEventType(payload.type);
|
|
186
|
+
if (baseType !== 'session.created' && baseType !== 'session.updated') return payload;
|
|
187
|
+
|
|
188
|
+
const info = extractSessionInfoFromPayload(payload);
|
|
189
|
+
if (!info) return payload;
|
|
190
|
+
|
|
191
|
+
let host = null;
|
|
192
|
+
if (typeof readHostMetadata === 'function') {
|
|
193
|
+
try {
|
|
194
|
+
host = readHostMetadata(info.id);
|
|
195
|
+
} catch {
|
|
196
|
+
host = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const projected = projectSessionWithHostMetadata(info, isPlainObject(host) ? host : null);
|
|
200
|
+
if (projected === info) return payload;
|
|
201
|
+
|
|
202
|
+
const next = { ...payload };
|
|
203
|
+
if (isPlainObject(payload.properties) && payload.properties.info === info) {
|
|
204
|
+
next.properties = { ...payload.properties, info: projected };
|
|
205
|
+
} else if (isPlainObject(payload.properties) && payload.properties === info) {
|
|
206
|
+
next.properties = projected;
|
|
207
|
+
}
|
|
208
|
+
if (isPlainObject(payload.data) && payload.data.info === info) {
|
|
209
|
+
next.data = { ...payload.data, info: projected };
|
|
210
|
+
} else if (isPlainObject(payload.data) && payload.data === info) {
|
|
211
|
+
next.data = projected;
|
|
212
|
+
}
|
|
213
|
+
// Keep original type string (including version suffix).
|
|
214
|
+
return next;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Project session.created / session.updated event payloads.
|
|
219
|
+
* Supports GlobalEvent wrap, versioned types, legacy properties.info, and
|
|
220
|
+
* native v2 data.info. Non-lifecycle payloads are returned unchanged.
|
|
221
|
+
* Envelope fields outside the inner payload are preserved.
|
|
222
|
+
*
|
|
223
|
+
* @param {unknown} payload
|
|
224
|
+
* @param {(sessionId: string) => object | null | undefined} readHostMetadata
|
|
225
|
+
* @param {{ hostReady?: boolean }} [options]
|
|
226
|
+
* When `hostReady === false`, lifecycle payloads return `null` (caller drops
|
|
227
|
+
* outbound publish). Transcript/message events are unaffected.
|
|
228
|
+
*/
|
|
229
|
+
export const projectSessionLifecyclePayload = (payload, readHostMetadata, options = {}) => {
|
|
230
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
|
231
|
+
|
|
232
|
+
// GlobalEvent wrap: { directory, payload: { type, ... } } — preserve envelope.
|
|
233
|
+
if (isPlainObject(payload.payload) && typeof payload.payload.type === 'string') {
|
|
234
|
+
const innerType = normalizeSessionEventType(payload.payload.type);
|
|
235
|
+
if (SESSION_LIFECYCLE_BASE.has(innerType) || innerType.startsWith('session.')) {
|
|
236
|
+
if (options.hostReady === false && isSessionLifecycleEventType(payload.payload.type)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const inner = projectSessionLifecyclePayload(payload.payload, readHostMetadata, options);
|
|
240
|
+
if (inner == null) return null;
|
|
241
|
+
return inner === payload.payload ? payload : { ...payload, payload: inner };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const type = typeof payload.type === 'string' ? payload.type : '';
|
|
246
|
+
if (options.hostReady === false && isSessionLifecycleEventType(type)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
if (!isSessionLifecycleEventType(type) && normalizeSessionEventType(type) !== 'session.created'
|
|
250
|
+
&& normalizeSessionEventType(type) !== 'session.updated') {
|
|
251
|
+
return payload;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return projectLifecycleInner(payload, readHostMetadata);
|
|
255
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractSessionInfoFromPayload,
|
|
5
|
+
normalizeSessionEventType,
|
|
6
|
+
projectSessionLifecyclePayload,
|
|
7
|
+
resolveSessionDirectory,
|
|
8
|
+
} from './session-projection.js';
|
|
9
|
+
|
|
10
|
+
describe('session projection event shapes', () => {
|
|
11
|
+
it('normalizes version suffixes', () => {
|
|
12
|
+
expect(normalizeSessionEventType('session.updated.v2')).toBe('session.updated');
|
|
13
|
+
expect(normalizeSessionEventType('session.created')).toBe('session.created');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('extracts info from properties.info and data.info', () => {
|
|
17
|
+
expect(extractSessionInfoFromPayload({
|
|
18
|
+
type: 'session.updated',
|
|
19
|
+
properties: { info: { id: 'ses_a' } },
|
|
20
|
+
})?.id).toBe('ses_a');
|
|
21
|
+
expect(extractSessionInfoFromPayload({
|
|
22
|
+
type: 'session.updated.v2',
|
|
23
|
+
data: { info: { id: 'ses_b' } },
|
|
24
|
+
})?.id).toBe('ses_b');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('prefers location.directory', () => {
|
|
28
|
+
expect(resolveSessionDirectory({
|
|
29
|
+
location: { directory: '/loc' },
|
|
30
|
+
directory: '/legacy',
|
|
31
|
+
})).toBe('/loc');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('projects GlobalEvent wrap and preserves envelope directory', () => {
|
|
35
|
+
const projected = projectSessionLifecyclePayload({
|
|
36
|
+
directory: '/repo',
|
|
37
|
+
payload: {
|
|
38
|
+
type: 'session.updated.v2',
|
|
39
|
+
data: {
|
|
40
|
+
info: {
|
|
41
|
+
id: 'ses_1',
|
|
42
|
+
time: { created: 1, updated: 2, archived: 50 },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}, () => ({ openchamber: { archive: { archivedAt: 99 } } }));
|
|
47
|
+
|
|
48
|
+
expect(projected.directory).toBe('/repo');
|
|
49
|
+
expect(projected.payload.type).toBe('session.updated.v2');
|
|
50
|
+
expect(projected.payload.data.info.time.archived).toBe(99);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns null for lifecycle when hostReady is false', () => {
|
|
54
|
+
expect(projectSessionLifecyclePayload({
|
|
55
|
+
type: 'session.updated',
|
|
56
|
+
properties: { info: { id: 'ses_1', time: {} } },
|
|
57
|
+
}, () => null, { hostReady: false })).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('leaves message events alone when hostReady is false', () => {
|
|
61
|
+
const msg = { type: 'message.updated', properties: { info: { role: 'user' } } };
|
|
62
|
+
expect(projectSessionLifecyclePayload(msg, () => null, { hostReady: false })).toBe(msg);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-owned isolation markers for system sessions.
|
|
3
|
+
*
|
|
4
|
+
* Sidebar / session-index / notifications hide a session only when these
|
|
5
|
+
* fields are present. Title prefixes are labels and never ownership.
|
|
6
|
+
*
|
|
7
|
+
* Every system session creator (scheduled tasks, LLM attachment gateway,
|
|
8
|
+
* Assistant bindings) must persist the matching patch through the Host
|
|
9
|
+
* session-metadata store. OpenCode create-time `metadata` is a hint only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const LLM_SESSION_PURPOSE = 'chat-completions';
|
|
13
|
+
|
|
14
|
+
const nonEmpty = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
15
|
+
|
|
16
|
+
export const buildScheduledTaskMetadata = ({ projectID, taskID, runID, name } = {}) => {
|
|
17
|
+
const id = nonEmpty(taskID);
|
|
18
|
+
if (!id) {
|
|
19
|
+
throw new Error('scheduled-task metadata requires taskID');
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
openchamber: {
|
|
23
|
+
scheduledTask: {
|
|
24
|
+
taskID: id,
|
|
25
|
+
...(nonEmpty(projectID) ? { projectID: nonEmpty(projectID) } : {}),
|
|
26
|
+
...(nonEmpty(runID) ? { runID: nonEmpty(runID) } : {}),
|
|
27
|
+
...(nonEmpty(name) ? { name: nonEmpty(name) } : {}),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const buildLlmSessionMetadata = (purpose = LLM_SESSION_PURPOSE) => {
|
|
34
|
+
const resolved = nonEmpty(purpose) || LLM_SESSION_PURPOSE;
|
|
35
|
+
return {
|
|
36
|
+
openchamber: {
|
|
37
|
+
llm: { purpose: resolved },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const buildAssistantSessionMetadata = ({ assistantID, name } = {}) => {
|
|
43
|
+
const id = nonEmpty(assistantID);
|
|
44
|
+
if (!id) {
|
|
45
|
+
throw new Error('assistant session metadata requires assistantID');
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
openchamber: {
|
|
49
|
+
assistant: {
|
|
50
|
+
assistantID: id,
|
|
51
|
+
...(nonEmpty(name) ? { name: nonEmpty(name) } : {}),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -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).
|