@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-CzTtVMnI.js} +1 -1
- package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-hOi72Wdx.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
- package/dist/assets/Icon-B8fSTLdP.js +1 -0
- package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-iX_hb_Xt.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-CAMUupwR.js} +2 -2
- package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-O4cGdz5p.js} +1 -1
- package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
- package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-B8d8qIQ7.js} +1 -1
- package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
- package/dist/assets/appThemeRegistry-BGGHio8T.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-DIIrsMjW.js} +1 -1
- package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CnvUeYst.js} +1 -1
- package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-BGBUpZEQ.js} +1 -1
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-rvn7_qBM.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-CQ63sc9E.js} +1 -1
- package/dist/assets/es-Dsmc0A_3.js +15 -0
- package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
- package/dist/assets/fr-C_ZlZDI2.js +13 -0
- package/dist/assets/gitApi-BF8vuoZc.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-BVu-iykl.js} +1 -1
- package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
- package/dist/assets/ja-BqqTaWTi.js +15 -0
- package/dist/assets/ko-DP2XPM37.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-B7t9lavz.js} +2 -2
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/main-DWP-prNB.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-D9-B1wdY.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
- package/dist/assets/miniChat-DgOrEQbN.js +2 -0
- package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
- package/dist/assets/multirun-DelyUO6k.js +4 -0
- package/dist/assets/persistence-D0y7226-.js +3 -0
- package/dist/assets/pl-BCf5uLlK.js +15 -0
- package/dist/assets/platform-CurqO28D.js +6 -0
- package/dist/assets/projectMeta-CPElzJ0n.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-pOE93w86.js +2 -0
- package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-DSS0KxG3.js} +1 -1
- package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
- package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-Bss8M20i.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
- package/dist/assets/src-lS1keDVV.css +2 -0
- package/dist/assets/store-jaP79mqI.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-mDR2jXTo.js} +1 -1
- package/dist/assets/textarea-XD2oX_Ee.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-DYeQ_Hvy.js} +1 -1
- package/dist/assets/uk-BgFzz8m2.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
- package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-Bjx8zkGM.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
- package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/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
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenChamber
|
|
2
|
+
* OpenChamber session metadata cache and migration source.
|
|
3
3
|
*
|
|
4
|
-
* OpenCode
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The OpenCode session record is the durable authority for title, archive, and
|
|
5
|
+
* other OpenChamber metadata (`opencode-session-record.js`). This JSON file
|
|
6
|
+
* remains the cache and the pre-migration source. A failed or corrupt read is
|
|
7
|
+
* never empty success: the store stays unwritable and does not overwrite
|
|
8
|
+
* unknown disk state or the OpenCode record.
|
|
9
9
|
*
|
|
10
10
|
* Writes are a JSON Merge Patch (RFC 7386): nested objects merge key by key and
|
|
11
|
-
* a `null` deletes. That
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* a `null` deletes. That keeps two features writing into the same `openchamber`
|
|
12
|
+
* namespace from erasing each other — goal mode saving progress must not drop
|
|
13
|
+
* an assist recap. When a record reader and writer are wired, the merged object
|
|
14
|
+
* is written onto the OpenCode session before the local cache commits. A failed
|
|
15
|
+
* record read is not treated as empty metadata.
|
|
16
|
+
*
|
|
17
|
+
* Mutations are fully serialized. Committed `entries` update only after a
|
|
18
|
+
* successful persist so concurrent readers never observe uncommitted drafts.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import fsDefault from 'node:fs';
|
|
17
22
|
import pathDefault from 'node:path';
|
|
18
23
|
|
|
24
|
+
import { mergeOwnedMetadata } from './opencode-session-record.js';
|
|
25
|
+
|
|
19
26
|
const METADATA_FILE_NAME = 'sessions-metadata.json';
|
|
27
|
+
const HOST_ARCHIVE_KEY = 'archive';
|
|
20
28
|
|
|
21
29
|
const asNonEmptyString = (value) => {
|
|
22
30
|
if (typeof value !== 'string') return null;
|
|
@@ -43,34 +51,98 @@ export const mergeMetadataPatch = (current, patch) => {
|
|
|
43
51
|
return base;
|
|
44
52
|
};
|
|
45
53
|
|
|
54
|
+
const readPriorArchivedAt = (metadata) => {
|
|
55
|
+
if (!isPlainObject(metadata?.openchamber)) return null;
|
|
56
|
+
if (!Object.prototype.hasOwnProperty.call(metadata.openchamber, HOST_ARCHIVE_KEY)) return null;
|
|
57
|
+
const archive = metadata.openchamber[HOST_ARCHIVE_KEY];
|
|
58
|
+
if (!isPlainObject(archive) || !Object.prototype.hasOwnProperty.call(archive, 'archivedAt')) return null;
|
|
59
|
+
const value = archive.archivedAt;
|
|
60
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Strip Host archive writes from generic metadata patches. */
|
|
64
|
+
const stripArchiveFromMetadataPatch = (patch) => {
|
|
65
|
+
if (!isPlainObject(patch) || !isPlainObject(patch.openchamber)) return patch;
|
|
66
|
+
if (!Object.prototype.hasOwnProperty.call(patch.openchamber, HOST_ARCHIVE_KEY)) return patch;
|
|
67
|
+
const openchamber = { ...patch.openchamber };
|
|
68
|
+
delete openchamber[HOST_ARCHIVE_KEY];
|
|
69
|
+
return { ...patch, openchamber };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Restore reserved archive after a generic merge so null ancestor deletes
|
|
74
|
+
* (`openchamber: null` / `archive: null`) cannot wipe Host archive state.
|
|
75
|
+
*/
|
|
76
|
+
const applyReservedArchiveProtection = (previous, patch, merged, { allowArchive = false } = {}) => {
|
|
77
|
+
if (allowArchive) return merged;
|
|
78
|
+
const priorArchive = readPriorArchivedAt(previous);
|
|
79
|
+
const result = isPlainObject(merged) ? { ...merged } : {};
|
|
80
|
+
const openchamber = isPlainObject(result.openchamber) ? { ...result.openchamber } : {};
|
|
81
|
+
|
|
82
|
+
if (isPlainObject(patch?.openchamber) && Object.prototype.hasOwnProperty.call(patch.openchamber, HOST_ARCHIVE_KEY)) {
|
|
83
|
+
delete openchamber[HOST_ARCHIVE_KEY];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (priorArchive !== null) {
|
|
87
|
+
openchamber[HOST_ARCHIVE_KEY] = {
|
|
88
|
+
...(isPlainObject(previous?.openchamber?.[HOST_ARCHIVE_KEY])
|
|
89
|
+
? previous.openchamber[HOST_ARCHIVE_KEY]
|
|
90
|
+
: {}),
|
|
91
|
+
archivedAt: priorArchive,
|
|
92
|
+
};
|
|
93
|
+
} else {
|
|
94
|
+
delete openchamber[HOST_ARCHIVE_KEY];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Object.keys(openchamber).length > 0) result.openchamber = openchamber;
|
|
98
|
+
else delete result.openchamber;
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
|
|
46
102
|
/**
|
|
47
103
|
* @param {object} options
|
|
48
104
|
* @param {string} options.dataDir OpenChamber data directory for this instance.
|
|
49
105
|
* @param {typeof fsDefault.promises} [options.fsPromises]
|
|
50
106
|
* @param {typeof pathDefault} [options.path]
|
|
51
107
|
* @param {() => number} [options.now]
|
|
108
|
+
* @param {(sessionID: string, directory?: string | null) => Promise<object | null>} [options.recordReader]
|
|
109
|
+
* OpenCode session read. Null means the session is gone. Any other failure throws.
|
|
110
|
+
* @param {(sessionID: string, metadata: object, directory?: string | null) => Promise<void>} [options.recordWriter]
|
|
111
|
+
* Writes the full merged metadata onto the OpenCode session record. Must not send title.
|
|
52
112
|
*/
|
|
53
113
|
export const createSessionMetadataStore = ({
|
|
54
114
|
dataDir,
|
|
55
115
|
fsPromises = fsDefault.promises,
|
|
56
116
|
path = pathDefault,
|
|
57
117
|
now = Date.now,
|
|
118
|
+
recordReader = null,
|
|
119
|
+
recordWriter = null,
|
|
58
120
|
}) => {
|
|
121
|
+
if (typeof recordWriter === 'function' && typeof recordReader !== 'function') {
|
|
122
|
+
throw new Error('session record writer requires a record reader');
|
|
123
|
+
}
|
|
59
124
|
const filePath = path.join(dataDir, METADATA_FILE_NAME);
|
|
60
125
|
|
|
61
|
-
/** sessionID → metadata
|
|
126
|
+
/** sessionID → metadata. Committed only — never holds an in-flight draft. */
|
|
62
127
|
const entries = new Map();
|
|
63
128
|
let loaded = false;
|
|
64
129
|
let loadPromise = null;
|
|
65
130
|
/**
|
|
66
131
|
* Writes stay disabled until one load has told us what is already on disk.
|
|
67
|
-
* A failed read is not evidence that nothing is stored
|
|
68
|
-
* file we could not read would drop exactly the state we are protecting.
|
|
132
|
+
* A failed/corrupt read is not evidence that nothing is stored.
|
|
69
133
|
*/
|
|
70
134
|
let writable = false;
|
|
71
|
-
let
|
|
135
|
+
let loadFailureReason = null;
|
|
136
|
+
/** Full RMW + persist chain — concurrent writers cannot interleave merges. */
|
|
137
|
+
let mutationChain = Promise.resolve();
|
|
138
|
+
|
|
139
|
+
const snapshotFrom = (map) => Object.fromEntries(map);
|
|
140
|
+
const snapshot = () => snapshotFrom(entries);
|
|
72
141
|
|
|
73
|
-
const
|
|
142
|
+
const unavailableError = (detail = loadFailureReason) => {
|
|
143
|
+
const suffix = detail ? `: ${detail}` : ': its file could not be read';
|
|
144
|
+
return new Error(`session metadata is unavailable${suffix.startsWith(':') ? suffix : `: ${suffix}`}`);
|
|
145
|
+
};
|
|
74
146
|
|
|
75
147
|
const parseStored = (raw) => {
|
|
76
148
|
const parsed = JSON.parse(raw);
|
|
@@ -80,8 +152,6 @@ export const createSessionMetadataStore = ({
|
|
|
80
152
|
const result = new Map();
|
|
81
153
|
for (const [sessionID, value] of Object.entries(parsed)) {
|
|
82
154
|
const id = asNonEmptyString(sessionID);
|
|
83
|
-
// A non-object entry is not metadata; dropping it is better than handing
|
|
84
|
-
// a consumer something it will read fields off.
|
|
85
155
|
if (id && isPlainObject(value)) result.set(id, value);
|
|
86
156
|
}
|
|
87
157
|
return result;
|
|
@@ -92,25 +162,20 @@ export const createSessionMetadataStore = ({
|
|
|
92
162
|
try {
|
|
93
163
|
raw = await fsPromises.readFile(filePath, 'utf8');
|
|
94
164
|
} catch (error) {
|
|
95
|
-
// No file yet is the normal first run: nothing is stored, and writing is
|
|
96
|
-
// safe because there is no state to lose.
|
|
97
165
|
if (error?.code === 'ENOENT') return { ok: true, stored: new Map() };
|
|
98
166
|
console.warn('[session-metadata] could not read the session metadata file:', error?.message ?? error);
|
|
99
|
-
return { ok: false, stored: null };
|
|
167
|
+
return { ok: false, stored: null, reason: error?.message || 'read failed' };
|
|
100
168
|
}
|
|
101
169
|
|
|
102
170
|
try {
|
|
103
171
|
return { ok: true, stored: parseStored(raw) };
|
|
104
172
|
} catch (error) {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// rather than collapsing to "no metadata".
|
|
108
|
-
const backup = `${filePath}.corrupt-${now()}`;
|
|
109
|
-
await fsPromises.rename(filePath, backup).catch(() => undefined);
|
|
173
|
+
// Corrupt bytes must not become empty success. Keep the file, stay
|
|
174
|
+
// unwritable, and surface the error so callers can report/retry.
|
|
110
175
|
console.warn(
|
|
111
|
-
`[session-metadata] session metadata file
|
|
176
|
+
`[session-metadata] session metadata file is corrupt and will not be overwritten: ${error?.message ?? error}`,
|
|
112
177
|
);
|
|
113
|
-
return { ok:
|
|
178
|
+
return { ok: false, stored: null, reason: error?.message || 'corrupt metadata file' };
|
|
114
179
|
}
|
|
115
180
|
};
|
|
116
181
|
|
|
@@ -118,117 +183,239 @@ export const createSessionMetadataStore = ({
|
|
|
118
183
|
if (!loadPromise) {
|
|
119
184
|
loadPromise = readFile().then((result) => {
|
|
120
185
|
if (result.ok) {
|
|
121
|
-
// Merge rather than replace: a write that happened while the first
|
|
122
|
-
// load was still running must survive it.
|
|
123
186
|
for (const [id, metadata] of result.stored) {
|
|
124
187
|
if (!entries.has(id)) entries.set(id, metadata);
|
|
125
188
|
}
|
|
126
189
|
loaded = true;
|
|
127
190
|
writable = true;
|
|
191
|
+
loadFailureReason = null;
|
|
128
192
|
} else {
|
|
129
|
-
// Let a later call retry; until one succeeds the store answers reads
|
|
130
|
-
// from memory and refuses writes.
|
|
131
193
|
loadPromise = null;
|
|
194
|
+
loaded = false;
|
|
195
|
+
writable = false;
|
|
196
|
+
loadFailureReason = result.reason || 'its file could not be read';
|
|
132
197
|
}
|
|
133
|
-
return { ok: result.ok, entries: snapshot() };
|
|
198
|
+
return { ok: result.ok, entries: snapshot(), reason: result.reason || null };
|
|
134
199
|
});
|
|
135
200
|
}
|
|
136
201
|
return loadPromise;
|
|
137
202
|
};
|
|
138
203
|
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
const ensureReadable = async () => {
|
|
205
|
+
const result = await load();
|
|
206
|
+
if (!result.ok || !writable) throw unavailableError(result.reason);
|
|
207
|
+
return result;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const persistMap = async (map) => {
|
|
211
|
+
const payload = JSON.stringify(snapshotFrom(map));
|
|
212
|
+
await fsPromises.mkdir(dataDir, { recursive: true });
|
|
213
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
214
|
+
await fsPromises.writeFile(tmpPath, payload, 'utf8');
|
|
215
|
+
await fsPromises.rename(tmpPath, filePath);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @template T
|
|
220
|
+
* @param {() => Promise<T>} work
|
|
221
|
+
* @returns {Promise<T>}
|
|
222
|
+
*/
|
|
223
|
+
const runExclusive = (work) => {
|
|
224
|
+
const next = mutationChain.then(work, work);
|
|
225
|
+
mutationChain = next.then(
|
|
226
|
+
() => undefined,
|
|
227
|
+
() => undefined,
|
|
228
|
+
);
|
|
152
229
|
return next;
|
|
153
230
|
};
|
|
154
231
|
|
|
155
|
-
const
|
|
232
|
+
const readRecordMetadata = async (id, directory) => {
|
|
233
|
+
if (typeof recordReader !== 'function') return undefined;
|
|
234
|
+
try {
|
|
235
|
+
const record = await recordReader(id, directory);
|
|
236
|
+
if (record == null) return null;
|
|
237
|
+
return isPlainObject(record.metadata) ? record.metadata : {};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (entries.has(id)) return undefined;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const get = async (sessionID, options = {}) => {
|
|
156
245
|
const id = asNonEmptyString(sessionID);
|
|
157
246
|
if (!id) return {};
|
|
158
|
-
await
|
|
159
|
-
|
|
247
|
+
await ensureReadable();
|
|
248
|
+
const side = entries.get(id);
|
|
249
|
+
const fromRecord = await readRecordMetadata(id, options.directory);
|
|
250
|
+
if (fromRecord === undefined) return side ?? {};
|
|
251
|
+
if (fromRecord === null) return side ?? {};
|
|
252
|
+
return mergeOwnedMetadata(fromRecord, side);
|
|
160
253
|
};
|
|
161
254
|
|
|
162
|
-
/** Whether the store holds anything for the session (an empty object counts as nothing). */
|
|
163
255
|
const has = async (sessionID) => {
|
|
164
256
|
const id = asNonEmptyString(sessionID);
|
|
165
257
|
if (!id) return false;
|
|
166
|
-
await
|
|
258
|
+
await ensureReadable();
|
|
167
259
|
return entries.has(id);
|
|
168
260
|
};
|
|
169
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Shared exclusive commit path: merge → persist draft → publish committed.
|
|
264
|
+
* Readers never observe the draft; persist failure leaves prior committed state.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} id
|
|
267
|
+
* @param {object | undefined} previous
|
|
268
|
+
* @param {object} patch
|
|
269
|
+
* @param {{ allowArchive?: boolean }} options
|
|
270
|
+
*/
|
|
271
|
+
const commitMergedPatch = async (id, previous, patch, options = {}) => {
|
|
272
|
+
const allowArchive = options.allowArchive === true;
|
|
273
|
+
let base = previous;
|
|
274
|
+
if (typeof recordReader === 'function') {
|
|
275
|
+
let record;
|
|
276
|
+
try {
|
|
277
|
+
record = await recordReader(id, options.directory);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// A failed read is not an empty record. Do not replace metadata we could not see.
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
if (record == null) {
|
|
283
|
+
const error = new Error(`session ${id} was not found`);
|
|
284
|
+
error.status = 404;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
base = mergeOwnedMetadata(record.metadata, previous);
|
|
288
|
+
}
|
|
289
|
+
const effectivePatch = allowArchive ? patch : stripArchiveFromMetadataPatch(patch);
|
|
290
|
+
|
|
291
|
+
let merged = mergeMetadataPatch(base, effectivePatch);
|
|
292
|
+
merged = applyReservedArchiveProtection(base, patch, merged, { allowArchive });
|
|
293
|
+
|
|
294
|
+
if (typeof recordWriter === 'function') {
|
|
295
|
+
await recordWriter(id, merged, options.directory);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const nextMap = new Map(entries);
|
|
299
|
+
if (Object.keys(merged).length === 0) nextMap.delete(id);
|
|
300
|
+
else nextMap.set(id, merged);
|
|
301
|
+
|
|
302
|
+
// OpenCode write already landed. Publish the local cache only after its persist succeeds.
|
|
303
|
+
await persistMap(nextMap);
|
|
304
|
+
|
|
305
|
+
if (Object.keys(merged).length === 0) entries.delete(id);
|
|
306
|
+
else entries.set(id, merged);
|
|
307
|
+
|
|
308
|
+
return merged;
|
|
309
|
+
};
|
|
310
|
+
|
|
170
311
|
/**
|
|
171
312
|
* Applies a merge patch and returns the session's full metadata afterwards.
|
|
172
|
-
*
|
|
173
|
-
*
|
|
313
|
+
* Committed map updates only after persist succeeds — readers never see drafts.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} sessionID
|
|
316
|
+
* @param {object} patch
|
|
317
|
+
* @param {{ allowArchive?: boolean }} [options]
|
|
174
318
|
*/
|
|
175
|
-
const setSessionMetadata = async (sessionID, patch) => {
|
|
319
|
+
const setSessionMetadata = async (sessionID, patch, options = {}) => {
|
|
176
320
|
const id = asNonEmptyString(sessionID);
|
|
177
321
|
if (!id) throw new Error('a session id is required to store session metadata');
|
|
178
322
|
if (!isPlainObject(patch)) throw new Error('a session metadata patch must be an object');
|
|
179
323
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
324
|
+
return runExclusive(async () => {
|
|
325
|
+
await ensureReadable();
|
|
326
|
+
return commitMergedPatch(id, entries.get(id), patch, options);
|
|
327
|
+
});
|
|
328
|
+
};
|
|
184
329
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Conditional metadata mutation on the same exclusive serial boundary as
|
|
332
|
+
* setSessionMetadata. `decide(committedMetadata)` runs while the lock is held
|
|
333
|
+
* and must return synchronously:
|
|
334
|
+
* - `{ ok: false, reason? }` — no persist; readers keep prior committed state
|
|
335
|
+
* - `{ ok: true, patch }` — merge-patch, persist, then publish
|
|
336
|
+
*
|
|
337
|
+
* Use this for goal generation / status preconditions so a stale auditor
|
|
338
|
+
* cannot commit after a user pause that already landed.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} sessionID
|
|
341
|
+
* @param {(current: object) => { ok: false, reason?: string } | { ok: true, patch: object }} decide
|
|
342
|
+
* @param {{ allowArchive?: boolean }} [options]
|
|
343
|
+
* @returns {Promise<{ committed: boolean, reason?: string, metadata: object }>}
|
|
344
|
+
*/
|
|
345
|
+
const mutateSessionMetadata = async (sessionID, decide, options = {}) => {
|
|
346
|
+
const id = asNonEmptyString(sessionID);
|
|
347
|
+
if (!id) throw new Error('a session id is required to store session metadata');
|
|
348
|
+
if (typeof decide !== 'function') throw new Error('a decide function is required to mutate session metadata');
|
|
190
349
|
|
|
191
|
-
|
|
192
|
-
await
|
|
193
|
-
} catch (error) {
|
|
194
|
-
if (had) entries.set(id, previous);
|
|
195
|
-
else entries.delete(id);
|
|
196
|
-
throw error;
|
|
197
|
-
}
|
|
350
|
+
return runExclusive(async () => {
|
|
351
|
+
await ensureReadable();
|
|
198
352
|
|
|
199
|
-
|
|
353
|
+
const previous = entries.get(id);
|
|
354
|
+
const current = previous ?? {};
|
|
355
|
+
const decision = decide(current);
|
|
356
|
+
if (!decision || decision.ok !== true) {
|
|
357
|
+
return {
|
|
358
|
+
committed: false,
|
|
359
|
+
reason: typeof decision?.reason === 'string' && decision.reason
|
|
360
|
+
? decision.reason
|
|
361
|
+
: 'rejected',
|
|
362
|
+
metadata: current,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!isPlainObject(decision.patch)) {
|
|
366
|
+
throw new Error('a successful mutate decision must include an object patch');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const merged = await commitMergedPatch(id, previous, decision.patch, options);
|
|
370
|
+
return { committed: true, metadata: merged };
|
|
371
|
+
});
|
|
200
372
|
};
|
|
201
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Drop a session's metadata.
|
|
376
|
+
* @returns {Promise<boolean>} true if a row was removed; false if already absent
|
|
377
|
+
* (idempotent). Disk/write failures **throw** so callers can retry — never
|
|
378
|
+
* conflate not-found with write failure.
|
|
379
|
+
*/
|
|
202
380
|
const removeSession = async (sessionID) => {
|
|
203
381
|
const id = asNonEmptyString(sessionID);
|
|
204
382
|
if (!id) return false;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
383
|
+
|
|
384
|
+
return runExclusive(async () => {
|
|
385
|
+
await ensureReadable();
|
|
386
|
+
if (!entries.has(id)) return false;
|
|
387
|
+
|
|
388
|
+
const nextMap = new Map(entries);
|
|
389
|
+
nextMap.delete(id);
|
|
390
|
+
// Write failures propagate — do not report as "already gone".
|
|
391
|
+
await persistMap(nextMap);
|
|
392
|
+
entries.delete(id);
|
|
393
|
+
return true;
|
|
394
|
+
});
|
|
217
395
|
};
|
|
218
396
|
|
|
219
397
|
const getAll = async () => {
|
|
220
|
-
await
|
|
398
|
+
await ensureReadable();
|
|
221
399
|
return snapshot();
|
|
222
400
|
};
|
|
223
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Synchronous committed snapshot for event hot paths.
|
|
404
|
+
* Returns `null` when not successfully loaded (unknown — never empty).
|
|
405
|
+
*/
|
|
406
|
+
const getSnapshotSync = () => (loaded && writable ? snapshot() : null);
|
|
407
|
+
|
|
224
408
|
return {
|
|
225
409
|
load,
|
|
226
410
|
get,
|
|
227
411
|
getAll,
|
|
228
412
|
list: getAll,
|
|
229
|
-
|
|
413
|
+
getSnapshotSync,
|
|
414
|
+
isLoaded: () => loaded && writable,
|
|
415
|
+
getLoadFailureReason: () => loadFailureReason,
|
|
230
416
|
has,
|
|
231
417
|
setSessionMetadata,
|
|
418
|
+
mutateSessionMetadata,
|
|
232
419
|
removeSession,
|
|
233
420
|
filePath,
|
|
234
421
|
};
|