cli-jaw 2.17.62 → 2.17.63
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.ja.md +17 -17
- package/README.ko.md +17 -17
- package/README.md +17 -17
- package/README.zh-CN.md +17 -17
- package/dist/bin/commands/tui/input-handler.js +19 -0
- package/dist/bin/commands/tui/input-handler.js.map +1 -1
- package/dist/bin/commands/tui/overlays.js +8 -4
- package/dist/bin/commands/tui/overlays.js.map +1 -1
- package/dist/bin/star-prompt.js +1 -1
- package/dist/bin/star-prompt.js.map +1 -1
- package/dist/server.js +14 -1
- package/dist/server.js.map +1 -1
- package/dist/src/agent/claude-runtime-pool.js +2 -1
- package/dist/src/agent/claude-runtime-pool.js.map +1 -1
- package/dist/src/agent/claude-runtime-run.js +28 -7
- package/dist/src/agent/claude-runtime-run.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +34 -6
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-events.js +136 -9
- package/dist/src/agent/runtime/claude-sdk-events.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-history-loader.js +24 -0
- package/dist/src/agent/runtime/claude-sdk-history-loader.js.map +1 -0
- package/dist/src/agent/runtime/claude-sdk-hooks.js +10 -4
- package/dist/src/agent/runtime/claude-sdk-hooks.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-messages.js +25 -2
- package/dist/src/agent/runtime/claude-sdk-messages.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-options.js +36 -3
- package/dist/src/agent/runtime/claude-sdk-options.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-permissions.js +32 -9
- package/dist/src/agent/runtime/claude-sdk-permissions.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-session.js +165 -17
- package/dist/src/agent/runtime/claude-sdk-session.js.map +1 -1
- package/dist/src/agent/runtime/claude-sdk-version.js +8 -0
- package/dist/src/agent/runtime/claude-sdk-version.js.map +1 -0
- package/dist/src/agent/spawn/backend-codex-app.js +619 -0
- package/dist/src/agent/spawn/backend-codex-app.js.map +1 -0
- package/dist/src/agent/spawn/backend-context.js +2 -0
- package/dist/src/agent/spawn/backend-context.js.map +1 -0
- package/dist/src/agent/spawn/backend-copilot.js +385 -0
- package/dist/src/agent/spawn/backend-copilot.js.map +1 -0
- package/dist/src/agent/spawn/backend-native-acp.js +379 -0
- package/dist/src/agent/spawn/backend-native-acp.js.map +1 -0
- package/dist/src/agent/spawn/backend-pi.js +565 -0
- package/dist/src/agent/spawn/backend-pi.js.map +1 -0
- package/dist/src/agent/spawn/types.js +2 -0
- package/dist/src/agent/spawn/types.js.map +1 -0
- package/dist/src/agent/spawn.js +44 -1858
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/claude-default-model-boot.js +36 -0
- package/dist/src/cli/claude-default-model-boot.js.map +1 -0
- package/dist/src/cli/claude-model-discovery.js +13 -1
- package/dist/src/cli/claude-model-discovery.js.map +1 -1
- package/dist/src/cli/claude-models.js +7 -1
- package/dist/src/cli/claude-models.js.map +1 -1
- package/dist/src/cli/commands.js +5 -15
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/tui/composer.js +3 -3
- package/dist/src/cli/tui/composer.js.map +1 -1
- package/dist/src/cli/tui/skill-mention.js +15 -0
- package/dist/src/cli/tui/skill-mention.js.map +1 -0
- package/dist/src/code-mode/manager.js +174 -9
- package/dist/src/code-mode/manager.js.map +1 -1
- package/dist/src/code-mode/providers/catalog.js +5 -3
- package/dist/src/code-mode/providers/catalog.js.map +1 -1
- package/dist/src/code-mode/providers/claude-history.js +132 -0
- package/dist/src/code-mode/providers/claude-history.js.map +1 -0
- package/dist/src/code-mode/providers/claude.js +74 -7
- package/dist/src/code-mode/providers/claude.js.map +1 -1
- package/dist/src/code-mode/providers/codex-app.js +7 -1
- package/dist/src/code-mode/providers/codex-app.js.map +1 -1
- package/dist/src/code-mode/session.js +158 -11
- package/dist/src/code-mode/session.js.map +1 -1
- package/dist/src/code-mode/store.js +416 -28
- package/dist/src/code-mode/store.js.map +1 -1
- package/dist/src/command-contract/catalog.js +3 -22
- package/dist/src/command-contract/catalog.js.map +1 -1
- package/dist/src/core/codex-config.js +3 -62
- package/dist/src/core/codex-config.js.map +1 -1
- package/dist/src/core/config.js +116 -14
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/runtime-settings.js +20 -10
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-watch.js +1 -0
- package/dist/src/core/settings-watch.js.map +1 -1
- package/dist/src/core/skill-mentions.js +83 -0
- package/dist/src/core/skill-mentions.js.map +1 -0
- package/dist/src/manager/registry.js +5 -1
- package/dist/src/manager/registry.js.map +1 -1
- package/dist/src/manager/routes/desktop-status.js +1 -1
- package/dist/src/manager/routes/desktop-status.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +11 -1
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/prompt/builder.js +10 -4
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +1 -1
- package/dist/src/routes/code-native.js +51 -3
- package/dist/src/routes/code-native.js.map +1 -1
- package/dist/src/routes/settings.js +1 -4
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/routes/skills.js +7 -0
- package/dist/src/routes/skills.js.map +1 -1
- package/dist/src/shared/skill-mention.js +61 -0
- package/dist/src/shared/skill-mention.js.map +1 -0
- package/dist/src/shared/tool-input.js +137 -0
- package/dist/src/shared/tool-input.js.map +1 -0
- package/package.json +7 -5
- package/public/css/activity.css +83 -20
- package/public/css/layout.css +2 -2
- package/public/css/sidebar.css +18 -0
- package/public/css/variables.css +23 -2
- package/public/dist/.vite/manifest.json +302 -334
- package/public/dist/assets/Activity-DPLlOJ46.js +1 -0
- package/public/dist/assets/AdvancedExport-BgOgGvc5.js +1 -0
- package/public/dist/assets/Agent-O_U8ny90.js +1 -0
- package/public/dist/assets/Browser-m6VynQXV.js +1 -0
- package/public/dist/assets/BrowserPanel-Bb-Bge6B.js +3 -0
- package/public/dist/assets/ChannelEnablementControl-BSuKQhTW.js +85 -0
- package/public/dist/assets/ChannelSetupEntry-DHLoZEzJ.js +1 -0
- package/public/dist/assets/ChannelsDiscord-B-G8vsYy.js +1 -0
- package/public/dist/assets/ChannelsSlack-oPuh922z.js +1 -0
- package/public/dist/assets/ChannelsTelegram-PmTv43KQ.js +1 -0
- package/public/dist/assets/CodeCanvas-CVbdp0S-.js +3 -0
- package/public/dist/assets/DashboardMeta-BqEiJrvr.js +1 -0
- package/public/dist/assets/DesignPanel-BOb-HMbl.js +2 -0
- package/public/dist/assets/{Developer-BdKPcw8Z.js → Developer-CbySB7_Z.js} +1 -1
- package/public/dist/assets/{DiffPanel-DtJuvU5y.js → DiffPanel-D9Cch5FG.js} +1 -1
- package/public/dist/assets/Display-C0Zdamcb.js +1 -0
- package/public/dist/assets/Display-DU4_76cS.js +1 -0
- package/public/dist/assets/DocPanel-DvtFw8AP.js +1 -0
- package/public/dist/assets/{Embedding-DvdTj1BF.js → Embedding-CCFMOB_2.js} +1 -1
- package/public/dist/assets/Employees-BanvF58d.js +1 -0
- package/public/dist/assets/FolderPanel-CJvwRwIV.js +3 -0
- package/public/dist/assets/Heartbeat-uyi_MpUd.js +1 -0
- package/public/dist/assets/InlineWarn-Cq4z28F3.js +1 -0
- package/public/dist/assets/MarkdownRenderer-Cc3DeHvm.js +1 -0
- package/public/dist/assets/{MarkdownRenderer-DhonsAPC.js → MarkdownRenderer-DzN66XVb.js} +1 -1
- package/public/dist/assets/Mcp-rjPoL_Zo.js +4 -0
- package/public/dist/assets/Memory-C9I33pIi.js +1 -0
- package/public/dist/assets/{MilkdownWysiwygEditor-D6JaLDE4.js → MilkdownWysiwygEditor-xObhviw7.js} +1 -1
- package/public/dist/assets/ModelProvider-fiUktMaO.js +1 -0
- package/public/dist/assets/Network-CuqFrOvR.js +1 -0
- package/public/dist/assets/NotesGraphWorkspace-DO9LtVGF.js +1 -0
- package/public/dist/assets/PanelLayoutProvider-DRZ4QkBC.js +1 -0
- package/public/dist/assets/Permissions-BltXiZnz.js +1 -0
- package/public/dist/assets/{Permissions-BwiMEoTV.js → Permissions-DcCbFMIq.js} +1 -1
- package/public/dist/assets/Profile-DUL7-RE_.js +1 -0
- package/public/dist/assets/Prompts-C9vY8ivQ.js +1 -0
- package/public/dist/assets/SpeechKeys-BJ8V7U5E.js +1 -0
- package/public/dist/assets/TelegramHub-DtyjLGFR.js +1 -0
- package/public/dist/assets/{TerminalPanel-Bu9i-P2f.js → TerminalPanel-c1xhiy53.js} +1 -1
- package/public/dist/assets/app-BjKzqXCv.css +1 -0
- package/public/dist/assets/app-CZwx0V6K.js +59 -0
- package/public/dist/assets/bgtask-badge-CnVkcp3c.js +1 -0
- package/public/dist/assets/{bgtask-badge-DfiHuzxT.js → bgtask-badge-Cp26S8Uq.js} +1 -1
- package/public/dist/assets/client-platform-WO8m6kPQ.js +1 -0
- package/public/dist/assets/{employees-BQaJNp-u.js → employees-CsvI0_5V.js} +1 -1
- package/public/dist/assets/error-normalize-clOi-AAA.js +1 -0
- package/public/dist/assets/fields-CicOWY-X.js +1 -0
- package/public/dist/assets/iframe-renderer-DWx9v2Iz.js +1 -0
- package/public/dist/assets/iframe-renderer-DmHYOI1A.js +142 -0
- package/public/dist/assets/{react-C_pPTFsM.js → jsx-runtime-CkEUZM6i.js} +1 -1
- package/public/dist/assets/manager-CCXm5Yfe.js +5 -0
- package/public/dist/assets/manager-D9rhKFKc.css +1 -0
- package/public/dist/assets/manager-shortcuts-Cqmr0mEq.js +1 -0
- package/public/dist/assets/{memory-EG1B51up.js → memory-C6qtAMdJ.js} +1 -1
- package/public/dist/assets/memory-PPofQPeB.js +1 -0
- package/public/dist/assets/message-history-DcjBuubA.js +4 -0
- package/public/dist/assets/message-history-bw4qF01Z.js +1 -0
- package/public/dist/assets/page-shell-DfinU3mb.js +1 -0
- package/public/dist/assets/react-dom-URBLzV3w.js +1 -0
- package/public/dist/assets/{render-Dp07lZAJ.js → render-DfYro-fG.js} +1 -1
- package/public/dist/assets/{settings-jlI4X-zt.js → settings-CvsLWlJn.js} +38 -30
- package/public/dist/assets/settings-DMiQe469.js +1 -0
- package/public/dist/assets/settings-DgAlohx9.js +1 -0
- package/public/dist/assets/settings-core-BH7J_Rbm.js +1 -0
- package/public/dist/assets/{settings-core-B5Uf_lWY.js → settings-core-pnpw-eZv.js} +1 -1
- package/public/dist/assets/{settings-embedding-BfWzWKMM.js → settings-embedding-BRW0WZvh.js} +3 -3
- package/public/dist/assets/{settings-embedding-DhEdT86W.css → settings-embedding-CUQ8Is3y.css} +1 -1
- package/public/dist/assets/settings-types-CYKy0xz4.js +1 -0
- package/public/dist/assets/shared-D1begqvi.js +1 -0
- package/public/dist/assets/{sidebar-BUemG2ZN.js → sidebar-C_80w4iY.js} +3 -3
- package/public/dist/assets/{skills-CKUcEN9m.js → skills-D_2adqge.js} +1 -1
- package/public/dist/assets/skills-DeNVAVgt.js +1 -0
- package/public/dist/assets/{slash-commands-D34GgHFX.js → slash-commands-B_Hq5RkC.js} +2 -2
- package/public/dist/assets/slash-commands-Cj5qoRWc.js +1 -0
- package/public/dist/assets/tool-input-DI7uu8Ah.js +2 -0
- package/public/dist/assets/{trace-drawer-DBmK-5F_.js → trace-drawer-B6XuPodh.js} +1 -1
- package/public/dist/assets/ui-BbR_fFiK.js +1 -0
- package/public/dist/assets/{ui-vm2iuLcS.js → ui-D8xzm7DN.js} +1 -1
- package/public/dist/index.html +11 -3
- package/public/dist/manager/index.html +3 -3
- package/public/dist/settings/index.html +2 -2
- package/public/index.html +9 -1
- package/public/js/features/activity-history.ts +50 -21
- package/public/js/features/activity-rows.ts +158 -10
- package/public/js/features/activity-view.ts +5 -5
- package/public/js/features/chat.ts +2 -0
- package/public/js/features/employees.ts +1 -0
- package/public/js/features/resize-guard.ts +39 -0
- package/public/js/features/settings-cli-status.ts +49 -17
- package/public/js/features/settings-types.ts +39 -1
- package/public/js/features/skill-mentions.ts +143 -0
- package/public/js/main.ts +15 -0
- package/public/js/virtual-scroll.ts +67 -1
- package/public/locales/en.json +2 -0
- package/public/locales/ja.json +2 -0
- package/public/locales/ko.json +2 -0
- package/public/locales/zh.json +2 -0
- package/public/manager/src/AppChrome.tsx +2 -0
- package/public/manager/src/InstancePreview.tsx +1 -1
- package/public/manager/src/SidebarRailRouter.tsx +3 -1
- package/public/manager/src/browser-panel/BrowserPanel.tsx +47 -12
- package/public/manager/src/browser-panel/browser-address-state.ts +11 -0
- package/public/manager/src/client-platform.ts +18 -0
- package/public/manager/src/code/CodeCanvas.tsx +4 -2
- package/public/manager/src/code/CodeComposer.tsx +22 -10
- package/public/manager/src/code/CodeDraftEmptyState.tsx +64 -0
- package/public/manager/src/code/CodeSessionList.tsx +154 -40
- package/public/manager/src/code/CodeTranscript.tsx +104 -16
- package/public/manager/src/code/CodeWorkbench.tsx +21 -11
- package/public/manager/src/code/CodeWorkspaceHeader.tsx +5 -10
- package/public/manager/src/code/ComposerFooter.tsx +9 -1
- package/public/manager/src/code/code-composer.css +1 -1
- package/public/manager/src/code/code-controller-draft-storage.ts +16 -8
- package/public/manager/src/code/code-controller-drafts.ts +33 -6
- package/public/manager/src/code/code-controller-runtime.ts +189 -17
- package/public/manager/src/code/code-controller-types.ts +19 -1
- package/public/manager/src/code/code-recent-workspaces.ts +25 -0
- package/public/manager/src/code/code-session-client.ts +19 -1
- package/public/manager/src/code/code-session-sidebar.css +38 -0
- package/public/manager/src/code/code-session-state.ts +2 -0
- package/public/manager/src/code/code-types.ts +47 -0
- package/public/manager/src/code/code-workbench.css +36 -1
- package/public/manager/src/code/code.css +171 -7
- package/public/manager/src/code/session-order.ts +80 -0
- package/public/manager/src/code/tool-summary.ts +45 -11
- package/public/manager/src/folder-panel/folder-shortcuts.ts +5 -9
- package/public/manager/src/hooks/useCommandPalette.ts +3 -7
- package/public/manager/src/main.tsx +1 -0
- package/public/manager/src/manager-components.css +25 -23
- package/public/manager/src/manager-dashboard-reminders.css +2 -2
- package/public/manager/src/manager-layout.css +1 -1
- package/public/manager/src/manager-shortcut-runner.ts +9 -2
- package/public/manager/src/manager-shortcuts.ts +83 -21
- package/public/manager/src/panels/desktop-bridge.ts +4 -1
- package/public/manager/src/panels/panels.css +9 -0
- package/public/manager/src/preview.ts +19 -4
- package/public/manager/src/settings/pages/AdvancedExport.tsx +28 -30
- package/public/manager/src/settings/pages/Browser.tsx +45 -47
- package/public/manager/src/settings/pages/ChannelsDiscord.tsx +51 -26
- package/public/manager/src/settings/pages/ChannelsSlack.tsx +59 -17
- package/public/manager/src/settings/pages/ChannelsTelegram.tsx +51 -26
- package/public/manager/src/settings/pages/Display.tsx +2 -1
- package/public/manager/src/settings/pages/Employees.tsx +3 -3
- package/public/manager/src/settings/pages/Mcp.tsx +220 -200
- package/public/manager/src/settings/pages/Memory.tsx +35 -41
- package/public/manager/src/settings/pages/ModelProvider.tsx +23 -98
- package/public/manager/src/settings/pages/Network.tsx +5 -4
- package/public/manager/src/settings/pages/Permissions.tsx +38 -16
- package/public/manager/src/settings/pages/Prompts.tsx +12 -9
- package/public/manager/src/settings/pages/SpeechKeys.tsx +132 -139
- package/public/manager/src/settings/pages/TelegramHub.tsx +67 -20
- package/public/manager/src/settings/pages/components/AvatarCard.tsx +9 -8
- package/public/manager/src/settings/pages/components/ChannelSetupEntry.tsx +2 -2
- package/public/manager/src/settings/pages/components/EmployeeRow.tsx +6 -12
- package/public/manager/src/settings/pages/components/HealthBadge.tsx +11 -6
- package/public/manager/src/settings/pages/components/HeartbeatJobRow.tsx +35 -16
- package/public/manager/src/settings/pages/components/HeartbeatJobsSection.tsx +11 -11
- package/public/manager/src/settings/pages/components/HeartbeatMarkdownSection.tsx +7 -7
- package/public/manager/src/settings/pages/components/InlineWarn.tsx +4 -9
- package/public/manager/src/settings/pages/components/McpServerCard.tsx +39 -27
- package/public/manager/src/settings/pages/components/MemoryRow.tsx +21 -16
- package/public/manager/src/settings/pages/components/PerCliRow.tsx +122 -121
- package/public/manager/src/settings/pages/components/PiProfileDialog.tsx +9 -6
- package/public/manager/src/settings/pages/components/SlackSetup.tsx +8 -8
- package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +14 -10
- package/public/manager/src/settings/pages/components/agent/AgentEmployeesSection.tsx +26 -26
- package/public/manager/src/settings/pages/components/agent/PermissionQuickSection.tsx +5 -5
- package/public/manager/src/settings/pages/components/agent/RuntimeEmployeeRow.tsx +14 -11
- package/public/manager/src/settings/pages/components/agent/RuntimeHeader.tsx +1 -3
- package/public/manager/src/settings/pages/components/agent/agent-meta.ts +0 -2
- package/public/manager/src/settings/pages/components/runtime-transport-field.tsx +2 -2
- package/public/manager/src/settings/pages/manager/Activity.tsx +6 -2
- package/public/manager/src/settings/pages/manager/Display.tsx +3 -8
- package/public/manager/src/settings/pages/manager/shared.tsx +44 -33
- package/public/manager/src/settings/pages/page-shell.tsx +66 -0
- package/public/manager/src/settings-agent.css +185 -36
- package/public/manager/src/settings-standalone.tsx +1 -0
- package/public/manager/src/settings-surface.css +96 -0
- package/public/manager/src/types.ts +1 -0
- package/scripts/ci/install-ripgrep.sh +35 -0
- package/scripts/collect-fresh-install-evidence.sh +2 -2
- package/scripts/install-risk-gate.mjs +2 -3
- package/scripts/install-wsl.sh +1 -1
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/release-1.6.0.sh +1 -1
- package/scripts/release-gates.mjs +48 -13
- package/scripts/release-preview.sh +2 -2
- package/scripts/restack-pr.sh +88 -0
- package/scripts/verify-electron-update-metadata.mjs +1 -1
- package/scripts/windows-support-contract.json +1 -1
- package/public/dist/assets/Activity-BUrp73Re.js +0 -1
- package/public/dist/assets/AdvancedExport-B7h_ZvBu.js +0 -1
- package/public/dist/assets/Agent-DnbZJP7b.js +0 -1
- package/public/dist/assets/Browser-CiBoK5Qx.js +0 -1
- package/public/dist/assets/BrowserPanel-SZAUc9T2.js +0 -3
- package/public/dist/assets/ChannelEnablementControl-Dl0uYx38.js +0 -85
- package/public/dist/assets/ChannelSetupEntry-DxtOrN01.js +0 -1
- package/public/dist/assets/ChannelsDiscord-BEGGwd_3.js +0 -1
- package/public/dist/assets/ChannelsSlack-BryC2Ahr.js +0 -1
- package/public/dist/assets/ChannelsTelegram-CGSpaImJ.js +0 -1
- package/public/dist/assets/CodeCanvas-DpkFi5Au.js +0 -2
- package/public/dist/assets/DashboardMeta-Dh-tasbY.js +0 -1
- package/public/dist/assets/DesignPanel-NLNK9wCo.js +0 -2
- package/public/dist/assets/Display-BHGFX3O0.js +0 -1
- package/public/dist/assets/Display-BW-_pKRf.js +0 -1
- package/public/dist/assets/DocPanel-BM41cYpS.js +0 -1
- package/public/dist/assets/Employees-BuFw6-fT.js +0 -1
- package/public/dist/assets/FolderPanel-C-eQT3Yq.js +0 -3
- package/public/dist/assets/Heartbeat-CIVtKWNc.js +0 -1
- package/public/dist/assets/InlineWarn-DIMbcbzk.js +0 -1
- package/public/dist/assets/MarkdownRenderer-DyVx6DxM.js +0 -1
- package/public/dist/assets/Mcp-uKjor7vh.js +0 -4
- package/public/dist/assets/Memory-BSDIXT0B.js +0 -1
- package/public/dist/assets/ModelProvider-DMh2tXUx.js +0 -1
- package/public/dist/assets/Network-4Pa6JkhK.js +0 -1
- package/public/dist/assets/NotesGraphWorkspace-CgdC-fM6.js +0 -1
- package/public/dist/assets/PanelLayoutProvider-mn8sI0h-.js +0 -1
- package/public/dist/assets/Permissions-CodIW-Yd.js +0 -1
- package/public/dist/assets/Profile-DPqrYA44.js +0 -1
- package/public/dist/assets/Prompts-CvAx7QqR.js +0 -1
- package/public/dist/assets/SpeechKeys-CVBCqoBn.js +0 -1
- package/public/dist/assets/TelegramHub-4zKwpk0P.js +0 -1
- package/public/dist/assets/app-ByMfeXp-.js +0 -55
- package/public/dist/assets/app-DAJWqs33.css +0 -1
- package/public/dist/assets/bgtask-badge-Ch88fnhf.js +0 -1
- package/public/dist/assets/error-normalize-CL69Bl7z.js +0 -1
- package/public/dist/assets/fields-mlGpofqQ.js +0 -1
- package/public/dist/assets/iframe-renderer-Bj1JolO8.js +0 -142
- package/public/dist/assets/iframe-renderer-DWnvUSrx.js +0 -1
- package/public/dist/assets/jsx-runtime-NZYk81nU.js +0 -1
- package/public/dist/assets/manager-Bi_xw7TW.js +0 -5
- package/public/dist/assets/manager-BnQHAKk6.css +0 -1
- package/public/dist/assets/manager-shortcuts-BDOzd8zm.js +0 -1
- package/public/dist/assets/memory-Bk_cb3sw.js +0 -1
- package/public/dist/assets/message-history-D2vniu04.js +0 -4
- package/public/dist/assets/message-history-GAtF_wbZ.js +0 -1
- package/public/dist/assets/page-shell-BeGD0rRQ.js +0 -1
- package/public/dist/assets/react-dom-DyUOYpVL.js +0 -1
- package/public/dist/assets/settings-W4KG9ivu.js +0 -1
- package/public/dist/assets/settings-WVGhtneN.js +0 -1
- package/public/dist/assets/settings-client-CmwRsVd1.js +0 -1
- package/public/dist/assets/settings-core-CgKGnMA5.js +0 -1
- package/public/dist/assets/settings-types-D3cd6SWy.js +0 -1
- package/public/dist/assets/shared-CiNP20HB.js +0 -1
- package/public/dist/assets/skills-Co7Mr-qP.js +0 -1
- package/public/dist/assets/slash-commands-BEr-vmIr.js +0 -1
- package/public/dist/assets/ui-TUyJsMUf.js +0 -1
- package/public/manager/src/settings/pages/components/EngineSubsection.tsx +0 -25
|
@@ -36,13 +36,17 @@ CREATE TABLE IF NOT EXISTS code_sessions (
|
|
|
36
36
|
native_cursor TEXT, native_started INTEGER NOT NULL DEFAULT 0,
|
|
37
37
|
native_policy_json TEXT, capabilities_json TEXT NOT NULL,
|
|
38
38
|
epoch INTEGER NOT NULL DEFAULT 0, sequence INTEGER NOT NULL DEFAULT 0,
|
|
39
|
-
revision INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, last_used_at INTEGER NOT NULL
|
|
39
|
+
revision INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, last_used_at INTEGER NOT NULL,
|
|
40
|
+
last_turn_completed_at INTEGER, last_visited_at INTEGER, thinking INTEGER,
|
|
41
|
+
replay_floor_sequence INTEGER NOT NULL DEFAULT 0, history_generation INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
rollback_since INTEGER
|
|
40
43
|
);
|
|
41
44
|
CREATE TABLE IF NOT EXISTS code_turns (
|
|
42
45
|
session_id TEXT NOT NULL, turn_id TEXT NOT NULL, client_turn_key TEXT NOT NULL,
|
|
43
46
|
prompt_hash TEXT NOT NULL, status TEXT NOT NULL, accepted_sequence INTEGER NOT NULL,
|
|
44
47
|
event_bytes INTEGER NOT NULL DEFAULT 0, control_event_bytes INTEGER NOT NULL DEFAULT 0,
|
|
45
48
|
settlement_bytes INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
native_prompt_uuid TEXT, removed_generation INTEGER,
|
|
46
50
|
PRIMARY KEY(session_id, turn_id), UNIQUE(session_id, client_turn_key)
|
|
47
51
|
);
|
|
48
52
|
CREATE TABLE IF NOT EXISTS code_events (
|
|
@@ -54,6 +58,13 @@ CREATE TABLE IF NOT EXISTS code_items (
|
|
|
54
58
|
item_json TEXT NOT NULL, PRIMARY KEY(session_id, item_id)
|
|
55
59
|
);
|
|
56
60
|
CREATE INDEX IF NOT EXISTS idx_code_items_order ON code_items(session_id, first_sequence);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS code_steers (
|
|
62
|
+
session_id TEXT NOT NULL, turn_id TEXT NOT NULL, client_turn_key TEXT NOT NULL,
|
|
63
|
+
prompt_hash TEXT NOT NULL, status TEXT NOT NULL, accepted_sequence INTEGER, native_uuid TEXT,
|
|
64
|
+
PRIMARY KEY(session_id, client_turn_key)
|
|
65
|
+
);
|
|
66
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_code_steers_one_per_turn ON code_steers(session_id, turn_id)
|
|
67
|
+
WHERE status IN ('reserved', 'committed', 'unknown');
|
|
57
68
|
`;
|
|
58
69
|
export class CodeStoreError extends Error {
|
|
59
70
|
code;
|
|
@@ -65,11 +76,29 @@ export class CodeStoreError extends Error {
|
|
|
65
76
|
this.name = 'CodeStoreError';
|
|
66
77
|
}
|
|
67
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The first kept user row whose turn still has a native prompt boundary, as a correlated
|
|
81
|
+
* subquery on `code_sessions.session_id`. User rows are admitted in turn order, so the earliest
|
|
82
|
+
* boundary turn (walked on idx_code_turns_boundary) that still has its row owns the minimum.
|
|
83
|
+
*/
|
|
84
|
+
const ROLLBACK_SINCE_SQL = `(SELECT i.first_sequence FROM code_turns t
|
|
85
|
+
JOIN code_items i ON i.session_id = t.session_id AND i.item_id = t.turn_id || ':user'
|
|
86
|
+
WHERE t.session_id = code_sessions.session_id AND t.native_prompt_uuid IS NOT NULL AND t.removed_generation IS NULL
|
|
87
|
+
ORDER BY t.accepted_sequence LIMIT 1)`;
|
|
88
|
+
// `rollback_since` is stored: it is refreshed only where boundaries or user rows change, so a
|
|
89
|
+
// session read on the streaming path never walks the session's turns.
|
|
68
90
|
const SESSION_COLUMNS = `session_id, provider, cwd, title, model, effort, permission_mode,
|
|
69
91
|
status, active_turn_id, archived_at, error_json, native_cursor, native_started,
|
|
70
|
-
native_policy_json, capabilities_json, epoch, sequence, revision, created_at, last_used_at
|
|
71
|
-
|
|
72
|
-
const
|
|
92
|
+
native_policy_json, capabilities_json, epoch, sequence, revision, created_at, last_used_at,
|
|
93
|
+
last_turn_completed_at, last_visited_at, thinking, replay_floor_sequence, history_generation, rollback_since`;
|
|
94
|
+
const TURN_COLUMNS = 'turn_id, client_turn_key, prompt_hash, status, accepted_sequence, removed_generation';
|
|
95
|
+
const STEER_COLUMNS = 'turn_id, client_turn_key, prompt_hash, status, accepted_sequence, native_uuid';
|
|
96
|
+
const promptHash = (text) => createHash('sha256').update(text).digest('hex');
|
|
97
|
+
function steerItem(turnId, clientTurnKey, text, at) {
|
|
98
|
+
return { itemId: `${turnId}:steer:${clientTurnKey}`, turnId, kind: 'user_message', status: 'done',
|
|
99
|
+
text, clientTurnKey, createdAt: at, updatedAt: at };
|
|
100
|
+
}
|
|
101
|
+
export const isBusy = (status) => status === 'starting' || status === 'streaming' || status === 'stopping';
|
|
73
102
|
function mapCapabilities(value) {
|
|
74
103
|
return {
|
|
75
104
|
resume: value.resume, interrupt: value.interrupt, permissions: value.permissions,
|
|
@@ -144,14 +173,18 @@ export function toCodeSessionInfo(record) {
|
|
|
144
173
|
const reason = record.archivedAt !== null ? 'archived'
|
|
145
174
|
: !record.capabilities.resume ? 'unsupported'
|
|
146
175
|
: record.nativeCursor ? null : record.nativeStarted ? 'resume_unavailable' : 'not_started';
|
|
176
|
+
const rollback = record.provider !== 'claude' ? 'unsupported' : reason ?? (record.rollbackSince === null ? 'no_boundary' : null);
|
|
147
177
|
return {
|
|
148
178
|
sessionId: record.sessionId, provider: record.provider, cwd: record.cwd, title: record.title,
|
|
149
179
|
model: record.model, effort: record.effort, permissionMode: record.permissionMode,
|
|
150
180
|
status: record.status, turnId: record.turnId, archivedAt: record.archivedAt,
|
|
151
181
|
error: mapError(record.error), resume: { available: reason === null, reason },
|
|
182
|
+
rollback: { available: rollback === null, reason: rollback, sinceSequence: record.rollbackSince },
|
|
183
|
+
historyGeneration: record.historyGeneration,
|
|
152
184
|
capabilities: mapCapabilities(record.capabilities), epoch: record.epoch,
|
|
153
185
|
sequence: record.sequence, revision: record.revision, createdAt: record.createdAt,
|
|
154
|
-
lastUsedAt: record.lastUsedAt,
|
|
186
|
+
lastUsedAt: record.lastUsedAt, lastTurnCompletedAt: record.lastTurnCompletedAt,
|
|
187
|
+
lastVisitedAt: record.lastVisitedAt, thinking: record.thinking,
|
|
155
188
|
};
|
|
156
189
|
}
|
|
157
190
|
function rowToRecord(row) {
|
|
@@ -165,11 +198,17 @@ function rowToRecord(row) {
|
|
|
165
198
|
capabilities: JSON.parse(row.capabilities_json),
|
|
166
199
|
epoch: row.epoch, sequence: row.sequence, revision: row.revision,
|
|
167
200
|
createdAt: row.created_at, lastUsedAt: row.last_used_at,
|
|
201
|
+
lastTurnCompletedAt: row.last_turn_completed_at, lastVisitedAt: row.last_visited_at,
|
|
202
|
+
// Claude thinking defaults on, including rows written before the column existed.
|
|
203
|
+
thinking: row.provider === 'claude' ? row.thinking !== 0 : null,
|
|
204
|
+
replayFloorSequence: row.replay_floor_sequence, historyGeneration: row.history_generation,
|
|
205
|
+
rollbackSince: row.rollback_since,
|
|
168
206
|
};
|
|
169
207
|
}
|
|
208
|
+
/** A turn a rollback removed is no longer current: its receipt reads cancelled. */
|
|
170
209
|
function receipt(row) {
|
|
171
210
|
return { turnId: row.turn_id, clientTurnKey: row.client_turn_key,
|
|
172
|
-
sequence: row.accepted_sequence, status: row.status };
|
|
211
|
+
sequence: row.accepted_sequence, status: row.removed_generation === null ? row.status : 'cancelled' };
|
|
173
212
|
}
|
|
174
213
|
function pageLimit(value, max) {
|
|
175
214
|
if (value === undefined)
|
|
@@ -197,6 +236,8 @@ export class CodeStore {
|
|
|
197
236
|
this.database.exec(CREATE_CODE_SCHEMA_SQL);
|
|
198
237
|
this.ensureSessionListIndex();
|
|
199
238
|
this.ensureBudgetColumns();
|
|
239
|
+
this.ensureActivityColumns();
|
|
240
|
+
this.ensureRollbackColumns();
|
|
200
241
|
}
|
|
201
242
|
/** Rebuild the list index only when it does not already match the page key. */
|
|
202
243
|
ensureSessionListIndex() {
|
|
@@ -209,11 +250,63 @@ export class CodeStore {
|
|
|
209
250
|
this.database.exec(`CREATE INDEX idx_code_sessions_list ON code_sessions(${SESSION_LIST_INDEX_COLUMNS})`);
|
|
210
251
|
})();
|
|
211
252
|
}
|
|
253
|
+
/** Adds the sidebar activity clocks and the thinking switch to databases created before them; all stay NULL. */
|
|
254
|
+
ensureActivityColumns() {
|
|
255
|
+
const names = new Set(this.database.prepare('PRAGMA table_info(code_sessions)').all()
|
|
256
|
+
.map(column => column.name));
|
|
257
|
+
if (names.has('last_turn_completed_at') && names.has('last_visited_at') && names.has('thinking'))
|
|
258
|
+
return;
|
|
259
|
+
this.database.transaction(() => {
|
|
260
|
+
if (!names.has('last_turn_completed_at'))
|
|
261
|
+
this.database.exec('ALTER TABLE code_sessions ADD COLUMN last_turn_completed_at INTEGER');
|
|
262
|
+
if (!names.has('last_visited_at'))
|
|
263
|
+
this.database.exec('ALTER TABLE code_sessions ADD COLUMN last_visited_at INTEGER');
|
|
264
|
+
if (!names.has('thinking'))
|
|
265
|
+
this.database.exec('ALTER TABLE code_sessions ADD COLUMN thinking INTEGER');
|
|
266
|
+
}).immediate();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Adds the rollback columns to databases created before them. Existing turns keep a
|
|
270
|
+
* NULL prompt boundary, so they are never rollback targets; floors and generations start at 0.
|
|
271
|
+
* A stored rollback start that did not exist yet is computed once for every session.
|
|
272
|
+
*/
|
|
273
|
+
ensureRollbackColumns() {
|
|
274
|
+
const columns = (table) => new Set(this.database.prepare(`PRAGMA table_info(${table})`).all()
|
|
275
|
+
.map(column => column.name));
|
|
276
|
+
const sessions = columns('code_sessions'), turns = columns('code_turns');
|
|
277
|
+
const indexed = !!this.database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_code_turns_boundary'").get();
|
|
278
|
+
if (indexed && sessions.has('rollback_since') && sessions.has('replay_floor_sequence') && sessions.has('history_generation')
|
|
279
|
+
&& turns.has('native_prompt_uuid') && turns.has('removed_generation'))
|
|
280
|
+
return;
|
|
281
|
+
this.database.transaction(() => {
|
|
282
|
+
if (!sessions.has('replay_floor_sequence'))
|
|
283
|
+
this.database.exec('ALTER TABLE code_sessions ADD COLUMN replay_floor_sequence INTEGER NOT NULL DEFAULT 0');
|
|
284
|
+
if (!sessions.has('history_generation'))
|
|
285
|
+
this.database.exec('ALTER TABLE code_sessions ADD COLUMN history_generation INTEGER NOT NULL DEFAULT 0');
|
|
286
|
+
if (!turns.has('native_prompt_uuid'))
|
|
287
|
+
this.database.exec('ALTER TABLE code_turns ADD COLUMN native_prompt_uuid TEXT');
|
|
288
|
+
if (!turns.has('removed_generation'))
|
|
289
|
+
this.database.exec('ALTER TABLE code_turns ADD COLUMN removed_generation INTEGER');
|
|
290
|
+
this.database.exec(`CREATE INDEX IF NOT EXISTS idx_code_turns_boundary ON code_turns(session_id, accepted_sequence)
|
|
291
|
+
WHERE native_prompt_uuid IS NOT NULL AND removed_generation IS NULL`);
|
|
292
|
+
if (!sessions.has('rollback_since')) {
|
|
293
|
+
this.database.exec('ALTER TABLE code_sessions ADD COLUMN rollback_since INTEGER');
|
|
294
|
+
this.database.exec(`UPDATE code_sessions SET rollback_since = ${ROLLBACK_SINCE_SQL}`);
|
|
295
|
+
}
|
|
296
|
+
}).immediate();
|
|
297
|
+
}
|
|
298
|
+
/** Adds the per-turn byte budgets; an up-to-date database returns before taking the write lock. */
|
|
212
299
|
ensureBudgetColumns() {
|
|
300
|
+
const budgetColumns = ['event_bytes', 'control_event_bytes', 'settlement_bytes'];
|
|
301
|
+
const present = () => new Set(this.database.prepare('PRAGMA table_info(code_turns)').all()
|
|
302
|
+
.map(column => column.name));
|
|
303
|
+
const before = present();
|
|
304
|
+
if (budgetColumns.every(column => before.has(column)))
|
|
305
|
+
return;
|
|
213
306
|
this.database.transaction(() => {
|
|
214
|
-
|
|
215
|
-
const names =
|
|
216
|
-
for (const column of
|
|
307
|
+
// Read again under the lock: another process may have migrated in between.
|
|
308
|
+
const names = present();
|
|
309
|
+
for (const column of budgetColumns) {
|
|
217
310
|
if (!names.has(column))
|
|
218
311
|
this.database.exec(`ALTER TABLE code_turns ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`);
|
|
219
312
|
}
|
|
@@ -252,6 +345,14 @@ export class CodeStore {
|
|
|
252
345
|
throw new CodeStoreError('session_not_found', 'Code session not found', 404);
|
|
253
346
|
return record;
|
|
254
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Refresh the stored rollback start after boundaries or user rows change, before the session
|
|
350
|
+
* event that saves it. Every writer of either calls this.
|
|
351
|
+
*/
|
|
352
|
+
readRollbackSince(sessionId) {
|
|
353
|
+
return this.database.prepare(`SELECT ${ROLLBACK_SINCE_SQL} AS since FROM code_sessions WHERE session_id = ?`)
|
|
354
|
+
.get(sessionId)?.since ?? null;
|
|
355
|
+
}
|
|
255
356
|
read(sessionId) {
|
|
256
357
|
const record = this.readRecord(sessionId);
|
|
257
358
|
return record ? toCodeSessionInfo(record) : null;
|
|
@@ -353,9 +454,13 @@ export class CodeStore {
|
|
|
353
454
|
}
|
|
354
455
|
const size = pageLimit(limit, CODE_EVENT_PAGE_MAX);
|
|
355
456
|
return this.database.transaction(() => {
|
|
356
|
-
const
|
|
457
|
+
const record = this.requireRecord(sessionId);
|
|
458
|
+
const throughSequence = record.sequence;
|
|
357
459
|
if (afterSequence > throughSequence)
|
|
358
460
|
throw new CodeStoreError('invalid_sequence', 'Sequence exceeds session watermark', 409);
|
|
461
|
+
// Events before a rollback describe items it removed; only a new snapshot is consistent.
|
|
462
|
+
if (afterSequence < record.replayFloorSequence)
|
|
463
|
+
throw new CodeStoreError('invalid_sequence', 'Conversation was rolled back; take a new snapshot', 409);
|
|
359
464
|
const events = [];
|
|
360
465
|
let bytes = jsonBytes({ events, nextSequence: throughSequence, throughSequence, hasMore: false });
|
|
361
466
|
if (bytes > this.limits.maxReplayPageBytes)
|
|
@@ -437,8 +542,10 @@ export class CodeStore {
|
|
|
437
542
|
save(record) {
|
|
438
543
|
this.database.prepare(`UPDATE code_sessions SET title = ?, model = ?, effort = ?, permission_mode = ?,
|
|
439
544
|
status = ?, active_turn_id = ?, archived_at = ?, error_json = ?, native_cursor = ?, native_started = ?,
|
|
440
|
-
native_policy_json = ?, epoch = ?, sequence = ?, revision = ?, last_used_at =
|
|
441
|
-
|
|
545
|
+
native_policy_json = ?, epoch = ?, sequence = ?, revision = ?, last_used_at = ?,
|
|
546
|
+
last_turn_completed_at = ?, last_visited_at = ?, thinking = ?, replay_floor_sequence = ?,
|
|
547
|
+
history_generation = ?, rollback_since = ? WHERE session_id = ?`)
|
|
548
|
+
.run(record.title, record.model, record.effort, record.permissionMode, record.status, record.turnId, record.archivedAt, record.error === null ? null : JSON.stringify(mapError(record.error)), record.nativeCursor, Number(record.nativeStarted), record.nativePolicy === null ? null : JSON.stringify(record.nativePolicy), record.epoch, record.sequence, record.revision, record.lastUsedAt, record.lastTurnCompletedAt, record.lastVisitedAt, record.thinking === null ? null : Number(record.thinking), record.replayFloorSequence, record.historyGeneration, record.rollbackSince, record.sessionId);
|
|
442
549
|
}
|
|
443
550
|
persistEvent(record, event, mode, settlementDelta = 0, budgetTurnId = record.turnId) {
|
|
444
551
|
const encoded = JSON.stringify(event);
|
|
@@ -467,7 +574,8 @@ export class CodeStore {
|
|
|
467
574
|
this.save(record);
|
|
468
575
|
return event;
|
|
469
576
|
}
|
|
470
|
-
|
|
577
|
+
/** `reserve` holds extra settlement bytes for a frame this item may need at settlement. */
|
|
578
|
+
event(record, item, mode = 'ordinary', budgetTurnId = record.turnId, reserve = 0) {
|
|
471
579
|
record.sequence += 1;
|
|
472
580
|
let retainedItem;
|
|
473
581
|
let previousItem;
|
|
@@ -491,7 +599,7 @@ export class CodeStore {
|
|
|
491
599
|
sequence: record.sequence, epoch: record.epoch,
|
|
492
600
|
...(update ? { update } : retainedItem ? { item: retainedItem } : { session: toCodeSessionInfo(record) }),
|
|
493
601
|
};
|
|
494
|
-
this.persistEvent(record, event, mode, settlementCost(record.sessionId, retainedItem) - settlementCost(record.sessionId, previousItem), budgetTurnId);
|
|
602
|
+
this.persistEvent(record, event, mode, settlementCost(record.sessionId, retainedItem) - settlementCost(record.sessionId, previousItem) + reserve, budgetTurnId);
|
|
495
603
|
if (item)
|
|
496
604
|
this.database.prepare(`INSERT INTO code_items (session_id, item_id, first_sequence, item_json)
|
|
497
605
|
VALUES (?, ?, ?, ?) ON CONFLICT(session_id, item_id) DO UPDATE SET item_json = excluded.item_json`)
|
|
@@ -504,8 +612,8 @@ export class CodeStore {
|
|
|
504
612
|
const sessionId = input.sessionId ?? this.newId();
|
|
505
613
|
this.database.prepare(`INSERT INTO code_sessions
|
|
506
614
|
(session_id, provider, cwd, title, model, effort, permission_mode, status,
|
|
507
|
-
capabilities_json, created_at, last_used_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'idle', ?, ?, ?)`)
|
|
508
|
-
.run(sessionId, input.provider, input.cwd, input.title ?? null, input.model, input.effort, input.permissionMode, JSON.stringify(mapCapabilities(input.capabilities)), now, now);
|
|
615
|
+
capabilities_json, created_at, last_used_at, thinking) VALUES (?, ?, ?, ?, ?, ?, ?, 'idle', ?, ?, ?, ?)`)
|
|
616
|
+
.run(sessionId, input.provider, input.cwd, input.title ?? null, input.model, input.effort, input.permissionMode, JSON.stringify(mapCapabilities(input.capabilities)), now, now, input.thinking === undefined ? null : Number(input.thinking));
|
|
509
617
|
const record = this.requireRecord(sessionId);
|
|
510
618
|
const events = [this.event(record)];
|
|
511
619
|
return { session: toCodeSessionInfo(record), events };
|
|
@@ -521,21 +629,139 @@ export class CodeStore {
|
|
|
521
629
|
readTurn(sessionId, clientTurnKey) {
|
|
522
630
|
const row = this.database.prepare(`SELECT ${TURN_COLUMNS} FROM code_turns WHERE session_id = ? AND client_turn_key = ?`)
|
|
523
631
|
.get(sessionId, clientTurnKey);
|
|
632
|
+
if (!row)
|
|
633
|
+
this.refuseSteerKey(sessionId, clientTurnKey);
|
|
524
634
|
return row ? receipt(row) : null;
|
|
525
635
|
}
|
|
636
|
+
/** One client key names one message: a prompt key is never a follow-up key, or the reverse. */
|
|
637
|
+
refuseSteerKey(sessionId, clientTurnKey) {
|
|
638
|
+
if (this.database.prepare('SELECT 1 FROM code_steers WHERE session_id = ? AND client_turn_key = ?').get(sessionId, clientTurnKey)) {
|
|
639
|
+
throw new CodeStoreError('turn_key_conflict', 'Client turn key was used for a follow-up', 409);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
steerRow(sessionId, clientTurnKey) {
|
|
643
|
+
return this.database.prepare(`SELECT ${STEER_COLUMNS} FROM code_steers WHERE session_id = ? AND client_turn_key = ?`)
|
|
644
|
+
.get(sessionId, clientTurnKey);
|
|
645
|
+
}
|
|
646
|
+
/** A follow-up receipt reports its owning turn as a prompt receipt does: a turn a rollback removed reads cancelled. */
|
|
647
|
+
turnStatus(sessionId, turnId) {
|
|
648
|
+
const turn = this.database.prepare('SELECT status, removed_generation FROM code_turns WHERE session_id = ? AND turn_id = ?')
|
|
649
|
+
.get(sessionId, turnId);
|
|
650
|
+
return turn.removed_generation === null ? turn.status : 'cancelled';
|
|
651
|
+
}
|
|
652
|
+
/** The follow-up key state machine; null means a new key that may be reserved. */
|
|
653
|
+
steerKey(sessionId, clientTurnKey, text) {
|
|
654
|
+
if (!text.trim() || !clientTurnKey.trim())
|
|
655
|
+
throw new CodeStoreError('invalid_prompt', 'Text and client turn key are required', 400);
|
|
656
|
+
this.requireRecord(sessionId);
|
|
657
|
+
if (this.database.prepare('SELECT 1 FROM code_turns WHERE session_id = ? AND client_turn_key = ?').get(sessionId, clientTurnKey)) {
|
|
658
|
+
throw new CodeStoreError('turn_key_conflict', 'Client turn key was used for a prompt', 409);
|
|
659
|
+
}
|
|
660
|
+
const row = this.steerRow(sessionId, clientTurnKey);
|
|
661
|
+
if (!row)
|
|
662
|
+
return null;
|
|
663
|
+
if (row.prompt_hash !== promptHash(text))
|
|
664
|
+
throw new CodeStoreError('turn_key_conflict', 'Client turn key was used for different content', 409);
|
|
665
|
+
if (row.status === 'reserved')
|
|
666
|
+
throw new CodeStoreError('steer_in_flight', 'This follow-up is still being delivered', 409);
|
|
667
|
+
if (row.status === 'rejected')
|
|
668
|
+
throw new CodeStoreError('steer_key_spent', 'This follow-up was refused; send it again as a new message', 409);
|
|
669
|
+
if (row.status === 'unknown')
|
|
670
|
+
throw new CodeStoreError('steer_outcome_unknown', 'Follow-up delivery could not be confirmed', 503);
|
|
671
|
+
return { turnId: row.turn_id, clientTurnKey, sequence: row.accepted_sequence, status: this.turnStatus(sessionId, row.turn_id) };
|
|
672
|
+
}
|
|
673
|
+
/** The committed receipt of a follow-up key, whatever its turn has become since. */
|
|
674
|
+
readSteer(sessionId, input) {
|
|
675
|
+
return this.database.transaction(() => this.steerKey(sessionId, input.clientTurnKey, input.text))();
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Take the running turn's one follow-up slot before any native offer. No events: the
|
|
679
|
+
* message joins the transcript only through commitSteer, after the runtime accepted it.
|
|
680
|
+
*/
|
|
681
|
+
reserveSteer(input) {
|
|
682
|
+
return this.write(() => {
|
|
683
|
+
const replay = this.steerKey(input.sessionId, input.clientTurnKey, input.text);
|
|
684
|
+
if (replay)
|
|
685
|
+
return { reservationId: input.clientTurnKey, receipt: replay };
|
|
686
|
+
const record = this.requireRecord(input.sessionId);
|
|
687
|
+
if (record.epoch !== input.epoch || record.turnId !== input.turnId) {
|
|
688
|
+
throw new CodeStoreError('stale_owner', 'Code turn ownership has changed', 409);
|
|
689
|
+
}
|
|
690
|
+
if (record.archivedAt !== null || record.status !== 'streaming' || record.turnId === null) {
|
|
691
|
+
throw new CodeStoreError('session_not_steerable', 'This turn does not take a follow-up right now', 409);
|
|
692
|
+
}
|
|
693
|
+
const turnId = record.turnId;
|
|
694
|
+
if (this.database.prepare(`SELECT 1 FROM code_steers WHERE session_id = ? AND turn_id = ?
|
|
695
|
+
AND status IN ('reserved', 'committed', 'unknown')`).get(input.sessionId, turnId)) {
|
|
696
|
+
throw new CodeStoreError('steer_queue_full', 'The running turn already has its follow-up', 409);
|
|
697
|
+
}
|
|
698
|
+
// Refuse before the native offer what commitSteer could not store, including the reserve for
|
|
699
|
+
// the settlement frame that may re-mark this item; digits a later sequence adds are covered.
|
|
700
|
+
const item = steerItem(turnId, input.clientTurnKey, input.text, this.now());
|
|
701
|
+
const bytes = jsonBytes({ topic: 'code', event: 'code_item', sessionId: record.sessionId, sequence: record.sequence + 1,
|
|
702
|
+
epoch: record.epoch, item: mapItem(item, record.sequence + 1) }) + 32;
|
|
703
|
+
const budget = this.database.prepare(`SELECT event_bytes, control_event_bytes, settlement_bytes FROM code_turns
|
|
704
|
+
WHERE session_id = ? AND turn_id = ?`).get(record.sessionId, turnId);
|
|
705
|
+
if (bytes > this.limits.maxEventBytes
|
|
706
|
+
|| budget.event_bytes - budget.control_event_bytes + bytes > this.limits.maxTurnEventBytes
|
|
707
|
+
|| budget.control_event_bytes + budget.settlement_bytes + settlementCost(record.sessionId, { ...item, status: 'pending' })
|
|
708
|
+
+ CODE_SETTLEMENT_TAIL_BYTES > CODE_TERMINAL_RESERVE_BYTES) {
|
|
709
|
+
throw new CodeStoreError('transcript_limit', 'Code turn event byte limit reached', 409);
|
|
710
|
+
}
|
|
711
|
+
this.database.prepare(`INSERT INTO code_steers (session_id, turn_id, client_turn_key, prompt_hash, status)
|
|
712
|
+
VALUES (?, ?, ?, ?, 'reserved')`).run(input.sessionId, turnId, input.clientTurnKey, promptHash(input.text));
|
|
713
|
+
return { reservationId: input.clientTurnKey, receipt: null };
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/** The runtime accepted the follow-up: append it to the running turn and spend the reservation. */
|
|
717
|
+
commitSteer(sessionId, reservationId, text, nativeId) {
|
|
718
|
+
return this.write(() => {
|
|
719
|
+
const row = this.steerRow(sessionId, reservationId);
|
|
720
|
+
const record = this.requireRecord(sessionId);
|
|
721
|
+
if (!row || row.status !== 'reserved' || row.prompt_hash !== promptHash(text) || record.turnId !== row.turn_id
|
|
722
|
+
|| record.archivedAt !== null || record.status !== 'streaming') {
|
|
723
|
+
throw new CodeStoreError('stale_owner', 'Follow-up reservation is no longer open', 409);
|
|
724
|
+
}
|
|
725
|
+
const item = steerItem(row.turn_id, reservationId, text, this.now());
|
|
726
|
+
const events = [this.event(record, item, 'ordinary', row.turn_id, settlementCost(sessionId, { ...item, status: 'pending' }))];
|
|
727
|
+
this.database.prepare(`UPDATE code_steers SET status = 'committed', accepted_sequence = ?, native_uuid = ?
|
|
728
|
+
WHERE session_id = ? AND client_turn_key = ?`).run(record.sequence, nativeId, sessionId, reservationId);
|
|
729
|
+
return { session: toCodeSessionInfo(record), events,
|
|
730
|
+
receipt: { turnId: row.turn_id, clientTurnKey: reservationId, sequence: record.sequence, status: this.turnStatus(sessionId, row.turn_id) } };
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* A definitive refusal before native acceptance: the key is spent, the turn's slot is free again.
|
|
735
|
+
* A turn that settled while the offer was in flight left the row `unknown`; the in-flight call
|
|
736
|
+
* that then learns the runtime refused the offer is the one owner that can make it definitive.
|
|
737
|
+
*/
|
|
738
|
+
rejectSteer(sessionId, reservationId) {
|
|
739
|
+
this.write(() => {
|
|
740
|
+
this.database.prepare(`UPDATE code_steers SET status = 'rejected' WHERE session_id = ? AND client_turn_key = ?
|
|
741
|
+
AND status IN ('reserved', 'unknown')`).run(sessionId, reservationId);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/** Native input left the process without a durable record: the slot stays spent and the key answers unknown. */
|
|
745
|
+
markSteerUnknown(sessionId, reservationId) {
|
|
746
|
+
this.write(() => {
|
|
747
|
+
this.database.prepare(`UPDATE code_steers SET status = 'unknown' WHERE session_id = ? AND client_turn_key = ?
|
|
748
|
+
AND status <> 'committed'`).run(sessionId, reservationId);
|
|
749
|
+
});
|
|
750
|
+
}
|
|
526
751
|
admitTurn(input) {
|
|
527
752
|
return this.write(() => {
|
|
528
753
|
if (!input.text.trim() || !input.clientTurnKey.trim())
|
|
529
754
|
throw new CodeStoreError('invalid_prompt', 'Text and client turn key are required', 400);
|
|
530
755
|
const record = this.requireRecord(input.sessionId);
|
|
531
|
-
const
|
|
756
|
+
const hash = promptHash(input.text);
|
|
532
757
|
const previous = this.database.prepare(`SELECT ${TURN_COLUMNS} FROM code_turns WHERE session_id = ? AND client_turn_key = ?`)
|
|
533
758
|
.get(input.sessionId, input.clientTurnKey);
|
|
534
759
|
if (previous) {
|
|
535
|
-
if (previous.prompt_hash !==
|
|
760
|
+
if (previous.prompt_hash !== hash)
|
|
536
761
|
throw new CodeStoreError('turn_key_conflict', 'Client turn key was used for different content', 409);
|
|
537
|
-
return { session: toCodeSessionInfo(record), events: [], receipt: receipt(previous), duplicate: true };
|
|
762
|
+
return { session: toCodeSessionInfo(record), events: [], receipt: receipt(previous), duplicate: true, promptUuid: null };
|
|
538
763
|
}
|
|
764
|
+
this.refuseSteerKey(input.sessionId, input.clientTurnKey);
|
|
539
765
|
if (record.archivedAt !== null)
|
|
540
766
|
throw new CodeStoreError('session_archived', 'Code session is archived', 409);
|
|
541
767
|
if (input.expectedRevision !== undefined && input.expectedRevision !== record.revision) {
|
|
@@ -556,17 +782,20 @@ export class CodeStore {
|
|
|
556
782
|
record.title = initialTitle(input.text) || null;
|
|
557
783
|
record.nativePolicy = { model: record.model, effort: record.effort, permissionMode: record.permissionMode };
|
|
558
784
|
const acceptedSequence = record.sequence + 3;
|
|
785
|
+
// The private native input identity; the provider sends the prompt under it.
|
|
786
|
+
const promptUuid = record.provider === 'claude' ? randomUUID() : null;
|
|
559
787
|
this.database.prepare(`INSERT INTO code_turns
|
|
560
|
-
(session_id, turn_id, client_turn_key, prompt_hash, status, accepted_sequence)
|
|
561
|
-
VALUES (?, ?, ?, ?, 'accepted', ?)`)
|
|
562
|
-
.run(input.sessionId, turnId, input.clientTurnKey,
|
|
788
|
+
(session_id, turn_id, client_turn_key, prompt_hash, status, accepted_sequence, native_prompt_uuid)
|
|
789
|
+
VALUES (?, ?, ?, ?, 'accepted', ?, ?)`)
|
|
790
|
+
.run(input.sessionId, turnId, input.clientTurnKey, hash, acceptedSequence, promptUuid);
|
|
563
791
|
const events = [
|
|
564
792
|
this.event(record, { itemId: `${turnId}:user`, turnId, kind: 'user_message', status: 'done',
|
|
565
793
|
text: input.text, clientTurnKey: input.clientTurnKey, createdAt: now, updatedAt: now }),
|
|
566
794
|
this.event(record, { itemId: `${turnId}:started`, turnId, kind: 'turn_started', status: 'running', createdAt: now, updatedAt: now }),
|
|
567
|
-
this.event(record),
|
|
568
795
|
];
|
|
569
|
-
|
|
796
|
+
record.rollbackSince = this.readRollbackSince(input.sessionId);
|
|
797
|
+
events.push(this.event(record));
|
|
798
|
+
return { session: toCodeSessionInfo(record), events, duplicate: false, promptUuid,
|
|
570
799
|
receipt: { turnId, clientTurnKey: input.clientTurnKey, sequence: acceptedSequence, status: 'accepted' } };
|
|
571
800
|
});
|
|
572
801
|
}
|
|
@@ -591,6 +820,15 @@ export class CodeStore {
|
|
|
591
820
|
return { session: toCodeSessionInfo(record), events };
|
|
592
821
|
});
|
|
593
822
|
}
|
|
823
|
+
/** The Manager opened this session. A read receipt: the metadata revision does not move. */
|
|
824
|
+
markVisited(sessionId) {
|
|
825
|
+
return this.write(() => {
|
|
826
|
+
const record = this.requireRecord(sessionId);
|
|
827
|
+
record.lastVisitedAt = this.now();
|
|
828
|
+
const events = [this.event(record)];
|
|
829
|
+
return { session: toCodeSessionInfo(record), events };
|
|
830
|
+
});
|
|
831
|
+
}
|
|
594
832
|
/** Reserve an explicit resume without inventing a prompt or consuming a client key. */
|
|
595
833
|
beginAttach(sessionId, expectedRevision) {
|
|
596
834
|
return this.write(() => {
|
|
@@ -642,14 +880,22 @@ export class CodeStore {
|
|
|
642
880
|
throw new CodeStoreError('stale_owner', 'Native cursor owner is no longer writable', 409);
|
|
643
881
|
if (cursor !== null && !cursor.trim())
|
|
644
882
|
throw new CodeStoreError('invalid_cursor', 'Native cursor must be nonempty', 400);
|
|
883
|
+
// Boundaries belong to the native session that recorded them. A replaced identity
|
|
884
|
+
// retires them; the first identity, and the active turn now sent to it, keep theirs.
|
|
885
|
+
if (cursor !== null && record.nativeCursor !== null && cursor !== record.nativeCursor) {
|
|
886
|
+
this.database.prepare('UPDATE code_turns SET native_prompt_uuid = NULL WHERE session_id = ? AND turn_id IS NOT ?')
|
|
887
|
+
.run(record.sessionId, record.turnId);
|
|
888
|
+
}
|
|
645
889
|
record.nativeStarted = true;
|
|
646
890
|
if (cursor !== null)
|
|
647
891
|
record.nativeCursor = cursor;
|
|
892
|
+
record.rollbackSince = this.readRollbackSince(record.sessionId);
|
|
648
893
|
const events = [this.event(record)];
|
|
649
894
|
return { session: toCodeSessionInfo(record), events };
|
|
650
895
|
});
|
|
651
896
|
}
|
|
652
|
-
|
|
897
|
+
/** `lostRuntime`: a restart settles the turn, and nothing can confirm what its runtime consumed. */
|
|
898
|
+
finish(record, result, lostRuntime = false) {
|
|
653
899
|
const turn = this.database.prepare(`SELECT ${TURN_COLUMNS} FROM code_turns WHERE session_id = ? AND turn_id = ?`)
|
|
654
900
|
.get(record.sessionId, record.turnId);
|
|
655
901
|
if (!turn)
|
|
@@ -684,16 +930,49 @@ export class CodeStore {
|
|
|
684
930
|
.run(status, now, record.sessionId, item.item_id);
|
|
685
931
|
after = item.first_sequence;
|
|
686
932
|
}
|
|
933
|
+
// A follow-up no native result consumed may never have reached Claude; say so on its item.
|
|
934
|
+
// After a restart every committed follow-up of the turn reads that way.
|
|
935
|
+
const undelivered = new Set(result.undeliveredFollowUps ?? []);
|
|
936
|
+
const steers = this.database.prepare(`SELECT client_turn_key, native_uuid FROM code_steers
|
|
937
|
+
WHERE session_id = ? AND turn_id = ? AND status = 'committed'`).all(record.sessionId, turnId);
|
|
938
|
+
for (const steer of steers) {
|
|
939
|
+
const itemId = `${turnId}:steer:${steer.client_turn_key}`;
|
|
940
|
+
const projected = this.database.prepare('SELECT first_sequence FROM code_items WHERE session_id = ? AND item_id = ?')
|
|
941
|
+
.get(record.sessionId, itemId);
|
|
942
|
+
const unconfirmed = lostRuntime || (steer.native_uuid !== null && undelivered.has(steer.native_uuid));
|
|
943
|
+
if (!unconfirmed || !projected)
|
|
944
|
+
continue;
|
|
945
|
+
record.sequence += 1;
|
|
946
|
+
events.push(this.persistEvent(record, {
|
|
947
|
+
topic: 'code', event: 'code_item_update', sessionId: record.sessionId, sequence: record.sequence, epoch: record.epoch,
|
|
948
|
+
update: { itemId, turnId, firstSequence: projected.first_sequence, phase: 'unknown', updatedAt: now },
|
|
949
|
+
}, 'settlement', -settlementCost(record.sessionId, { itemId, turnId, status: 'pending' }), turnId));
|
|
950
|
+
this.database.prepare(`UPDATE code_items SET item_json = json_set(item_json, '$.phase', 'unknown', '$.updatedAt', ?)
|
|
951
|
+
WHERE session_id = ? AND item_id = ?`).run(now, record.sessionId, itemId);
|
|
952
|
+
}
|
|
953
|
+
// A reservation still open here may already have been offered to the runtime: its key answers
|
|
954
|
+
// unknown (never "send it again"), and the turn's slot stays spent.
|
|
955
|
+
this.database.prepare(`UPDATE code_steers SET status = 'unknown' WHERE session_id = ? AND turn_id = ? AND status = 'reserved'`)
|
|
956
|
+
.run(record.sessionId, turnId);
|
|
687
957
|
const kind = result.status === 'completed' ? 'turn_completed' : result.status === 'failed' ? 'turn_failed' : 'turn_cancelled';
|
|
688
958
|
events.push(this.event(record, { itemId: `${record.turnId}:terminal`, turnId: record.turnId, kind,
|
|
689
959
|
status: result.status === 'completed' ? 'done' : result.status === 'failed' ? 'error' : 'cancelled',
|
|
690
960
|
...(error ? { text: error.message } : {}), createdAt: now, updatedAt: now }, 'settlement', turnId));
|
|
691
961
|
this.database.prepare('UPDATE code_turns SET status = ? WHERE session_id = ? AND turn_id = ?')
|
|
692
962
|
.run(result.status, record.sessionId, record.turnId);
|
|
963
|
+
// A prompt that never reached the native runtime is absent from its history.
|
|
964
|
+
if (result.dispatched === false) {
|
|
965
|
+
this.database.prepare('UPDATE code_turns SET native_prompt_uuid = NULL WHERE session_id = ? AND turn_id = ?')
|
|
966
|
+
.run(record.sessionId, record.turnId);
|
|
967
|
+
record.rollbackSince = this.readRollbackSince(record.sessionId);
|
|
968
|
+
}
|
|
693
969
|
record.turnId = null;
|
|
694
970
|
record.status = result.status === 'failed' ? 'failed' : 'idle';
|
|
695
971
|
record.error = error;
|
|
696
972
|
record.lastUsedAt = now;
|
|
973
|
+
// The unread clock: a user-stopped turn is one they were watching, so it does not count.
|
|
974
|
+
if (result.status !== 'cancelled')
|
|
975
|
+
record.lastTurnCompletedAt = now;
|
|
697
976
|
events.push(this.event(record, undefined, 'settlement', turnId));
|
|
698
977
|
return { session: toCodeSessionInfo(record), events, receipt: { ...receipt(turn), status: result.status } };
|
|
699
978
|
}
|
|
@@ -718,7 +997,8 @@ export class CodeStore {
|
|
|
718
997
|
const record = this.requireRecord(sessionId);
|
|
719
998
|
if (record.revision !== patch.expectedRevision)
|
|
720
999
|
throw new CodeStoreError('revision_conflict', 'Code metadata changed', 409);
|
|
721
|
-
const policyChange = patch.model !== undefined || patch.effort !== undefined || patch.permissionMode !== undefined
|
|
1000
|
+
const policyChange = patch.model !== undefined || patch.effort !== undefined || patch.permissionMode !== undefined
|
|
1001
|
+
|| patch.thinking !== undefined;
|
|
722
1002
|
if (isBusy(record.status) && (policyChange || patch.archived !== undefined)) {
|
|
723
1003
|
throw new CodeStoreError('session_busy', 'Active Code sessions cannot change policy or archive', 409);
|
|
724
1004
|
}
|
|
@@ -730,6 +1010,8 @@ export class CodeStore {
|
|
|
730
1010
|
record.effort = patch.effort;
|
|
731
1011
|
if (patch.permissionMode !== undefined)
|
|
732
1012
|
record.permissionMode = patch.permissionMode;
|
|
1013
|
+
if (patch.thinking !== undefined)
|
|
1014
|
+
record.thinking = patch.thinking;
|
|
733
1015
|
if (patch.archived !== undefined)
|
|
734
1016
|
record.archivedAt = patch.archived ? record.archivedAt ?? this.now() : null;
|
|
735
1017
|
// Invalidates idle runtime callbacks after reconfiguration or archive.
|
|
@@ -741,6 +1023,109 @@ export class CodeStore {
|
|
|
741
1023
|
return { session: toCodeSessionInfo(record), events };
|
|
742
1024
|
});
|
|
743
1025
|
}
|
|
1026
|
+
/** Kept and later turns around a settled `${turnId}:user` target; removed turns are not part of the conversation. */
|
|
1027
|
+
rollbackTurns(record, upToItemId) {
|
|
1028
|
+
const item = this.database.prepare(`SELECT json_extract(item_json, '$.kind') AS kind, json_extract(item_json, '$.turnId') AS turn_id
|
|
1029
|
+
FROM code_items WHERE session_id = ? AND item_id = ?`).get(record.sessionId, upToItemId);
|
|
1030
|
+
const turnId = item?.turn_id;
|
|
1031
|
+
if (!item || item.kind !== 'user_message' || typeof turnId !== 'string' || `${turnId}:user` !== upToItemId) {
|
|
1032
|
+
throw new CodeStoreError('rollback_target_not_found', 'Rollback target is not a turn in this conversation', 404);
|
|
1033
|
+
}
|
|
1034
|
+
const turns = this.database.prepare(`SELECT turn_id, status, accepted_sequence, native_prompt_uuid FROM code_turns
|
|
1035
|
+
WHERE session_id = ? AND removed_generation IS NULL ORDER BY accepted_sequence`).all(record.sessionId);
|
|
1036
|
+
const index = turns.findIndex(turn => turn.turn_id === turnId);
|
|
1037
|
+
const target = turns[index];
|
|
1038
|
+
if (!target)
|
|
1039
|
+
throw new CodeStoreError('rollback_target_not_found', 'Rollback target is not a turn in this conversation', 404);
|
|
1040
|
+
if (target.status !== 'completed' && target.status !== 'failed' && target.status !== 'cancelled') {
|
|
1041
|
+
throw new CodeStoreError('session_busy', 'The rollback target has not settled', 409);
|
|
1042
|
+
}
|
|
1043
|
+
return { kept: turns.slice(0, index + 1), later: turns.slice(index + 1) };
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Validate a rollback to the item's turn without writing anything. The target must carry a
|
|
1047
|
+
* boundary; later turns are passed as they are, boundary or not, and the provider proves
|
|
1048
|
+
* where the fork ends (see `forkClaudeHistory`) before any fork.
|
|
1049
|
+
*/
|
|
1050
|
+
readRollbackPlan(sessionId, upToItemId) {
|
|
1051
|
+
return this.database.transaction(() => {
|
|
1052
|
+
const record = this.requireRecord(sessionId);
|
|
1053
|
+
if (record.archivedAt !== null)
|
|
1054
|
+
throw new CodeStoreError('session_archived', 'Code session is archived', 409);
|
|
1055
|
+
if (record.provider !== 'claude' || !record.nativeCursor)
|
|
1056
|
+
throw new CodeStoreError('rollback_unavailable', 'Conversation history is unavailable', 409);
|
|
1057
|
+
const { kept, later } = this.rollbackTurns(record, upToItemId);
|
|
1058
|
+
const target = kept.at(-1);
|
|
1059
|
+
if (!later.length)
|
|
1060
|
+
throw new CodeStoreError('rollback_noop', 'The target is already the latest turn', 409);
|
|
1061
|
+
if (target.native_prompt_uuid === null) {
|
|
1062
|
+
throw new CodeStoreError('rollback_boundary_unavailable', 'This turn has no recorded conversation boundary', 409);
|
|
1063
|
+
}
|
|
1064
|
+
const turn = (row) => ({ turnId: row.turn_id, promptUuid: row.native_prompt_uuid });
|
|
1065
|
+
return { target: { turnId: target.turn_id, promptUuid: target.native_prompt_uuid }, kept: kept.map(turn),
|
|
1066
|
+
later: later.map(turn), revision: record.revision, epoch: record.epoch, nativeCursor: record.nativeCursor,
|
|
1067
|
+
title: record.title };
|
|
1068
|
+
})();
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Commit a verified fork in one transaction, compare-and-swap on the revision, epoch and
|
|
1072
|
+
* native cursor the plan was read at. Later items and their events leave; their turn rows
|
|
1073
|
+
* stay so client keys keep answering, marked removed. Replay below the new floor fails with
|
|
1074
|
+
* `invalid_sequence`, and the history generation tells live readers to take a snapshot.
|
|
1075
|
+
* Follow-up rows (`code_steers`) stay as they are: their keys remain spent and their native
|
|
1076
|
+
* ids are not remapped (only a running turn reads them). The follow-up items of removed turns
|
|
1077
|
+
* leave with the other later items, and their receipts read cancelled like the turns'.
|
|
1078
|
+
*/
|
|
1079
|
+
commitRollback(input) {
|
|
1080
|
+
return this.write(() => {
|
|
1081
|
+
const record = this.requireRecord(input.sessionId);
|
|
1082
|
+
if (record.revision !== input.expectedRevision || record.epoch !== input.expectedEpoch
|
|
1083
|
+
|| record.nativeCursor !== input.expectedCursor || record.turnId !== null || isBusy(record.status)
|
|
1084
|
+
|| record.archivedAt !== null || record.provider !== 'claude') {
|
|
1085
|
+
throw new CodeStoreError('revision_conflict', 'Code session changed during rollback', 409);
|
|
1086
|
+
}
|
|
1087
|
+
if (!input.forkCursor.trim() || input.forkCursor === input.expectedCursor) {
|
|
1088
|
+
throw new CodeStoreError('invalid_cursor', 'Rollback fork must be a new native session', 400);
|
|
1089
|
+
}
|
|
1090
|
+
const { kept, later } = this.rollbackTurns(record, input.upToItemId);
|
|
1091
|
+
if (!later.length)
|
|
1092
|
+
throw new CodeStoreError('rollback_noop', 'The target is already the latest turn', 409);
|
|
1093
|
+
const keptIds = new Set(kept.map(turn => turn.turn_id));
|
|
1094
|
+
if (!input.remapped.every(turn => keptIds.has(turn.turnId) && !!turn.promptUuid.trim())
|
|
1095
|
+
|| !input.cleared.every(turnId => keptIds.has(turnId))) {
|
|
1096
|
+
throw new CodeStoreError('rollback_unavailable', 'Rollback boundaries do not match the kept turns', 409);
|
|
1097
|
+
}
|
|
1098
|
+
const generation = record.historyGeneration + 1;
|
|
1099
|
+
const after = kept.at(-1).accepted_sequence;
|
|
1100
|
+
const cut = this.database.prepare(`SELECT MIN(first_sequence) AS cut FROM code_items WHERE session_id = ?
|
|
1101
|
+
AND json_extract(item_json, '$.turnId') IN (SELECT turn_id FROM code_turns WHERE session_id = ?
|
|
1102
|
+
AND removed_generation IS NULL AND accepted_sequence > ?)`)
|
|
1103
|
+
.get(record.sessionId, record.sessionId, after).cut;
|
|
1104
|
+
if (cut !== null) {
|
|
1105
|
+
this.database.prepare('DELETE FROM code_items WHERE session_id = ? AND first_sequence >= ?').run(record.sessionId, cut);
|
|
1106
|
+
this.database.prepare('DELETE FROM code_events WHERE session_id = ? AND sequence >= ?').run(record.sessionId, cut);
|
|
1107
|
+
}
|
|
1108
|
+
this.database.prepare(`UPDATE code_turns SET removed_generation = ? WHERE session_id = ?
|
|
1109
|
+
AND removed_generation IS NULL AND accepted_sequence > ?`).run(generation, record.sessionId, after);
|
|
1110
|
+
// Every source boundary is retired; only the ones found again in the fork return.
|
|
1111
|
+
this.database.prepare('UPDATE code_turns SET native_prompt_uuid = NULL WHERE session_id = ?').run(record.sessionId);
|
|
1112
|
+
const remap = this.database.prepare('UPDATE code_turns SET native_prompt_uuid = ? WHERE session_id = ? AND turn_id = ?');
|
|
1113
|
+
for (const turn of input.remapped)
|
|
1114
|
+
remap.run(turn.promptUuid, record.sessionId, turn.turnId);
|
|
1115
|
+
record.historyGeneration = generation;
|
|
1116
|
+
record.nativeCursor = input.forkCursor;
|
|
1117
|
+
record.nativeStarted = true;
|
|
1118
|
+
record.revision += 1;
|
|
1119
|
+
record.epoch += 1;
|
|
1120
|
+
record.status = 'idle';
|
|
1121
|
+
record.error = null;
|
|
1122
|
+
record.lastUsedAt = this.now();
|
|
1123
|
+
record.rollbackSince = this.readRollbackSince(record.sessionId);
|
|
1124
|
+
record.replayFloorSequence = record.sequence + 1;
|
|
1125
|
+
const events = [this.event(record)];
|
|
1126
|
+
return { session: toCodeSessionInfo(record), events };
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
744
1129
|
/** Call once before the manager admits work. Never replays a native prompt. */
|
|
745
1130
|
recoverInterrupted() {
|
|
746
1131
|
return this.write(() => {
|
|
@@ -751,8 +1136,11 @@ export class CodeStore {
|
|
|
751
1136
|
const record = rowToRecord(row);
|
|
752
1137
|
record.epoch += 1;
|
|
753
1138
|
const error = { code: 'orphaned_turn', message: 'Code turn interrupted by server restart', at: this.now(), recoverable: true };
|
|
754
|
-
|
|
755
|
-
|
|
1139
|
+
// A turn is marked streaming before its prompt is handed to the runtime, so one still
|
|
1140
|
+
// starting never sent it: its boundary cannot be in native history.
|
|
1141
|
+
if (record.turnId !== null) {
|
|
1142
|
+
events.push(...this.finish(record, { status: 'failed', error, ...(row.status === 'starting' ? { dispatched: false } : {}) }, true).events);
|
|
1143
|
+
}
|
|
756
1144
|
else {
|
|
757
1145
|
record.status = 'failed';
|
|
758
1146
|
record.error = error;
|