@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
- package/dist/assets/AssistantView-pBywAsqy.js +3 -0
- package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B984aE-R.js} +1 -1
- package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
- package/dist/assets/Icon-BN87CyS4.js +1 -0
- package/dist/assets/MarkdownRenderer-DHiNU8n3.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-xJHMGHju.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-BZflns6x.js +3 -0
- package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DUvxsvrB.js} +1 -1
- package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-DWo-J3Vc.js} +3 -3
- package/dist/assets/MobileSurface-B-Y6U5xm.js +2 -0
- package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-rYiVSNKK.js} +1 -1
- package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
- package/dist/assets/SettingsView-pjD0LrHE.js +25 -0
- package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CFOQRwjA.js} +1 -1
- package/dist/assets/TerminalView-Dw5jaYkc.js +8 -0
- package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-B4v-ona2.js} +1 -1
- package/dist/assets/appThemeRegistry-BqDCGocy.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
- package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
- package/dist/assets/collapsible-nU99prkD.js +1 -0
- package/dist/assets/context-menu-DMzZN1oi.js +1 -0
- package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
- package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
- package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
- package/dist/assets/dialog-ByGUkS3c.js +1 -0
- package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
- package/dist/assets/es-DCjl24dV.js +15 -0
- package/dist/assets/fileMentionTouchSelection-Cgkxwzqb.js +2 -0
- package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
- package/dist/assets/fr-DfJ5j0t5.js +13 -0
- package/dist/assets/{gitApi-CM42qUed.js → gitApi-Dwqkc2nZ.js} +3 -3
- package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
- package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
- package/dist/assets/insertionBoundaries-BhWfAOPY.js +1 -0
- package/dist/assets/ja-B_9_w2Xf.js +15 -0
- package/dist/assets/ko-DlGbo3kV.js +15 -0
- package/dist/assets/main-CGMinhgQ.js +169 -0
- package/dist/assets/main-pMpcLJha.js +2 -0
- package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-DfrW3VHF.js} +1 -1
- package/dist/assets/miniChat-YOt30ECc.js +2 -0
- package/dist/assets/mobile-BK1bBCYD.js +2 -0
- package/dist/assets/multirun-Dua24A69.js +3 -0
- package/dist/assets/persistence-Dnj1QOdX.js +3 -0
- package/dist/assets/pl-DjUsS8Ei.js +15 -0
- package/dist/assets/platform-X3Y3GyM5.js +6 -0
- package/dist/assets/projectMeta-DXdoPmbz.js +1 -0
- package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-BOuEz_oT.js +2 -0
- package/dist/assets/renderMobileApp-C8Alpv-B.js +28 -0
- package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
- package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
- package/dist/assets/runtimeConfig-DX_NP2Zm.js +2 -0
- package/dist/assets/runtimeEndpointReset-BSQU3e5U.js +148 -0
- package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
- package/dist/assets/sessionChangeSummary-8AIBkExf.js +2 -0
- package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-Bdwd1Fit.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
- package/dist/assets/src-OzbnJsph.css +2 -0
- package/dist/assets/store-Cuktref-.js +15 -0
- package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
- package/dist/assets/textarea-biAqiqnJ.js +1 -0
- package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
- package/dist/assets/uk-Bh7HXyLp.js +15 -0
- package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
- package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-bafOEpgK.js} +57 -52
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-CkCVOkxa.js +1 -0
- package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-B7htpJfU.js} +1 -1
- package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
- package/dist/assets/useMobileNavigationStore-D2tnhKYV.js +415 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BC5OOH3M.js +2 -0
- package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
- package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-BNQUAMEB.js} +1 -1
- package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
- package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
- package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
- package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
- package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
- package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
- package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
- package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
- package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
- package/dist/index.html +15 -15
- package/dist/mini-chat.html +15 -15
- package/dist/mobile.html +15 -15
- package/package.json +3 -2
- package/server/index.js +46 -27
- package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/message-queue/service.js +1 -1
- package/server/lib/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +128 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +36 -19
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +16 -124
- package/server/lib/opencode/config-entity-routes.test.js +48 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +18 -1
- package/server/lib/opencode/feature-routes-runtime.js +19 -7
- package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugin-routes.test.js +4 -2
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/plugins.test.js +5 -3
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +13 -1
- package/server/lib/opencode/proxy.test.js +31 -3
- package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
- package/server/lib/opencode/routes.js +40 -8
- package/server/lib/opencode/routes.test.js +51 -10
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/update-discovery.js +39 -0
- package/server/lib/opencode/update-discovery.test.js +39 -0
- package/server/lib/opencode/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +292 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -0
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
- package/server/lib/question-auto-delegate/core.d.ts +5 -1
- package/server/lib/question-auto-delegate/core.js +21 -13
- package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
- package/server/lib/question-auto-delegate/form-io.js +39 -0
- package/server/lib/question-auto-delegate/runtime.js +4 -24
- package/server/lib/question-auto-delegate/runtime.test.js +138 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +3 -1
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
- package/server/lib/scheduled-tasks/runtime.js +12 -4
- package/server/lib/scheduled-tasks/runtime.test.js +81 -6
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/session-goal/runtime.js +4 -4
- package/server/lib/session-goal/runtime.test.js +32 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +5 -1
- package/server/lib/session-metadata/session-archive.js +1 -1
- package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
- package/server/lib/session-metadata/session-metadata-store.js +63 -16
- package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
- package/server/lib/session-metadata/session-projection.d.ts +1 -0
- package/server/lib/session-metadata/session-projection.js +49 -2
- package/server/lib/session-metadata/session-projection.test.js +35 -0
- package/server/lib/session-title/DOCUMENTATION.md +52 -14
- package/server/lib/session-title/native-runtime.test.js +59 -5
- package/server/lib/session-title/runtime.js +65 -19
- package/server/lib/session-title/session-access.js +108 -3
- package/server/lib/session-title/session-access.test.js +125 -7
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
- package/dist/assets/Icon-BiEQEcih.js +0 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
- package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
- package/dist/assets/SettingsView-BRxELjow.js +0 -25
- package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
- package/dist/assets/collapsible-DXbDynN4.js +0 -3
- package/dist/assets/context-menu-CqolsLCm.js +0 -1
- package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
- package/dist/assets/dialog-DQYl3W_o.js +0 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
- package/dist/assets/es-CN3E_zXj.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
- package/dist/assets/fr-9zZvQZD2.js +0 -13
- package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
- package/dist/assets/ja-Dnjk_82N.js +0 -15
- package/dist/assets/ko-BxCEE99C.js +0 -15
- package/dist/assets/main-BILgBI3B.js +0 -169
- package/dist/assets/main-BeveiW6F.js +0 -2
- package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
- package/dist/assets/miniChat-BgrzM3u7.js +0 -2
- package/dist/assets/mobile-DtYoQE8C.js +0 -2
- package/dist/assets/multirun-jy8dEI96.js +0 -4
- package/dist/assets/persistence-C9LQpiOS.js +0 -3
- package/dist/assets/pl-CNNsWnPM.js +0 -15
- package/dist/assets/platform-BQghHRfp.js +0 -6
- package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
- package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
- package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
- package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
- package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
- package/dist/assets/src-CLN7Z-zx.css +0 -2
- package/dist/assets/store-DLCiZaA6.js +0 -15
- package/dist/assets/textarea-CakKvqFw.js +0 -1
- package/dist/assets/uk-rwQrxeYM.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
- package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
- package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -13,6 +13,21 @@ import {
|
|
|
13
13
|
} from './reasoning-projection.js';
|
|
14
14
|
|
|
15
15
|
describe('shouldIncludeReasoning / query helpers', () => {
|
|
16
|
+
it('projects v2 list/context and exact snapshots without dropping rows or cursor', () => {
|
|
17
|
+
const row = { id: 'm', type: 'assistant', tokens: { reasoning: 12 }, content: [
|
|
18
|
+
{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
|
|
19
|
+
] };
|
|
20
|
+
const payload = { data: [row], cursor: { next: 'older' } };
|
|
21
|
+
const projected = projectMessagesPayloadForReasoning(payload, false);
|
|
22
|
+
expect(projected).toEqual({ data: [{ ...row, content: [row.content[1]] }], cursor: payload.cursor });
|
|
23
|
+
expect(projectMessagesPayloadForReasoning(row, false)).toEqual(projected.data[0]);
|
|
24
|
+
expect(projectMessagesPayloadForReasoning(payload, true)).toBe(payload);
|
|
25
|
+
expect(row.content).toHaveLength(2);
|
|
26
|
+
const filter = createReasoningOutboundFilter();
|
|
27
|
+
expect(filter.projectEvent({ type: 'session.reasoning.delta.1', data: { delta: 'hidden' } })).toBeNull();
|
|
28
|
+
const text = { type: 'session.text.delta', data: { delta: 'visible' } };
|
|
29
|
+
expect(filter.projectEvent(text)).toBe(text);
|
|
30
|
+
});
|
|
16
31
|
it('only treats the strict string false as disabled', () => {
|
|
17
32
|
expect(shouldIncludeReasoning(undefined)).toBe(true);
|
|
18
33
|
expect(shouldIncludeReasoning(null)).toBe(true);
|
|
@@ -54,6 +54,7 @@ The following functions are exported and used by the web server:
|
|
|
54
54
|
- `validateWorktreeCreate(directory, input)`: Validate worktree creation parameters (mode, branchName, startRef, upstream config).
|
|
55
55
|
- `createWorktree(directory, input)`: Create a new worktree (supports 'new' and 'existing' modes, upstream setup).
|
|
56
56
|
- `removeWorktree(directory, input)`: Remove a worktree (optionally delete local branch).
|
|
57
|
+
- `removeWorktree(directory, input, { beforeRemove })`: optional host hook after primary-workspace/catalog validation, immediately before removal of a matched worktree or managed orphan. The hook never runs for unrelated orphan paths. Production hosts use the official v2 `debug.location.evict` with the validated target and current upstream URL/auth; the 2s budget returns `released`, `failed`, `timeout`, or `unavailable`. Eviction is best effort and never counts as successful Git removal. DELETE responses include `locationRelease` when attempted. Shared-service explicit removal retains the existing force-delete semantics for that target; it is not automatic idle cleanup and does not interrupt unrelated sessions. An eviction acknowledgement is cache detachment, not a guarantee that active borrowers released OS handles. Queue rollback and topology publication remain tied to the Git result.
|
|
57
58
|
- `isLinkedWorktree(directory)`: Check if directory is a linked worktree (not primary).
|
|
58
59
|
|
|
59
60
|
### Commit and Remote Operations
|
package/server/lib/git/routes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function registerGitRoutes(app, { messageQueueService = null, createRequestID = () => globalThis.crypto.randomUUID(), broadcastWorktreeTopologyChanged = () => {} } = {}) {
|
|
1
|
+
export function registerGitRoutes(app, { messageQueueService = null, createRequestID = () => globalThis.crypto.randomUUID(), broadcastWorktreeTopologyChanged = () => {}, releaseWorktreeLocation } = {}) {
|
|
2
2
|
const normalizeWorktreePath = (value) => typeof value === 'string' ? value.trim().replace(/\\/g, '/').replace(/\/+$/, '') : '';
|
|
3
3
|
const lifecycleOperations = new Map();
|
|
4
4
|
const acquireWorktreeLifecycleOperation = (runtimeKey, directory) => {
|
|
@@ -1282,11 +1282,14 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
|
|
|
1282
1282
|
}
|
|
1283
1283
|
|
|
1284
1284
|
let result;
|
|
1285
|
+
let locationRelease;
|
|
1285
1286
|
try {
|
|
1286
1287
|
result = await removeWorktree(directory, {
|
|
1287
1288
|
directory: worktreeDirectory,
|
|
1288
1289
|
deleteLocalBranch: req.body?.deleteLocalBranch === true,
|
|
1289
|
-
})
|
|
1290
|
+
}, ...(releaseWorktreeLocation ? [{ beforeRemove: async (target) => {
|
|
1291
|
+
locationRelease = await releaseWorktreeLocation(target);
|
|
1292
|
+
} }] : []));
|
|
1290
1293
|
} catch (error) {
|
|
1291
1294
|
if (messageQueueService) {
|
|
1292
1295
|
try {
|
|
@@ -1328,7 +1331,7 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
|
|
|
1328
1331
|
console.warn('Failed to roll back message queue worktree deletion');
|
|
1329
1332
|
}
|
|
1330
1333
|
}
|
|
1331
|
-
return res.json({ success: false });
|
|
1334
|
+
return res.json({ success: false, ...(locationRelease ? { locationRelease } : {}) });
|
|
1332
1335
|
}
|
|
1333
1336
|
|
|
1334
1337
|
notifyWorktreeTopologyChanged(directory, worktreeDirectory, 'removed');
|
|
@@ -1346,7 +1349,7 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
|
|
|
1346
1349
|
return res.status(500).json({ error: 'Failed to commit worktree queue deletion' });
|
|
1347
1350
|
}
|
|
1348
1351
|
}
|
|
1349
|
-
res.json({ success: true });
|
|
1352
|
+
res.json({ success: true, ...(locationRelease ? { locationRelease } : {}) });
|
|
1350
1353
|
} finally {
|
|
1351
1354
|
releaseWorktreeLifecycleOperation(operation);
|
|
1352
1355
|
}
|
|
@@ -69,6 +69,40 @@ const createMockResponse = () => {
|
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
describe('worktree eviction best effort', () => {
|
|
73
|
+
it.each(['released', 'failed', 'timeout', 'unavailable'])('preserves successful deletion with release outcome %s', async (state) => {
|
|
74
|
+
const { app, getRoute } = createRouteRegistry();
|
|
75
|
+
const releaseWorktreeLocation = vi.fn(async () => ({ state }));
|
|
76
|
+
const broadcastWorktreeTopologyChanged = vi.fn();
|
|
77
|
+
registerGitRoutes(app, { releaseWorktreeLocation, broadcastWorktreeTopologyChanged });
|
|
78
|
+
gitLibraries.removeWorktree.mockImplementation(async (_root, _input, options) => {
|
|
79
|
+
await options.beforeRemove('/validated/worktree');
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
const response = createMockResponse();
|
|
83
|
+
await getRoute('DELETE', '/api/git/worktrees')({ query: { directory: '/repo' }, body: { directory: '/requested/worktree' } }, response);
|
|
84
|
+
expect(releaseWorktreeLocation).toHaveBeenCalledWith('/validated/worktree');
|
|
85
|
+
expect(response.body).toEqual({ success: true, locationRelease: { state } });
|
|
86
|
+
expect(broadcastWorktreeTopologyChanged).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
it('does not publish topology when removal fails after successful release', async () => {
|
|
89
|
+
const { app, getRoute } = createRouteRegistry();
|
|
90
|
+
const broadcastWorktreeTopologyChanged = vi.fn();
|
|
91
|
+
registerGitRoutes(app, { releaseWorktreeLocation: async () => ({ state: 'released' }), broadcastWorktreeTopologyChanged });
|
|
92
|
+
gitLibraries.removeWorktree.mockImplementation(async (_root, _input, options) => {
|
|
93
|
+
await options.beforeRemove('/repo/worktree');
|
|
94
|
+
throw new Error('deletion failed');
|
|
95
|
+
});
|
|
96
|
+
const response = createMockResponse();
|
|
97
|
+
const log = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
98
|
+
try {
|
|
99
|
+
await getRoute('DELETE', '/api/git/worktrees')({ query: { directory: '/repo' }, body: { directory: '/repo/worktree' } }, response);
|
|
100
|
+
} finally { log.mockRestore(); }
|
|
101
|
+
expect(response.statusCode).toBe(500);
|
|
102
|
+
expect(broadcastWorktreeTopologyChanged).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
72
106
|
describe('git routes batch discover', () => {
|
|
73
107
|
beforeEach(() => {
|
|
74
108
|
gitLibraries.isGitRepository.mockReset();
|
|
@@ -3851,7 +3851,7 @@ export async function getWorktreeBootstrapStatus(directory) {
|
|
|
3851
3851
|
};
|
|
3852
3852
|
}
|
|
3853
3853
|
|
|
3854
|
-
export async function removeWorktree(directory, input = {}) {
|
|
3854
|
+
export async function removeWorktree(directory, input = {}, { beforeRemove } = {}) {
|
|
3855
3855
|
const targetDirectory = normalizeDirectoryPath(input?.directory);
|
|
3856
3856
|
if (!targetDirectory) {
|
|
3857
3857
|
throw new Error('Worktree directory is required');
|
|
@@ -3887,6 +3887,7 @@ export async function removeWorktree(directory, input = {}) {
|
|
|
3887
3887
|
|
|
3888
3888
|
const targetExists = await checkPathExists(targetDirectory);
|
|
3889
3889
|
if (targetExists && isManagedOrphan) {
|
|
3890
|
+
await beforeRemove?.(targetDirectory);
|
|
3890
3891
|
await fsp.rm(targetDirectory, { recursive: true, force: true });
|
|
3891
3892
|
}
|
|
3892
3893
|
|
|
@@ -3901,6 +3902,7 @@ export async function removeWorktree(directory, input = {}) {
|
|
|
3901
3902
|
return true;
|
|
3902
3903
|
}
|
|
3903
3904
|
|
|
3905
|
+
await beforeRemove?.(matchedEntry.worktree);
|
|
3904
3906
|
await runGitCommandOrThrow(
|
|
3905
3907
|
context.primaryWorktree,
|
|
3906
3908
|
['worktree', 'remove', '--force', matchedEntry.worktree],
|
|
@@ -39,8 +39,8 @@ call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
|
|
|
39
39
|
`openchamber-text` agent (same temp root, same deny-all verification and
|
|
40
40
|
`session.create` / `session.remove` lifecycle as the attachment path below):
|
|
41
41
|
|
|
42
|
-
1. Ensure `.opencode/agent/openchamber-text.md` and verify its
|
|
43
|
-
permission rule is deny-all before creating the session.
|
|
42
|
+
1. Ensure `.opencode/agent/openchamber-text.md` and verify its last catch-all
|
|
43
|
+
permission rule is deny-all, with only deny rules after it, before creating the session.
|
|
44
44
|
2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
|
|
45
45
|
3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
|
|
46
46
|
the session's provider context, no tool loop, no messages written. System
|
|
@@ -62,8 +62,10 @@ data-URL file parts:
|
|
|
62
62
|
|
|
63
63
|
1. Ensure the throwaway temp directory agent markdown (`openchamber-llm`,
|
|
64
64
|
permissions array ending in `action:* resource:* effect:deny`).
|
|
65
|
-
2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **
|
|
66
|
-
permissions rule to be deny-all.
|
|
65
|
+
2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **last catch-all**
|
|
66
|
+
permissions rule to be deny-all, with only deny rules after it. This accepts
|
|
67
|
+
OpenCode's appended `browser` deny while rejecting trailing allow/ask rules.
|
|
68
|
+
Failure → `llm_attachment_generation_unavailable`
|
|
67
69
|
**before** any `session.prompt`. Title is never treated as permission isolation.
|
|
68
70
|
3. `session.create({ location, agent, model: { id, providerID, variant? },
|
|
69
71
|
metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
|
|
@@ -18,7 +18,7 @@ const positiveMs = (value, fallback) => (
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Agent frontmatter for official client agent permissions (action/resource/effect).
|
|
21
|
-
* The final
|
|
21
|
+
* The final catch-all must deny, with only denies after it; verified before prompt.
|
|
22
22
|
*/
|
|
23
23
|
const AGENT_MARKDOWN = `---
|
|
24
24
|
mode: primary
|
|
@@ -308,7 +308,7 @@ const readGeneratorAgent = async ({ client, location, signal, agentID, waitMs, p
|
|
|
308
308
|
};
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
|
-
* Verify the generator agent
|
|
311
|
+
* Verify the generator agent has a deny-all rule followed only by denies.
|
|
312
312
|
* Title alone is never treated as isolation. Failure must happen before prompt.
|
|
313
313
|
*/
|
|
314
314
|
export async function assertLlmAgentDenyAll({
|
|
@@ -336,13 +336,12 @@ export async function assertLlmAgentDenyAll({
|
|
|
336
336
|
'llm_attachment_generation_unavailable',
|
|
337
337
|
);
|
|
338
338
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
) {
|
|
339
|
+
const lastCatchAll = permissions.findLastIndex(
|
|
340
|
+
(rule) => rule?.action === '*' && rule?.resource === '*',
|
|
341
|
+
);
|
|
342
|
+
const isolated = lastCatchAll > -1
|
|
343
|
+
&& permissions.slice(lastCatchAll).every((rule) => rule?.effect === 'deny');
|
|
344
|
+
if (!isolated) {
|
|
346
345
|
failGenerate(
|
|
347
346
|
'OpenCode LLM agent final permission is not deny-all; generation blocked',
|
|
348
347
|
'llm_attachment_generation_unavailable',
|
|
@@ -127,8 +127,16 @@ function textSessionClient({ generate = vi.fn(async () => ({ text: 'reply' })),
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
describe('generateOpenCodeText — text path', () => {
|
|
130
|
-
it
|
|
131
|
-
|
|
130
|
+
it.each([
|
|
131
|
+
{ name: 'deny-all alone', suffix: [] },
|
|
132
|
+
{ name: 'appended browser deny', suffix: [{ action: 'browser', resource: '*', effect: 'deny' }] },
|
|
133
|
+
])('runs session.generate in a verified throwaway session ($name) and removes it', async ({ suffix }) => {
|
|
134
|
+
const { client, agentGet, create, generate, remove, prompt } = textSessionClient({
|
|
135
|
+
agentGet: vi.fn(async () => ({
|
|
136
|
+
...denyAllTextAgent,
|
|
137
|
+
data: { ...denyAllTextAgent.data, permissions: [...denyAllTextAgent.data.permissions, ...suffix] },
|
|
138
|
+
})),
|
|
139
|
+
})
|
|
132
140
|
const ensureTempDirectory = vi.fn(async ({ agentName, agentMarkdown }) => {
|
|
133
141
|
expect(agentName).toBe(_test.TEXT_AGENT_NAME)
|
|
134
142
|
expect(agentMarkdown).toBe(_test.TEXT_AGENT_MARKDOWN)
|
|
@@ -188,10 +196,23 @@ describe('generateOpenCodeText — text path', () => {
|
|
|
188
196
|
}
|
|
189
197
|
})
|
|
190
198
|
|
|
191
|
-
it(
|
|
199
|
+
it.each([
|
|
200
|
+
{ name: 'no catch-all', permissions: [{ action: 'browser', resource: '*', effect: 'deny' }] },
|
|
201
|
+
...['allow', 'ask'].flatMap((effect) => ['*', 'edit'].map((action) => ({
|
|
202
|
+
name: `trailing ${action} ${effect} before browser deny`,
|
|
203
|
+
permissions: [
|
|
204
|
+
...denyAllTextAgent.data.permissions,
|
|
205
|
+
{ action, resource: '*', effect },
|
|
206
|
+
{ action: 'browser', resource: '*', effect: 'deny' },
|
|
207
|
+
],
|
|
208
|
+
}))),
|
|
209
|
+
{ name: 'missing permissions', permissions: undefined },
|
|
210
|
+
{ name: 'empty permissions', permissions: [] },
|
|
211
|
+
{ name: 'invalid trailing rule', permissions: [...denyAllTextAgent.data.permissions, null] },
|
|
212
|
+
])('blocks before creating a session: $name', async ({ permissions }) => {
|
|
192
213
|
const { client, create, generate } = textSessionClient({
|
|
193
214
|
agentGet: vi.fn(async () => ({
|
|
194
|
-
data: { id: 'openchamber-text', permissions
|
|
215
|
+
data: { id: 'openchamber-text', permissions },
|
|
195
216
|
})),
|
|
196
217
|
})
|
|
197
218
|
await expect(generateOpenCodeText({
|
|
@@ -316,8 +337,14 @@ describe('generateOpenCodeText — text path', () => {
|
|
|
316
337
|
})
|
|
317
338
|
|
|
318
339
|
describe('generateOpenCodeText — attachment session path', () => {
|
|
319
|
-
it(
|
|
320
|
-
|
|
340
|
+
it.each([
|
|
341
|
+
{ name: 'deny-all alone', suffix: [] },
|
|
342
|
+
{ name: 'appended browser deny', suffix: [{ action: 'browser', resource: '*', effect: 'deny' }] },
|
|
343
|
+
])('verifies deny-all ($name) before prompt, prompts with files, waits, lists, and removes', async ({ suffix }) => {
|
|
344
|
+
const agentGet = vi.fn(async () => ({
|
|
345
|
+
...denyAllAgent,
|
|
346
|
+
data: { ...denyAllAgent.data, permissions: [...denyAllAgent.data.permissions, ...suffix] },
|
|
347
|
+
}))
|
|
321
348
|
const create = vi.fn(async () => ({ id: 'ses_tmp' }))
|
|
322
349
|
const put = vi.fn(async () => undefined)
|
|
323
350
|
const prompt = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }))
|
|
@@ -246,7 +246,7 @@ export const createMessageQueueService = ({ dbPath, getRuntimeConfig = () => nul
|
|
|
246
246
|
totalPasteLength += reference.text.length;
|
|
247
247
|
if (totalPasteLength > MAX_COMPOSER_PASTE_TOTAL_LENGTH) fail('validation_error');
|
|
248
248
|
canonical += reference.text;
|
|
249
|
-
} else if (reference.kind === 'skill' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'skillName'].includes(key)) && nonEmptyString(reference.skillName, MAX_COMPOSER_SKILL_NAME) && /^[
|
|
249
|
+
} else if (reference.kind === 'skill' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'skillName'].includes(key)) && nonEmptyString(reference.skillName, MAX_COMPOSER_SKILL_NAME) && /^[^\s/\\[\]\r\n]+$/u.test(reference.skillName) && [`/${reference.skillName}`, `/\u2003${reference.skillName}`, `@${reference.skillName}`, `@\u2003${reference.skillName}`].includes(reference.display)) canonical += `[skill:${reference.skillName}]`;
|
|
250
250
|
else if (reference.kind === 'command' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'commandName', 'reference'].includes(key)) && nonEmptyString(reference.commandName, MAX_COMPOSER_COMMAND_NAME) && /^[\p{L}\p{N}._/-]+$/u.test(reference.commandName) && nonEmptyString(reference.reference, MAX_COMPOSER_COMMAND_REFERENCE) && !/[\]\r\n]/u.test(reference.reference) && (reference.display === `/${reference.commandName}` || reference.display === `/\u2003${reference.commandName}`)) canonical += `[command:${reference.reference}]`;
|
|
251
251
|
else fail('validation_error');
|
|
252
252
|
previousEnd = reference.end;
|
|
@@ -107,7 +107,9 @@ This module provides notification message preparation utilities for the web serv
|
|
|
107
107
|
- `extractLastMessageText(payload, maxLength?)`
|
|
108
108
|
- `fetchLastAssistantMessageText(sessionId, messageId, maxLength?)`
|
|
109
109
|
- `maybeCacheSessionInfoFromEvent(payload)`
|
|
110
|
-
- `buildTemplateVariables(payload, sessionId)`
|
|
110
|
+
- `buildTemplateVariables(payload, sessionId)` — `session_name` is the session title. OpenCode 2 returns that title on `GET /api/session/:id` as `{ data: { title } }` and on `session.created` / `session.renamed` as `data.title`. Legacy `properties.info.title` still counts. An empty title stays empty so push can fall back to the localized "Session" label. `agent_name` / `model_name` fall back to that same session record when the triggering event has none. `fetchLastAssistantMessageText` accepts a legacy message array and an OpenCode 2 `{ data }` page (`order=desc`).
|
|
111
|
+
- Question prompts come from legacy `question.asked` or OpenCode 2 `form.created` (`data.form`). Permission prompts read `permission.asked` from `properties` or `data` (`action`, `message`, `id`). `form.replied` / `form.cancelled` cancel a pending question notification the same way `permission.replied` cancels a permission notification.
|
|
112
|
+
- iOS APNs and Android FCM share this payload: native banners stay scenario title + session name. Desktop, in-app, and web-push use the resolved template title and body.
|
|
111
113
|
- `getCachedZenModels()`
|
|
112
114
|
|
|
113
115
|
## Constants
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
import { readNotificationSessionRecord } from './template-runtime.js';
|
|
2
|
+
|
|
3
|
+
const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
|
|
4
|
+
|
|
5
|
+
const readNotificationEventFields = (payload) => {
|
|
6
|
+
const properties = payload?.properties && typeof payload.properties === 'object' && !Array.isArray(payload.properties)
|
|
7
|
+
? payload.properties
|
|
8
|
+
: {};
|
|
9
|
+
const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
|
|
10
|
+
? payload.data
|
|
11
|
+
: {};
|
|
12
|
+
const form = (properties.form && typeof properties.form === 'object' ? properties.form : null)
|
|
13
|
+
|| (data.form && typeof data.form === 'object' ? data.form : null);
|
|
14
|
+
const questions = Array.isArray(properties.questions)
|
|
15
|
+
? properties.questions
|
|
16
|
+
: (Array.isArray(data.questions) ? data.questions : []);
|
|
17
|
+
const firstQuestion = questions[0] && typeof questions[0] === 'object' ? questions[0] : null;
|
|
18
|
+
const field = Array.isArray(form?.fields)
|
|
19
|
+
? form.fields.find((item) => item && typeof item === 'object' && item.hidden !== true)
|
|
20
|
+
: null;
|
|
21
|
+
return {
|
|
22
|
+
requestId: trimmedText(properties.id)
|
|
23
|
+
|| trimmedText(properties.requestID)
|
|
24
|
+
|| trimmedText(properties.requestId)
|
|
25
|
+
|| trimmedText(data.id)
|
|
26
|
+
|| trimmedText(data.requestID)
|
|
27
|
+
|| trimmedText(data.requestId)
|
|
28
|
+
|| trimmedText(form?.id),
|
|
29
|
+
permission: trimmedText(properties.permission) || trimmedText(data.permission) || trimmedText(data.action),
|
|
30
|
+
message: trimmedText(data.message) || trimmedText(properties.message),
|
|
31
|
+
header: trimmedText(firstQuestion?.header) || trimmedText(field?.title) || trimmedText(form?.title),
|
|
32
|
+
questionText: trimmedText(firstQuestion?.question)
|
|
33
|
+
|| trimmedText(field?.description)
|
|
34
|
+
|| trimmedText(field?.title)
|
|
35
|
+
|| trimmedText(form?.title),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
1
39
|
export const createNotificationTriggerRuntime = (deps) => {
|
|
2
40
|
const {
|
|
3
41
|
readSettingsFromDisk,
|
|
@@ -255,14 +293,14 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
255
293
|
if (!response.ok) {
|
|
256
294
|
return cached;
|
|
257
295
|
}
|
|
258
|
-
const session = await response.json().catch(() => null);
|
|
259
|
-
if (!session
|
|
296
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
297
|
+
if (!session) {
|
|
260
298
|
return cached;
|
|
261
299
|
}
|
|
262
300
|
|
|
263
301
|
const meta = {
|
|
264
302
|
parentID: normalizeParentID(session.parentID),
|
|
265
|
-
title:
|
|
303
|
+
title: trimmedText(session.title) || null,
|
|
266
304
|
complete: true,
|
|
267
305
|
...metaFromOpenchamber(session.metadata?.openchamber),
|
|
268
306
|
};
|
|
@@ -346,13 +384,24 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
346
384
|
|
|
347
385
|
const extractSessionIdFromPayload = (payload) => {
|
|
348
386
|
if (!payload || typeof payload !== 'object') return null;
|
|
349
|
-
const props = payload.properties
|
|
387
|
+
const props = payload.properties && typeof payload.properties === 'object'
|
|
388
|
+
? payload.properties
|
|
389
|
+
: (payload.data && typeof payload.data === 'object' ? payload.data : null);
|
|
350
390
|
const info = props?.info;
|
|
391
|
+
const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
|
|
392
|
+
? payload.data
|
|
393
|
+
: null;
|
|
351
394
|
const sessionId =
|
|
352
395
|
info?.sessionID ??
|
|
353
396
|
info?.sessionId ??
|
|
354
397
|
props?.sessionID ??
|
|
355
398
|
props?.sessionId ??
|
|
399
|
+
props?.form?.sessionID ??
|
|
400
|
+
props?.form?.sessionId ??
|
|
401
|
+
data?.sessionID ??
|
|
402
|
+
data?.sessionId ??
|
|
403
|
+
data?.form?.sessionID ??
|
|
404
|
+
data?.form?.sessionId ??
|
|
356
405
|
props?.session ??
|
|
357
406
|
null;
|
|
358
407
|
return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
|
|
@@ -360,11 +409,32 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
360
409
|
|
|
361
410
|
const extractDirectoryFromPayload = (payload) => {
|
|
362
411
|
if (!payload || typeof payload !== 'object') return undefined;
|
|
363
|
-
const props = payload.properties;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
412
|
+
const props = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
|
|
413
|
+
const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
|
|
414
|
+
const info = props.info && typeof props.info === 'object' ? props.info : {};
|
|
415
|
+
const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
|
|
416
|
+
const candidates = [
|
|
417
|
+
props.directory,
|
|
418
|
+
info.directory,
|
|
419
|
+
info.location?.directory,
|
|
420
|
+
payload.location?.directory,
|
|
421
|
+
data.directory,
|
|
422
|
+
data.location?.directory,
|
|
423
|
+
dataInfo.directory,
|
|
424
|
+
dataInfo.location?.directory,
|
|
425
|
+
];
|
|
426
|
+
for (const value of candidates) {
|
|
427
|
+
const directory = trimmedText(value);
|
|
428
|
+
if (directory) return directory;
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const resolvePushSessionName = async (variables, sessionId, directory) => {
|
|
434
|
+
const fromTemplate = trimmedText(variables?.session_name);
|
|
435
|
+
if (fromTemplate) return fromTemplate;
|
|
436
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
437
|
+
return trimmedText(meta?.title);
|
|
368
438
|
};
|
|
369
439
|
|
|
370
440
|
// A session with an ACTIVE goal suppresses per-turn ready notifications;
|
|
@@ -381,7 +451,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
381
451
|
signal: AbortSignal.timeout(2000),
|
|
382
452
|
});
|
|
383
453
|
if (!response.ok) return false;
|
|
384
|
-
const session = await response.json().catch(() => null);
|
|
454
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
385
455
|
const goal = session?.metadata?.openchamber?.goal;
|
|
386
456
|
return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
|
|
387
457
|
} catch {
|
|
@@ -417,20 +487,31 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
417
487
|
const reason = payload.properties?.reason ?? payload.data?.reason;
|
|
418
488
|
if (reason === 'shutdown') return;
|
|
419
489
|
}
|
|
420
|
-
|
|
421
|
-
|
|
490
|
+
// v2 completion is session.execution.succeeded. session.idle is a deprecated
|
|
491
|
+
// fallback; the ready-notification cooldown below collapses the pair.
|
|
492
|
+
if ((
|
|
493
|
+
payload.type === 'session.idle'
|
|
494
|
+
|| payload.type === 'session.execution.succeeded'
|
|
495
|
+
|| payload.type === 'session.error'
|
|
496
|
+
|| payload.type === 'session.execution.failed'
|
|
497
|
+
) && sessionId) {
|
|
498
|
+
const body = payload.properties && typeof payload.properties === 'object'
|
|
499
|
+
? payload.properties
|
|
500
|
+
: (payload.data && typeof payload.data === 'object' ? payload.data : {});
|
|
501
|
+
const error = body?.error;
|
|
422
502
|
const errorText = typeof error?.message === 'string'
|
|
423
503
|
? error.message
|
|
424
504
|
: typeof error === 'string' ? error : '';
|
|
505
|
+
const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
|
|
425
506
|
await maybeSendPushForTrigger({
|
|
426
507
|
...payload,
|
|
427
508
|
type: 'message.updated',
|
|
428
509
|
properties: {
|
|
429
|
-
...payload.properties,
|
|
510
|
+
...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
|
|
430
511
|
info: {
|
|
431
512
|
sessionID: sessionId,
|
|
432
513
|
role: 'assistant',
|
|
433
|
-
finish:
|
|
514
|
+
finish: failed ? 'error' : 'stop',
|
|
434
515
|
...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
|
|
435
516
|
},
|
|
436
517
|
},
|
|
@@ -492,12 +573,12 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
492
573
|
const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
|
|
493
574
|
|
|
494
575
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
495
|
-
sessionName =
|
|
576
|
+
sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
|
|
496
577
|
|
|
497
578
|
const messageId = info?.id;
|
|
498
579
|
let lastMessage = extractLastMessageText(payload);
|
|
499
580
|
if (!lastMessage) {
|
|
500
|
-
lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
|
|
581
|
+
lastMessage = await fetchLastAssistantMessageText(sessionId, messageId, undefined, notificationDirectory);
|
|
501
582
|
}
|
|
502
583
|
|
|
503
584
|
variables.last_message = await prepareNotificationLastMessage({
|
|
@@ -548,7 +629,21 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
548
629
|
return;
|
|
549
630
|
}
|
|
550
631
|
|
|
551
|
-
if (
|
|
632
|
+
if ((
|
|
633
|
+
payload.type === 'form.replied'
|
|
634
|
+
|| payload.type === 'form.cancelled'
|
|
635
|
+
|| payload.type === 'question.replied'
|
|
636
|
+
|| payload.type === 'question.rejected'
|
|
637
|
+
) && sessionId) {
|
|
638
|
+
const existingTimer = pushQuestionDebounceTimers.get(sessionId);
|
|
639
|
+
if (existingTimer) {
|
|
640
|
+
clearTimeout(existingTimer);
|
|
641
|
+
pushQuestionDebounceTimers.delete(sessionId);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if ((payload.type === 'question.asked' || payload.type === 'form.created') && sessionId) {
|
|
552
647
|
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
553
648
|
return;
|
|
554
649
|
}
|
|
@@ -569,9 +664,9 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
569
664
|
return;
|
|
570
665
|
}
|
|
571
666
|
|
|
572
|
-
const
|
|
573
|
-
const header =
|
|
574
|
-
const questionText =
|
|
667
|
+
const prompt = readNotificationEventFields(payload);
|
|
668
|
+
const header = prompt.header;
|
|
669
|
+
const questionText = prompt.questionText;
|
|
575
670
|
|
|
576
671
|
let title = 'Needs your answer';
|
|
577
672
|
let body = '';
|
|
@@ -579,7 +674,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
579
674
|
|
|
580
675
|
try {
|
|
581
676
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
582
|
-
sessionName =
|
|
677
|
+
sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
|
|
583
678
|
variables.last_message = questionText || header || '';
|
|
584
679
|
|
|
585
680
|
const templates = settings.notificationTemplates || {};
|
|
@@ -630,8 +725,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
630
725
|
}
|
|
631
726
|
|
|
632
727
|
if (payload.type === 'permission.replied' && sessionId) {
|
|
633
|
-
const requestId = payload.
|
|
634
|
-
const requestKey =
|
|
728
|
+
const requestId = readNotificationEventFields(payload).requestId;
|
|
729
|
+
const requestKey = requestId ? `${sessionId}:${requestId}` : null;
|
|
635
730
|
const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
|
|
636
731
|
if (!pendingNotification) {
|
|
637
732
|
return;
|
|
@@ -651,9 +746,10 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
651
746
|
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
652
747
|
return;
|
|
653
748
|
}
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
const
|
|
749
|
+
const permissionFields = readNotificationEventFields(payload);
|
|
750
|
+
const requestId = permissionFields.requestId;
|
|
751
|
+
const permission = permissionFields.permission;
|
|
752
|
+
const requestKey = requestId ? `${sessionId}:${requestId}` : null;
|
|
657
753
|
if (requestKey && notifiedPermissionRequests.has(requestKey)) {
|
|
658
754
|
return;
|
|
659
755
|
}
|
|
@@ -691,11 +787,12 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
691
787
|
return;
|
|
692
788
|
}
|
|
693
789
|
|
|
694
|
-
const sessionTitle = payload.properties?.sessionTitle;
|
|
790
|
+
const sessionTitle = payload.properties?.sessionTitle ?? payload.data?.sessionTitle;
|
|
695
791
|
const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
|
|
792
|
+
const detail = permissionFields.message;
|
|
696
793
|
const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
|
|
697
794
|
? sessionTitle.trim()
|
|
698
|
-
: permissionText || 'Agent is waiting for your approval';
|
|
795
|
+
: detail || permissionText || 'Agent is waiting for your approval';
|
|
699
796
|
|
|
700
797
|
let title = 'Needs permission';
|
|
701
798
|
let body = '';
|
|
@@ -703,7 +800,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
703
800
|
|
|
704
801
|
try {
|
|
705
802
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
706
|
-
sessionName =
|
|
803
|
+
sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
|
|
707
804
|
variables.last_message = fallbackMessage;
|
|
708
805
|
|
|
709
806
|
const templates = settings.notificationTemplates || {};
|
|
@@ -771,8 +868,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
771
868
|
signal: AbortSignal.timeout(2000),
|
|
772
869
|
});
|
|
773
870
|
if (response.ok) {
|
|
774
|
-
const session = await response.json().catch(() => null);
|
|
775
|
-
|
|
871
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
872
|
+
sessionName = trimmedText(session?.title);
|
|
776
873
|
}
|
|
777
874
|
} catch {
|
|
778
875
|
// Session name is presentation sugar for the mobile push — never block on it.
|