@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
|
@@ -72,60 +72,103 @@ describe('generate cancellation', () => {
|
|
|
72
72
|
expect(interrupt).toHaveBeenCalledWith({ sessionID: 'ses_abort_fixture', continue: false })
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
it('aborts an in-flight generate
|
|
75
|
+
it('aborts an in-flight session.generate and still removes the throwaway session', async () => {
|
|
76
76
|
const controller = new AbortController()
|
|
77
|
-
const
|
|
77
|
+
const generate = vi.fn(async (_args, { signal }) => {
|
|
78
78
|
controller.abort()
|
|
79
79
|
signal.throwIfAborted()
|
|
80
80
|
})
|
|
81
|
+
const { client, remove, interrupt } = textSessionClient({ generate })
|
|
81
82
|
await expect(generateOpenCodeText({
|
|
82
83
|
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
83
84
|
getOpenCodeAuthHeaders: () => ({}),
|
|
84
85
|
providerID: 'opencode',
|
|
85
86
|
modelID: 'gpt-5-nano',
|
|
86
87
|
messages: [{ role: 'user', content: 'work' }],
|
|
87
|
-
clientFactory: () =>
|
|
88
|
+
clientFactory: () => client,
|
|
89
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
88
90
|
signal: controller.signal,
|
|
89
91
|
})).rejects.toMatchObject({ name: 'AbortError' })
|
|
90
|
-
expect(
|
|
92
|
+
expect(generate).toHaveBeenCalledTimes(1)
|
|
93
|
+
expect(interrupt).toHaveBeenCalledWith({ sessionID: 'ses_text', continue: false })
|
|
94
|
+
expect(remove).toHaveBeenCalledExactlyOnceWith({ sessionID: 'ses_text' })
|
|
91
95
|
})
|
|
92
96
|
})
|
|
93
97
|
|
|
98
|
+
const denyAllTextAgent = {
|
|
99
|
+
location: { directory: '/tmp/openchamber-text' },
|
|
100
|
+
data: {
|
|
101
|
+
id: 'openchamber-text',
|
|
102
|
+
name: 'openchamber-text',
|
|
103
|
+
mode: 'primary',
|
|
104
|
+
hidden: true,
|
|
105
|
+
permissions: [{ action: '*', resource: '*', effect: 'deny' }],
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function textSessionClient({ generate = vi.fn(async () => ({ text: 'reply' })), agentGet } = {}) {
|
|
110
|
+
const create = vi.fn(async () => ({ id: 'ses_text' }))
|
|
111
|
+
const remove = vi.fn(async () => undefined)
|
|
112
|
+
const interrupt = vi.fn(async () => undefined)
|
|
113
|
+
const prompt = vi.fn()
|
|
114
|
+
const get = agentGet || vi.fn(async () => denyAllTextAgent)
|
|
115
|
+
return {
|
|
116
|
+
client: {
|
|
117
|
+
agent: { get },
|
|
118
|
+
session: { create, generate, remove, interrupt, prompt },
|
|
119
|
+
},
|
|
120
|
+
agentGet: get,
|
|
121
|
+
create,
|
|
122
|
+
generate,
|
|
123
|
+
remove,
|
|
124
|
+
interrupt,
|
|
125
|
+
prompt,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
describe('generateOpenCodeText — text path', () => {
|
|
95
|
-
it('
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
session: { create, prompt },
|
|
102
|
-
agent: { get: vi.fn() },
|
|
130
|
+
it('runs session.generate in a verified deny-all throwaway session and removes it', async () => {
|
|
131
|
+
const { client, agentGet, create, generate, remove, prompt } = textSessionClient()
|
|
132
|
+
const ensureTempDirectory = vi.fn(async ({ agentName, agentMarkdown }) => {
|
|
133
|
+
expect(agentName).toBe(_test.TEXT_AGENT_NAME)
|
|
134
|
+
expect(agentMarkdown).toBe(_test.TEXT_AGENT_MARKDOWN)
|
|
135
|
+
return '/tmp/openchamber-text'
|
|
103
136
|
})
|
|
104
137
|
|
|
105
138
|
const result = await generateOpenCodeText({
|
|
106
139
|
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
107
140
|
getOpenCodeAuthHeaders: () => ({}),
|
|
108
|
-
providerID: 'opencode',
|
|
109
|
-
modelID: '
|
|
141
|
+
providerID: 'opencode-go',
|
|
142
|
+
modelID: 'deepseek-v4-flash',
|
|
110
143
|
messages: [
|
|
111
144
|
{ role: 'system', content: 'Be brief' },
|
|
112
145
|
{ role: 'user', content: 'hi' },
|
|
113
146
|
],
|
|
114
|
-
clientFactory,
|
|
147
|
+
clientFactory: () => client,
|
|
148
|
+
ensureTempDirectory,
|
|
115
149
|
})
|
|
116
150
|
|
|
117
|
-
expect(result).toEqual({ text: 'reply', source: 'generate
|
|
118
|
-
expect(
|
|
119
|
-
|
|
120
|
-
|
|
151
|
+
expect(result).toEqual({ text: 'reply', source: 'session.generate' })
|
|
152
|
+
expect(agentGet).toHaveBeenCalledWith(expect.objectContaining({
|
|
153
|
+
agentID: 'openchamber-text',
|
|
154
|
+
location: { directory: '/tmp/openchamber-text' },
|
|
121
155
|
}), expect.anything())
|
|
122
|
-
expect(
|
|
123
|
-
|
|
156
|
+
expect(create).toHaveBeenCalledWith(expect.objectContaining({
|
|
157
|
+
agent: 'openchamber-text',
|
|
158
|
+
model: { id: 'deepseek-v4-flash', providerID: 'opencode-go' },
|
|
159
|
+
location: { directory: '/tmp/openchamber-text' },
|
|
160
|
+
}), expect.anything())
|
|
161
|
+
expect(agentGet.mock.invocationCallOrder[0]).toBeLessThan(create.mock.invocationCallOrder[0])
|
|
162
|
+
const generateArgs = generate.mock.calls[0][0]
|
|
163
|
+
expect(generateArgs.sessionID).toBe('ses_text')
|
|
164
|
+
expect(generateArgs.prompt.startsWith('Be brief')).toBe(true)
|
|
165
|
+
expect(generateArgs.prompt).toContain('User: hi')
|
|
124
166
|
expect(prompt).not.toHaveBeenCalled()
|
|
167
|
+
expect(remove).toHaveBeenCalledExactlyOnceWith({ sessionID: 'ses_text' })
|
|
125
168
|
})
|
|
126
169
|
|
|
127
|
-
it.each(['high', undefined])('passes variant %s
|
|
128
|
-
const
|
|
170
|
+
it.each(['high', undefined])('passes variant %s on the session model', async (variant) => {
|
|
171
|
+
const { client, create } = textSessionClient()
|
|
129
172
|
const result = await generateOpenCodeText({
|
|
130
173
|
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
131
174
|
getOpenCodeAuthHeaders: () => ({}),
|
|
@@ -133,10 +176,11 @@ describe('generateOpenCodeText — text path', () => {
|
|
|
133
176
|
modelID: 'gpt-5-nano',
|
|
134
177
|
messages: [{ role: 'user', content: 'hi' }],
|
|
135
178
|
variant,
|
|
136
|
-
clientFactory: () =>
|
|
179
|
+
clientFactory: () => client,
|
|
180
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
137
181
|
})
|
|
138
|
-
expect(result.text).toBe('
|
|
139
|
-
const model =
|
|
182
|
+
expect(result.text).toBe('reply')
|
|
183
|
+
const model = create.mock.calls[0][0].model
|
|
140
184
|
if (variant) {
|
|
141
185
|
expect(model).toEqual({ id: 'gpt-5-nano', providerID: 'opencode', variant })
|
|
142
186
|
} else {
|
|
@@ -144,7 +188,85 @@ describe('generateOpenCodeText — text path', () => {
|
|
|
144
188
|
}
|
|
145
189
|
})
|
|
146
190
|
|
|
147
|
-
it('
|
|
191
|
+
it('blocks before creating a session when the text agent is not deny-all', async () => {
|
|
192
|
+
const { client, create, generate } = textSessionClient({
|
|
193
|
+
agentGet: vi.fn(async () => ({
|
|
194
|
+
data: { id: 'openchamber-text', permissions: [{ action: 'bash', resource: '*', effect: 'allow' }] },
|
|
195
|
+
})),
|
|
196
|
+
})
|
|
197
|
+
await expect(generateOpenCodeText({
|
|
198
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
199
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
200
|
+
providerID: 'opencode',
|
|
201
|
+
modelID: 'gpt-5-nano',
|
|
202
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
203
|
+
clientFactory: () => client,
|
|
204
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
205
|
+
})).rejects.toMatchObject({ code: 'llm_attachment_generation_unavailable' })
|
|
206
|
+
expect(create).not.toHaveBeenCalled()
|
|
207
|
+
expect(generate).not.toHaveBeenCalled()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('waits for a lazily loaded agent instead of failing on the first read', async () => {
|
|
211
|
+
let reads = 0
|
|
212
|
+
const agentGet = vi.fn(async () => {
|
|
213
|
+
reads += 1
|
|
214
|
+
if (reads < 3) throw new Error('Agent not found: openchamber-text')
|
|
215
|
+
return denyAllTextAgent
|
|
216
|
+
})
|
|
217
|
+
const client = { agent: { get: agentGet } }
|
|
218
|
+
const agent = await _test.assertLlmAgentDenyAll({
|
|
219
|
+
client,
|
|
220
|
+
location: { directory: '/tmp/openchamber-text' },
|
|
221
|
+
agentID: 'openchamber-text',
|
|
222
|
+
waitMs: 1_000,
|
|
223
|
+
pollMs: 1,
|
|
224
|
+
})
|
|
225
|
+
expect(agent.id).toBe('openchamber-text')
|
|
226
|
+
expect(agentGet).toHaveBeenCalledTimes(3)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('fails when the agent never appears within the wait window', async () => {
|
|
230
|
+
const agentGet = vi.fn(async () => { throw new Error('Agent not found') })
|
|
231
|
+
await expect(_test.assertLlmAgentDenyAll({
|
|
232
|
+
client: { agent: { get: agentGet } },
|
|
233
|
+
agentID: 'openchamber-text',
|
|
234
|
+
waitMs: 0,
|
|
235
|
+
pollMs: 1,
|
|
236
|
+
})).rejects.toMatchObject({ code: 'llm_attachment_generation_unavailable' })
|
|
237
|
+
expect(agentGet).toHaveBeenCalledTimes(1)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('surfaces session.generate errors and still removes the session', async () => {
|
|
241
|
+
const { client, remove } = textSessionClient({
|
|
242
|
+
generate: vi.fn(async () => { throw new Error('Request is missing x-opencode-session') }),
|
|
243
|
+
})
|
|
244
|
+
await expect(generateOpenCodeText({
|
|
245
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
246
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
247
|
+
providerID: 'opencode-go',
|
|
248
|
+
modelID: 'deepseek-v4-flash',
|
|
249
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
250
|
+
clientFactory: () => client,
|
|
251
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
252
|
+
})).rejects.toMatchObject({ code: 'upstream_error', message: 'Request is missing x-opencode-session' })
|
|
253
|
+
expect(remove).toHaveBeenCalledWith({ sessionID: 'ses_text' })
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('rejects an empty session.generate reply instead of returning blank success', async () => {
|
|
257
|
+
const { client } = textSessionClient({ generate: vi.fn(async () => ({ text: ' ' })) })
|
|
258
|
+
await expect(generateOpenCodeText({
|
|
259
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
260
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
261
|
+
providerID: 'opencode',
|
|
262
|
+
modelID: 'gpt-5-nano',
|
|
263
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
264
|
+
clientFactory: () => client,
|
|
265
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
266
|
+
})).rejects.toMatchObject({ message: 'OpenCode session.generate returned no text' })
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('does not invent deltas on the session.generate path', async () => {
|
|
148
270
|
const onTextDelta = vi.fn()
|
|
149
271
|
const subscribers = new Set()
|
|
150
272
|
const globalEventHub = {
|
|
@@ -153,19 +275,19 @@ describe('generateOpenCodeText — text path', () => {
|
|
|
153
275
|
return () => { subscribers.delete(fn) }
|
|
154
276
|
},
|
|
155
277
|
}
|
|
278
|
+
const { client } = textSessionClient({ generate: vi.fn(async () => ({ text: 'full reply' })) })
|
|
156
279
|
const result = await generateOpenCodeText({
|
|
157
280
|
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
158
281
|
getOpenCodeAuthHeaders: () => ({}),
|
|
159
282
|
providerID: 'opencode',
|
|
160
283
|
modelID: 'gpt-5-nano',
|
|
161
284
|
messages: [{ role: 'user', content: 'hi' }],
|
|
162
|
-
clientFactory: () =>
|
|
163
|
-
|
|
164
|
-
}),
|
|
285
|
+
clientFactory: () => client,
|
|
286
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
165
287
|
onTextDelta,
|
|
166
288
|
globalEventHub,
|
|
167
289
|
})
|
|
168
|
-
expect(result).toEqual({ text: 'full reply', source: 'generate
|
|
290
|
+
expect(result).toEqual({ text: 'full reply', source: 'session.generate' })
|
|
169
291
|
expect(onTextDelta).not.toHaveBeenCalled()
|
|
170
292
|
expect(subscribers.size).toBe(0)
|
|
171
293
|
})
|
|
@@ -176,23 +298,20 @@ describe('generateOpenCodeText — text path', () => {
|
|
|
176
298
|
expect(_test.filesForPrompt([image, file], false)).toEqual([file])
|
|
177
299
|
expect(_test.imageFilesForSession([image, file], false)).toEqual([])
|
|
178
300
|
|
|
179
|
-
const
|
|
180
|
-
const create = vi.fn()
|
|
301
|
+
const { client, generate, prompt } = textSessionClient({ generate: vi.fn(async () => ({ text: 'cannot see images' })) })
|
|
181
302
|
await generateOpenCodeText({
|
|
182
303
|
buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
|
|
183
304
|
getOpenCodeAuthHeaders: () => ({}),
|
|
184
305
|
providerID: 'opencode',
|
|
185
306
|
modelID: 'deepseek-v4-flash',
|
|
186
307
|
messages: [{ role: 'user', content: 'look', parts: [image, file] }],
|
|
187
|
-
clientFactory: () =>
|
|
188
|
-
|
|
189
|
-
session: { create },
|
|
190
|
-
}),
|
|
308
|
+
clientFactory: () => client,
|
|
309
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
191
310
|
forwardImageParts: false,
|
|
192
311
|
})
|
|
193
|
-
expect(
|
|
194
|
-
expect(
|
|
195
|
-
expect(
|
|
312
|
+
expect(prompt).not.toHaveBeenCalled()
|
|
313
|
+
expect(generate.mock.calls[0][0].prompt).toContain('[image: shot.png (image/png)]')
|
|
314
|
+
expect(generate.mock.calls[0][0].prompt).toContain('[file: notes.txt (text/plain)]')
|
|
196
315
|
})
|
|
197
316
|
})
|
|
198
317
|
|
|
@@ -207,6 +326,8 @@ describe('generateOpenCodeText — attachment session path', () => {
|
|
|
207
326
|
const remove = vi.fn(async () => undefined)
|
|
208
327
|
const interrupt = vi.fn(async () => undefined)
|
|
209
328
|
const text = vi.fn()
|
|
329
|
+
const persistSessionMetadata = vi.fn(async () => ({}))
|
|
330
|
+
const onSystemSessionPersisted = vi.fn()
|
|
210
331
|
const image = { type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' }
|
|
211
332
|
|
|
212
333
|
const result = await generateOpenCodeText({
|
|
@@ -219,6 +340,8 @@ describe('generateOpenCodeText — attachment session path', () => {
|
|
|
219
340
|
{ role: 'user', content: 'look', parts: [image] },
|
|
220
341
|
],
|
|
221
342
|
variant: 'high',
|
|
343
|
+
persistSessionMetadata,
|
|
344
|
+
onSystemSessionPersisted,
|
|
222
345
|
clientFactory: () => ({
|
|
223
346
|
generate: { text },
|
|
224
347
|
agent: { get: agentGet },
|
|
@@ -250,6 +373,7 @@ describe('generateOpenCodeText — attachment session path', () => {
|
|
|
250
373
|
agent: 'openchamber-llm',
|
|
251
374
|
model: { id: 'gpt-4o', providerID: 'opencode', variant: 'high' },
|
|
252
375
|
location: { directory: '/tmp/openchamber-llm' },
|
|
376
|
+
metadata: { openchamber: { llm: { purpose: 'chat-completions' } } },
|
|
253
377
|
}), expect.anything())
|
|
254
378
|
expect(put).toHaveBeenCalledWith(expect.objectContaining({
|
|
255
379
|
sessionID: 'ses_tmp',
|
|
@@ -270,6 +394,14 @@ describe('generateOpenCodeText — attachment session path', () => {
|
|
|
270
394
|
order: 'desc',
|
|
271
395
|
}), expect.anything())
|
|
272
396
|
expect(remove).toHaveBeenCalledWith({ sessionID: 'ses_tmp' })
|
|
397
|
+
expect(persistSessionMetadata).toHaveBeenCalledWith('ses_tmp', {
|
|
398
|
+
openchamber: { llm: { purpose: 'chat-completions' } },
|
|
399
|
+
})
|
|
400
|
+
expect(onSystemSessionPersisted).toHaveBeenCalledWith({
|
|
401
|
+
sessionID: 'ses_tmp',
|
|
402
|
+
directory: '/tmp/openchamber-llm',
|
|
403
|
+
metadata: { openchamber: { llm: { purpose: 'chat-completions' } } },
|
|
404
|
+
})
|
|
273
405
|
expect(text).not.toHaveBeenCalled()
|
|
274
406
|
})
|
|
275
407
|
|
|
@@ -581,17 +713,39 @@ describe('data URL validation', () => {
|
|
|
581
713
|
})
|
|
582
714
|
|
|
583
715
|
describe('generateOpenCodeText — real OpenCode.make + fake HTTP', () => {
|
|
584
|
-
it('
|
|
716
|
+
it('session.generate text completion through the real client', async () => {
|
|
585
717
|
const { OpenCode } = await import('@opencode/client')
|
|
718
|
+
const calls = []
|
|
719
|
+
const emptyOk = { ok: true, status: 204, headers: { get: () => null }, text: async () => '', json: async () => null, arrayBuffer: async () => new ArrayBuffer(0) }
|
|
586
720
|
const fetchImpl = vi.fn(async (url, init) => {
|
|
587
721
|
const path = String(url)
|
|
588
|
-
|
|
722
|
+
const method = init?.method || 'GET'
|
|
723
|
+
calls.push(`${method} ${path.replace(/^https?:\/\/[^/]+/, '').split('?')[0]}`)
|
|
724
|
+
if (path.includes('/api/agent/openchamber-text')) {
|
|
725
|
+
return jsonResponse(200, {
|
|
726
|
+
location: { directory: '/tmp/openchamber-text', project: { id: 'p', directory: '/tmp', canonical: '/tmp' } },
|
|
727
|
+
data: { ...denyAllTextAgent.data, request: {} },
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
if (path.endsWith('/api/session') && method === 'POST') {
|
|
731
|
+
return jsonResponse(200, {
|
|
732
|
+
data: {
|
|
733
|
+
id: 'ses_text',
|
|
734
|
+
projectID: 'p',
|
|
735
|
+
cost: 0,
|
|
736
|
+
tokens: {},
|
|
737
|
+
time: { created: 1, updated: 1 },
|
|
738
|
+
location: { directory: '/tmp/openchamber-text' },
|
|
739
|
+
},
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
if (path.includes('/api/session/ses_text/generate') && method === 'POST') {
|
|
589
743
|
const body = JSON.parse(init.body)
|
|
590
|
-
expect(body.model).toEqual({ id: 'gpt-5-nano', providerID: 'opencode' })
|
|
591
744
|
expect(body.prompt).toContain('User: hi')
|
|
592
745
|
return jsonResponse(200, { data: { text: 'hello from generate' } })
|
|
593
746
|
}
|
|
594
|
-
|
|
747
|
+
if (path.includes('/api/session/ses_text') && method === 'DELETE') return emptyOk
|
|
748
|
+
return jsonResponse(500, { _tag: 'UnknownError', message: `unexpected ${method} ${path}` })
|
|
595
749
|
})
|
|
596
750
|
|
|
597
751
|
const result = await generateOpenCodeText({
|
|
@@ -605,10 +759,11 @@ describe('generateOpenCodeText — real OpenCode.make + fake HTTP', () => {
|
|
|
605
759
|
headers: { Authorization: 'Basic test' },
|
|
606
760
|
fetch: fetchImpl,
|
|
607
761
|
}),
|
|
762
|
+
ensureTempDirectory: async () => '/tmp/openchamber-text',
|
|
608
763
|
})
|
|
609
764
|
|
|
610
|
-
expect(result).toEqual({ text: 'hello from generate', source: 'generate
|
|
611
|
-
expect(
|
|
765
|
+
expect(result).toEqual({ text: 'hello from generate', source: 'session.generate' })
|
|
766
|
+
expect(calls.some((c) => c.startsWith('DELETE ') && c.includes('/api/session/ses_text'))).toBe(true)
|
|
612
767
|
})
|
|
613
768
|
|
|
614
769
|
it('attachment deny-all failure happens before prompt on the real client path shape', async () => {
|
package/server/lib/llm/routes.js
CHANGED
|
@@ -40,6 +40,8 @@ export const registerLlmRoutes = (app, dependencies) => {
|
|
|
40
40
|
getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders,
|
|
41
41
|
clientFactory: client,
|
|
42
42
|
ensureTempDirectory: ensureLlmTempDirectory,
|
|
43
|
+
persistSessionMetadata: dependencies.persistSessionMetadata,
|
|
44
|
+
onSystemSessionPersisted: dependencies.onSystemSessionPersisted,
|
|
43
45
|
}))
|
|
44
46
|
.then(({ completion }) => {
|
|
45
47
|
res.status(200).json(completion);
|
|
@@ -35,13 +35,26 @@ export const createMessageQueueRuntime = ({ dbPath, attachmentRoot, service, att
|
|
|
35
35
|
const observeSessionEvent = (event) => {
|
|
36
36
|
if (!isCurrentRuntimeEvent(event)) return false;
|
|
37
37
|
const payload = event?.payload?.payload ?? event?.payload;
|
|
38
|
-
|
|
38
|
+
// Accept bridge `{ properties }` and native v2 `{ data }` envelopes.
|
|
39
|
+
const properties = (payload?.properties && typeof payload.properties === 'object')
|
|
40
|
+
? payload.properties
|
|
41
|
+
: ((payload?.data && typeof payload.data === 'object') ? payload.data : null);
|
|
39
42
|
const directory = typeof event?.directory === 'string' && event.directory !== 'global' ? event.directory : '';
|
|
40
43
|
const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
|
|
41
44
|
if (!directory || !sessionID) return false;
|
|
45
|
+
// Ticket 07: shutdown interrupt keeps the upstream claim — do not treat as
|
|
46
|
+
// idle/error settlement for queue wake; wait for authoritative terminal.
|
|
47
|
+
if (payload.type === 'session.execution.interrupted' && properties?.reason === 'shutdown') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
42
50
|
const phase = payload.type === 'session.status'
|
|
43
51
|
? properties?.status?.type ?? properties?.info?.type
|
|
44
|
-
: payload.type === 'session.idle' ? 'idle'
|
|
52
|
+
: payload.type === 'session.idle' ? 'idle'
|
|
53
|
+
: payload.type === 'session.error' ? 'error'
|
|
54
|
+
: payload.type === 'session.execution.succeeded' ? 'idle'
|
|
55
|
+
: payload.type === 'session.execution.failed' ? 'error'
|
|
56
|
+
: payload.type === 'session.execution.interrupted' ? 'idle'
|
|
57
|
+
: '';
|
|
45
58
|
if (typeof phase !== 'string' || !phase) return false;
|
|
46
59
|
openCodeAdapter.observeSessionEvent?.({ directory, sessionID }, phase);
|
|
47
60
|
return true;
|
|
@@ -346,7 +346,9 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
346
346
|
|
|
347
347
|
const extractSessionIdFromPayload = (payload) => {
|
|
348
348
|
if (!payload || typeof payload !== 'object') return null;
|
|
349
|
-
const props = payload.properties
|
|
349
|
+
const props = payload.properties && typeof payload.properties === 'object'
|
|
350
|
+
? payload.properties
|
|
351
|
+
: (payload.data && typeof payload.data === 'object' ? payload.data : null);
|
|
350
352
|
const info = props?.info;
|
|
351
353
|
const sessionId =
|
|
352
354
|
info?.sessionID ??
|
|
@@ -412,20 +414,36 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
412
414
|
}
|
|
413
415
|
}
|
|
414
416
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
+
// Ticket 07: shutdown interrupt is not a completion terminal — no ready push.
|
|
418
|
+
if (payload.type === 'session.execution.interrupted') {
|
|
419
|
+
const reason = payload.properties?.reason ?? payload.data?.reason;
|
|
420
|
+
if (reason === 'shutdown') return;
|
|
421
|
+
}
|
|
422
|
+
// v2 completion is session.execution.succeeded. session.idle is a deprecated
|
|
423
|
+
// fallback; the ready-notification cooldown below collapses the pair.
|
|
424
|
+
if ((
|
|
425
|
+
payload.type === 'session.idle'
|
|
426
|
+
|| payload.type === 'session.execution.succeeded'
|
|
427
|
+
|| payload.type === 'session.error'
|
|
428
|
+
|| payload.type === 'session.execution.failed'
|
|
429
|
+
) && sessionId) {
|
|
430
|
+
const body = payload.properties && typeof payload.properties === 'object'
|
|
431
|
+
? payload.properties
|
|
432
|
+
: (payload.data && typeof payload.data === 'object' ? payload.data : {});
|
|
433
|
+
const error = body?.error;
|
|
417
434
|
const errorText = typeof error?.message === 'string'
|
|
418
435
|
? error.message
|
|
419
436
|
: typeof error === 'string' ? error : '';
|
|
437
|
+
const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
|
|
420
438
|
await maybeSendPushForTrigger({
|
|
421
439
|
...payload,
|
|
422
440
|
type: 'message.updated',
|
|
423
441
|
properties: {
|
|
424
|
-
...payload.properties,
|
|
442
|
+
...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
|
|
425
443
|
info: {
|
|
426
444
|
sessionID: sessionId,
|
|
427
445
|
role: 'assistant',
|
|
428
|
-
finish:
|
|
446
|
+
finish: failed ? 'error' : 'stop',
|
|
429
447
|
...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
|
|
430
448
|
},
|
|
431
449
|
},
|
|
@@ -457,6 +457,17 @@ describe('contact turn notifications', () => {
|
|
|
457
457
|
});
|
|
458
458
|
|
|
459
459
|
describe('notification trigger live activity end', () => {
|
|
460
|
+
it('treats session.execution.succeeded as completion, including a v2 data envelope', async () => {
|
|
461
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse('ses_done')));
|
|
462
|
+
const { runtime, sendLiveActivityEnd, emitDesktopNotification } = createRuntime();
|
|
463
|
+
await runtime.maybeSendPushForTrigger({
|
|
464
|
+
type: 'session.execution.succeeded',
|
|
465
|
+
data: { sessionID: 'ses_done' },
|
|
466
|
+
});
|
|
467
|
+
expect(sendLiveActivityEnd).toHaveBeenCalledWith({ sessionId: 'ses_done', status: 'complete' });
|
|
468
|
+
expect(emitDesktopNotification).toHaveBeenCalled();
|
|
469
|
+
});
|
|
470
|
+
|
|
460
471
|
it('ends the live activity on top-level completion and error', async () => {
|
|
461
472
|
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
462
473
|
const { runtime, sendLiveActivityEnd } = createRuntime();
|