@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
|
@@ -4,7 +4,6 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
6
6
|
import { validAssistantDeliveryParts } from '../assistant-delivery-parts.js';
|
|
7
|
-
import { reduceBackfillState } from './history-state.js';
|
|
8
7
|
import { createContactMemoryReader } from './memory.js';
|
|
9
8
|
import { getWorktrees as defaultListWorktrees } from '../git/service.js';
|
|
10
9
|
import { contactCardIdentity, parseContactCard, parseContactPart } from './cards.js';
|
|
@@ -78,6 +77,7 @@ import {
|
|
|
78
77
|
} from './assign.js';
|
|
79
78
|
import { runContactTurn as defaultRunContactTurn } from './harness.js';
|
|
80
79
|
import { loadConnectedCatalog } from '../llm/catalog.js';
|
|
80
|
+
import { buildAssistantSessionMetadata } from '../session-metadata/system-session.js';
|
|
81
81
|
|
|
82
82
|
const require = createRequire(import.meta.url);
|
|
83
83
|
const SCHEMA_VERSION = 13;
|
|
@@ -236,7 +236,7 @@ const awaitWithDeadline = async (work, ms = CONTACT_CATALOG_DEADLINE_MS, code =
|
|
|
236
236
|
}
|
|
237
237
|
};
|
|
238
238
|
|
|
239
|
-
export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, getOpenCodeAuthHeaders, getServerId = async () => null, getAllowedRoots = () => [], listProjects = async () => [], readModelPreferences = async () => null, listScheduledTasks = null, sessionIndexService = null, upsertScheduledTask = null, syncScheduledTaskProject = null, globalEventHub = null, onRevisionTip = null, onContactTurnEvent = null, onContactTurnComplete = null, clock = () => Date.now(), setIntervalFn = setInterval, clearIntervalFn = clearInterval, setImmediateFn = setImmediate, reconcileIntervalMs = 60_000, clientFactory, fetchImpl, createChatCompletion = null, runContactTurn = defaultRunContactTurn, listWorktrees = defaultListWorktrees } = {}) => {
|
|
239
|
+
export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, getOpenCodeAuthHeaders, getServerId = async () => null, getAllowedRoots = () => [], listProjects = async () => [], readModelPreferences = async () => null, listScheduledTasks = null, sessionIndexService = null, upsertScheduledTask = null, syncScheduledTaskProject = null, globalEventHub = null, onRevisionTip = null, onContactTurnEvent = null, onContactTurnComplete = null, /** Host archive op: ({ sessionID, directory, archivedAt }) => Promise */ archiveSessionHost = null, /** Idempotent Host metadata drop after successful SDK delete */ forgetSessionHost = null, /** Host store write: (sessionId, patch) => Promise */ persistSessionMetadata = null, /** After Host isolation metadata is committed. */ onSystemSessionPersisted = null, clock = () => Date.now(), setIntervalFn = setInterval, clearIntervalFn = clearInterval, setImmediateFn = setImmediate, reconcileIntervalMs = 60_000, clientFactory, fetchImpl, createChatCompletion = null, runContactTurn = defaultRunContactTurn, listWorktrees = defaultListWorktrees } = {}) => {
|
|
240
240
|
if (!dbPath || !dataDir) return null;
|
|
241
241
|
const Database = require('better-sqlite3');
|
|
242
242
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
@@ -320,15 +320,11 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
320
320
|
const existing = db.prepare('SELECT directory FROM assistant_session_history WHERE assistant_id=? AND session_id=?').get(assistantID, sessionID);
|
|
321
321
|
if (existing) {
|
|
322
322
|
if (existing.directory == null && directory != null) db.prepare('UPDATE assistant_session_history SET directory=? WHERE assistant_id=? AND session_id=?').run(directory, assistantID, sessionID);
|
|
323
|
-
db.prepare('DELETE FROM assistant_message_backfill WHERE assistant_id=? AND session_id=?').run(assistantID, sessionID);
|
|
324
|
-
db.prepare("UPDATE assistant_message_mirror SET covered=0 WHERE assistant_id=? AND session_id=? AND COALESCE(json_extract(info_json,'$.role'),'')<>'user' AND COALESCE(json_extract(info_json,'$.openchamberAssistantAdmission'),0)<>1").run(assistantID, sessionID);
|
|
325
323
|
return;
|
|
326
324
|
}
|
|
327
325
|
const effectiveDirectory = directory ?? effectiveWorkspace(editable(assistantID));
|
|
328
326
|
const ordinal = Number(db.prepare('SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_session_history WHERE assistant_id=?').get(assistantID).next);
|
|
329
327
|
db.prepare('INSERT INTO assistant_session_history(assistant_id, session_id, ordinal, directory, created_at) VALUES (?,?,?,?,?)').run(assistantID, sessionID, ordinal, effectiveDirectory, now());
|
|
330
|
-
db.prepare('DELETE FROM assistant_message_backfill WHERE assistant_id=? AND session_id=?').run(assistantID, sessionID);
|
|
331
|
-
db.prepare("UPDATE assistant_message_mirror SET covered=0 WHERE assistant_id=? AND session_id=? AND COALESCE(json_extract(info_json,'$.role'),'')<>'user' AND COALESCE(json_extract(info_json,'$.openchamberAssistantAdmission'),0)<>1").run(assistantID, sessionID);
|
|
332
328
|
};
|
|
333
329
|
const activeContactTurnsFor = (assistantID) => activeContactTurnsByAssistant.get(assistantID) ?? new Map();
|
|
334
330
|
const rememberActiveContactTurn = (assistantID, input) => {
|
|
@@ -430,20 +426,34 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
430
426
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
431
427
|
authHeaders: getOpenCodeAuthHeaders(),
|
|
432
428
|
});
|
|
433
|
-
const metadata = (row) => ({
|
|
429
|
+
const metadata = (row) => buildAssistantSessionMetadata({
|
|
430
|
+
assistantID: row.assistant_id,
|
|
431
|
+
name: row.name,
|
|
432
|
+
});
|
|
433
|
+
const persistAssistantIsolation = async (sessionID, directory, isolation) => {
|
|
434
|
+
if (typeof persistSessionMetadata !== 'function') return;
|
|
435
|
+
try {
|
|
436
|
+
await persistSessionMetadata(sessionID, isolation);
|
|
437
|
+
onSystemSessionPersisted?.({ sessionID, directory, metadata: isolation });
|
|
438
|
+
} catch {
|
|
439
|
+
// Archive still isolates; Host persist is the sidebar authority when it succeeds.
|
|
440
|
+
}
|
|
441
|
+
};
|
|
434
442
|
const createSession = async (row) => {
|
|
435
443
|
const directory = effectiveWorkspace(row);
|
|
444
|
+
const isolation = metadata(row);
|
|
436
445
|
const result = await invokeSession(() => client().session.create({
|
|
437
446
|
title: `[Assistant] ${row.name}`,
|
|
438
447
|
location: { directory },
|
|
448
|
+
metadata: isolation,
|
|
439
449
|
...(row.agent ? { agent: row.agent } : {}),
|
|
440
450
|
...(row.provider_id && row.model_id ? { model: { id: row.model_id, providerID: row.provider_id, ...(row.variant ? { variant: row.variant } : {}) } } : {}),
|
|
441
451
|
}));
|
|
442
452
|
const sessionID = sessionIDOf(result);
|
|
443
453
|
if (result?.error || !sessionID) fail('upstream_error');
|
|
454
|
+
// Host metadata isolates the session if archive is delayed or unavailable.
|
|
455
|
+
await persistAssistantIsolation(sessionID, directory, isolation);
|
|
444
456
|
// Archive before binding so ordinary session lists never flash system sessions.
|
|
445
|
-
// Metadata still isolates the session if archive fails after create.
|
|
446
|
-
// v2 has no session.update archive-metadata write; local history + title prefix are the equivalent isolation.
|
|
447
457
|
return { sessionID, directory };
|
|
448
458
|
};
|
|
449
459
|
const sessionExists = async (row) => { if (!row.current_session_id) return false; const result = await invokeSession(() => client().session.get({ sessionID: row.current_session_id })); if (isMissing(result)) return false; if (result.error) fail('upstream_error'); return Boolean(sessionIDOf(result) || result); };
|
|
@@ -488,73 +498,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
488
498
|
db.prepare("INSERT OR REPLACE INTO assistant_meta(key,value) VALUES ('schema_version',?)").run(String(SCHEMA_VERSION));
|
|
489
499
|
};
|
|
490
500
|
migrate();
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const existing = db.prepare('SELECT ordinal FROM assistant_message_mirror WHERE assistant_id=? AND session_id=? AND message_id=?').get(assistantID, sessionID, messageID);
|
|
495
|
-
const nextOrdinal = Number.isSafeInteger(ordinal) ? ordinal : existing?.ordinal ?? Number(db.prepare('SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_message_mirror WHERE assistant_id=? AND session_id=?').get(assistantID, sessionID).next);
|
|
496
|
-
db.prepare('INSERT INTO assistant_message_mirror(assistant_id,session_id,message_id,info_json,ordinal,covered,updated_at) VALUES (?,?,?,?,?,?,?) ON CONFLICT(assistant_id,session_id,message_id) DO UPDATE SET info_json=excluded.info_json,ordinal=excluded.ordinal,covered=CASE WHEN assistant_message_mirror.covered=1 OR excluded.covered=1 THEN 1 ELSE 0 END,updated_at=excluded.updated_at').run(assistantID, sessionID, messageID, json(info), nextOrdinal, covered ? 1 : 0, now());
|
|
497
|
-
};
|
|
498
|
-
const mirrorPart = (assistantID, sessionID, part, ordinal) => {
|
|
499
|
-
const messageID = part?.messageID; const partID = part?.id;
|
|
500
|
-
if (!nonEmptyString(assistantID) || !nonEmptyString(sessionID) || !nonEmptyString(messageID) || !nonEmptyString(partID) || !plainObject(part)) return;
|
|
501
|
-
const existing = db.prepare('SELECT ordinal FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? AND part_id=?').get(assistantID, sessionID, messageID, partID);
|
|
502
|
-
const nextOrdinal = Number.isSafeInteger(ordinal) ? ordinal : existing?.ordinal ?? Number(db.prepare('SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=?').get(assistantID, sessionID, messageID).next);
|
|
503
|
-
db.prepare('INSERT INTO assistant_message_part_mirror(assistant_id,session_id,message_id,part_id,part_json,ordinal,updated_at) VALUES (?,?,?,?,?,?,?) ON CONFLICT(assistant_id,session_id,message_id,part_id) DO UPDATE SET part_json=excluded.part_json,ordinal=excluded.ordinal,updated_at=excluded.updated_at').run(assistantID, sessionID, messageID, partID, json(part), nextOrdinal, now());
|
|
504
|
-
};
|
|
505
|
-
const mirrorAdmittedUserMessage = (row, sessionID, messageID, parts, config) => {
|
|
506
|
-
const existingMessage = db.prepare('SELECT info_json FROM assistant_message_mirror WHERE assistant_id=? AND session_id=? AND message_id=?').get(row.assistant_id, sessionID, messageID);
|
|
507
|
-
const existingInfo = existingMessage ? parse(existingMessage.info_json) : null;
|
|
508
|
-
const info = plainObject(existingInfo) && existingInfo.role === 'user'
|
|
509
|
-
? existingInfo
|
|
510
|
-
: {
|
|
511
|
-
id: messageID,
|
|
512
|
-
sessionID,
|
|
513
|
-
role: 'user',
|
|
514
|
-
time: { created: now() },
|
|
515
|
-
...(config?.agent ? { agent: config.agent } : {}),
|
|
516
|
-
...(plainObject(config?.model) ? { model: config.model } : {}),
|
|
517
|
-
...(typeof config?.system === 'string' && config.system ? { system: config.system } : {}),
|
|
518
|
-
summary: { diffs: [] },
|
|
519
|
-
openchamberAssistantAdmission: true,
|
|
520
|
-
};
|
|
521
|
-
db.exec('BEGIN IMMEDIATE');
|
|
522
|
-
try {
|
|
523
|
-
mirrorMessage(row.assistant_id, sessionID, info, undefined, true);
|
|
524
|
-
const hasAuthoritativeParts = Number(db.prepare("SELECT COUNT(*) AS count FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? AND part_id NOT GLOB 'oc_asst_admission:*'").get(row.assistant_id, sessionID, messageID).count) > 0;
|
|
525
|
-
if (!hasAuthoritativeParts) {
|
|
526
|
-
db.prepare("DELETE FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? AND part_id GLOB 'oc_asst_admission:*'").run(row.assistant_id, sessionID, messageID);
|
|
527
|
-
parts.forEach((part, index) => mirrorPart(row.assistant_id, sessionID, {
|
|
528
|
-
...part,
|
|
529
|
-
id: `oc_asst_admission:${index + 1}`,
|
|
530
|
-
sessionID,
|
|
531
|
-
messageID,
|
|
532
|
-
}, index + 1));
|
|
533
|
-
}
|
|
534
|
-
db.exec('COMMIT');
|
|
535
|
-
} catch (error) {
|
|
536
|
-
db.exec('ROLLBACK');
|
|
537
|
-
throw error;
|
|
538
|
-
}
|
|
539
|
-
bump();
|
|
540
|
-
};
|
|
501
|
+
// V2: OpenCode projection is the sole message-body authority. Legacy mirror /
|
|
502
|
+
// backfill tables may still exist for leftover rows but are never written or
|
|
503
|
+
// read for history serving.
|
|
541
504
|
const mappedAssistants = (sessionID) => db.prepare("SELECT assistant_id FROM assistant_v2 WHERE current_session_id=? AND tombstone_at IS NULL UNION SELECT h.assistant_id FROM assistant_session_history h JOIN assistant_v2 a ON a.assistant_id=h.assistant_id WHERE h.session_id=? AND a.tombstone_at IS NULL").all(sessionID, sessionID).map((row) => row.assistant_id);
|
|
542
|
-
const invalidateCoverage = (assistantID, sessionID) => {
|
|
543
|
-
db.prepare('UPDATE assistant_message_mirror SET covered=0 WHERE assistant_id=? AND session_id=?').run(assistantID, sessionID);
|
|
544
|
-
db.prepare('INSERT INTO assistant_message_backfill(assistant_id,session_id,cursor,complete,updated_at) VALUES (?,?,?,?,?) ON CONFLICT(assistant_id,session_id) DO UPDATE SET cursor=NULL,complete=0,updated_at=excluded.updated_at').run(assistantID, sessionID, null, 0, now());
|
|
545
|
-
};
|
|
546
|
-
// Reopen demand backfill without clearing covered/provisional mirrors.
|
|
547
|
-
const invalidateBackfill = (assistantID, sessionID) => {
|
|
548
|
-
const current = db.prepare('SELECT cursor,complete FROM assistant_message_backfill WHERE assistant_id=? AND session_id=?').get(assistantID, sessionID);
|
|
549
|
-
const next = reduceBackfillState({ cursor: current?.cursor ?? null, complete: Boolean(current?.complete) }, 'invalidate');
|
|
550
|
-
db.prepare('INSERT INTO assistant_message_backfill(assistant_id,session_id,cursor,complete,updated_at) VALUES (?,?,?,?,?) ON CONFLICT(assistant_id,session_id) DO UPDATE SET cursor=excluded.cursor,complete=excluded.complete,updated_at=excluded.updated_at').run(assistantID, sessionID, next.cursor, next.complete ? 1 : 0, now());
|
|
551
|
-
};
|
|
552
|
-
// Structural part deletes may leave a covered message incomplete; re-demand that session only.
|
|
553
|
-
// Do not blanket-uncover on ordinary message/part upserts — that blanks served history until re-backfill.
|
|
554
|
-
const invalidateMessageCoverage = (assistantID, sessionID, messageID) => {
|
|
555
|
-
db.prepare('UPDATE assistant_message_mirror SET covered=0 WHERE assistant_id=? AND session_id=? AND message_id=?').run(assistantID, sessionID, messageID);
|
|
556
|
-
db.prepare('INSERT INTO assistant_message_backfill(assistant_id,session_id,cursor,complete,updated_at) VALUES (?,?,?,?,?) ON CONFLICT(assistant_id,session_id) DO UPDATE SET cursor=NULL,complete=0,updated_at=excluded.updated_at').run(assistantID, sessionID, null, 0, now());
|
|
557
|
-
};
|
|
558
505
|
// Accept bridge/legacy `{ properties }` and native v2 `{ data }` envelopes.
|
|
559
506
|
const eventBody = (payload) => {
|
|
560
507
|
if (!plainObject(payload)) return null;
|
|
@@ -608,9 +555,14 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
608
555
|
if (type === 'session.error' || type === 'session.execution.failed') {
|
|
609
556
|
return isUserAbort(body?.error) ? 'cancelled' : 'error';
|
|
610
557
|
}
|
|
611
|
-
//
|
|
558
|
+
// Official reason: user | shutdown | superseded | inactivity (ticket 07).
|
|
559
|
+
// Shutdown keeps the upstream execution claim for restart recovery — do not
|
|
560
|
+
// settle the watch or schedule contact resume; wait for authoritative terminal.
|
|
612
561
|
if (type === 'session.execution.interrupted') {
|
|
613
|
-
|
|
562
|
+
const reason = typeof body?.reason === 'string' ? body.reason.trim() : '';
|
|
563
|
+
if (reason === 'shutdown') return null;
|
|
564
|
+
if (reason === 'user' || isUserAbort(body?.error)) return 'cancelled';
|
|
565
|
+
return 'error';
|
|
614
566
|
}
|
|
615
567
|
if (type === 'session.execution.succeeded') return 'complete';
|
|
616
568
|
if (type === 'session.execution.started' || type === 'session.retry.scheduled') return 'busy';
|
|
@@ -909,25 +861,26 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
909
861
|
if (!nonEmptyString(sessionID) || !plainObject(info)) return false;
|
|
910
862
|
const cancelled = info.role === 'assistant' && isUserAbort(info.error)
|
|
911
863
|
? reportAssignedSession(sessionID, 'cancelled') : false;
|
|
912
|
-
|
|
864
|
+
// Body authority is OpenCode projection GET — do not mirror message bodies.
|
|
865
|
+
return cancelled || mappedAssistants(sessionID).length > 0;
|
|
913
866
|
}
|
|
914
867
|
if (payload.type === 'message.part.updated') {
|
|
915
868
|
if (!plainObject(body)) return false;
|
|
916
869
|
const part = body.part; const sessionID = body.sessionID ?? part?.sessionID;
|
|
917
870
|
if (!nonEmptyString(sessionID) || !plainObject(part)) return false;
|
|
918
|
-
|
|
871
|
+
return mappedAssistants(sessionID).length > 0;
|
|
919
872
|
}
|
|
920
873
|
if (payload.type === 'message.removed') {
|
|
921
874
|
if (!plainObject(body)) return false;
|
|
922
875
|
const sessionID = body.sessionID; const messageID = body.messageID;
|
|
923
876
|
if (!nonEmptyString(sessionID) || !nonEmptyString(messageID)) return false;
|
|
924
|
-
|
|
877
|
+
return mappedAssistants(sessionID).length > 0;
|
|
925
878
|
}
|
|
926
879
|
if (payload.type === 'message.part.removed') {
|
|
927
880
|
if (!plainObject(body)) return false;
|
|
928
881
|
const sessionID = body.sessionID; const messageID = body.messageID ?? body.part?.messageID; const partID = body.partID ?? body.part?.id;
|
|
929
882
|
if (!nonEmptyString(sessionID) || !nonEmptyString(messageID) || !nonEmptyString(partID)) return false;
|
|
930
|
-
|
|
883
|
+
return mappedAssistants(sessionID).length > 0;
|
|
931
884
|
}
|
|
932
885
|
const sessionID = plainObject(body) ? eventSessionID(body) : '';
|
|
933
886
|
if (payload.type === 'session.execution.started' || payload.type === 'session.retry.scheduled') {
|
|
@@ -939,9 +892,13 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
939
892
|
|| payload.type === 'session.execution.interrupted') {
|
|
940
893
|
if (!sessionID) return false;
|
|
941
894
|
const status = settleStatusFromEvent(payload, body);
|
|
942
|
-
|
|
895
|
+
// shutdown interrupt: status null — keep watch in-flight (pending recovery).
|
|
896
|
+
if (!status) {
|
|
897
|
+
return listWatchesBySession(db, sessionID).length > 0
|
|
898
|
+
|| mappedAssistants(sessionID).length > 0;
|
|
899
|
+
}
|
|
943
900
|
const reported = reportAssignedSession(sessionID, status);
|
|
944
|
-
const assistants = mappedAssistants(sessionID);
|
|
901
|
+
const assistants = mappedAssistants(sessionID);
|
|
945
902
|
return reported || assistants.some((assistantID) => assistant(assistantID)?.current_session_id !== sessionID);
|
|
946
903
|
}
|
|
947
904
|
if (payload.type === 'session.idle' || payload.type === 'session.error') {
|
|
@@ -949,7 +906,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
949
906
|
// session.idle is a weak fallback: watermarked watches need execution.* or reconcile.
|
|
950
907
|
const status = settleStatusFromEvent(payload, body);
|
|
951
908
|
const reported = reportAssignedSession(sessionID, status, { requireExecutionOwned: payload.type === 'session.idle' });
|
|
952
|
-
const assistants = mappedAssistants(sessionID);
|
|
909
|
+
const assistants = mappedAssistants(sessionID);
|
|
953
910
|
return reported || assistants.some((assistantID) => assistant(assistantID)?.current_session_id !== sessionID);
|
|
954
911
|
}
|
|
955
912
|
if (payload.type === 'session.status') {
|
|
@@ -957,7 +914,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
957
914
|
const status = settleStatusFromEvent(payload, body);
|
|
958
915
|
if (!status) return false;
|
|
959
916
|
const reported = reportAssignedSession(sessionID, status, { requireExecutionOwned: status === 'complete' });
|
|
960
|
-
const assistants = mappedAssistants(sessionID);
|
|
917
|
+
const assistants = mappedAssistants(sessionID);
|
|
961
918
|
return reported || assistants.some((assistantID) => assistant(assistantID)?.current_session_id !== sessionID);
|
|
962
919
|
}
|
|
963
920
|
if (payload.type === 'question.asked' || payload.type === 'permission.asked'
|
|
@@ -975,162 +932,364 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
975
932
|
}
|
|
976
933
|
return null;
|
|
977
934
|
};
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const writeBackfillState = (assistantID, sessionID, next) => {
|
|
983
|
-
db.prepare('INSERT INTO assistant_message_backfill(assistant_id,session_id,cursor,complete,updated_at) VALUES (?,?,?,?,?) ON CONFLICT(assistant_id,session_id) DO UPDATE SET cursor=excluded.cursor,complete=excluded.complete,updated_at=excluded.updated_at').run(assistantID, sessionID, next.cursor, next.complete ? 1 : 0, now());
|
|
984
|
-
};
|
|
985
|
-
// Authoritative 404 means this session ID is gone from OpenCode. Converge its
|
|
986
|
-
// backfill so one deleted archive cannot block demand scans of other sessions.
|
|
987
|
-
// Covered/admitted rows stay; only uncovered event mirrors are dropped. Safe
|
|
988
|
-
// under concurrent ensure: a replaced current binding archives under its old
|
|
989
|
-
// ID, while a still-current missing ID is recreated with a new session ID.
|
|
990
|
-
const completeMissingSessionBackfill = (assistantID, sessionID) => {
|
|
991
|
-
db.exec('BEGIN IMMEDIATE');
|
|
935
|
+
// Host history cursor: binding ordinal + official opaque upstream cursor +
|
|
936
|
+
// page-local skip. Carries positions/IDs only — never message bodies.
|
|
937
|
+
const decodeCursor = (value) => {
|
|
938
|
+
if (value == null || value === '') return null;
|
|
992
939
|
try {
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
return
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
940
|
+
const parsed = parse(Buffer.from(String(value), 'base64url').toString('utf8'));
|
|
941
|
+
if (!Number.isSafeInteger(parsed?.sessionOrdinal)) fail('validation_error');
|
|
942
|
+
if (parsed.upstream != null && !nonEmptyString(parsed.upstream)) fail('validation_error');
|
|
943
|
+
const skip = parsed.skip == null ? 0 : parsed.skip;
|
|
944
|
+
if (!Number.isSafeInteger(skip) || skip < 0) fail('validation_error');
|
|
945
|
+
return {
|
|
946
|
+
sessionOrdinal: parsed.sessionOrdinal,
|
|
947
|
+
upstream: parsed.upstream ?? null,
|
|
948
|
+
skip,
|
|
949
|
+
};
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (error instanceof AssistantError) throw error;
|
|
952
|
+
fail('validation_error');
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
const encodeCursor = (state) => Buffer.from(json({
|
|
956
|
+
sessionOrdinal: state.sessionOrdinal,
|
|
957
|
+
upstream: state.upstream ?? null,
|
|
958
|
+
skip: state.skip ?? 0,
|
|
959
|
+
})).toString('base64url');
|
|
960
|
+
// Official v2: GET /api/session/:id/message — first page order=desc; continuation
|
|
961
|
+
// cursor only (never combine order+cursor). Prefer fetchImpl when injected.
|
|
962
|
+
const fetchProjectionMessages = async (sessionID, directory, cursor, pageLimit = BACKFILL_PAGE_SIZE) => {
|
|
1002
963
|
for (let attempt = 0; attempt < BACKFILL_MESSAGES_ATTEMPTS; attempt++) {
|
|
1003
964
|
try {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
965
|
+
if (typeof fetchImpl === 'function') {
|
|
966
|
+
const base = String(buildOpenCodeUrl('/', '') || '').replace(/\/$/, '') || 'http://opencode.local';
|
|
967
|
+
const url = new URL(`${base}/api/session/${encodeURIComponent(sessionID)}/message`);
|
|
968
|
+
url.searchParams.set('limit', String(pageLimit));
|
|
969
|
+
// Official contract: do not combine order with cursor.
|
|
970
|
+
if (cursor) url.searchParams.set('cursor', cursor);
|
|
971
|
+
else url.searchParams.set('order', 'desc');
|
|
972
|
+
if (directory) url.searchParams.set('directory', directory);
|
|
973
|
+
const headers = {
|
|
974
|
+
Accept: 'application/json',
|
|
975
|
+
...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
|
|
976
|
+
};
|
|
977
|
+
const response = await fetchImpl(url, { method: 'GET', headers });
|
|
978
|
+
const status = response?.status ?? response?.statusCode;
|
|
979
|
+
if (status === 404) return { error: { status: 404 }, status: 404 };
|
|
980
|
+
if (status != null && status >= 400) {
|
|
981
|
+
const errBody = { error: { status }, status };
|
|
982
|
+
if (!isTransientMessagesFailure(errBody, null) || attempt === BACKFILL_MESSAGES_ATTEMPTS - 1) return errBody;
|
|
983
|
+
} else {
|
|
984
|
+
const payload = typeof response?.json === 'function' ? await response.json() : null;
|
|
985
|
+
return {
|
|
986
|
+
data: Array.isArray(payload?.data) ? payload.data : (Array.isArray(payload) ? payload : payload?.data),
|
|
987
|
+
cursor: payload?.cursor,
|
|
988
|
+
response: {
|
|
989
|
+
status: status ?? 200,
|
|
990
|
+
headers: {
|
|
991
|
+
get: (name) => {
|
|
992
|
+
if (typeof response?.headers?.get === 'function') return response.headers.get(name);
|
|
993
|
+
return null;
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
const result = await invokeSession(() => client().message.list({
|
|
1001
|
+
sessionID,
|
|
1002
|
+
limit: pageLimit,
|
|
1003
|
+
// First page: order=desc. Continuation: opaque cursor only.
|
|
1004
|
+
...(cursor ? { cursor } : { order: 'desc' }),
|
|
1005
|
+
}));
|
|
1006
|
+
if (!result?.error || isMissing(result)) return result;
|
|
1007
|
+
if (!isTransientMessagesFailure(result, null) || attempt === BACKFILL_MESSAGES_ATTEMPTS - 1) return result;
|
|
1008
|
+
}
|
|
1007
1009
|
} catch (error) {
|
|
1008
1010
|
if (error instanceof AssistantError) throw error;
|
|
1009
|
-
if (
|
|
1011
|
+
if (isMissingError(error)) return { error: { status: 404 }, status: 404 };
|
|
1012
|
+
if (!isTransientMessagesFailure(null, error) || attempt === BACKFILL_MESSAGES_ATTEMPTS - 1) {
|
|
1013
|
+
const status = getHttpStatus(error);
|
|
1014
|
+
return { error: { status: status ?? 500 }, status: status ?? 500 };
|
|
1015
|
+
}
|
|
1010
1016
|
}
|
|
1011
1017
|
await sleep(BACKFILL_RETRY_MS[Math.min(attempt, BACKFILL_RETRY_MS.length - 1)]);
|
|
1012
1018
|
}
|
|
1013
|
-
|
|
1019
|
+
return { error: { status: 500 }, status: 500 };
|
|
1014
1020
|
};
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
if (
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1021
|
+
const resolveBindingDirectory = async (binding) => {
|
|
1022
|
+
let directory = binding.directory ?? null;
|
|
1023
|
+
if (directory != null) return directory;
|
|
1024
|
+
const session = await invokeSession(() => client().session.get({ sessionID: binding.session_id })).catch(() => null);
|
|
1025
|
+
if (!session?.error) {
|
|
1026
|
+
const resolved = resolveArchivedDirectory(
|
|
1027
|
+
binding.assistant_id,
|
|
1028
|
+
sessionDirectoryOf(session),
|
|
1029
|
+
session?.data?.directory,
|
|
1030
|
+
session?.data?.project?.worktree,
|
|
1031
|
+
);
|
|
1023
1032
|
if (resolved) {
|
|
1024
|
-
db.prepare('UPDATE assistant_session_history SET directory=? WHERE assistant_id=? AND session_id=? AND directory IS NULL')
|
|
1033
|
+
db.prepare('UPDATE assistant_session_history SET directory=? WHERE assistant_id=? AND session_id=? AND directory IS NULL')
|
|
1034
|
+
.run(resolved, binding.assistant_id, binding.session_id);
|
|
1025
1035
|
directory = resolved;
|
|
1026
1036
|
}
|
|
1027
1037
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
let
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1038
|
+
return directory;
|
|
1039
|
+
};
|
|
1040
|
+
/**
|
|
1041
|
+
* Walk one binding from an official upstream position (tip or opaque cursor).
|
|
1042
|
+
* Emits rows in authoritative projection order (order=desc ⇒ newest first).
|
|
1043
|
+
* Host page boundary is `skip` within the current upstream page — no tip rescan.
|
|
1044
|
+
*/
|
|
1045
|
+
const loadProjectionBinding = async (binding, {
|
|
1046
|
+
upstream = null,
|
|
1047
|
+
skip = 0,
|
|
1048
|
+
need = BACKFILL_PAGE_SIZE,
|
|
1049
|
+
maxPages = BACKFILL_MAX_PAGES,
|
|
1050
|
+
pageLimit = BACKFILL_PAGE_SIZE,
|
|
1051
|
+
} = {}) => {
|
|
1052
|
+
const directory = await resolveBindingDirectory(binding);
|
|
1053
|
+
const collected = [];
|
|
1054
|
+
let cursor = upstream;
|
|
1055
|
+
let pageSkip = Math.max(0, skip);
|
|
1056
|
+
let exhausted = false;
|
|
1057
|
+
let failed = false;
|
|
1058
|
+
let failedStatus = null;
|
|
1059
|
+
let pagesUsed = 0;
|
|
1060
|
+
let resumeUpstream = upstream;
|
|
1061
|
+
let resumeSkip = pageSkip;
|
|
1062
|
+
const pageSize = Math.min(BACKFILL_PAGE_SIZE, Math.max(1, pageLimit));
|
|
1063
|
+
|
|
1064
|
+
for (let page = 0; page < maxPages; page++) {
|
|
1065
|
+
resumeUpstream = cursor;
|
|
1066
|
+
resumeSkip = pageSkip;
|
|
1067
|
+
const result = await fetchProjectionMessages(binding.session_id, directory, cursor, pageSize);
|
|
1068
|
+
pagesUsed += 1;
|
|
1069
|
+
if (isMissing(result)) {
|
|
1070
|
+
exhausted = true;
|
|
1071
|
+
return {
|
|
1072
|
+
entries: collected, directory, missing: true, failed: false, failedStatus: null,
|
|
1073
|
+
exhausted: true, pagesUsed, resumeUpstream: null, resumeSkip: 0,
|
|
1074
|
+
};
|
|
1056
1075
|
}
|
|
1076
|
+
if (result?.error) {
|
|
1077
|
+
failed = true;
|
|
1078
|
+
failedStatus = messagesErrorStatus(result) ?? result.status ?? 500;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
const entries = projectionEntries(result, binding.session_id);
|
|
1082
|
+
if (!entries) {
|
|
1083
|
+
failed = true;
|
|
1084
|
+
failedStatus = 500;
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
// Empty page is the authoritative end (non-empty pages may still carry next).
|
|
1088
|
+
if (entries.length === 0) {
|
|
1089
|
+
exhausted = true;
|
|
1090
|
+
resumeUpstream = null;
|
|
1091
|
+
resumeSkip = 0;
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
let index = 0;
|
|
1095
|
+
let stoppedEarly = false;
|
|
1096
|
+
for (; index < entries.length; index++) {
|
|
1097
|
+
if (index < pageSkip) continue;
|
|
1098
|
+
const entry = entries[index];
|
|
1099
|
+
const info = entry?.info ?? entry;
|
|
1100
|
+
if (!plainObject(info) || !nonEmptyString(info.id)) continue;
|
|
1101
|
+
if (nonEmptyString(info.sessionID) && info.sessionID !== binding.session_id) continue;
|
|
1102
|
+
collected.push({
|
|
1103
|
+
session_id: binding.session_id,
|
|
1104
|
+
session_ordinal: binding.ordinal,
|
|
1105
|
+
directory,
|
|
1106
|
+
message_id: info.id,
|
|
1107
|
+
// Source seq within authoritative page order (not time.created).
|
|
1108
|
+
seq: collected.length,
|
|
1109
|
+
info,
|
|
1110
|
+
parts: Array.isArray(entry?.parts) ? entry.parts : [],
|
|
1111
|
+
});
|
|
1112
|
+
if (collected.length >= need) {
|
|
1113
|
+
// Keep resumeSkip on this row when it is the probe past `limit`.
|
|
1114
|
+
stoppedEarly = true;
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
pageSkip = 0;
|
|
1119
|
+
if (collected.length >= need) {
|
|
1120
|
+
// Page boundary: resume at the probe row (or next upstream page if none left).
|
|
1121
|
+
if (stoppedEarly && index < entries.length) {
|
|
1122
|
+
resumeUpstream = cursor;
|
|
1123
|
+
resumeSkip = index;
|
|
1124
|
+
} else {
|
|
1125
|
+
const next = projectionCursor(result);
|
|
1126
|
+
resumeUpstream = next || null;
|
|
1127
|
+
resumeSkip = 0;
|
|
1128
|
+
// Official: last non-empty page may still expose next; empty follow-up ends.
|
|
1129
|
+
if (!next) exhausted = true;
|
|
1130
|
+
}
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
const next = projectionCursor(result);
|
|
1134
|
+
if (!next) {
|
|
1135
|
+
exhausted = true;
|
|
1136
|
+
resumeUpstream = null;
|
|
1137
|
+
resumeSkip = 0;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
cursor = next;
|
|
1141
|
+
pageSkip = 0;
|
|
1142
|
+
resumeUpstream = next;
|
|
1143
|
+
resumeSkip = 0;
|
|
1057
1144
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1145
|
+
|
|
1146
|
+
return {
|
|
1147
|
+
entries: collected,
|
|
1148
|
+
directory,
|
|
1149
|
+
missing: false,
|
|
1150
|
+
failed,
|
|
1151
|
+
failedStatus,
|
|
1152
|
+
exhausted: exhausted && !failed,
|
|
1153
|
+
pagesUsed,
|
|
1154
|
+
resumeUpstream,
|
|
1155
|
+
resumeSkip,
|
|
1156
|
+
};
|
|
1157
|
+
};
|
|
1158
|
+
const listHistoryBindings = (row) => {
|
|
1159
|
+
const history = db.prepare('SELECT session_id, directory, ordinal FROM assistant_session_history WHERE assistant_id=? ORDER BY ordinal ASC')
|
|
1160
|
+
.all(row.assistant_id);
|
|
1161
|
+
const bindings = history.map((item) => ({
|
|
1162
|
+
assistant_id: row.assistant_id,
|
|
1163
|
+
session_id: item.session_id,
|
|
1164
|
+
directory: item.directory ?? null,
|
|
1165
|
+
ordinal: item.ordinal,
|
|
1166
|
+
}));
|
|
1167
|
+
const currentIsArchived = row.current_session_id != null
|
|
1168
|
+
&& Boolean(db.prepare('SELECT 1 FROM assistant_session_history WHERE assistant_id=? AND session_id=?').get(row.assistant_id, row.current_session_id));
|
|
1169
|
+
if (row.current_session_id && !currentIsArchived) {
|
|
1170
|
+
const nextOrdinal = Number(db.prepare('SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_session_history WHERE assistant_id=?').get(row.assistant_id).next);
|
|
1171
|
+
bindings.push({
|
|
1172
|
+
assistant_id: row.assistant_id,
|
|
1173
|
+
session_id: row.current_session_id,
|
|
1174
|
+
directory: effectiveWorkspace(row),
|
|
1175
|
+
ordinal: nextOrdinal,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
return bindings;
|
|
1072
1179
|
};
|
|
1073
1180
|
const historicalMessages = async (assistantID, input = {}) => {
|
|
1074
|
-
const row = editable(assistantID);
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1181
|
+
const row = editable(assistantID);
|
|
1182
|
+
const limit = input.limit == null ? 50 : Number(input.limit);
|
|
1183
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) fail('validation_error');
|
|
1184
|
+
const before = decodeCursor(input.before);
|
|
1185
|
+
const bindings = listHistoryBindings(row);
|
|
1186
|
+
// Newest bindings first; within a binding keep official projection order.
|
|
1187
|
+
const scanOrder = [...bindings].sort((left, right) => right.ordinal - left.ordinal);
|
|
1188
|
+
let startIndex = 0;
|
|
1189
|
+
if (before != null) {
|
|
1190
|
+
const found = scanOrder.findIndex((binding) => binding.ordinal === before.sessionOrdinal);
|
|
1191
|
+
// Binding disappeared (archive GC) — resume at the nearest older binding.
|
|
1192
|
+
startIndex = found >= 0 ? found : scanOrder.findIndex((binding) => binding.ordinal < before.sessionOrdinal);
|
|
1193
|
+
if (startIndex < 0) startIndex = scanOrder.length;
|
|
1194
|
+
}
|
|
1195
|
+
const collected = [];
|
|
1196
|
+
const failedScope = [];
|
|
1197
|
+
let anySuccess = false;
|
|
1198
|
+
let pagesUsed = 0;
|
|
1199
|
+
let nextState = null;
|
|
1200
|
+
|
|
1201
|
+
for (let index = startIndex; index < scanOrder.length; index++) {
|
|
1202
|
+
if (collected.length > limit) break;
|
|
1203
|
+
if (pagesUsed >= BACKFILL_MAX_PAGES) {
|
|
1204
|
+
const binding = scanOrder[index];
|
|
1205
|
+
nextState = {
|
|
1206
|
+
sessionOrdinal: binding.ordinal,
|
|
1207
|
+
upstream: index === startIndex && before ? before.upstream : null,
|
|
1208
|
+
skip: index === startIndex && before ? before.skip : 0,
|
|
1209
|
+
};
|
|
1103
1210
|
break;
|
|
1104
1211
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1212
|
+
const binding = scanOrder[index];
|
|
1213
|
+
const resumeHere = index === startIndex && before != null;
|
|
1214
|
+
const loaded = await loadProjectionBinding(binding, {
|
|
1215
|
+
upstream: resumeHere ? before.upstream : null,
|
|
1216
|
+
skip: resumeHere ? before.skip : 0,
|
|
1217
|
+
need: limit + 1 - collected.length,
|
|
1218
|
+
maxPages: BACKFILL_MAX_PAGES - pagesUsed,
|
|
1219
|
+
pageLimit: limit,
|
|
1220
|
+
});
|
|
1221
|
+
pagesUsed += loaded.pagesUsed;
|
|
1222
|
+
if (loaded.failed) {
|
|
1223
|
+
failedScope.push({
|
|
1224
|
+
sessionID: binding.session_id,
|
|
1225
|
+
sessionOrdinal: binding.ordinal,
|
|
1226
|
+
status: loaded.failedStatus ?? 500,
|
|
1227
|
+
});
|
|
1228
|
+
// Do not advance past a failed binding — cursor stays retryable here.
|
|
1229
|
+
// Newer successes already in `collected` are still delivered.
|
|
1230
|
+
nextState = {
|
|
1231
|
+
sessionOrdinal: binding.ordinal,
|
|
1232
|
+
upstream: loaded.resumeUpstream,
|
|
1233
|
+
skip: loaded.resumeSkip,
|
|
1234
|
+
};
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
if (loaded.missing) {
|
|
1238
|
+
anySuccess = true;
|
|
1239
|
+
continue;
|
|
1129
1240
|
}
|
|
1241
|
+
anySuccess = true;
|
|
1242
|
+
collected.push(...loaded.entries);
|
|
1243
|
+
if (collected.length > limit) {
|
|
1244
|
+
// Overshot: drop the probe row and resume at the page boundary of the last binding load.
|
|
1245
|
+
nextState = {
|
|
1246
|
+
sessionOrdinal: binding.ordinal,
|
|
1247
|
+
upstream: loaded.resumeUpstream,
|
|
1248
|
+
skip: loaded.resumeSkip,
|
|
1249
|
+
};
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
if (!loaded.exhausted) {
|
|
1253
|
+
nextState = {
|
|
1254
|
+
sessionOrdinal: binding.ordinal,
|
|
1255
|
+
upstream: loaded.resumeUpstream,
|
|
1256
|
+
skip: loaded.resumeSkip,
|
|
1257
|
+
};
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
// Binding exhausted — fall through to the next older binding.
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Total failure must not masquerade as authoritative empty success.
|
|
1264
|
+
if (collected.length === 0 && failedScope.length > 0 && !anySuccess) fail('upstream_error');
|
|
1265
|
+
|
|
1266
|
+
const partial = failedScope.length > 0;
|
|
1267
|
+
// collected is newest-first (binding order + desc projection). Public page is ascending.
|
|
1268
|
+
const pageDesc = collected.slice(0, limit);
|
|
1269
|
+
let nextCursor = nextState != null ? encodeCursor(nextState) : null;
|
|
1270
|
+
if (nextCursor == null && partial && failedScope[0]) {
|
|
1271
|
+
nextCursor = encodeCursor({
|
|
1272
|
+
sessionOrdinal: failedScope[0].sessionOrdinal,
|
|
1273
|
+
upstream: null,
|
|
1274
|
+
skip: 0,
|
|
1275
|
+
});
|
|
1130
1276
|
}
|
|
1131
|
-
|
|
1277
|
+
// complete only when nothing remains and no partial failure left to retry.
|
|
1278
|
+
const complete = nextCursor == null && !partial;
|
|
1279
|
+
const ordered = [...pageDesc].reverse().map((entry) => ({
|
|
1280
|
+
sessionID: entry.session_id,
|
|
1281
|
+
directory: entry.directory,
|
|
1282
|
+
info: entry.info,
|
|
1283
|
+
parts: entry.parts,
|
|
1284
|
+
}));
|
|
1285
|
+
return {
|
|
1286
|
+
entries: ordered,
|
|
1287
|
+
nextCursor: complete ? null : nextCursor,
|
|
1288
|
+
complete,
|
|
1289
|
+
partial,
|
|
1290
|
+
...(partial ? { failed: failedScope } : {}),
|
|
1291
|
+
};
|
|
1132
1292
|
};
|
|
1133
|
-
for (const current of db.prepare('SELECT * FROM assistant_v2 WHERE current_session_id IS NOT NULL AND tombstone_at IS NULL').all()) void backfillSession({ assistant_id: current.assistant_id, session_id: current.current_session_id, directory: effectiveWorkspace(current) }).catch(() => {});
|
|
1134
1293
|
const unsubscribeEvents = typeof globalEventHub?.subscribeEvent === 'function' ? globalEventHub.subscribeEvent(processEvent) : null;
|
|
1135
1294
|
const createAssistant = (input) => { const allowed = new Set(['enabled', 'name', 'defaultPrompt', 'providerID', 'modelID', 'agent', 'variant', 'mode', 'workspacePath']); if (!plainObject(input) || Object.keys(input).some((key) => !allowed.has(key))) fail('validation_error'); const mode = input.mode == null ? 'continuous' : input.mode === 'stateless' || input.mode === 'continuous' ? input.mode : fail('validation_error'); const assistantID = id(); const workspacePath = input.workspacePath == null ? null : workspace(input.workspacePath, assistantID); effectiveWorkspace({ assistant_id: assistantID, workspace_path: workspacePath }); const at = now(); db.exec('BEGIN IMMEDIATE'); try { if (Number(db.prepare('SELECT COUNT(*) AS count FROM assistant_v2 WHERE tombstone_at IS NULL').get().count) >= 100) fail('assistant_limit'); db.prepare('INSERT INTO assistant_v2 (assistant_id,revision,enabled,name,default_prompt,workspace_path,provider_id,model_id,agent,variant,mode,current_session_id,session_generation,created_at,updated_at,tombstone_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)').run(assistantID, 1, input.enabled === false ? 0 : 1, string(input.name, 256, true), input.defaultPrompt ? string(input.defaultPrompt, 200_000) : '', workspacePath, string(input.providerID, 256, true), string(input.modelID, 256, true), input.agent == null ? null : string(input.agent, 256), input.variant == null ? null : string(input.variant, 256), mode, null, 0, at, at, null); bump(); db.exec('COMMIT'); return output(assistant(assistantID)); } catch (error) { db.exec('ROLLBACK'); throw error; } };
|
|
1136
1295
|
const updateAssistant = async (assistantID, input) => { const row = editable(assistantID); const allowed = new Set(['expectedRevision', 'enabled', 'name', 'defaultPrompt', 'providerID', 'modelID', 'agent', 'variant', 'mode', 'workspacePath']); if (!plainObject(input) || !Number.isInteger(input.expectedRevision) || Object.keys(input).some((key) => !allowed.has(key))) fail('validation_error'); const next = { enabled: input.enabled === undefined ? row.enabled : input.enabled ? 1 : 0, name: input.name === undefined ? row.name : string(input.name, 256, true), prompt: input.defaultPrompt === undefined ? row.default_prompt : string(input.defaultPrompt, 200_000), provider: input.providerID === undefined ? row.provider_id : string(input.providerID, 256, true), model: input.modelID === undefined ? row.model_id : string(input.modelID, 256, true), agent: input.agent === undefined ? row.agent : input.agent === null ? null : string(input.agent, 256), variant: input.variant === undefined ? row.variant : input.variant === null ? null : string(input.variant, 256), mode: input.mode === undefined ? (row.mode === 'stateless' ? 'stateless' : 'continuous') : input.mode === 'continuous' || input.mode === 'stateless' ? input.mode : fail('validation_error'), workspacePath: input.workspacePath === undefined ? row.workspace_path : input.workspacePath == null ? null : workspace(input.workspacePath, assistantID) }; const nextRow = { ...row, workspace_path: next.workspacePath, name: next.name }; effectiveWorkspace(nextRow); const workspaceChanged = next.workspacePath !== row.workspace_path; if (workspaceChanged && row.current_session_id) archiveSession(assistantID, row.current_session_id); const created = workspaceChanged ? await createSession(nextRow) : null; const result = db.prepare('UPDATE assistant_v2 SET enabled=?,name=?,default_prompt=?,provider_id=?,model_id=?,agent=?,variant=?,mode=?,workspace_path=?,current_session_id=?,session_generation=session_generation+?,revision=revision+1,updated_at=? WHERE assistant_id=? AND revision=? AND session_generation=? AND tombstone_at IS NULL').run(next.enabled, next.name, next.prompt, next.provider, next.model, next.agent, next.variant, next.mode, next.workspacePath, created?.sessionID ?? row.current_session_id, workspaceChanged ? 1 : 0, now(), assistantID, input.expectedRevision, row.session_generation); if (!result.changes) fail('revision_conflict'); bump(); return output(assistant(assistantID)); };
|
|
@@ -1149,10 +1308,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1149
1308
|
const target = active(row.assistant_id);
|
|
1150
1309
|
const targetConfig = configuration(target);
|
|
1151
1310
|
result = await sendPrompt(restored.sessionID, targetConfig);
|
|
1152
|
-
if (promptAdmitted(result)) mirrorAdmittedUserMessage(target, restored.sessionID, messageID, parts, targetConfig);
|
|
1153
1311
|
return { result, binding: restored };
|
|
1154
1312
|
}
|
|
1155
|
-
if (promptAdmitted(result)) mirrorAdmittedUserMessage(row, sessionID, messageID, parts, config);
|
|
1156
1313
|
return { result, binding: binding(row) };
|
|
1157
1314
|
};
|
|
1158
1315
|
const extractUserText = (input) => {
|
|
@@ -1823,14 +1980,17 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1823
1980
|
const { signal } = params;
|
|
1824
1981
|
signal?.throwIfAborted();
|
|
1825
1982
|
const resolved = await resolveReferencedSession({ sessionID: params.sessionID, signal, includeMessages: false });
|
|
1983
|
+
// First page: order=desc. Continuation: opaque cursor only (upstream 400s on both).
|
|
1826
1984
|
const result = await invokeSession(() => client().message.list({
|
|
1827
|
-
sessionID: resolved.sessionID, limit,
|
|
1828
|
-
...(params.before ? { cursor: params.before } : {}),
|
|
1985
|
+
sessionID: resolved.sessionID, limit,
|
|
1986
|
+
...(params.before ? { cursor: params.before } : { order: 'desc' }),
|
|
1829
1987
|
}));
|
|
1830
1988
|
signal?.throwIfAborted();
|
|
1831
1989
|
if (isMissing(result)) throw new AssignError(ASSIGN_CODES.NOT_FOUND, 'That session no longer exists.');
|
|
1832
|
-
|
|
1833
|
-
|
|
1990
|
+
const projected = result?.error ? null : projectionEntries(result, resolved.sessionID);
|
|
1991
|
+
if (!projected) throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Could not read session messages.');
|
|
1992
|
+
// Desc pages arrive newest-first; quote them oldest→newest.
|
|
1993
|
+
const rows = projected.slice().reverse();
|
|
1834
1994
|
if (rows.some((entry) => (entry?.info?.sessionID && entry.info.sessionID !== resolved.sessionID))) throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session message identity mismatch.');
|
|
1835
1995
|
let remaining = 24000;
|
|
1836
1996
|
let partial = rows.length > limit;
|
|
@@ -1856,8 +2016,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1856
2016
|
}
|
|
1857
2017
|
return { messageID: info.id, role: info.role, parts };
|
|
1858
2018
|
});
|
|
1859
|
-
const
|
|
1860
|
-
const nextCursor = typeof cursor === 'string' && cursor ? cursor : null;
|
|
2019
|
+
const nextCursor = projectionCursor(result);
|
|
1861
2020
|
return { sessionID: resolved.sessionID, directory: resolved.directory, title: resolved.title.slice(0, 500), messages, nextCursor, partial: partial || nextCursor !== null };
|
|
1862
2021
|
};
|
|
1863
2022
|
const assignWork = async (row, params = {}) => {
|
|
@@ -2148,10 +2307,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2148
2307
|
return { sessionID, directory: resolved.directory, operation, messageID, admitted: true };
|
|
2149
2308
|
}
|
|
2150
2309
|
if (operation === 'archive') {
|
|
2151
|
-
|
|
2310
|
+
// OC2: Host owns archive stamps (openchamber.archive.archivedAt). Do not
|
|
2311
|
+
// call upstream session.update time.archived — it is unavailable.
|
|
2312
|
+
if (typeof archiveSessionHost !== 'function') {
|
|
2152
2313
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session archive is unavailable.');
|
|
2153
2314
|
}
|
|
2154
|
-
result = await
|
|
2315
|
+
result = await archiveSessionHost({
|
|
2316
|
+
sessionID,
|
|
2317
|
+
directory: resolved.directory,
|
|
2318
|
+
archivedAt: now(),
|
|
2319
|
+
});
|
|
2155
2320
|
} else if (operation === 'delete') {
|
|
2156
2321
|
if (typeof api.session.remove !== 'function') {
|
|
2157
2322
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session delete is unavailable.');
|
|
@@ -2164,12 +2329,33 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2164
2329
|
if (error instanceof AssignError) throw error;
|
|
2165
2330
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, error?.message || `Session ${operation} failed.`);
|
|
2166
2331
|
}
|
|
2332
|
+
if (operation === 'archive') {
|
|
2333
|
+
if (!result?.session?.id) {
|
|
2334
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, `Session ${operation} was not confirmed. Do not resend automatically.`);
|
|
2335
|
+
}
|
|
2336
|
+
reportAssignedSession(sessionID, 'cancelled', false);
|
|
2337
|
+
return { sessionID, directory: resolved.directory, operation, archived: true };
|
|
2338
|
+
}
|
|
2167
2339
|
if (isMissing(result)) throw new AssignError(ASSIGN_CODES.NOT_FOUND, `No coding session ${sessionID} was found.`);
|
|
2168
2340
|
if (result?.error || result?.data === false) {
|
|
2169
2341
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, `Session ${operation} was not confirmed. Do not resend automatically.`);
|
|
2170
2342
|
}
|
|
2343
|
+
// Successful delete: direct idempotent Host metadata cleanup; session.deleted
|
|
2344
|
+
// events remain compensatory. Upstream failure never reaches here.
|
|
2345
|
+
if (typeof forgetSessionHost === 'function') {
|
|
2346
|
+
try {
|
|
2347
|
+
const cleanup = forgetSessionHost(sessionID);
|
|
2348
|
+
if (cleanup && typeof cleanup.catch === 'function') {
|
|
2349
|
+
cleanup.catch((error) => {
|
|
2350
|
+
console.warn('[assistants] metadata cleanup after delete failed (retry via event):', error?.message ?? error);
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
console.warn('[assistants] metadata cleanup after delete failed (retry via event):', error?.message ?? error);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2171
2357
|
reportAssignedSession(sessionID, 'cancelled', false);
|
|
2172
|
-
return { sessionID, directory: resolved.directory, operation,
|
|
2358
|
+
return { sessionID, directory: resolved.directory, operation, deleted: true };
|
|
2173
2359
|
};
|
|
2174
2360
|
const matchRegisteredProject = async (directory) => {
|
|
2175
2361
|
const projects = await listProjects();
|
|
@@ -3523,7 +3709,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
3523
3709
|
)),
|
|
3524
3710
|
};
|
|
3525
3711
|
};
|
|
3526
|
-
return { capability: async () => ({ supported: true, enabled: enabled(), revision: revision(), serverInstanceID: await getServerId(), sharingAvailable: false, archiveMetadataAvailable:
|
|
3712
|
+
return { capability: async () => ({ supported: true, enabled: enabled(), revision: revision(), serverInstanceID: await getServerId(), sharingAvailable: false, archiveMetadataAvailable: typeof archiveSessionHost === 'function', sessionMetadataAvailable: false }), snapshot, createAssistant, updateAssistant, setEnabled: (input) => { if (!plainObject(input) || typeof input.enabled !== 'boolean' || input.expectedRevision !== revision()) fail('revision_conflict'); db.prepare("UPDATE assistant_meta SET value=? WHERE key='enabled'").run(input.enabled ? '1' : '0'); return { enabled: input.enabled, revision: bump() }; }, removeAssistant: (assistantID, expectedRevision) => {
|
|
3527
3713
|
db.exec('BEGIN IMMEDIATE');
|
|
3528
3714
|
try {
|
|
3529
3715
|
const result = db.prepare('UPDATE assistant_v2 SET tombstone_at=?,revision=revision+1,updated_at=? WHERE assistant_id=? AND revision=? AND tombstone_at IS NULL').run(now(), now(), assistantID, expectedRevision);
|