@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3
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 +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
- package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
- package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
- package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
- package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
- package/dist/assets/SettingsView-BRxELjow.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
- package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
- package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
- package/dist/assets/collapsible-DXbDynN4.js +3 -0
- package/dist/assets/context-menu-CqolsLCm.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-CM42qUed.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
- package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/main-BILgBI3B.js +169 -0
- package/dist/assets/main-BeveiW6F.js +2 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
- package/dist/assets/miniChat-BgrzM3u7.js +2 -0
- package/dist/assets/mobile-DtYoQE8C.js +2 -0
- package/dist/assets/multirun-jy8dEI96.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-BQghHRfp.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
- package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
- package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
- package/dist/assets/src-CLN7Z-zx.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
- package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
- package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.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-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.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-opencode-client-BOe2qpQb.js +12 -0
- 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-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- 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 +2 -2
- package/server/index.js +392 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- 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 +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/runtime.js +5 -0
- package/server/lib/opencode/DOCUMENTATION.md +103 -16
- 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 +111 -102
- package/server/lib/opencode/config-entity-routes.test.js +123 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +80 -5
- 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 +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- 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/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- 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-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- 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 +368 -28
- package/server/lib/opencode/proxy.test.js +338 -0
- package/server/lib/opencode/routes.js +303 -76
- package/server/lib/opencode/routes.test.js +315 -50
- 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 +35 -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 +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- 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/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- 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/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- 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 +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- 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 +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
- package/server/lib/session-metadata/routes.js +193 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- 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 +375 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- 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 +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -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 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- 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 +125 -241
- 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 +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/collapsible-D5Phaykh.js +0 -1
- package/dist/assets/context-menu-CubSDW2g.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.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 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -31,6 +31,18 @@ const flush = async () => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
describe('permission auto-accept runtime', () => {
|
|
34
|
+
it('approves a session with no explicit policy', async () => {
|
|
35
|
+
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
36
|
+
const path = new URL(url).pathname;
|
|
37
|
+
if (path === '/session/root') return Response.json({ id: 'root' });
|
|
38
|
+
if (init.method === 'POST') return Response.json({});
|
|
39
|
+
return new Response('', { status: 404 });
|
|
40
|
+
});
|
|
41
|
+
const { runtime } = createRuntime({ fetchImpl });
|
|
42
|
+
await expect(runtime.processPermission({ id: 'perm', sessionID: 'root' }, '/project')).resolves.toBe(true);
|
|
43
|
+
expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/session/root/permission/perm/reply' && JSON.parse(init.body).decision === 'always')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
34
46
|
it('persists explicit session policies across runtime restarts', async () => {
|
|
35
47
|
const first = createRuntime();
|
|
36
48
|
await first.runtime.setSessionPolicy('root', true);
|
|
@@ -55,7 +67,7 @@ describe('permission auto-accept runtime', () => {
|
|
|
55
67
|
it('fetches missing subagent lineage before replying', async () => {
|
|
56
68
|
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
57
69
|
const path = new URL(url).pathname;
|
|
58
|
-
if (path === '/permission') return new Response('[]');
|
|
70
|
+
if (path === '/permission/request') return new Response('[]');
|
|
59
71
|
if (path === '/session/child') return Response.json({ id: 'child', parentID: 'root', directory: '/project' });
|
|
60
72
|
if (init.method === 'POST') return Response.json({});
|
|
61
73
|
return new Response('', { status: 404 });
|
|
@@ -65,15 +77,15 @@ describe('permission auto-accept runtime', () => {
|
|
|
65
77
|
fetchImpl,
|
|
66
78
|
});
|
|
67
79
|
await expect(runtime.processPermission({ id: 'perm', sessionID: 'child' }, '/project')).resolves.toBe(true);
|
|
68
|
-
expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/permission/perm/reply' && init.method === 'POST')).toBe(true);
|
|
80
|
+
expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/session/child/permission/perm/reply' && init.method === 'POST' && JSON.parse(init.body).decision === 'always')).toBe(true);
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
it('retries a transient reply failure and deduplicates concurrent events', async () => {
|
|
72
84
|
let replyAttempts = 0;
|
|
73
85
|
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
74
86
|
const path = new URL(url).pathname;
|
|
75
|
-
if (path === '/permission') return new Response('[]');
|
|
76
|
-
if (path === '/permission/perm/reply' && init.method === 'POST') {
|
|
87
|
+
if (path === '/permission/request') return new Response('[]');
|
|
88
|
+
if (path === '/session/root/permission/perm/reply' && init.method === 'POST') {
|
|
77
89
|
replyAttempts += 1;
|
|
78
90
|
return replyAttempts === 1 ? new Response('', { status: 503 }) : Response.json({});
|
|
79
91
|
}
|
|
@@ -94,8 +106,8 @@ describe('permission auto-accept runtime', () => {
|
|
|
94
106
|
it('reconciles pending permissions after reconnect', async () => {
|
|
95
107
|
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
96
108
|
const path = new URL(url).pathname;
|
|
97
|
-
if (path === '/permission') return Response.json([{ id: 'pending', sessionID: 'root' }]);
|
|
98
|
-
if (path === '/permission/pending/reply' && init.method === 'POST') return Response.json({});
|
|
109
|
+
if (path === '/permission/request') return Response.json([{ id: 'pending', sessionID: 'root' }]);
|
|
110
|
+
if (path === '/session/root/permission/pending/reply' && init.method === 'POST') return Response.json({});
|
|
99
111
|
return Response.json({ id: 'root' });
|
|
100
112
|
});
|
|
101
113
|
const { connect } = createRuntime({
|
|
@@ -104,14 +116,14 @@ describe('permission auto-accept runtime', () => {
|
|
|
104
116
|
});
|
|
105
117
|
connect();
|
|
106
118
|
await flush();
|
|
107
|
-
expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/permission/pending/reply')).toBe(true);
|
|
119
|
+
expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/session/root/permission/pending/reply')).toBe(true);
|
|
108
120
|
});
|
|
109
121
|
|
|
110
122
|
it('accepts existing pending permissions when a session policy is enabled', async () => {
|
|
111
123
|
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
112
124
|
const parsed = new URL(url);
|
|
113
125
|
const path = parsed.pathname;
|
|
114
|
-
if (path === '/permission') {
|
|
126
|
+
if (path === '/permission/request') {
|
|
115
127
|
return parsed.searchParams.get('directory') === '/project'
|
|
116
128
|
? Response.json([
|
|
117
129
|
{ id: 'root-pending', sessionID: 'root' },
|
|
@@ -119,19 +131,22 @@ describe('permission auto-accept runtime', () => {
|
|
|
119
131
|
])
|
|
120
132
|
: Response.json([]);
|
|
121
133
|
}
|
|
122
|
-
if (
|
|
134
|
+
if (init.method === 'POST' && path.endsWith('/reply')) return Response.json({});
|
|
123
135
|
if (path === '/session/other') return Response.json({ id: 'other' });
|
|
124
136
|
return new Response('', { status: 404 });
|
|
125
137
|
});
|
|
126
|
-
const { runtime } = createRuntime({
|
|
138
|
+
const { runtime } = createRuntime({
|
|
139
|
+
stored: { permissionAutoAccept: { sessions: { other: false } } },
|
|
140
|
+
fetchImpl,
|
|
141
|
+
});
|
|
127
142
|
|
|
128
143
|
await runtime.setSessionPolicy('root', true, '/project');
|
|
129
144
|
|
|
130
145
|
const replyPaths = fetchImpl.mock.calls
|
|
131
146
|
.filter(([, init]) => init?.method === 'POST')
|
|
132
147
|
.map(([url]) => new URL(url).pathname);
|
|
133
|
-
expect(replyPaths).toEqual(['/permission/root-pending/reply']);
|
|
148
|
+
expect(replyPaths).toEqual(['/session/root/permission/root-pending/reply']);
|
|
134
149
|
expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
|
|
135
|
-
expect(await runtime.load()).toEqual({ sessions: { root: true } });
|
|
150
|
+
expect(await runtime.load()).toEqual({ sessions: { other: false, root: true } });
|
|
136
151
|
});
|
|
137
152
|
});
|
|
@@ -147,7 +147,11 @@ const startFakeOpenCode = async (): Promise<{
|
|
|
147
147
|
next();
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
// Production proxy forwards official v2 paths with `/api` restored
|
|
151
|
+
// (`toUpstreamOpenCodeApiPath`). Host mounts both `/api/global/event` and
|
|
152
|
+
// `/api/event`; fake upstream must match those upstream targets — not the
|
|
153
|
+
// pre-v2 bare `/global/event` or `/event`.
|
|
154
|
+
const writeSseFixture = (res: express.Response) => {
|
|
151
155
|
res.writeHead(200, {
|
|
152
156
|
'content-type': 'text/event-stream; charset=utf-8',
|
|
153
157
|
'cache-control': 'no-cache',
|
|
@@ -243,9 +247,16 @@ const startFakeOpenCode = async (): Promise<{
|
|
|
243
247
|
);
|
|
244
248
|
|
|
245
249
|
res.end();
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
app.get('/api/global/event', (_req, res) => {
|
|
253
|
+
writeSseFixture(res);
|
|
254
|
+
});
|
|
255
|
+
app.get('/api/event', (_req, res) => {
|
|
256
|
+
writeSseFixture(res);
|
|
246
257
|
});
|
|
247
258
|
|
|
248
|
-
app.get('/session/:sessionID/message', (
|
|
259
|
+
app.get('/api/session/:sessionID/message', (_req, res) => {
|
|
249
260
|
res.json([
|
|
250
261
|
{
|
|
251
262
|
info: {
|
|
@@ -513,11 +524,15 @@ describe('relay reasoning projection e2e (Host filter over private relay)', () =
|
|
|
513
524
|
for (const url of upstream.observation.urls) {
|
|
514
525
|
expect(url).not.toContain('includeReasoning');
|
|
515
526
|
}
|
|
516
|
-
// Sanity: upstream still saw
|
|
517
|
-
expect(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
527
|
+
// Sanity: upstream still saw official v2 event + message routes.
|
|
528
|
+
expect(
|
|
529
|
+
upstream.observation.urls.some(
|
|
530
|
+
(u) => u.includes('/api/global/event') || u.includes('/api/event'),
|
|
531
|
+
),
|
|
532
|
+
).toBe(true);
|
|
533
|
+
expect(
|
|
534
|
+
upstream.observation.urls.some((u) => u.includes('/api/session/ses_1/message')),
|
|
535
|
+
).toBe(true);
|
|
521
536
|
|
|
522
537
|
// Unauthenticated tunnel request still hits real auth gate (not open).
|
|
523
538
|
const denied = await client.fetch('/api/global/event?includeReasoning=false', {
|
|
@@ -15,7 +15,7 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
|
|
15
15
|
- Next-run computation (daily/weekly/cron compatibility)
|
|
16
16
|
- Timer scheduling and queueing
|
|
17
17
|
- Concurrency controls
|
|
18
|
-
- Session create →
|
|
18
|
+
- Session create → goal/prompt/command execution
|
|
19
19
|
- Emits OpenChamber task-run events
|
|
20
20
|
- Isolates project sync failures, retries each failed project up to three times,
|
|
21
21
|
and clears pending retries during shutdown
|
|
@@ -69,6 +69,14 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
|
|
69
69
|
- `runNow(projectId, taskId)`
|
|
70
70
|
- `observeSessionEvent(event)`
|
|
71
71
|
|
|
72
|
+
Isolation dependencies (required for sidebar hide):
|
|
73
|
+
|
|
74
|
+
- `persistSessionMetadata(sessionID, patch)` — Host store write. Missing
|
|
75
|
+
persist refuses the run **before** `session.create`. Persist failure after
|
|
76
|
+
create fails the run before prompt.
|
|
77
|
+
- `onSystemSessionPersisted({ sessionID, directory, metadata })` — drop the
|
|
78
|
+
session from the sidebar index immediately after Host commit.
|
|
79
|
+
|
|
72
80
|
Dependencies include optional `runHistoryStore` with:
|
|
73
81
|
|
|
74
82
|
- `startRun(record)`
|
|
@@ -186,44 +194,55 @@ Every actual run (timer or manual):
|
|
|
186
194
|
4. Create an OpenCode session with:
|
|
187
195
|
- title: `[Scheduled] ${taskName} yyyy-LL-dd HH:mm` (total length ≤ 120)
|
|
188
196
|
- metadata: `openchamber.scheduledTask = { projectID, taskID, runID, name }`
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
5. `attachSession(runID, sessionID)` immediately after create.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
**Admission is not completion:** `
|
|
197
|
+
- SDK/HTTP calls receive `{ signal }` as the second request-options argument
|
|
198
|
+
(`client.session.create(params, { signal })`, same for `prompt` /
|
|
199
|
+
`command` / `command.list`; raw `fetch` for goal PATCH
|
|
200
|
+
pass `signal` in `RequestInit`).
|
|
201
|
+
5. `attachSession(runID, sessionID)` immediately after create.
|
|
202
|
+
- If attach fails, the runtime fails the run without prompting.
|
|
203
|
+
6. When `goalEnabled`: refuse before OpenCode ready / session create if
|
|
204
|
+
`persistSessionGoal` is not injected; otherwise write objective file,
|
|
205
|
+
persist an active goal into the Host session-metadata store, optionally
|
|
206
|
+
notify `onGoalPersisted` to arm the goal loop, then command or
|
|
207
|
+
`session.prompt`. Non-cancellable async gaps (small-model distill,
|
|
208
|
+
objective file write) check `signal.throwIfAborted()` before continuing so
|
|
209
|
+
a timed-out run never prompts.
|
|
210
|
+
**Admission is not completion:** `session.prompt` / command return when the
|
|
203
211
|
turn is accepted, not when the agent finishes.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
7. Wait for the real session outcome (bounded by the same watchdog):
|
|
213
|
+
- Poll `session.active` + `message.list` until the session is idle and
|
|
214
|
+
the **latest-side** message page settles. Settlement always uses
|
|
215
|
+
`message.list({ limit: 50, order: 'desc' })` (newest first) — never
|
|
216
|
+
`order: 'asc'` page-0 + `at(-1)`, which reads an old page once the
|
|
217
|
+
session exceeds the limit.
|
|
218
|
+
- Real terminal success requires an assistant tail with `time.completed`
|
|
219
|
+
and finish not `tool-calls` (prefer `finish: 'stop'`). `finish:
|
|
220
|
+
'tool-calls'`, incomplete assistants (no `time.completed`), and
|
|
221
|
+
non-assistant tails such as `model-switched` never succeed — including
|
|
222
|
+
after multiple incomplete idle probes.
|
|
223
|
+
- `session.active` / `message.list` failures stay **unknown** (keep
|
|
224
|
+
polling under the watchdog); they never masquerade as idle success.
|
|
225
|
+
- Assistant `error` (and terminal finish `error` / `length` /
|
|
226
|
+
`content-filter`) finalizes as `error`.
|
|
227
|
+
- Goal-enabled runs: settle via Host goal terminal status when the store
|
|
228
|
+
seam is available (`readSessionMetadata` preferred over OpenCode
|
|
229
|
+
`session.get`). Store read failure is never empty-goal success.
|
|
230
|
+
- `durationMs` / `finishedAt` are wall-clock from run start through this
|
|
231
|
+
settlement — not the prompt admission latency.
|
|
232
|
+
8. On watchdog timeout after a session exists: immediately throw the canonical
|
|
214
233
|
`schedule run timed out` without awaiting non-cancellable helper work. A
|
|
215
234
|
once-only abort listener registered after session create starts best-effort
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
`client.session.interrupt({ sessionID, continue: false })` as soon as the watchdog
|
|
236
|
+
aborts the signal (or right after create if the signal already aborted).
|
|
237
|
+
Interrupt failures must not replace the timeout error. Successful runs and
|
|
238
|
+
ordinary non-timeout failures never call session interrupt.
|
|
239
|
+
9. Finalize history with the ultimate run status (timeout → `error` with
|
|
221
240
|
`schedule run timed out`; assistant/goal failure → `error` with the
|
|
222
241
|
recorded reason). An already-attached `session_id` remains on the history
|
|
223
242
|
row so the session stays openable from history. Task state persistence
|
|
224
243
|
failures also finalize history as `error`. A failed history finalize
|
|
225
244
|
returns error and may leave a `running` row for next-start convergence.
|
|
226
|
-
|
|
245
|
+
10. `lastSessionId` is written on the task state whenever the run has a
|
|
227
246
|
session (success or error). If the run has no session, omit
|
|
228
247
|
`lastSessionId` from the patch so a previous value is preserved. Full
|
|
229
248
|
association still lives in the history table; history rows are not
|
|
@@ -239,14 +258,35 @@ that same history session. `observeSessionEvent` corrects **only task state**
|
|
|
239
258
|
|
|
240
259
|
- `session.status` busy/retry while `lastStatus` is `error` or `success` →
|
|
241
260
|
`lastStatus` `running`, clear `lastError`, keep/set `lastSessionId`
|
|
242
|
-
- later `session.idle` (or idle `session.status`) with a success snapshot
|
|
243
|
-
`lastStatus` `success`
|
|
261
|
+
- later `session.idle` (or idle `session.status`) with a success snapshot from
|
|
262
|
+
`session.active` + latest-side `message.list` → `lastStatus` `success`
|
|
244
263
|
- do not change `lastRunAt` / `lastDurationMs` / `nextRunAt`
|
|
264
|
+
- history rows are never rewritten; `finishRun` is not called again
|
|
245
265
|
|
|
246
266
|
A live run (`runningTaskKeys`) owns settlement; the observer is a no-op while
|
|
247
267
|
the task is in `runningTaskKeys`. Idle correction is a no-op if `lastStatus`
|
|
248
|
-
is already `success`, or if the snapshot is still error/busy/unknown
|
|
249
|
-
|
|
268
|
+
is already `success`, or if the snapshot is still error/busy/unknown
|
|
269
|
+
(`session.active` failure and goal-unsupported goal tasks stay unknown).
|
|
270
|
+
Observer failures are logged and must not throw out of the event bus.
|
|
271
|
+
|
|
272
|
+
## Goal safety gating
|
|
273
|
+
|
|
274
|
+
Session goal Host state is advertised by
|
|
275
|
+
`packages/web/server/lib/session-goal/capability.js` as `{ supported: true }`.
|
|
276
|
+
Goal records live in the OpenChamber session-metadata store (same seam as
|
|
277
|
+
manual UI / `session-goal` runtime). Scheduled runs inject
|
|
278
|
+
`persistSessionGoal`, `readSessionMetadata`, and optional `onGoalPersisted`
|
|
279
|
+
from `server/index.js`.
|
|
280
|
+
|
|
281
|
+
| Path | Behavior |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `PUT` scheduled task / managed-tool create·update with `execution.goalEnabled` | Allowed when capability supported |
|
|
284
|
+
| Existing `goalEnabled` task due or manual run | Requires `persistSessionGoal`; without it fail **before** OpenCode ready / session create / objective write / prompt; with it write objective + active goal then prompt |
|
|
285
|
+
| Ordinary non-goal task | Unchanged |
|
|
286
|
+
| Post-run observer on a goal-enabled task | Prefer Host store goal terminal status; never invent success from assistant-tail idle alone when goal is active |
|
|
287
|
+
|
|
288
|
+
Do not treat objective-file hints or first-turn assistant idle as a registered
|
|
289
|
+
goal success without a store-backed goal record.
|
|
250
290
|
|
|
251
291
|
Run start also emits `scheduled-task-ran` with `status: running` as soon as
|
|
252
292
|
task state is persisted, so the outer task list can show in-progress before
|
|
@@ -255,21 +295,21 @@ the session is attached.
|
|
|
255
295
|
## Watchdog cancel / upstream abort
|
|
256
296
|
|
|
257
297
|
- Timeout path: abort the per-run signal → cancel in-flight cancellable
|
|
258
|
-
SDK/HTTP → start best-effort OpenCode `session.
|
|
298
|
+
SDK/HTTP → start best-effort OpenCode `session.interrupt` via the create-time
|
|
259
299
|
abort listener (independent of whether the current await is cancellable) →
|
|
260
300
|
**do not wait** for non-cancellable helpers (small-model distill /
|
|
261
301
|
`writeObjective`) to finish → history and task state finalize as `error`
|
|
262
302
|
with `schedule run timed out` within the watchdog bound. The detached
|
|
263
303
|
`runPromise` is settled with a catch so late rejections are not unhandled.
|
|
264
304
|
- When uncancellable helper work eventually finishes, post-await
|
|
265
|
-
`throwIfAborted` gates stop goal PATCH / command / `
|
|
266
|
-
- Non-timeout failures: no timeout abort; no forced `session.
|
|
305
|
+
`throwIfAborted` gates stop goal PATCH / command / `session.prompt`.
|
|
306
|
+
- Non-timeout failures: no timeout abort; no forced `session.interrupt` from the
|
|
267
307
|
watchdog.
|
|
268
308
|
- Runtime `stop()` clears timers, project-sync retries, and the queue only; it
|
|
269
309
|
does not abort in-flight run controllers.
|
|
270
310
|
- Remaining non-cancellable edges: work already fully admitted and completed
|
|
271
311
|
HTTP-wise before timeout cannot be rolled back by aborting the HTTP request
|
|
272
|
-
alone; `session.
|
|
312
|
+
alone; `session.interrupt` is the upstream stop for admitted command/prompt
|
|
273
313
|
execution. Small-model distillation and objective file writes are not passed
|
|
274
314
|
a signal; the watchdog does not await them, and abort gates block later
|
|
275
315
|
stages after they complete.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSessionGoalCapability,
|
|
3
|
+
isSessionGoalSupported,
|
|
4
|
+
sessionGoalUnavailableMessage,
|
|
5
|
+
} from '../session-goal/capability.js';
|
|
1
6
|
import { MANAGED_SCHEDULED_TASK_TOOL_PATH } from './managed-tool-contract.js';
|
|
2
7
|
|
|
3
8
|
const MAX_ID_LENGTH = 512;
|
|
@@ -263,6 +268,17 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
|
|
|
263
268
|
: new Set(['taskId', 'name', 'enabled', 'schedule', 'execution']), 'input');
|
|
264
269
|
validateTaskInputFields(input);
|
|
265
270
|
|
|
271
|
+
const refuseGoalExecution = (execution) => {
|
|
272
|
+
if (execution?.goalEnabled !== true || isSessionGoalSupported()) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const capability = getSessionGoalCapability();
|
|
276
|
+
const error = new Error(sessionGoalUnavailableMessage(capability));
|
|
277
|
+
error.statusCode = 501;
|
|
278
|
+
error.capability = capability;
|
|
279
|
+
return error;
|
|
280
|
+
};
|
|
281
|
+
|
|
266
282
|
if (operation === 'update') {
|
|
267
283
|
const patch = {
|
|
268
284
|
...(input.name === undefined ? {} : { name: input.name }),
|
|
@@ -270,6 +286,8 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
|
|
|
270
286
|
...(input.schedule === undefined ? {} : { schedule: input.schedule }),
|
|
271
287
|
...(input.execution === undefined ? {} : { execution: input.execution }),
|
|
272
288
|
};
|
|
289
|
+
const goalRefusal = refuseGoalExecution(patch.execution);
|
|
290
|
+
if (goalRefusal) throw goalRefusal;
|
|
273
291
|
const patched = await projectConfigRuntime.patchScheduledTask(project.id, taskID, patch);
|
|
274
292
|
if (!patched?.task) return res.status(404).json({ error: 'Schedule not found' });
|
|
275
293
|
const schedulerSynced = await syncAfterMutation(project.id, operation);
|
|
@@ -285,6 +303,8 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
|
|
|
285
303
|
if (operation === 'create' && (!execution.providerID || !execution.modelID)) {
|
|
286
304
|
throw new Error('execution.providerID and execution.modelID are required from the user message or input');
|
|
287
305
|
}
|
|
306
|
+
const goalRefusal = refuseGoalExecution(execution);
|
|
307
|
+
if (goalRefusal) throw goalRefusal;
|
|
288
308
|
const taskInput = {
|
|
289
309
|
...(input.name === undefined ? {} : { name: input.name }),
|
|
290
310
|
...(input.enabled === undefined ? {} : { enabled: input.enabled }),
|
|
@@ -296,7 +316,17 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
|
|
|
296
316
|
return res.status(201).json({ projectId: project.id, created: upserted.created, task: upserted.task, tasks: upserted.tasks, schedulerSynced });
|
|
297
317
|
} catch (error) {
|
|
298
318
|
const statusCode = error?.statusCode || (isValidationError(error?.message || '') ? 400 : 500);
|
|
299
|
-
if (statusCode >= 500
|
|
319
|
+
if (statusCode >= 500 && statusCode !== 501) {
|
|
320
|
+
logger.error?.('[ScheduledTaskTool] request failed', { operation: req.body?.operation, statusCode });
|
|
321
|
+
}
|
|
322
|
+
if (statusCode === 501) {
|
|
323
|
+
const capability = error?.capability || getSessionGoalCapability();
|
|
324
|
+
return res.status(501).json({
|
|
325
|
+
error: error?.message || sessionGoalUnavailableMessage(capability),
|
|
326
|
+
reason: capability.reason,
|
|
327
|
+
capability,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
300
330
|
return res.status(statusCode).json({ error: statusCode >= 500 ? 'Failed to manage schedule' : (error?.message || 'Invalid schedule request') });
|
|
301
331
|
}
|
|
302
332
|
});
|
|
@@ -265,4 +265,27 @@ describe('managed scheduled task tool route', () => {
|
|
|
265
265
|
expect(oversized.status).toBe(413);
|
|
266
266
|
} finally { fixture.restore(); }
|
|
267
267
|
});
|
|
268
|
+
|
|
269
|
+
it('allows create/update with goalEnabled when Host goal state is supported', async () => {
|
|
270
|
+
const fixture = createApp();
|
|
271
|
+
try {
|
|
272
|
+
const created = await request(fixture.app).post(MANAGED_SCHEDULED_TASK_TOOL_PATH).send(body('create', {
|
|
273
|
+
name: 'Goal task',
|
|
274
|
+
schedule: { kind: 'daily', time: '10:00', timezone: 'UTC' },
|
|
275
|
+
execution: { prompt: 'review', goalEnabled: true },
|
|
276
|
+
}));
|
|
277
|
+
expect(created.status).toBe(201);
|
|
278
|
+
expect(fixture.projectConfigRuntime.upsertScheduledTask).toHaveBeenCalled();
|
|
279
|
+
expect(fixture.projectConfigRuntime.upsertScheduledTask.mock.calls[0][1]).toMatchObject({
|
|
280
|
+
execution: expect.objectContaining({ goalEnabled: true }),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const updated = await request(fixture.app).post(MANAGED_SCHEDULED_TASK_TOOL_PATH).send(body('update', {
|
|
284
|
+
taskId: 'task_1',
|
|
285
|
+
execution: { goalEnabled: true },
|
|
286
|
+
}));
|
|
287
|
+
expect(updated.status).toBe(200);
|
|
288
|
+
expect(fixture.projectConfigRuntime.patchScheduledTask).toHaveBeenCalled();
|
|
289
|
+
} finally { fixture.restore(); }
|
|
290
|
+
});
|
|
268
291
|
});
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSessionGoalCapability,
|
|
3
|
+
isSessionGoalSupported,
|
|
4
|
+
sessionGoalUnavailableMessage,
|
|
5
|
+
} from '../session-goal/capability.js';
|
|
6
|
+
|
|
1
7
|
const asNonEmptyString = (value) => {
|
|
2
8
|
if (typeof value !== 'string') {
|
|
3
9
|
return null;
|
|
@@ -6,6 +12,22 @@ const asNonEmptyString = (value) => {
|
|
|
6
12
|
return trimmed.length > 0 ? trimmed : null;
|
|
7
13
|
};
|
|
8
14
|
|
|
15
|
+
/** Reject create/edit that enables goal when Host goal state is unavailable. */
|
|
16
|
+
const rejectUnsupportedGoalExecution = (taskInput) => {
|
|
17
|
+
if (taskInput?.execution?.goalEnabled !== true) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (isSessionGoalSupported()) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const capability = getSessionGoalCapability();
|
|
24
|
+
return {
|
|
25
|
+
error: sessionGoalUnavailableMessage(capability),
|
|
26
|
+
reason: capability.reason,
|
|
27
|
+
capability,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
9
31
|
const parseProjectID = (req) => asNonEmptyString(req?.params?.projectId);
|
|
10
32
|
const parseTaskID = (req) => asNonEmptyString(req?.params?.taskId);
|
|
11
33
|
|
|
@@ -182,6 +204,11 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
|
|
|
182
204
|
return res.status(404).json({ error: 'Project not found' });
|
|
183
205
|
}
|
|
184
206
|
|
|
207
|
+
const goalRefusal = rejectUnsupportedGoalExecution(taskInput);
|
|
208
|
+
if (goalRefusal) {
|
|
209
|
+
return res.status(501).json(goalRefusal);
|
|
210
|
+
}
|
|
211
|
+
|
|
185
212
|
const upserted = await projectConfigRuntime.upsertScheduledTask(projectID, taskInput);
|
|
186
213
|
const schedulerSynced = await syncAfterMutation(projectID);
|
|
187
214
|
|
|
@@ -359,4 +359,98 @@ describe('scheduled task mutation routes', () => {
|
|
|
359
359
|
await Promise.resolve();
|
|
360
360
|
expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledTimes(4);
|
|
361
361
|
});
|
|
362
|
+
|
|
363
|
+
it('allows create/edit with goalEnabled when Host goal state is supported', async () => {
|
|
364
|
+
const savedTask = {
|
|
365
|
+
id: 'goal-task',
|
|
366
|
+
name: 'Goal task',
|
|
367
|
+
execution: { prompt: 'do the work', goalEnabled: true },
|
|
368
|
+
};
|
|
369
|
+
const projectConfigRuntime = {
|
|
370
|
+
upsertScheduledTask: vi.fn(async () => ({ task: savedTask, tasks: [savedTask], created: true })),
|
|
371
|
+
deleteScheduledTask: vi.fn(),
|
|
372
|
+
listScheduledTasks: vi.fn(),
|
|
373
|
+
};
|
|
374
|
+
const routes = registerMutationRoutes({
|
|
375
|
+
projectConfigRuntime,
|
|
376
|
+
scheduledTasksRuntime: { syncProject: vi.fn(async () => {}) },
|
|
377
|
+
scheduleSyncRetry: vi.fn(),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const created = createResponse();
|
|
381
|
+
await routes.put({
|
|
382
|
+
params: { projectId: 'project-a' },
|
|
383
|
+
body: {
|
|
384
|
+
task: {
|
|
385
|
+
name: 'Goal task',
|
|
386
|
+
execution: {
|
|
387
|
+
prompt: 'do the work',
|
|
388
|
+
providerID: 'openai',
|
|
389
|
+
modelID: 'gpt',
|
|
390
|
+
goalEnabled: true,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
}, created);
|
|
395
|
+
|
|
396
|
+
expect(created.statusCode).toBe(200);
|
|
397
|
+
expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(1);
|
|
398
|
+
expect(projectConfigRuntime.upsertScheduledTask.mock.calls[0][1]).toMatchObject({
|
|
399
|
+
execution: expect.objectContaining({ goalEnabled: true }),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const updatedTask = { ...savedTask, id: 'existing', name: 'Existing' };
|
|
403
|
+
projectConfigRuntime.upsertScheduledTask.mockResolvedValueOnce({
|
|
404
|
+
task: updatedTask,
|
|
405
|
+
tasks: [updatedTask],
|
|
406
|
+
created: false,
|
|
407
|
+
});
|
|
408
|
+
const updated = createResponse();
|
|
409
|
+
await routes.put({
|
|
410
|
+
params: { projectId: 'project-a' },
|
|
411
|
+
body: {
|
|
412
|
+
task: {
|
|
413
|
+
id: 'existing',
|
|
414
|
+
name: 'Existing',
|
|
415
|
+
execution: {
|
|
416
|
+
prompt: 'do the work',
|
|
417
|
+
providerID: 'openai',
|
|
418
|
+
modelID: 'gpt',
|
|
419
|
+
goalEnabled: true,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
}, updated);
|
|
424
|
+
|
|
425
|
+
expect(updated.statusCode).toBe(200);
|
|
426
|
+
expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(2);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('allows ordinary non-goal task create when goal is unsupported', async () => {
|
|
430
|
+
const savedTask = { id: 'plain', name: 'Plain' };
|
|
431
|
+
const projectConfigRuntime = {
|
|
432
|
+
upsertScheduledTask: vi.fn(async () => ({ task: savedTask, tasks: [savedTask], created: true })),
|
|
433
|
+
deleteScheduledTask: vi.fn(),
|
|
434
|
+
listScheduledTasks: vi.fn(),
|
|
435
|
+
};
|
|
436
|
+
const routes = registerMutationRoutes({
|
|
437
|
+
projectConfigRuntime,
|
|
438
|
+
scheduledTasksRuntime: { syncProject: vi.fn(async () => {}) },
|
|
439
|
+
scheduleSyncRetry: vi.fn(),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const response = createResponse();
|
|
443
|
+
await routes.put({
|
|
444
|
+
params: { projectId: 'project-a' },
|
|
445
|
+
body: {
|
|
446
|
+
task: {
|
|
447
|
+
name: 'Plain',
|
|
448
|
+
execution: { prompt: 'hello', providerID: 'openai', modelID: 'gpt' },
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
}, response);
|
|
452
|
+
|
|
453
|
+
expect(response.statusCode).toBe(200);
|
|
454
|
+
expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(1);
|
|
455
|
+
});
|
|
362
456
|
});
|