@cjhyy/code-shell-core 0.7.1 → 0.8.0
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 +36 -23
- package/dist/agent/agent-definition-registry.d.ts +2 -2
- package/dist/agent/agent-definition-registry.js +47 -10
- package/dist/agent/agent-definition.d.ts +5 -0
- package/dist/agent/agent-definition.js +14 -0
- package/dist/automation/index.d.ts +7 -4
- package/dist/automation/index.js +4 -2
- package/dist/automation/runner.js +3 -1
- package/dist/automation/scheduler.d.ts +57 -1
- package/dist/automation/scheduler.js +123 -23
- package/dist/automation/store.d.ts +6 -2
- package/dist/automation/store.js +7 -3
- package/dist/capabilities/index.d.ts +109 -0
- package/dist/capabilities/index.js +80 -0
- package/dist/capability-control/disabled-lists.d.ts +2 -1
- package/dist/capability-control/disabled-lists.js +3 -5
- package/dist/capability-control/overlay.d.ts +14 -1
- package/dist/capability-control/overlay.js +40 -0
- package/dist/capability-control/service.js +6 -4
- package/dist/cli/agent-server-stdio.d.ts +1 -1
- package/dist/cli/agent-server-stdio.js +66 -6
- package/dist/cli/agent-server-tcp.js +7 -3
- package/dist/context/compaction.d.ts +39 -3
- package/dist/context/compaction.js +151 -39
- package/dist/context/manager.d.ts +17 -1
- package/dist/context/manager.js +36 -1
- package/dist/credentials/access.d.ts +3 -1
- package/dist/credentials/access.js +24 -3
- package/dist/credentials/index.d.ts +1 -1
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/oauth.d.ts +4 -1
- package/dist/credentials/oauth.js +14 -0
- package/dist/credentials/store.d.ts +15 -1
- package/dist/credentials/store.js +17 -4
- package/dist/credentials/types.d.ts +24 -0
- package/dist/credentials/use-credential-tool.js +15 -1
- package/dist/engine/auxiliary-pipeline.d.ts +40 -0
- package/dist/engine/auxiliary-pipeline.js +181 -0
- package/dist/{settings → engine}/disk-defaults.d.ts +2 -2
- package/dist/{settings → engine}/disk-defaults.js +5 -2
- package/dist/engine/dynamic-tool-defs.d.ts +4 -0
- package/dist/engine/dynamic-tool-defs.js +4 -0
- package/dist/engine/engine.d.ts +254 -195
- package/dist/engine/engine.js +1812 -2041
- package/dist/engine/file-history-hook.d.ts +11 -0
- package/dist/engine/file-history-hook.js +31 -0
- package/dist/engine/goal-judge-context.d.ts +9 -0
- package/dist/engine/goal-judge-context.js +234 -0
- package/dist/engine/injected-context-cache.d.ts +6 -0
- package/dist/engine/injected-context-cache.js +13 -0
- package/dist/engine/input-attachments.d.ts +1 -1
- package/dist/engine/model-facade.d.ts +3 -3
- package/dist/engine/model-facade.js +15 -14
- package/dist/engine/permission-controller.d.ts +55 -0
- package/dist/engine/permission-controller.js +148 -0
- package/dist/engine/prompt-cache-diagnostics.d.ts +58 -0
- package/dist/engine/prompt-cache-diagnostics.js +178 -0
- package/dist/engine/run-accounting.d.ts +41 -0
- package/dist/engine/run-accounting.js +126 -0
- package/dist/engine/run-context.d.ts +44 -0
- package/dist/engine/run-context.js +88 -0
- package/dist/engine/run-environment.d.ts +30 -0
- package/dist/engine/run-environment.js +77 -0
- package/dist/engine/run-finalize.d.ts +58 -0
- package/dist/engine/run-finalize.js +245 -0
- package/dist/engine/run-goal.d.ts +58 -0
- package/dist/engine/run-goal.js +188 -0
- package/dist/engine/run-image-input.d.ts +1 -1
- package/dist/engine/run-image-input.js +5 -3
- package/dist/engine/run-session-open.d.ts +41 -0
- package/dist/engine/run-session-open.js +186 -0
- package/dist/engine/run-setup.d.ts +42 -0
- package/dist/engine/run-setup.js +58 -0
- package/dist/engine/run-tooling.d.ts +125 -0
- package/dist/engine/run-tooling.js +283 -0
- package/dist/engine/run-types.d.ts +139 -0
- package/dist/engine/run-types.js +12 -0
- package/dist/engine/run-workspace.d.ts +50 -0
- package/dist/engine/run-workspace.js +114 -0
- package/dist/engine/steer-queue.d.ts +1 -1
- package/dist/engine/subagent-spawner.d.ts +32 -0
- package/dist/engine/subagent-spawner.js +287 -0
- package/dist/engine/tool-summary.d.ts +2 -2
- package/dist/engine/tool-summary.js +2 -2
- package/dist/engine/turn-loop.d.ts +49 -10
- package/dist/engine/turn-loop.js +370 -104
- package/dist/engine/types.d.ts +30 -3
- package/dist/engine/types.js +1 -1
- package/dist/{engine/goal.d.ts → goal/lifecycle.d.ts} +76 -4
- package/dist/goal/lifecycle.js +482 -0
- package/dist/hooks/events.d.ts +24 -10
- package/dist/hooks/goal-stop-hook.d.ts +17 -28
- package/dist/hooks/goal-stop-hook.js +110 -465
- package/dist/hooks/registry.js +1 -1
- package/dist/index.d.ts +48 -108
- package/dist/index.extension.d.ts +51 -0
- package/dist/index.extension.js +39 -0
- package/dist/index.internal.d.ts +86 -0
- package/dist/index.internal.js +92 -0
- package/dist/index.js +34 -123
- package/dist/links/cli.d.ts +48 -0
- package/dist/links/cli.js +617 -0
- package/dist/links/http.d.ts +40 -0
- package/dist/links/http.js +166 -0
- package/dist/links/index.d.ts +4 -0
- package/dist/links/index.js +3 -0
- package/dist/links/link-action-tool.d.ts +5 -0
- package/dist/links/link-action-tool.js +241 -0
- package/dist/links/providers.d.ts +8 -0
- package/dist/links/providers.js +665 -0
- package/dist/links/types.d.ts +44 -0
- package/dist/llm/client-base.d.ts +7 -0
- package/dist/llm/client-base.js +60 -0
- package/dist/llm/model-pool.d.ts +1 -1
- package/dist/llm/model-pool.js +1 -1
- package/dist/llm/providers/anthropic.d.ts +1 -0
- package/dist/llm/providers/anthropic.js +9 -0
- package/dist/llm/providers/openai.d.ts +1 -0
- package/dist/llm/providers/openai.js +17 -0
- package/dist/model-catalog/index.js +15 -2
- package/dist/model-catalog/resolve.d.ts +13 -1
- package/dist/model-catalog/resolve.js +37 -2
- package/dist/model-catalog/types.d.ts +74 -48
- package/dist/model-catalog/types.js +16 -5
- package/dist/model-catalog/upsert.d.ts +3 -1
- package/dist/model-catalog/upsert.js +9 -0
- package/dist/onboarding.d.ts +0 -1
- package/dist/onboarding.js +1 -22
- package/dist/panel-apps/bindings.d.ts +24 -0
- package/dist/panel-apps/bindings.js +69 -0
- package/dist/panel-apps/github-archive.d.ts +21 -0
- package/dist/panel-apps/github-archive.js +98 -0
- package/dist/panel-apps/index.d.ts +4 -0
- package/dist/panel-apps/index.js +4 -0
- package/dist/panel-apps/installer.d.ts +68 -0
- package/dist/panel-apps/installer.js +622 -0
- package/dist/panel-apps/manifest.d.ts +364 -0
- package/dist/panel-apps/manifest.js +189 -0
- package/dist/panel-apps/paths.d.ts +13 -0
- package/dist/panel-apps/paths.js +38 -0
- package/dist/panel-apps/registry.d.ts +51 -0
- package/dist/panel-apps/registry.js +99 -0
- package/dist/panel-apps/runtime.d.ts +80 -0
- package/dist/panel-apps/runtime.js +154 -0
- package/dist/plugins/gitOps.js +8 -1
- package/dist/plugins/installedPlugins.d.ts +2 -1
- package/dist/plugins/installedPlugins.js +146 -8
- package/dist/plugins/installer/codex/convertAgents.d.ts +4 -1
- package/dist/plugins/installer/codex/convertAgents.js +62 -8
- package/dist/plugins/installer/codex/convertCommands.d.ts +5 -5
- package/dist/plugins/installer/codex/convertCommands.js +35 -12
- package/dist/plugins/installer/codex/convertHooks.d.ts +10 -0
- package/dist/plugins/installer/codex/convertHooks.js +82 -0
- package/dist/plugins/installer/codex/convertSkills.js +41 -6
- package/dist/plugins/installer/install.d.ts +7 -1
- package/dist/plugins/installer/install.js +21 -87
- package/dist/plugins/installer/installFromArchive.d.ts +26 -4
- package/dist/plugins/installer/installFromArchive.js +44 -27
- package/dist/plugins/installer/installFromNpm.d.ts +31 -0
- package/dist/plugins/installer/installFromNpm.js +273 -0
- package/dist/plugins/installer/loadPluginAgents.js +17 -4
- package/dist/plugins/installer/loadPluginMcp.d.ts +14 -0
- package/dist/plugins/installer/loadPluginMcp.js +245 -35
- package/dist/plugins/installer/normalizeManifest.d.ts +13 -0
- package/dist/plugins/installer/normalizeManifest.js +306 -0
- package/dist/plugins/installer/npmTar.d.ts +13 -0
- package/dist/plugins/installer/npmTar.js +321 -0
- package/dist/plugins/installer/parseSource.d.ts +11 -0
- package/dist/plugins/installer/parseSource.js +48 -0
- package/dist/plugins/installer/preview.d.ts +101 -0
- package/dist/plugins/installer/preview.js +336 -0
- package/dist/plugins/installer/projectPluginSource.d.ts +14 -0
- package/dist/plugins/installer/projectPluginSource.js +140 -0
- package/dist/plugins/installer/types.d.ts +775 -0
- package/dist/plugins/installer/types.js +100 -0
- package/dist/plugins/installer/unzip.d.ts +11 -0
- package/dist/plugins/installer/unzip.js +123 -9
- package/dist/plugins/installer/update.d.ts +2 -2
- package/dist/plugins/installer/update.js +43 -4
- package/dist/plugins/loadPluginHooks.d.ts +19 -2
- package/dist/plugins/loadPluginHooks.js +97 -86
- package/dist/plugins/pluginAutomationTemplates.d.ts +18 -0
- package/dist/plugins/pluginAutomationTemplates.js +50 -0
- package/dist/plugins/pluginCatalog.d.ts +45 -0
- package/dist/plugins/pluginCatalog.js +87 -0
- package/dist/plugins/pluginCommandHook.d.ts +6 -0
- package/dist/plugins/pluginCommandHook.js +25 -8
- package/dist/plugins/pluginCommandsLoader.d.ts +19 -0
- package/dist/plugins/pluginCommandsLoader.js +181 -7
- package/dist/plugins/pluginContent.d.ts +10 -1
- package/dist/plugins/pluginContent.js +87 -30
- package/dist/plugins/pluginHookApproval.d.ts +30 -0
- package/dist/plugins/pluginHookApproval.js +160 -0
- package/dist/plugins/pluginHookIntegrity.d.ts +69 -0
- package/dist/plugins/pluginHookIntegrity.js +361 -0
- package/dist/plugins/pluginInstaller.js +71 -12
- package/dist/plugins/pluginMcpApproval.d.ts +19 -0
- package/dist/plugins/pluginMcpApproval.js +110 -0
- package/dist/plugins/pluginMcpIntegrity.d.ts +21 -0
- package/dist/plugins/pluginMcpIntegrity.js +74 -0
- package/dist/plugins/types.d.ts +30 -0
- package/dist/plugins/varRewrite.d.ts +9 -5
- package/dist/plugins/varRewrite.js +26 -17
- package/dist/preset/index.d.ts +11 -8
- package/dist/preset/index.js +62 -203
- package/dist/product/define.js +9 -3
- package/dist/product/types.d.ts +0 -2
- package/dist/profile/activation.d.ts +36 -0
- package/dist/profile/activation.js +57 -0
- package/dist/profile/catalog-store.d.ts +77 -0
- package/dist/profile/catalog-store.js +346 -0
- package/dist/profile/catalog.d.ts +54 -0
- package/dist/profile/catalog.js +152 -0
- package/dist/profile/index.d.ts +7 -0
- package/dist/profile/index.js +7 -0
- package/dist/profile/requirements.d.ts +76 -0
- package/dist/profile/requirements.js +117 -0
- package/dist/profile/resolve.d.ts +11 -0
- package/dist/profile/resolve.js +41 -0
- package/dist/profile/store.d.ts +17 -0
- package/dist/profile/store.js +167 -0
- package/dist/profile/types.d.ts +321 -0
- package/dist/profile/types.js +104 -0
- package/dist/prompt/composer.d.ts +46 -7
- package/dist/prompt/composer.js +78 -48
- package/dist/prompt/instruction-scanner.d.ts +6 -5
- package/dist/prompt/instruction-scanner.js +8 -23
- package/dist/prompt/section-loader.d.ts +2 -2
- package/dist/prompt/section-loader.js +7 -4
- package/dist/prompt/sections/browser.md +10 -9
- package/dist/prompt/sections/harness-base.md +9 -0
- package/dist/protocol/chat-session-manager.d.ts +64 -1
- package/dist/protocol/chat-session-manager.js +104 -0
- package/dist/protocol/chat-session.d.ts +61 -10
- package/dist/protocol/chat-session.js +135 -25
- package/dist/protocol/client.d.ts +33 -3
- package/dist/protocol/client.js +96 -5
- package/dist/protocol/index.d.ts +2 -2
- package/dist/protocol/index.js +2 -2
- package/dist/protocol/mobile-remote-types.d.ts +406 -0
- package/dist/protocol/mobile-remote-types.js +1 -0
- package/dist/protocol/server.d.ts +127 -3
- package/dist/protocol/server.js +1309 -182
- package/dist/protocol/types.d.ts +152 -34
- package/dist/protocol/types.js +16 -0
- package/dist/run/ArtifactTracker.d.ts +6 -1
- package/dist/run/ArtifactTracker.js +29 -21
- package/dist/run/EngineRunner.d.ts +2 -0
- package/dist/run/EngineRunner.js +1 -0
- package/dist/run/RunManager.d.ts +26 -0
- package/dist/run/RunManager.js +82 -11
- package/dist/run/factory.d.ts +3 -0
- package/dist/run/factory.js +2 -0
- package/dist/runtime/safe-spawn.d.ts +6 -0
- package/dist/runtime/safe-spawn.js +8 -13
- package/dist/services/auto-dream.d.ts +13 -1
- package/dist/services/auto-dream.js +56 -8
- package/dist/services/dream-consolidation.js +10 -1
- package/dist/services/memory-orchestrator.js +6 -2
- package/dist/services/oauth.js +4 -1
- package/dist/services/session-memory.d.ts +4 -4
- package/dist/services/session-memory.js +13 -10
- package/dist/session/memory.d.ts +26 -7
- package/dist/session/memory.js +79 -21
- package/dist/session/session-manager.d.ts +182 -28
- package/dist/session/session-manager.js +910 -130
- package/dist/session/session-message.d.ts +22 -0
- package/dist/session/session-message.js +1 -0
- package/dist/session/transcript.d.ts +46 -5
- package/dist/session/transcript.js +159 -3
- package/dist/settings/feature-flags.d.ts +40 -0
- package/dist/settings/feature-flags.js +40 -0
- package/dist/settings/manager.d.ts +39 -1
- package/dist/settings/manager.js +102 -41
- package/dist/settings/migrate-config.js +4 -2
- package/dist/settings/schema.d.ts +511 -177
- package/dist/settings/schema.js +98 -25
- package/dist/skills/scanner.d.ts +6 -2
- package/dist/skills/scanner.js +160 -9
- package/dist/sources/adapter.d.ts +14 -0
- package/dist/sources/adapter.js +7 -0
- package/dist/sources/adapters/local-files.d.ts +10 -0
- package/dist/sources/adapters/local-files.js +127 -0
- package/dist/sources/adapters/mcp-resource.d.ts +17 -0
- package/dist/sources/adapters/mcp-resource.js +41 -0
- package/dist/sources/adapters/mock.d.ts +3 -0
- package/dist/sources/adapters/mock.js +29 -0
- package/dist/sources/binding.d.ts +6 -0
- package/dist/sources/binding.js +24 -0
- package/dist/sources/catalog.d.ts +6 -0
- package/dist/sources/catalog.js +64 -0
- package/dist/sources/context-summary.d.ts +8 -0
- package/dist/sources/context-summary.js +13 -0
- package/dist/sources/credential-status.d.ts +2 -0
- package/dist/sources/credential-status.js +13 -0
- package/dist/sources/index.d.ts +10 -0
- package/dist/sources/index.js +10 -0
- package/dist/sources/resolve.d.ts +28 -0
- package/dist/sources/resolve.js +47 -0
- package/dist/sources/truncate-utf8.d.ts +7 -0
- package/dist/sources/truncate-utf8.js +18 -0
- package/dist/sources/types.d.ts +71 -0
- package/dist/sources/types.js +26 -0
- package/dist/testing/fetch-stub.d.ts +32 -0
- package/dist/testing/fetch-stub.js +24 -0
- package/dist/themes/image.d.ts +18 -0
- package/dist/themes/image.js +117 -0
- package/dist/themes/index.d.ts +4 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/installer.d.ts +50 -0
- package/dist/themes/installer.js +290 -0
- package/dist/themes/manifest.d.ts +102 -0
- package/dist/themes/manifest.js +86 -0
- package/dist/themes/paths.d.ts +14 -0
- package/dist/themes/paths.js +33 -0
- package/dist/tool-system/browser-bridge.d.ts +53 -11
- package/dist/tool-system/browser-bridge.js +8 -9
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +2 -2
- package/dist/tool-system/builtin/agent-heartbeat.js +20 -6
- package/dist/tool-system/builtin/agent-notifications.d.ts +127 -72
- package/dist/tool-system/builtin/agent-notifications.js +291 -101
- package/dist/tool-system/builtin/agent-output-file.js +1 -3
- package/dist/tool-system/builtin/agent-progress.d.ts +6 -0
- package/dist/tool-system/builtin/agent-progress.js +83 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +38 -2
- package/dist/tool-system/builtin/agent-registry.js +178 -9
- package/dist/tool-system/builtin/agent-transcript-translator.js +1 -1
- package/dist/tool-system/builtin/agent.d.ts +2 -0
- package/dist/tool-system/builtin/agent.js +274 -33
- package/dist/tool-system/builtin/background-jobs.d.ts +55 -2
- package/dist/tool-system/builtin/background-jobs.js +96 -4
- package/dist/tool-system/builtin/background-work.d.ts +6 -0
- package/dist/tool-system/builtin/background-work.js +8 -1
- package/dist/tool-system/builtin/bash.js +5 -2
- package/dist/tool-system/builtin/browser-tools.d.ts +6 -3
- package/dist/tool-system/builtin/browser-tools.js +89 -23
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +6 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +172 -12
- package/dist/tool-system/builtin/edit.js +2 -6
- package/dist/tool-system/builtin/file-cache.d.ts +2 -0
- package/dist/tool-system/builtin/file-cache.js +4 -0
- package/dist/tool-system/builtin/generate-image.js +17 -10
- package/dist/tool-system/builtin/generate-video.js +26 -7
- package/dist/tool-system/builtin/grep.js +2 -2
- package/dist/tool-system/builtin/image-uploader.js +7 -1
- package/dist/tool-system/builtin/index.d.ts +32 -7
- package/dist/tool-system/builtin/index.js +237 -167
- package/dist/tool-system/builtin/memory.d.ts +6 -0
- package/dist/tool-system/builtin/memory.js +36 -15
- package/dist/tool-system/builtin/panel.d.ts +9 -0
- package/dist/tool-system/builtin/panel.js +314 -0
- package/dist/tool-system/builtin/read.js +5 -1
- package/dist/tool-system/builtin/send-message-to-session.d.ts +6 -0
- package/dist/tool-system/builtin/send-message-to-session.js +67 -0
- package/dist/tool-system/builtin/sources.d.ts +8 -0
- package/dist/tool-system/builtin/sources.js +137 -0
- package/dist/tool-system/builtin/tool-search.js +7 -2
- package/dist/tool-system/builtin/update-automation-memory.js +2 -0
- package/dist/tool-system/builtin/web-fetch.js +17 -13
- package/dist/tool-system/builtin/web-search.js +1 -3
- package/dist/tool-system/capability-module.d.ts +93 -0
- package/dist/tool-system/capability-module.js +53 -0
- package/dist/tool-system/context.d.ts +77 -19
- package/dist/tool-system/executor.js +75 -50
- package/dist/tool-system/external-tool-exposure.d.ts +60 -0
- package/dist/tool-system/external-tool-exposure.js +304 -0
- package/dist/tool-system/mcp-manager.d.ts +11 -1
- package/dist/tool-system/mcp-manager.js +19 -4
- package/dist/tool-system/mcp-tool-policy.d.ts +9 -0
- package/dist/tool-system/mcp-tool-policy.js +34 -0
- package/dist/tool-system/panel-bridge.d.ts +53 -0
- package/dist/tool-system/panel-bridge.js +1 -0
- package/dist/tool-system/permission.d.ts +26 -0
- package/dist/tool-system/permission.js +46 -0
- package/dist/tool-system/registry.d.ts +12 -1
- package/dist/tool-system/registry.js +31 -7
- package/dist/tool-system/sandbox/index.js +3 -8
- package/dist/tool-system/sandbox/seatbelt.js +2 -6
- package/dist/tool-system/session-tool-host.d.ts +139 -0
- package/dist/tool-system/session-tool-host.js +167 -0
- package/dist/tool-system/testing/tool-registry-harness.d.ts +31 -0
- package/dist/tool-system/testing/tool-registry-harness.js +51 -0
- package/dist/tool-system/validation.d.ts +14 -7
- package/dist/tool-system/validation.js +701 -7
- package/dist/types.d.ts +186 -20
- package/dist/{cc-orchestrator → utils}/cwd-normalize.d.ts +1 -0
- package/dist/{cc-orchestrator → utils}/cwd-normalize.js +1 -0
- package/dist/utils/file-mutex.d.ts +65 -0
- package/dist/utils/file-mutex.js +145 -0
- package/dist/utils/json.d.ts +2 -4
- package/dist/utils/json.js +2 -4
- package/dist/utils/lockfile.d.ts +30 -1
- package/dist/utils/lockfile.js +2 -2
- package/dist/utils/secret-scrubber.d.ts +8 -0
- package/dist/utils/secret-scrubber.js +227 -0
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +20 -5
- package/THIRD_PARTY_NOTICES.md +0 -206
- package/dist/arena/arena.d.ts +0 -45
- package/dist/arena/arena.js +0 -362
- package/dist/arena/context/context-tools.d.ts +0 -16
- package/dist/arena/context/context-tools.js +0 -272
- package/dist/arena/context/within-root.d.ts +0 -7
- package/dist/arena/context/within-root.js +0 -15
- package/dist/arena/detect-mode.d.ts +0 -20
- package/dist/arena/detect-mode.js +0 -78
- package/dist/arena/digest-builder.d.ts +0 -25
- package/dist/arena/digest-builder.js +0 -120
- package/dist/arena/index.d.ts +0 -29
- package/dist/arena/index.js +0 -29
- package/dist/arena/iterate/convergence.d.ts +0 -25
- package/dist/arena/iterate/convergence.js +0 -103
- package/dist/arena/iterate/formats/index.d.ts +0 -22
- package/dist/arena/iterate/formats/index.js +0 -283
- package/dist/arena/iterate/index.d.ts +0 -11
- package/dist/arena/iterate/index.js +0 -9
- package/dist/arena/iterate/iterative-arena.d.ts +0 -23
- package/dist/arena/iterate/iterative-arena.js +0 -237
- package/dist/arena/iterate/parse.d.ts +0 -42
- package/dist/arena/iterate/parse.js +0 -123
- package/dist/arena/iterate/phases/argue.d.ts +0 -22
- package/dist/arena/iterate/phases/argue.js +0 -165
- package/dist/arena/iterate/phases/revise.d.ts +0 -16
- package/dist/arena/iterate/phases/revise.js +0 -62
- package/dist/arena/iterate/phases/tournament.d.ts +0 -34
- package/dist/arena/iterate/phases/tournament.js +0 -113
- package/dist/arena/iterate/tools/web-tools.d.ts +0 -13
- package/dist/arena/iterate/tools/web-tools.js +0 -54
- package/dist/arena/iterate/types.d.ts +0 -152
- package/dist/arena/iterate/types.js +0 -8
- package/dist/arena/ledger.d.ts +0 -47
- package/dist/arena/ledger.js +0 -159
- package/dist/arena/lenses/architecture.d.ts +0 -5
- package/dist/arena/lenses/architecture.js +0 -22
- package/dist/arena/lenses/engineering.d.ts +0 -5
- package/dist/arena/lenses/engineering.js +0 -22
- package/dist/arena/lenses/general.d.ts +0 -5
- package/dist/arena/lenses/general.js +0 -20
- package/dist/arena/lenses/index.d.ts +0 -16
- package/dist/arena/lenses/index.js +0 -47
- package/dist/arena/lenses/product.d.ts +0 -5
- package/dist/arena/lenses/product.js +0 -22
- package/dist/arena/model-presets.d.ts +0 -23
- package/dist/arena/model-presets.js +0 -44
- package/dist/arena/phases/adjudication.d.ts +0 -25
- package/dist/arena/phases/adjudication.js +0 -142
- package/dist/arena/phases/build-consensus.d.ts +0 -30
- package/dist/arena/phases/build-consensus.js +0 -85
- package/dist/arena/phases/claim-registry.d.ts +0 -26
- package/dist/arena/phases/claim-registry.js +0 -60
- package/dist/arena/phases/cross-review.d.ts +0 -47
- package/dist/arena/phases/cross-review.js +0 -224
- package/dist/arena/phases/debate-rounds.d.ts +0 -28
- package/dist/arena/phases/debate-rounds.js +0 -161
- package/dist/arena/phases/participant-research.d.ts +0 -39
- package/dist/arena/phases/participant-research.js +0 -316
- package/dist/arena/phases/planning-detail-expansion.d.ts +0 -39
- package/dist/arena/phases/planning-detail-expansion.js +0 -120
- package/dist/arena/planner.d.ts +0 -28
- package/dist/arena/planner.js +0 -312
- package/dist/arena/providers/docs.d.ts +0 -7
- package/dist/arena/providers/docs.js +0 -111
- package/dist/arena/providers/git.d.ts +0 -8
- package/dist/arena/providers/git.js +0 -174
- package/dist/arena/providers/index.d.ts +0 -32
- package/dist/arena/providers/index.js +0 -132
- package/dist/arena/providers/none.d.ts +0 -7
- package/dist/arena/providers/none.js +0 -11
- package/dist/arena/providers/repo.d.ts +0 -7
- package/dist/arena/providers/repo.js +0 -258
- package/dist/arena/render/session.d.ts +0 -17
- package/dist/arena/render/session.js +0 -190
- package/dist/arena/render/terminal.d.ts +0 -34
- package/dist/arena/render/terminal.js +0 -286
- package/dist/arena/strategies/discussion.d.ts +0 -25
- package/dist/arena/strategies/discussion.js +0 -143
- package/dist/arena/strategies/index.d.ts +0 -15
- package/dist/arena/strategies/index.js +0 -28
- package/dist/arena/strategies/language-wrapper.d.ts +0 -17
- package/dist/arena/strategies/language-wrapper.js +0 -102
- package/dist/arena/strategies/lens-wrapper.d.ts +0 -16
- package/dist/arena/strategies/lens-wrapper.js +0 -236
- package/dist/arena/strategies/planning.d.ts +0 -30
- package/dist/arena/strategies/planning.js +0 -225
- package/dist/arena/strategies/review.d.ts +0 -26
- package/dist/arena/strategies/review.js +0 -168
- package/dist/arena/strategies/utils.d.ts +0 -36
- package/dist/arena/strategies/utils.js +0 -603
- package/dist/arena/tools/selector.d.ts +0 -17
- package/dist/arena/tools/selector.js +0 -61
- package/dist/arena/transitions.d.ts +0 -53
- package/dist/arena/transitions.js +0 -97
- package/dist/arena/types.d.ts +0 -518
- package/dist/arena/types.js +0 -27
- package/dist/cc-orchestrator/agent-adapter.d.ts +0 -54
- package/dist/cc-orchestrator/agent-adapter.js +0 -143
- package/dist/cc-orchestrator/cc-capability.d.ts +0 -19
- package/dist/cc-orchestrator/cc-capability.js +0 -53
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +0 -24
- package/dist/cc-orchestrator/codex-session-discovery.js +0 -191
- package/dist/cc-orchestrator/codex-session-history.d.ts +0 -38
- package/dist/cc-orchestrator/codex-session-history.js +0 -247
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +0 -27
- package/dist/cc-orchestrator/external-agent-bindings.js +0 -150
- package/dist/cc-orchestrator/external-agent-changes.d.ts +0 -19
- package/dist/cc-orchestrator/external-agent-changes.js +0 -231
- package/dist/cc-orchestrator/external-agent-driver.d.ts +0 -19
- package/dist/cc-orchestrator/external-agent-driver.js +0 -284
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +0 -23
- package/dist/cc-orchestrator/external-agent-session-store.js +0 -146
- package/dist/cc-orchestrator/index.d.ts +0 -8
- package/dist/cc-orchestrator/index.js +0 -8
- package/dist/cc-orchestrator/relevance-judge.d.ts +0 -15
- package/dist/cc-orchestrator/relevance-judge.js +0 -29
- package/dist/cc-orchestrator/session-discovery.d.ts +0 -46
- package/dist/cc-orchestrator/session-discovery.js +0 -125
- package/dist/cc-orchestrator/session-history.d.ts +0 -54
- package/dist/cc-orchestrator/session-history.js +0 -150
- package/dist/cron/cron-runtime.d.ts +0 -2
- package/dist/cron/cron-runtime.js +0 -2
- package/dist/cron/cron-store.d.ts +0 -2
- package/dist/cron/cron-store.js +0 -2
- package/dist/cron/scheduler.d.ts +0 -7
- package/dist/cron/scheduler.js +0 -7
- package/dist/engine/goal.js +0 -206
- package/dist/external-agents/config.d.ts +0 -2
- package/dist/external-agents/config.js +0 -15
- package/dist/external-agents/types.d.ts +0 -31
- package/dist/git/parse-log.d.ts +0 -13
- package/dist/git/parse-log.js +0 -21
- package/dist/git/utils.d.ts +0 -49
- package/dist/git/utils.js +0 -161
- package/dist/git/worktree/crud.d.ts +0 -72
- package/dist/git/worktree/crud.js +0 -235
- package/dist/git/worktree/diff.d.ts +0 -14
- package/dist/git/worktree/diff.js +0 -82
- package/dist/git/worktree/git-exec.d.ts +0 -7
- package/dist/git/worktree/git-exec.js +0 -87
- package/dist/git/worktree/index.d.ts +0 -5
- package/dist/git/worktree/index.js +0 -5
- package/dist/git/worktree/query.d.ts +0 -43
- package/dist/git/worktree/query.js +0 -128
- package/dist/git/worktree/slug.d.ts +0 -11
- package/dist/git/worktree/slug.js +0 -58
- package/dist/git/worktree.d.ts +0 -1
- package/dist/git/worktree.js +0 -5
- package/dist/lsp/client.d.ts +0 -41
- package/dist/lsp/client.js +0 -186
- package/dist/lsp/manager.d.ts +0 -40
- package/dist/lsp/manager.js +0 -155
- package/dist/lsp/root-path.d.ts +0 -9
- package/dist/lsp/root-path.js +0 -12
- package/dist/lsp/servers.d.ts +0 -16
- package/dist/lsp/servers.js +0 -60
- package/dist/prompt/sections/coding.md +0 -35
- package/dist/quota/credentials.d.ts +0 -3
- package/dist/quota/credentials.js +0 -80
- package/dist/quota/index.d.ts +0 -36
- package/dist/quota/index.js +0 -155
- package/dist/quota/types.d.ts +0 -48
- package/dist/quota/types.js +0 -13
- package/dist/review/review-prompt.d.ts +0 -28
- package/dist/review/review-prompt.js +0 -81
- package/dist/state.d.ts +0 -160
- package/dist/state.js +0 -267
- package/dist/tool-system/builtin/apply-patch/applier.d.ts +0 -26
- package/dist/tool-system/builtin/apply-patch/applier.js +0 -308
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +0 -10
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +0 -30
- package/dist/tool-system/builtin/apply-patch/index.d.ts +0 -20
- package/dist/tool-system/builtin/apply-patch/index.js +0 -105
- package/dist/tool-system/builtin/apply-patch/parser.d.ts +0 -17
- package/dist/tool-system/builtin/apply-patch/parser.js +0 -212
- package/dist/tool-system/builtin/apply-patch/seek-sequence.d.ts +0 -18
- package/dist/tool-system/builtin/apply-patch/seek-sequence.js +0 -123
- package/dist/tool-system/builtin/apply-patch/types.d.ts +0 -49
- package/dist/tool-system/builtin/apply-patch/types.js +0 -13
- package/dist/tool-system/builtin/arena.d.ts +0 -31
- package/dist/tool-system/builtin/arena.js +0 -416
- package/dist/tool-system/builtin/brief.d.ts +0 -6
- package/dist/tool-system/builtin/brief.js +0 -41
- package/dist/tool-system/builtin/check-quota.d.ts +0 -15
- package/dist/tool-system/builtin/check-quota.js +0 -34
- package/dist/tool-system/builtin/drive-claude-code.d.ts +0 -51
- package/dist/tool-system/builtin/drive-claude-code.js +0 -698
- package/dist/tool-system/builtin/lsp.d.ts +0 -7
- package/dist/tool-system/builtin/lsp.js +0 -144
- package/dist/tool-system/builtin/notebook-edit.d.ts +0 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -127
- package/dist/tool-system/builtin/worktree.d.ts +0 -11
- package/dist/tool-system/builtin/worktree.js +0 -363
- /package/dist/{external-agents → links}/types.js +0 -0
- /package/dist/{engine/session-usage.d.ts → session/usage.d.ts} +0 -0
- /package/dist/{engine/session-usage.js → session/usage.js} +0 -0
|
@@ -4,21 +4,150 @@
|
|
|
4
4
|
import { closeSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { nanoid } from "nanoid";
|
|
8
9
|
import { Transcript } from "./transcript.js";
|
|
9
10
|
import { SessionError } from "../exceptions.js";
|
|
10
|
-
import { normalizeCumulativeUsageCounters } from "
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { addCumulativeUsage, addTokenUsage, normalizeCumulativeUsageCounters } from "./usage.js";
|
|
12
|
+
import { armGoalLifecycle, createGoalLifecycle, decodeGoalLifecycle, deriveLegacyGoalId, goalConfigFromLifecycle, isGoalLifecycleCurrent, isSameGoalVersion, mergeGoalTerminals, terminateGoalLifecycle, waitGoalLifecycle, } from "../goal/lifecycle.js";
|
|
13
|
+
import { lockSync } from "../utils/lockfile.js";
|
|
14
|
+
import { resolveCapabilities } from "../capabilities/index.js";
|
|
13
15
|
// Shared close epochs for SessionManager instances in this process. Concurrent
|
|
14
16
|
// Engines bind the same epoch; only close advances it. This intentionally does
|
|
15
17
|
// not claim cross-process/Worker protection.
|
|
16
18
|
const currentSessionCloseEpochs = new Map();
|
|
19
|
+
const SESSION_STATE_LOCK_STALE_MS = 10_000;
|
|
20
|
+
const SESSION_STATE_LOCK_RETRY_DELAYS_MS = [5, 10, 20, 40];
|
|
21
|
+
const syncSleepCell = new Int32Array(new SharedArrayBuffer(4));
|
|
22
|
+
function sleepSync(ms) {
|
|
23
|
+
Atomics.wait(syncSleepCell, 0, 0, ms);
|
|
24
|
+
}
|
|
25
|
+
function lifecycleTerminalReason(reason) {
|
|
26
|
+
return reason;
|
|
27
|
+
}
|
|
28
|
+
function newestLegacyTerminal(state) {
|
|
29
|
+
return mergeGoalTerminals(state.goalTerminals, state.goalTerminal).at(-1);
|
|
30
|
+
}
|
|
31
|
+
function legacyTerminalMatchesGoal(terminal, goal) {
|
|
32
|
+
if (terminal.goalId && goal.goalId) {
|
|
33
|
+
return (terminal.goalId === goal.goalId &&
|
|
34
|
+
Math.max(1, Math.floor(terminal.revision ?? 1)) ===
|
|
35
|
+
Math.max(1, Math.floor(goal.revision ?? 1)));
|
|
36
|
+
}
|
|
37
|
+
return terminal.objective === goal.objective && terminal.setAtMs === goal.setAtMs;
|
|
38
|
+
}
|
|
39
|
+
/** Decode the canonical union or derive it once from legacy aliases. */
|
|
40
|
+
function hydrateGoalLifecycle(state) {
|
|
41
|
+
const hasCanonical = Object.prototype.hasOwnProperty.call(state, "goalLifecycle");
|
|
42
|
+
const persistedLifecycle = state.goalLifecycle;
|
|
43
|
+
let lifecycle;
|
|
44
|
+
if (hasCanonical && persistedLifecycle !== undefined) {
|
|
45
|
+
lifecycle = decodeGoalLifecycle(persistedLifecycle);
|
|
46
|
+
if (!lifecycle) {
|
|
47
|
+
throw new SessionError(`Session goal lifecycle is unsupported or corrupt for ${state.sessionId}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (state.activeGoal) {
|
|
51
|
+
const active = {
|
|
52
|
+
...state.activeGoal,
|
|
53
|
+
goalId: state.activeGoal.goalId ?? deriveLegacyGoalId(state.sessionId, state.activeGoal),
|
|
54
|
+
revision: Math.max(1, Math.floor(state.activeGoal.revision ?? 1)),
|
|
55
|
+
};
|
|
56
|
+
const terminal = mergeGoalTerminals(state.goalTerminals, state.goalTerminal).find((candidate) => legacyTerminalMatchesGoal(candidate, active));
|
|
57
|
+
const base = createGoalLifecycle(active, active.paused === true ? "paused" : "active");
|
|
58
|
+
lifecycle = terminal
|
|
59
|
+
? terminateGoalLifecycle(base, lifecycleTerminalReason(terminal.reason), terminal.terminatedAtMs ?? Date.now())
|
|
60
|
+
: base;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const terminal = newestLegacyTerminal(state);
|
|
64
|
+
if (terminal) {
|
|
65
|
+
const goal = {
|
|
66
|
+
objective: terminal.objective,
|
|
67
|
+
goalId: terminal.goalId ??
|
|
68
|
+
deriveLegacyGoalId(state.sessionId, {
|
|
69
|
+
objective: terminal.objective,
|
|
70
|
+
setAtMs: terminal.setAtMs,
|
|
71
|
+
}),
|
|
72
|
+
revision: Math.max(1, Math.floor(terminal.revision ?? 1)),
|
|
73
|
+
...(terminal.setAtMs !== undefined ? { setAtMs: terminal.setAtMs } : {}),
|
|
74
|
+
};
|
|
75
|
+
lifecycle = terminateGoalLifecycle(createGoalLifecycle(goal, "active", terminal.terminatedAtMs ?? Date.now()), lifecycleTerminalReason(terminal.reason), terminal.terminatedAtMs ?? Date.now());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (lifecycle)
|
|
79
|
+
state.goalLifecycle = lifecycle;
|
|
80
|
+
else
|
|
81
|
+
delete state.goalLifecycle;
|
|
82
|
+
// Legacy aliases are migration inputs only. Never reconstruct them in RAM:
|
|
83
|
+
// all runtime readers and writers consume the canonical lifecycle union.
|
|
84
|
+
delete state.activeGoal;
|
|
85
|
+
delete state.goalTerminal;
|
|
86
|
+
delete state.goalTerminals;
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
/** New files and runtime state contain only the canonical union. */
|
|
90
|
+
function stateForPersistence(state) {
|
|
91
|
+
const persisted = { ...state };
|
|
92
|
+
delete persisted.activeGoal;
|
|
93
|
+
delete persisted.goalTerminal;
|
|
94
|
+
delete persisted.goalTerminals;
|
|
95
|
+
return persisted;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Temporary adapter for tests/older in-repo callers that still mutate the
|
|
99
|
+
* hydrated aliases before calling whole-state saveState. Production Goal paths
|
|
100
|
+
* use the domain methods below. The adapter never writes aliases to disk.
|
|
101
|
+
*/
|
|
102
|
+
function adoptCompatibilityGoalMutation(state) {
|
|
103
|
+
const activeTouched = Object.prototype.hasOwnProperty.call(state, "activeGoal");
|
|
104
|
+
const terminalTouched = Object.prototype.hasOwnProperty.call(state, "goalTerminal") ||
|
|
105
|
+
Object.prototype.hasOwnProperty.call(state, "goalTerminals");
|
|
106
|
+
if (!activeTouched && !terminalTouched)
|
|
107
|
+
return;
|
|
108
|
+
const lifecycle = decodeGoalLifecycle(state.goalLifecycle);
|
|
109
|
+
const alias = state.activeGoal
|
|
110
|
+
? {
|
|
111
|
+
...state.activeGoal,
|
|
112
|
+
goalId: state.activeGoal.goalId ?? deriveLegacyGoalId(state.sessionId, state.activeGoal),
|
|
113
|
+
revision: Math.max(1, Math.floor(state.activeGoal.revision ?? 1)),
|
|
114
|
+
}
|
|
115
|
+
: undefined;
|
|
116
|
+
const terminals = mergeGoalTerminals(state.goalTerminals, state.goalTerminal);
|
|
117
|
+
if (!lifecycle) {
|
|
118
|
+
if (alias) {
|
|
119
|
+
state.goalLifecycle = createGoalLifecycle(alias, alias.paused === true ? "paused" : "active");
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (alias) {
|
|
124
|
+
const sameIdentity = lifecycle.goalId === alias.goalId;
|
|
125
|
+
const sameRevision = lifecycle.revision === alias.revision;
|
|
126
|
+
const matchingTerminal = terminals.find((terminal) => legacyTerminalMatchesGoal(terminal, alias));
|
|
127
|
+
if (matchingTerminal) {
|
|
128
|
+
const base = createGoalLifecycle(alias, alias.paused === true ? "paused" : "active");
|
|
129
|
+
state.goalLifecycle = terminateGoalLifecycle(base, lifecycleTerminalReason(matchingTerminal.reason), matchingTerminal.terminatedAtMs ?? Date.now());
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (lifecycle.phase === "terminal" && sameIdentity && sameRevision)
|
|
133
|
+
return;
|
|
134
|
+
const canonicalGoal = goalConfigFromLifecycle(lifecycle);
|
|
135
|
+
if (!sameIdentity || !sameRevision || !isSameGoalVersion(alias, canonicalGoal)) {
|
|
136
|
+
state.goalLifecycle = createGoalLifecycle(alias, alias.paused === true ? "paused" : "active");
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const matchingTerminal = terminals.find((terminal) => legacyTerminalMatchesGoal(terminal, goalConfigFromLifecycle(lifecycle)));
|
|
141
|
+
if (matchingTerminal && lifecycle.phase !== "terminal") {
|
|
142
|
+
state.goalLifecycle = terminateGoalLifecycle(lifecycle, lifecycleTerminalReason(matchingTerminal.reason), matchingTerminal.terminatedAtMs ?? Date.now());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
17
145
|
const FORK_COPY_EVENT_TYPES = new Set([
|
|
18
146
|
"message",
|
|
19
147
|
"tool_use",
|
|
20
148
|
"tool_result",
|
|
21
149
|
"summary",
|
|
150
|
+
"context_transfer",
|
|
22
151
|
"content_replace",
|
|
23
152
|
"subagent",
|
|
24
153
|
"external_file_changes",
|
|
@@ -72,6 +201,18 @@ export function assertSafeSessionId(sessionId) {
|
|
|
72
201
|
throw new SessionError(`invalid session id: too long (max 128 chars)`);
|
|
73
202
|
}
|
|
74
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Runtime source of truth for sessions whose state must not outlive their UI
|
|
206
|
+
* lifecycle. The explicit marker covers modern side forks; the qchat namespace
|
|
207
|
+
* keeps historical/blank quick chats fail-closed if their older state file did
|
|
208
|
+
* not contain the marker yet.
|
|
209
|
+
*/
|
|
210
|
+
export function isEphemeralSessionState(state) {
|
|
211
|
+
return state.ephemeral === true || state.sessionId.startsWith("qchat-");
|
|
212
|
+
}
|
|
213
|
+
function normalizedSessionKind(kind) {
|
|
214
|
+
return kind === "pet" ? "pet" : "work";
|
|
215
|
+
}
|
|
75
216
|
/**
|
|
76
217
|
* Resolve the `.code-shell` home dir. `CODE_SHELL_HOME` overrides the default
|
|
77
218
|
* `~/.code-shell` — mirrors Codex's `CODEX_HOME`. Tests set it to a temp dir
|
|
@@ -82,6 +223,14 @@ export function assertSafeSessionId(sessionId) {
|
|
|
82
223
|
export function codeShellHome() {
|
|
83
224
|
return process.env.CODE_SHELL_HOME || join(homedir(), ".code-shell");
|
|
84
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Canonical root for every persisted CodeShell session. An explicit `home`
|
|
228
|
+
* (a `~/.code-shell`-equivalent data root) overrides the default resolution;
|
|
229
|
+
* absent → `codeShellHome()` exactly as before.
|
|
230
|
+
*/
|
|
231
|
+
export function sessionsRoot(home) {
|
|
232
|
+
return join(home ?? codeShellHome(), "sessions");
|
|
233
|
+
}
|
|
85
234
|
function isSessionWorkspace(value) {
|
|
86
235
|
if (!value || typeof value !== "object")
|
|
87
236
|
return false;
|
|
@@ -102,7 +251,16 @@ function isSessionWorkspace(value) {
|
|
|
102
251
|
wt.baseRef.length > 0 &&
|
|
103
252
|
wt.createdBy === "codeshell");
|
|
104
253
|
}
|
|
105
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Read the session's persisted main-project root from the legacy `state.cwd`
|
|
256
|
+
* field. The field name is part of the state.json compatibility contract and
|
|
257
|
+
* must not be rewritten to `projectRoot`; `state.workspace.root` separately
|
|
258
|
+
* identifies the current main/worktree execution root used on resume.
|
|
259
|
+
*/
|
|
260
|
+
export function sessionMainRoot(state) {
|
|
261
|
+
return typeof state.cwd === "string" && state.cwd.length > 0 ? state.cwd : undefined;
|
|
262
|
+
}
|
|
263
|
+
async function validateResumeWorktreeRoot(root, workspaceCapability) {
|
|
106
264
|
if (!existsSync(root))
|
|
107
265
|
return "no longer exists";
|
|
108
266
|
let stat;
|
|
@@ -116,15 +274,21 @@ async function validateResumeWorktreeRoot(root) {
|
|
|
116
274
|
return "is not a valid git worktree (symbolic link)";
|
|
117
275
|
if (!stat.isDirectory())
|
|
118
276
|
return "is not a valid git worktree (not a directory)";
|
|
119
|
-
if (!(await
|
|
120
|
-
return "is not a valid
|
|
277
|
+
if (!(await workspaceCapability.validateRoot(root)))
|
|
278
|
+
return "is not a valid workspace root";
|
|
121
279
|
return null;
|
|
122
280
|
}
|
|
123
281
|
export class SessionManager {
|
|
124
282
|
sessionsDir;
|
|
125
283
|
registeredCloseEpochs = new Map();
|
|
126
|
-
|
|
127
|
-
|
|
284
|
+
workspaceCapability;
|
|
285
|
+
constructor(storageDir, workspaceCapability) {
|
|
286
|
+
this.sessionsDir = storageDir ?? sessionsRoot();
|
|
287
|
+
this.workspaceCapability =
|
|
288
|
+
workspaceCapability ??
|
|
289
|
+
resolveCapabilities()
|
|
290
|
+
.map((capability) => capability.sessionWorkspace)
|
|
291
|
+
.find((candidate) => candidate !== undefined);
|
|
128
292
|
mkdirSync(this.sessionsDir, { recursive: true });
|
|
129
293
|
this.cleanupStaleForkStaging();
|
|
130
294
|
}
|
|
@@ -178,7 +342,7 @@ export class SessionManager {
|
|
|
178
342
|
* nanoid. Either way the on-disk directory is materialized and the
|
|
179
343
|
* state.json + transcript.jsonl files are written before return.
|
|
180
344
|
*/
|
|
181
|
-
create(cwd, model, provider, explicitSessionId, parentSessionId, origin) {
|
|
345
|
+
create(cwd, model, provider, explicitSessionId, parentSessionId, origin, kind = "work") {
|
|
182
346
|
// External callers may pass any string; nanoid output is trusted. Either
|
|
183
347
|
// way the ID gets joined into a filesystem path, so the public entry
|
|
184
348
|
// point validates before that join.
|
|
@@ -197,6 +361,8 @@ export class SessionManager {
|
|
|
197
361
|
}
|
|
198
362
|
const state = {
|
|
199
363
|
sessionId,
|
|
364
|
+
kind,
|
|
365
|
+
stateRevision: 0,
|
|
200
366
|
cwd,
|
|
201
367
|
workspace: { root: cwd, kind: "main" },
|
|
202
368
|
startedAt: Date.now(),
|
|
@@ -214,6 +380,7 @@ export class SessionManager {
|
|
|
214
380
|
// new top-level session (key present, null) apart from a legacy session
|
|
215
381
|
// (key absent) and from a sub-agent (key present, non-empty string).
|
|
216
382
|
parentSessionId: parentSessionId ?? null,
|
|
383
|
+
...(sessionId.startsWith("qchat-") ? { ephemeral: true } : {}),
|
|
217
384
|
...(origin ? { origin } : {}),
|
|
218
385
|
};
|
|
219
386
|
// Atomic write (tmp+rename) like saveState, so a crash during this one-time
|
|
@@ -229,6 +396,7 @@ export class SessionManager {
|
|
|
229
396
|
model,
|
|
230
397
|
provider,
|
|
231
398
|
startedAt: state.startedAt,
|
|
399
|
+
kind,
|
|
232
400
|
});
|
|
233
401
|
return { state, transcript };
|
|
234
402
|
}
|
|
@@ -250,16 +418,124 @@ export class SessionManager {
|
|
|
250
418
|
return existsSync(join(this.sessionsDir, sessionId));
|
|
251
419
|
}
|
|
252
420
|
/**
|
|
253
|
-
* Cheap
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
421
|
+
* Cheap persisted-main-root probe — reads only state.json, NOT the transcript
|
|
422
|
+
* (unlike resume()). This deliberately reads legacy `state.cwd`, not the
|
|
423
|
+
* current `state.workspace.root`; callers use it as the stable main-project
|
|
424
|
+
* fallback when a worktree is unavailable or is being released.
|
|
425
|
+
*/
|
|
426
|
+
readSessionMainRoot(sessionId) {
|
|
427
|
+
try {
|
|
428
|
+
assertSafeSessionId(sessionId);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
434
|
+
if (!existsSync(stateFile))
|
|
435
|
+
return undefined;
|
|
436
|
+
try {
|
|
437
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
438
|
+
return sessionMainRoot(state);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** Cheap durable classification read. Legacy sessions are ordinary work sessions. */
|
|
445
|
+
readSessionKind(sessionId) {
|
|
446
|
+
try {
|
|
447
|
+
assertSafeSessionId(sessionId);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
453
|
+
if (!existsSync(stateFile))
|
|
454
|
+
return undefined;
|
|
455
|
+
try {
|
|
456
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
457
|
+
return normalizedSessionKind(state.kind);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** Cheap durable digital-human binding read. */
|
|
464
|
+
readSessionWorkspaceProfile(sessionId) {
|
|
465
|
+
try {
|
|
466
|
+
assertSafeSessionId(sessionId);
|
|
467
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
468
|
+
if (!existsSync(stateFile))
|
|
469
|
+
return undefined;
|
|
470
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
471
|
+
return typeof state.workspaceProfile === "string" && state.workspaceProfile
|
|
472
|
+
? state.workspaceProfile
|
|
473
|
+
: undefined;
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Find durable Sessions currently bound to one digital-human profile without loading
|
|
481
|
+
* transcripts. Used by profile deletion guards so a reusable/historical
|
|
482
|
+
* Session cannot be left permanently unresumable.
|
|
483
|
+
*/
|
|
484
|
+
/**
|
|
485
|
+
* Sessions whose state pins this digital human.
|
|
486
|
+
*
|
|
487
|
+
* `includeArchived: false` drops archived Sessions: they are closed history,
|
|
488
|
+
* not a live binding, so they should not block deleting the profile. A
|
|
489
|
+
* dangling name is already handled downstream — resolveActiveWorkspaceProfile
|
|
490
|
+
* logs `profile.active_missing_from_library` and returns undefined.
|
|
261
491
|
*/
|
|
492
|
+
findSessionIdsByWorkspaceProfile(profileName, limit = 20, options = {}) {
|
|
493
|
+
const includeArchived = options.includeArchived !== false;
|
|
494
|
+
const target = typeof profileName === "string" ? profileName.trim() : "";
|
|
495
|
+
if (!target || target.length > 64 || limit <= 0)
|
|
496
|
+
return [];
|
|
497
|
+
const cap = Math.max(1, Math.min(100, Math.floor(limit)));
|
|
498
|
+
let entries;
|
|
499
|
+
try {
|
|
500
|
+
entries = readdirSync(this.sessionsDir, { withFileTypes: true });
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
const matches = [];
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
if (matches.length >= cap)
|
|
508
|
+
break;
|
|
509
|
+
if (!entry.isDirectory() ||
|
|
510
|
+
entry.name.startsWith(".pending-") ||
|
|
511
|
+
entry.name.startsWith("qchat-")) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
assertSafeSessionId(entry.name);
|
|
516
|
+
const stateFile = join(this.sessionsDir, entry.name, "state.json");
|
|
517
|
+
const info = lstatSync(stateFile);
|
|
518
|
+
if (info.isSymbolicLink() || !info.isFile() || info.size > 1024 * 1024)
|
|
519
|
+
continue;
|
|
520
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
521
|
+
if (state.workspaceProfile !== target)
|
|
522
|
+
continue;
|
|
523
|
+
if (!includeArchived && typeof state.archivedAt === "number")
|
|
524
|
+
continue;
|
|
525
|
+
matches.push(entry.name);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// Corrupt or unsafe Sessions are isolated from the deletion decision.
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return matches.sort();
|
|
532
|
+
}
|
|
533
|
+
/** @deprecated Use readSessionMainRoot; retained for public API compatibility. */
|
|
262
534
|
readCwd(sessionId) {
|
|
535
|
+
return this.readSessionMainRoot(sessionId);
|
|
536
|
+
}
|
|
537
|
+
/** Disk-only direct-parent ACL metadata. Undefined means unprovable/corrupt. */
|
|
538
|
+
readParentSessionId(sessionId) {
|
|
263
539
|
try {
|
|
264
540
|
assertSafeSessionId(sessionId);
|
|
265
541
|
}
|
|
@@ -271,7 +547,9 @@ export class SessionManager {
|
|
|
271
547
|
return undefined;
|
|
272
548
|
try {
|
|
273
549
|
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
274
|
-
return
|
|
550
|
+
return state.parentSessionId === null || typeof state.parentSessionId === "string"
|
|
551
|
+
? state.parentSessionId
|
|
552
|
+
: undefined;
|
|
275
553
|
}
|
|
276
554
|
catch {
|
|
277
555
|
return undefined;
|
|
@@ -296,9 +574,8 @@ export class SessionManager {
|
|
|
296
574
|
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
297
575
|
if (isSessionWorkspace(state.workspace))
|
|
298
576
|
return state.workspace;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
: undefined;
|
|
577
|
+
const mainRoot = sessionMainRoot(state);
|
|
578
|
+
return mainRoot ? { root: mainRoot, kind: "main" } : undefined;
|
|
302
579
|
}
|
|
303
580
|
catch {
|
|
304
581
|
return undefined;
|
|
@@ -314,19 +591,30 @@ export class SessionManager {
|
|
|
314
591
|
if (!isSessionWorkspace(workspace)) {
|
|
315
592
|
throw new SessionError(`invalid workspace for session ${sessionId}`);
|
|
316
593
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
594
|
+
return this.updateSessionState(sessionId, { workspace });
|
|
595
|
+
}
|
|
596
|
+
/** Read the durable archival timestamp; undefined = not archived / unprovable. */
|
|
597
|
+
readSessionArchivedAt(sessionId) {
|
|
598
|
+
try {
|
|
599
|
+
assertSafeSessionId(sessionId);
|
|
320
600
|
}
|
|
321
|
-
|
|
601
|
+
catch {
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
605
|
+
if (!existsSync(stateFile))
|
|
606
|
+
return undefined;
|
|
322
607
|
try {
|
|
323
|
-
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
608
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
609
|
+
return typeof state.archivedAt === "number" ? state.archivedAt : undefined;
|
|
324
610
|
}
|
|
325
|
-
catch
|
|
326
|
-
|
|
611
|
+
catch {
|
|
612
|
+
return undefined;
|
|
327
613
|
}
|
|
328
|
-
|
|
329
|
-
|
|
614
|
+
}
|
|
615
|
+
/** Set (number) or clear (undefined) the durable archival marker. */
|
|
616
|
+
setSessionArchived(sessionId, archivedAt) {
|
|
617
|
+
return this.updateSessionState(sessionId, { archivedAt });
|
|
330
618
|
}
|
|
331
619
|
recordWorkspaceHandoff(sessionId, from, to) {
|
|
332
620
|
assertSafeSessionId(sessionId);
|
|
@@ -368,9 +656,8 @@ export class SessionManager {
|
|
|
368
656
|
catch (err) {
|
|
369
657
|
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
370
658
|
}
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
: undefined;
|
|
659
|
+
const mainRoot = sessionMainRoot(state);
|
|
660
|
+
const legacyMain = mainRoot ? { root: mainRoot, kind: "main" } : undefined;
|
|
374
661
|
const workspace = isSessionWorkspace(state.workspace) ? state.workspace : legacyMain;
|
|
375
662
|
if (!workspace) {
|
|
376
663
|
throw new SessionError(`Session ${sessionId} has no recoverable cwd`);
|
|
@@ -383,16 +670,28 @@ export class SessionManager {
|
|
|
383
670
|
reason: isSessionWorkspace(state.workspace) ? "main" : "legacy",
|
|
384
671
|
};
|
|
385
672
|
}
|
|
386
|
-
|
|
673
|
+
if (!this.workspaceCapability) {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
cwd: mainRoot ?? workspace.root,
|
|
677
|
+
workspace,
|
|
678
|
+
reason: "workspace_capability_unavailable",
|
|
679
|
+
message: `Session ${sessionId} uses a product workspace, but this host did not install ` +
|
|
680
|
+
`the matching workspace capability. Install it before resuming this session.`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const invalidWorktreeReason = await validateResumeWorktreeRoot(workspace.root, this.workspaceCapability);
|
|
387
684
|
if (!invalidWorktreeReason) {
|
|
388
685
|
return { ok: true, cwd: workspace.root, workspace, reason: "worktree" };
|
|
389
686
|
}
|
|
390
687
|
const branch = workspace.worktree?.branch;
|
|
391
|
-
const
|
|
392
|
-
if (branch &&
|
|
688
|
+
const fallbackMainRoot = mainRoot ?? workspace.root;
|
|
689
|
+
if (branch &&
|
|
690
|
+
existsSync(fallbackMainRoot) &&
|
|
691
|
+
(await this.workspaceCapability.branchExists(fallbackMainRoot, branch))) {
|
|
393
692
|
return {
|
|
394
693
|
ok: false,
|
|
395
|
-
cwd:
|
|
694
|
+
cwd: fallbackMainRoot,
|
|
396
695
|
workspace,
|
|
397
696
|
reason: "worktree_missing_branch_exists",
|
|
398
697
|
message: `Session ${sessionId} is bound to worktree ${workspace.root}, but that directory ` +
|
|
@@ -401,23 +700,23 @@ export class SessionManager {
|
|
|
401
700
|
`back to main explicitly.`,
|
|
402
701
|
};
|
|
403
702
|
}
|
|
404
|
-
const fallback = { root:
|
|
703
|
+
const fallback = { root: fallbackMainRoot, kind: "main" };
|
|
405
704
|
state.workspace = fallback;
|
|
406
|
-
this.
|
|
705
|
+
state.stateRevision = this.updateSessionState(sessionId, { workspace: fallback });
|
|
407
706
|
return {
|
|
408
707
|
ok: true,
|
|
409
|
-
cwd:
|
|
708
|
+
cwd: fallbackMainRoot,
|
|
410
709
|
workspace: fallback,
|
|
411
710
|
reason: "worktree_missing_branch_gone",
|
|
412
711
|
message: `Session ${sessionId} was bound to worktree ${workspace.root}, but that directory ` +
|
|
413
712
|
`${invalidWorktreeReason}${branch ? ` and branch ${branch} is gone` : ""}; fell back to main ` +
|
|
414
|
-
`${
|
|
713
|
+
`${fallbackMainRoot}. Re-run the request if you want to continue there.`,
|
|
415
714
|
};
|
|
416
715
|
}
|
|
417
716
|
/**
|
|
418
717
|
* Cheap "does this session have a persisted goal?" probe — reads only
|
|
419
|
-
* state.json, NOT the transcript (like
|
|
420
|
-
*
|
|
718
|
+
* state.json, NOT the transcript (like readSessionMainRoot). A persistent goal lives in
|
|
719
|
+
* the canonical goalLifecycle union; it is never appended to the transcript as an event, so
|
|
421
720
|
* a session rebuilt from disk (e.g. localStorage wiped) can't recover the
|
|
422
721
|
* goal from its messages. The desktop host calls this on session load to
|
|
423
722
|
* re-surface the active-goal block + its Cancel button, which would otherwise
|
|
@@ -436,10 +735,18 @@ export class SessionManager {
|
|
|
436
735
|
if (!existsSync(stateFile))
|
|
437
736
|
return undefined;
|
|
438
737
|
try {
|
|
439
|
-
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
738
|
+
const state = hydrateGoalLifecycle(JSON.parse(readFileSync(stateFile, "utf-8")));
|
|
739
|
+
const lifecycle = state.goalLifecycle;
|
|
740
|
+
if (!lifecycle || lifecycle.phase === "terminal")
|
|
741
|
+
return undefined;
|
|
742
|
+
const goal = goalConfigFromLifecycle(lifecycle);
|
|
743
|
+
// Expose a concrete version even for legacy state.json so UI delete/edit
|
|
744
|
+
// operations can send an expected fence instead of acting unconditionally.
|
|
745
|
+
return {
|
|
746
|
+
...goal,
|
|
747
|
+
goalId: goal.goalId ?? deriveLegacyGoalId(sessionId, goal),
|
|
748
|
+
revision: goal.revision ?? 1,
|
|
749
|
+
};
|
|
443
750
|
}
|
|
444
751
|
catch {
|
|
445
752
|
return undefined;
|
|
@@ -447,8 +754,9 @@ export class SessionManager {
|
|
|
447
754
|
}
|
|
448
755
|
/**
|
|
449
756
|
* Disk-only "wipe this session's persistent goal" — the counterpart to
|
|
450
|
-
* readActiveGoal. Reads state.json,
|
|
451
|
-
*
|
|
757
|
+
* readActiveGoal. Reads state.json, transitions the matching goalLifecycle
|
|
758
|
+
* to terminal(user_cleared), and rewrites it through the field-level CAS.
|
|
759
|
+
* Returns true only if a goal was actually
|
|
452
760
|
* present (idempotent — clearing a session with no goal is a no-op returning
|
|
453
761
|
* false). Never throws on an unknown / malformed / traversal-shaped id
|
|
454
762
|
* (returns false), matching readActiveGoal's tolerance.
|
|
@@ -461,28 +769,107 @@ export class SessionManager {
|
|
|
461
769
|
* Engine.clearGoal also delegates its disk write here so the wipe logic lives
|
|
462
770
|
* in exactly one place.
|
|
463
771
|
*/
|
|
464
|
-
clearActiveGoal(sessionId) {
|
|
772
|
+
clearActiveGoal(sessionId, expected) {
|
|
465
773
|
try {
|
|
466
774
|
assertSafeSessionId(sessionId);
|
|
467
775
|
}
|
|
468
776
|
catch {
|
|
469
777
|
return false;
|
|
470
778
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
779
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
780
|
+
let state;
|
|
781
|
+
try {
|
|
782
|
+
state = this.readPersistedState(sessionId);
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
const lifecycle = state.goalLifecycle;
|
|
788
|
+
if (!lifecycle || !isGoalLifecycleCurrent(lifecycle))
|
|
789
|
+
return false;
|
|
790
|
+
const clearedGoal = goalConfigFromLifecycle(lifecycle);
|
|
791
|
+
const revision = lifecycle.revision;
|
|
792
|
+
if (expected?.goalId !== undefined && clearedGoal.goalId !== expected.goalId)
|
|
793
|
+
return false;
|
|
794
|
+
if (expected?.revision !== undefined && revision !== expected.revision)
|
|
795
|
+
return false;
|
|
796
|
+
const terminal = terminateGoalLifecycle(lifecycle, "user_cleared");
|
|
797
|
+
if (!terminal)
|
|
798
|
+
return false;
|
|
799
|
+
state.goalLifecycle = terminal;
|
|
800
|
+
hydrateGoalLifecycle(state);
|
|
801
|
+
const result = this.saveStateAttempt(state);
|
|
802
|
+
if (result.ok)
|
|
803
|
+
return true;
|
|
804
|
+
if (result.reason !== "revision_conflict")
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Edit or pause/resume the currently active goal with an identity-checked,
|
|
811
|
+
* field-level CAS. The goalId is preserved: editing the objective changes
|
|
812
|
+
* the same user-owned goal rather than terminally cancelling it and arming a
|
|
813
|
+
* replacement. A changed objective receives a fresh deadline anchor.
|
|
814
|
+
*
|
|
815
|
+
* Returns undefined when the session/goal is absent or the patch is invalid.
|
|
816
|
+
* The returned state lets a live Engine rebase its in-memory bundle after
|
|
817
|
+
* this control operation.
|
|
818
|
+
*/
|
|
819
|
+
updateActiveGoal(sessionId, patch) {
|
|
475
820
|
try {
|
|
476
|
-
|
|
821
|
+
assertSafeSessionId(sessionId);
|
|
477
822
|
}
|
|
478
823
|
catch {
|
|
479
|
-
return
|
|
824
|
+
return undefined;
|
|
480
825
|
}
|
|
481
|
-
if (
|
|
482
|
-
return
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
826
|
+
if (patch.objective === undefined && patch.paused === undefined)
|
|
827
|
+
return undefined;
|
|
828
|
+
if (patch.objective !== undefined && !patch.objective.trim())
|
|
829
|
+
return undefined;
|
|
830
|
+
if (patch.paused !== undefined && typeof patch.paused !== "boolean")
|
|
831
|
+
return undefined;
|
|
832
|
+
let expectedGoalId = patch.expectedGoalId;
|
|
833
|
+
let expectedRevision = patch.expectedRevision;
|
|
834
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
835
|
+
let state;
|
|
836
|
+
try {
|
|
837
|
+
state = this.readPersistedState(sessionId);
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const lifecycle = state.goalLifecycle;
|
|
843
|
+
if (!lifecycle || !isGoalLifecycleCurrent(lifecycle))
|
|
844
|
+
return undefined;
|
|
845
|
+
const current = goalConfigFromLifecycle(lifecycle);
|
|
846
|
+
const currentRevision = lifecycle.revision;
|
|
847
|
+
expectedGoalId ??= current.goalId;
|
|
848
|
+
expectedRevision ??= currentRevision;
|
|
849
|
+
if (current.goalId !== expectedGoalId || currentRevision !== expectedRevision) {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
const objective = patch.objective?.trim() ?? current.objective;
|
|
853
|
+
const objectiveChanged = objective !== current.objective;
|
|
854
|
+
const goal = {
|
|
855
|
+
...current,
|
|
856
|
+
objective,
|
|
857
|
+
revision: currentRevision + 1,
|
|
858
|
+
...(objectiveChanged ? { setAtMs: Date.now() } : {}),
|
|
859
|
+
};
|
|
860
|
+
if (patch.paused === true)
|
|
861
|
+
goal.paused = true;
|
|
862
|
+
else if (patch.paused === false)
|
|
863
|
+
delete goal.paused;
|
|
864
|
+
state.goalLifecycle = createGoalLifecycle(goal, goal.paused === true ? "paused" : "active");
|
|
865
|
+
hydrateGoalLifecycle(state);
|
|
866
|
+
const result = this.saveStateAttempt(state);
|
|
867
|
+
if (result.ok)
|
|
868
|
+
return { goal, stateRevision: state.stateRevision, state };
|
|
869
|
+
if (result.reason !== "revision_conflict")
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
return undefined;
|
|
486
873
|
}
|
|
487
874
|
resume(sessionId) {
|
|
488
875
|
assertSafeSessionId(sessionId);
|
|
@@ -496,7 +883,7 @@ export class SessionManager {
|
|
|
496
883
|
}
|
|
497
884
|
let state;
|
|
498
885
|
try {
|
|
499
|
-
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
886
|
+
state = hydrateGoalLifecycle(JSON.parse(readFileSync(stateFile, "utf-8")));
|
|
500
887
|
}
|
|
501
888
|
catch (err) {
|
|
502
889
|
// A corrupt state.json (external tampering, disk corruption, or a crash
|
|
@@ -506,89 +893,402 @@ export class SessionManager {
|
|
|
506
893
|
}
|
|
507
894
|
const transcriptFile = join(sessionDir, "transcript.jsonl");
|
|
508
895
|
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
896
|
+
state.kind = normalizedSessionKind(state.kind);
|
|
509
897
|
state.status = "active";
|
|
898
|
+
delete state.lastCompletionKind;
|
|
510
899
|
Object.assign(state, normalizeCumulativeUsageCounters(state, state.tokenUsage));
|
|
511
900
|
return { state, transcript };
|
|
512
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* Return the exact persistence directory owned by this manager.
|
|
904
|
+
*
|
|
905
|
+
* Cross-session sidecars (for example versioned handoffs) use this to stay
|
|
906
|
+
* inside an identity/custom-data-root boundary instead of falling back to
|
|
907
|
+
* the process-global default sessions directory.
|
|
908
|
+
*/
|
|
909
|
+
getStorageDir() {
|
|
910
|
+
return this.sessionsDir;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Read one durable session state without changing its lifecycle status.
|
|
914
|
+
* `resume()` intentionally marks the returned in-memory state active; host
|
|
915
|
+
* coordination code needs a read-only snapshot for completed cursors and
|
|
916
|
+
* workspace/profile metadata.
|
|
917
|
+
*/
|
|
918
|
+
readSessionState(sessionId) {
|
|
919
|
+
try {
|
|
920
|
+
assertSafeSessionId(sessionId);
|
|
921
|
+
return structuredClone(this.readPersistedState(sessionId));
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
return undefined;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
513
927
|
/**
|
|
514
928
|
* Merge a field-level state update into the latest persisted snapshot.
|
|
515
929
|
*
|
|
516
930
|
* Unlike saveState(), callers do not supply a potentially stale whole-state
|
|
517
|
-
* object.
|
|
518
|
-
*
|
|
519
|
-
* This is deliberately not a session-level lock. Engine.run rejects re-entry
|
|
520
|
-
* on one Engine (G6), and the generation check in saveState rejects an older
|
|
521
|
-
* registered Engine in this process. Workers and separate processes remain
|
|
522
|
-
* an unresolved finding requiring locking/CAS.
|
|
931
|
+
* object. saveState serializes the read/revision-check/write section with the
|
|
932
|
+
* per-session lock and returns the new persisted revision to the caller.
|
|
523
933
|
*/
|
|
524
934
|
updateSessionState(sessionId, partial) {
|
|
525
935
|
assertSafeSessionId(sessionId);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
936
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
937
|
+
const state = this.readPersistedState(sessionId);
|
|
938
|
+
if (partial.kind !== undefined &&
|
|
939
|
+
normalizedSessionKind(partial.kind) !== normalizedSessionKind(state.kind)) {
|
|
940
|
+
throw new SessionError(`Session kind is immutable for ${sessionId}`);
|
|
941
|
+
}
|
|
942
|
+
Object.assign(state, partial);
|
|
943
|
+
state.sessionId = sessionId;
|
|
944
|
+
const result = this.saveStateAttempt(state);
|
|
945
|
+
if (result.ok)
|
|
946
|
+
return state.stateRevision;
|
|
947
|
+
if (result.reason === "generation_conflict") {
|
|
948
|
+
throw new SessionError(`Session generation conflict for ${sessionId}`);
|
|
949
|
+
}
|
|
950
|
+
if (result.reason === "lock_conflict") {
|
|
951
|
+
throw new SessionError(`Session state lock contention for ${sessionId}`);
|
|
952
|
+
}
|
|
953
|
+
if (result.reason === "kind_conflict") {
|
|
954
|
+
throw new SessionError(`Session kind is immutable for ${sessionId}`);
|
|
955
|
+
}
|
|
956
|
+
// A field-level update is explicitly authorized to merge into the newest
|
|
957
|
+
// snapshot. Re-read it on the next iteration instead of treating its CAS
|
|
958
|
+
// miss like a lock collision.
|
|
529
959
|
}
|
|
530
|
-
|
|
960
|
+
throw new SessionError(`Session state revision conflict for ${sessionId}`);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Atomically add one billed auxiliary request to the latest source state.
|
|
964
|
+
* This field-level read/add/write avoids publishing a detached stale state
|
|
965
|
+
* snapshot and accounts prompt, completion, total, cache, and cost together.
|
|
966
|
+
*/
|
|
967
|
+
recordAuxiliaryUsage(sessionId, usage, costState) {
|
|
968
|
+
assertSafeSessionId(sessionId);
|
|
969
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
970
|
+
const state = this.readPersistedState(sessionId);
|
|
971
|
+
Object.assign(state, normalizeCumulativeUsageCounters(state, state.tokenUsage));
|
|
972
|
+
Object.assign(state, addCumulativeUsage(state, usage));
|
|
973
|
+
state.tokenUsage = addTokenUsage(state.tokenUsage, usage);
|
|
974
|
+
if (costState !== undefined)
|
|
975
|
+
state.costState = costState;
|
|
976
|
+
const result = this.saveStateAttempt(state);
|
|
977
|
+
if (result.ok)
|
|
978
|
+
return;
|
|
979
|
+
if (result.reason === "generation_conflict") {
|
|
980
|
+
throw new SessionError(`Session generation conflict for ${sessionId}`);
|
|
981
|
+
}
|
|
982
|
+
if (result.reason === "lock_conflict") {
|
|
983
|
+
throw new SessionError(`Session state lock contention for ${sessionId}`);
|
|
984
|
+
}
|
|
985
|
+
// Recompute the delta from the latest counters after a CAS miss so the
|
|
986
|
+
// auxiliary usage is added exactly once without overwriting another writer.
|
|
987
|
+
}
|
|
988
|
+
throw new SessionError(`Session state revision conflict for ${sessionId}`);
|
|
989
|
+
}
|
|
990
|
+
/** @deprecated Compatibility fixture writer; runtime code must use domain updates. */
|
|
991
|
+
saveState(state, generation) {
|
|
992
|
+
adoptCompatibilityGoalMutation(state);
|
|
993
|
+
return this.saveStateAttempt(state, generation).ok;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Persist one goal terminal transition without allowing a revision race to
|
|
997
|
+
* resurrect that goal. This is always a Goal-domain update: read the newest
|
|
998
|
+
* snapshot, transition only the matching lifecycle identity, then commit. A detached run
|
|
999
|
+
* snapshot is never used as the write candidate,
|
|
1000
|
+
* so a successful terminal transition cannot publish stale summary/workspace
|
|
1001
|
+
* or accounting fields. Lock/generation conflicts remain distinct failures.
|
|
1002
|
+
*/
|
|
1003
|
+
saveGoalTerminal(state, goal, reason) {
|
|
1004
|
+
return this.saveGoalTerminalOutcome(state, goal, reason) !== "failed";
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Persist a terminal transition and distinguish a durable commit from a stale
|
|
1008
|
+
* run whose Goal identity has already been replaced. Callers may publish a
|
|
1009
|
+
* terminal event only for `persisted`.
|
|
1010
|
+
*/
|
|
1011
|
+
saveGoalTerminalOutcome(state, goal, reason) {
|
|
1012
|
+
if (!goal)
|
|
1013
|
+
return "failed";
|
|
1014
|
+
const terminalGoal = {
|
|
1015
|
+
...goal,
|
|
1016
|
+
goalId: goal.goalId ?? deriveLegacyGoalId(state.sessionId, goal),
|
|
1017
|
+
revision: goal.revision ?? 1,
|
|
1018
|
+
};
|
|
1019
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
1020
|
+
const latest = this.readPersistedState(state.sessionId);
|
|
1021
|
+
const lifecycle = latest.goalLifecycle;
|
|
1022
|
+
if (!lifecycle)
|
|
1023
|
+
return "failed";
|
|
1024
|
+
if (lifecycle.goalId !== terminalGoal.goalId ||
|
|
1025
|
+
lifecycle.revision !== (terminalGoal.revision ?? 1)) {
|
|
1026
|
+
// A newer replacement/edit already owns the canonical slot. The stale
|
|
1027
|
+
// run is obsolete, but must not mutate that newer goal.
|
|
1028
|
+
this.rebaseLiveState(state, latest);
|
|
1029
|
+
return "obsolete";
|
|
1030
|
+
}
|
|
1031
|
+
if (lifecycle.phase !== "terminal") {
|
|
1032
|
+
latest.goalLifecycle = terminateGoalLifecycle(lifecycle, lifecycleTerminalReason(reason));
|
|
1033
|
+
hydrateGoalLifecycle(latest);
|
|
1034
|
+
}
|
|
1035
|
+
const result = this.saveStateAttempt(latest);
|
|
1036
|
+
if (result.ok) {
|
|
1037
|
+
this.rebaseLiveState(state, latest);
|
|
1038
|
+
return "persisted";
|
|
1039
|
+
}
|
|
1040
|
+
if (result.reason !== "revision_conflict")
|
|
1041
|
+
return "failed";
|
|
1042
|
+
}
|
|
1043
|
+
return "failed";
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Arm or migrate one active goal as a Goal-domain update. The newest state is
|
|
1047
|
+
* always the write base, so unrelated fields written by another host survive.
|
|
1048
|
+
* Replacement is explicit and terminally closes the latest active identity;
|
|
1049
|
+
* a non-replacement write may only normalize the same legacy/current goal.
|
|
1050
|
+
*/
|
|
1051
|
+
saveActiveGoal(state, goal, options = {}) {
|
|
1052
|
+
const armedGoal = {
|
|
1053
|
+
...goal,
|
|
1054
|
+
goalId: goal.goalId ?? deriveLegacyGoalId(state.sessionId, goal),
|
|
1055
|
+
revision: goal.revision ?? 1,
|
|
1056
|
+
};
|
|
1057
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
1058
|
+
const latest = this.readPersistedState(state.sessionId);
|
|
1059
|
+
const current = latest.goalLifecycle;
|
|
1060
|
+
if (current) {
|
|
1061
|
+
const sameIdentity = current.goalId === armedGoal.goalId;
|
|
1062
|
+
const sameRevision = current.revision === (armedGoal.revision ?? 1);
|
|
1063
|
+
if (current.phase === "terminal" && sameIdentity && sameRevision)
|
|
1064
|
+
return false;
|
|
1065
|
+
if (current.phase !== "terminal" && !options.replaceCurrent && !sameIdentity)
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
if (current?.phase === "waiting" &&
|
|
1069
|
+
current.goalId === armedGoal.goalId &&
|
|
1070
|
+
current.revision === (armedGoal.revision ?? 1)) {
|
|
1071
|
+
latest.goalLifecycle = armGoalLifecycle(current) ?? current;
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
latest.goalLifecycle = createGoalLifecycle(armedGoal, armedGoal.paused === true ? "paused" : "active");
|
|
1075
|
+
}
|
|
1076
|
+
hydrateGoalLifecycle(latest);
|
|
1077
|
+
const result = this.saveStateAttempt(latest);
|
|
1078
|
+
if (result.ok) {
|
|
1079
|
+
this.rebaseLiveState(state, latest);
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
if (result.reason !== "revision_conflict")
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
/** Persist active → waiting only for the expected concrete Goal version. */
|
|
1088
|
+
markGoalWaiting(state, goal) {
|
|
1089
|
+
const expectedGoalId = goal.goalId ?? deriveLegacyGoalId(state.sessionId, goal);
|
|
1090
|
+
const expectedRevision = Math.max(1, Math.floor(goal.revision ?? 1));
|
|
1091
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
1092
|
+
const latest = this.readPersistedState(state.sessionId);
|
|
1093
|
+
const lifecycle = latest.goalLifecycle;
|
|
1094
|
+
if (!lifecycle)
|
|
1095
|
+
return false;
|
|
1096
|
+
if (lifecycle.goalId !== expectedGoalId || lifecycle.revision !== expectedRevision) {
|
|
1097
|
+
this.rebaseLiveState(state, latest);
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
if (lifecycle.phase === "waiting") {
|
|
1101
|
+
this.rebaseLiveState(state, latest);
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
const waiting = waitGoalLifecycle(lifecycle);
|
|
1105
|
+
if (!waiting)
|
|
1106
|
+
return false;
|
|
1107
|
+
latest.goalLifecycle = waiting;
|
|
1108
|
+
hydrateGoalLifecycle(latest);
|
|
1109
|
+
const result = this.saveStateAttempt(latest);
|
|
1110
|
+
if (result.ok) {
|
|
1111
|
+
this.rebaseLiveState(state, latest);
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
if (result.reason !== "revision_conflict")
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
/** Drop a matching active goal only when its terminal identity is durable. */
|
|
1120
|
+
clearTerminatedActiveGoal(state, goal) {
|
|
1121
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
1122
|
+
const latest = this.readPersistedState(state.sessionId);
|
|
1123
|
+
const expectedGoalId = goal.goalId ?? deriveLegacyGoalId(state.sessionId, goal);
|
|
1124
|
+
const lifecycle = latest.goalLifecycle;
|
|
1125
|
+
if (!lifecycle ||
|
|
1126
|
+
lifecycle.goalId !== expectedGoalId ||
|
|
1127
|
+
lifecycle.revision !== Math.max(1, Math.floor(goal.revision ?? 1))) {
|
|
1128
|
+
this.rebaseLiveState(state, latest);
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
if (lifecycle.phase !== "terminal")
|
|
1132
|
+
return false;
|
|
1133
|
+
this.rebaseLiveState(state, latest);
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Commit only the fields owned by a live run. The detached `state` object is
|
|
1140
|
+
* used solely as a live-view target for the returned revision; it is never
|
|
1141
|
+
* serialized wholesale. This prevents a Goal/workspace/title domain write
|
|
1142
|
+
* from authorizing a later stale metadata overwrite.
|
|
1143
|
+
*/
|
|
1144
|
+
saveStateOrUpdateFields(state, partial) {
|
|
531
1145
|
try {
|
|
532
|
-
|
|
1146
|
+
const stateRevision = this.updateSessionState(state.sessionId, partial);
|
|
1147
|
+
Object.assign(state, partial, { stateRevision });
|
|
1148
|
+
return true;
|
|
533
1149
|
}
|
|
534
|
-
catch
|
|
535
|
-
|
|
1150
|
+
catch {
|
|
1151
|
+
return false;
|
|
536
1152
|
}
|
|
537
|
-
Object.assign(state, partial);
|
|
538
|
-
state.sessionId = sessionId;
|
|
539
|
-
this.saveState(state);
|
|
540
1153
|
}
|
|
541
|
-
|
|
1154
|
+
saveStateAttempt(state, generation) {
|
|
542
1155
|
// state.sessionId could come from a deserialized state.json that was
|
|
543
1156
|
// tampered with on disk. Validate before joining.
|
|
544
1157
|
assertSafeSessionId(state.sessionId);
|
|
1158
|
+
try {
|
|
1159
|
+
hydrateGoalLifecycle(state);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
return { ok: false, reason: "goal_schema_conflict" };
|
|
1163
|
+
}
|
|
545
1164
|
const writerGeneration = generation ?? this.registeredCloseEpochs.get(state.sessionId);
|
|
546
1165
|
if (writerGeneration !== undefined &&
|
|
547
1166
|
(currentSessionCloseEpochs.get(this.generationKey(state.sessionId)) ?? 0) !== writerGeneration) {
|
|
548
|
-
|
|
549
|
-
// invalidated. Reject explicitly without throwing into an unobserved
|
|
550
|
-
// engine finally path, and most importantly without touching the disk.
|
|
551
|
-
return false;
|
|
1167
|
+
return { ok: false, reason: "generation_conflict" };
|
|
552
1168
|
}
|
|
553
1169
|
const sessionDir = join(this.sessionsDir, state.sessionId);
|
|
554
1170
|
mkdirSync(sessionDir, { recursive: true });
|
|
555
|
-
// Atomic file replacement: stage to .tmp, then rename, preventing a torn
|
|
556
|
-
// state.json. Registered Engines share one epoch until close advances it,
|
|
557
|
-
// so concurrent opens are not fenced from each other. Workers, separate
|
|
558
|
-
// processes, and their stale-writer ordering still require locking/CAS.
|
|
559
1171
|
const target = join(sessionDir, "state.json");
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1172
|
+
// proper-lockfile uses an atomic mkdir lock plus an mtime lease. It
|
|
1173
|
+
// recovers crash-orphaned locks after `stale` and refreshes live locks so a
|
|
1174
|
+
// slow writer is not stolen. Short genuine contention gets bounded retry.
|
|
1175
|
+
const release = this.acquireStateLock(target);
|
|
1176
|
+
if (!release)
|
|
1177
|
+
return { ok: false, reason: "lock_conflict" };
|
|
1178
|
+
try {
|
|
1179
|
+
let persisted;
|
|
1180
|
+
if (existsSync(target)) {
|
|
1181
|
+
try {
|
|
1182
|
+
persisted = hydrateGoalLifecycle(JSON.parse(readFileSync(target, "utf-8")));
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
return { ok: false, reason: "goal_schema_conflict" };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const incomingKind = normalizedSessionKind(state.kind);
|
|
1189
|
+
const persistedKind = persisted ? normalizedSessionKind(persisted.kind) : incomingKind;
|
|
1190
|
+
if (persisted && incomingKind !== persistedKind) {
|
|
1191
|
+
return { ok: false, reason: "kind_conflict" };
|
|
1192
|
+
}
|
|
1193
|
+
state.kind = persistedKind;
|
|
1194
|
+
const persistedRevision = persisted?.stateRevision;
|
|
1195
|
+
const incomingRevision = state.stateRevision;
|
|
1196
|
+
const revisionsMatch = persisted === undefined ||
|
|
1197
|
+
(persistedRevision === undefined && incomingRevision === undefined) ||
|
|
1198
|
+
(typeof persistedRevision === "number" && incomingRevision === persistedRevision);
|
|
1199
|
+
if (!revisionsMatch)
|
|
1200
|
+
return { ok: false, reason: "revision_conflict" };
|
|
1201
|
+
// A title can be generated by a field-level writer while this bundle is
|
|
1202
|
+
// live. Preserve it when the incoming object never owned the field.
|
|
1203
|
+
if (persisted?.title !== undefined && !("title" in state)) {
|
|
1204
|
+
state.title = persisted.title;
|
|
1205
|
+
}
|
|
1206
|
+
state.stateRevision = (persistedRevision ?? incomingRevision ?? 0) + 1;
|
|
1207
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
1208
|
+
writeFileSync(tmp, JSON.stringify(stateForPersistence(state), null, 2), "utf-8");
|
|
1209
|
+
renameSync(tmp, target);
|
|
1210
|
+
return { ok: true };
|
|
1211
|
+
}
|
|
1212
|
+
finally {
|
|
566
1213
|
try {
|
|
567
|
-
|
|
1214
|
+
release();
|
|
568
1215
|
}
|
|
569
1216
|
catch {
|
|
570
|
-
//
|
|
571
|
-
// behavior and overwrite malformed state with the caller's snapshot.
|
|
1217
|
+
// A compromised/reaped lease must not mask the persistence result.
|
|
572
1218
|
}
|
|
573
1219
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1220
|
+
}
|
|
1221
|
+
acquireStateLock(target) {
|
|
1222
|
+
const lockPath = `${target}.lock`;
|
|
1223
|
+
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
1224
|
+
if (!this.prepareLegacyStateLock(lockPath)) {
|
|
1225
|
+
const delay = SESSION_STATE_LOCK_RETRY_DELAYS_MS[attempt];
|
|
1226
|
+
if (delay === undefined)
|
|
1227
|
+
return undefined;
|
|
1228
|
+
sleepSync(delay);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
try {
|
|
1232
|
+
return lockSync(target, {
|
|
1233
|
+
stale: SESSION_STATE_LOCK_STALE_MS,
|
|
1234
|
+
update: SESSION_STATE_LOCK_STALE_MS / 2,
|
|
1235
|
+
retries: 0,
|
|
1236
|
+
realpath: false,
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
const code = err.code;
|
|
1241
|
+
if (code !== "ELOCKED" && code !== "EEXIST")
|
|
1242
|
+
throw err;
|
|
1243
|
+
const delay = SESSION_STATE_LOCK_RETRY_DELAYS_MS[attempt];
|
|
1244
|
+
if (delay === undefined)
|
|
1245
|
+
return undefined;
|
|
1246
|
+
sleepSync(delay);
|
|
1247
|
+
}
|
|
580
1248
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1249
|
+
return undefined;
|
|
1250
|
+
}
|
|
1251
|
+
/** Recover only the old implementation's regular-file orphan lock. */
|
|
1252
|
+
prepareLegacyStateLock(lockPath) {
|
|
1253
|
+
if (!existsSync(lockPath))
|
|
1254
|
+
return true;
|
|
1255
|
+
let stat;
|
|
1256
|
+
try {
|
|
1257
|
+
stat = lstatSync(lockPath);
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
if (stat.isDirectory())
|
|
1263
|
+
return true;
|
|
1264
|
+
if (Date.now() - stat.mtimeMs <= SESSION_STATE_LOCK_STALE_MS)
|
|
1265
|
+
return false;
|
|
1266
|
+
try {
|
|
1267
|
+
rmSync(lockPath, { force: true });
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
catch {
|
|
1271
|
+
return false;
|
|
587
1272
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1273
|
+
}
|
|
1274
|
+
readPersistedState(sessionId) {
|
|
1275
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
1276
|
+
if (!existsSync(stateFile)) {
|
|
1277
|
+
throw new SessionError(`Session state file not found: ${sessionId}`);
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
return hydrateGoalLifecycle(JSON.parse(readFileSync(stateFile, "utf-8")));
|
|
1281
|
+
}
|
|
1282
|
+
catch (err) {
|
|
1283
|
+
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
rebaseLiveState(live, persisted) {
|
|
1287
|
+
for (const key of Object.keys(live)) {
|
|
1288
|
+
if (!(key in persisted))
|
|
1289
|
+
delete live[key];
|
|
1290
|
+
}
|
|
1291
|
+
Object.assign(live, persisted);
|
|
592
1292
|
}
|
|
593
1293
|
generationKey(sessionId) {
|
|
594
1294
|
return `${this.sessionsDir}\0${sessionId}`;
|
|
@@ -598,7 +1298,10 @@ export class SessionManager {
|
|
|
598
1298
|
assertSafeSessionId(sourceSessionId);
|
|
599
1299
|
if (options.targetSessionId !== undefined)
|
|
600
1300
|
assertSafeSessionId(options.targetSessionId);
|
|
601
|
-
|
|
1301
|
+
if (options.snapshotMode === "completed" && options.throughEventId !== undefined) {
|
|
1302
|
+
throw new SessionError(`Completed snapshot cannot use an explicit fork cursor`);
|
|
1303
|
+
}
|
|
1304
|
+
const snapshot = this.readForkSnapshot(sourceSessionId, options.throughEventId, options.snapshotMode ?? "tail");
|
|
602
1305
|
const targetSessionId = options.targetSessionId ?? nanoid(16);
|
|
603
1306
|
const createdAt = Date.now();
|
|
604
1307
|
const lineage = {
|
|
@@ -610,11 +1313,70 @@ export class SessionManager {
|
|
|
610
1313
|
createdAt,
|
|
611
1314
|
};
|
|
612
1315
|
const state = buildForkState(snapshot.sourceState, targetSessionId, lineage, createdAt);
|
|
1316
|
+
if (options.ephemeral === true)
|
|
1317
|
+
state.ephemeral = true;
|
|
613
1318
|
const events = buildForkTranscript(snapshot.copiedEvents, state);
|
|
614
1319
|
const bundle = this.publishSessionAtomically(targetSessionId, state, events);
|
|
615
1320
|
return { bundle, lineage, copiedEventCount: snapshot.copiedEvents.length };
|
|
616
1321
|
}
|
|
617
|
-
|
|
1322
|
+
/** Freeze and validate an inclusive source range before any model call. */
|
|
1323
|
+
selectContextPackage(sourceSessionId, range) {
|
|
1324
|
+
assertSafeSessionId(sourceSessionId);
|
|
1325
|
+
const transcriptFile = join(this.sessionsDir, sourceSessionId, "transcript.jsonl");
|
|
1326
|
+
const stateFile = join(this.sessionsDir, sourceSessionId, "state.json");
|
|
1327
|
+
if (!existsSync(stateFile))
|
|
1328
|
+
throw new SessionError(`Session not found: ${sourceSessionId}`);
|
|
1329
|
+
const parsed = Transcript.readEvents(transcriptFile);
|
|
1330
|
+
if (parsed.malformedLineCount > 0) {
|
|
1331
|
+
throw new SessionError(`Session transcript is malformed for ${sourceSessionId}: ${parsed.malformedLineCount} invalid line(s)`);
|
|
1332
|
+
}
|
|
1333
|
+
return Transcript.selectContextRange(parsed.events, range);
|
|
1334
|
+
}
|
|
1335
|
+
/** Publish a summary-only top-level fork after summarization has succeeded. */
|
|
1336
|
+
createSummaryFork(sourceSessionId, options) {
|
|
1337
|
+
assertSafeSessionId(sourceSessionId);
|
|
1338
|
+
if (options.targetSessionId !== undefined)
|
|
1339
|
+
assertSafeSessionId(options.targetSessionId);
|
|
1340
|
+
if (!options.fromEventId || !options.toEventId) {
|
|
1341
|
+
throw new SessionError("Summary fork requires a closed source event range");
|
|
1342
|
+
}
|
|
1343
|
+
if (!options.summary.trim())
|
|
1344
|
+
throw new SessionError("Summary fork requires a non-empty summary");
|
|
1345
|
+
const source = this.resume(sourceSessionId);
|
|
1346
|
+
const targetSessionId = options.targetSessionId ?? nanoid(16);
|
|
1347
|
+
const createdAt = Date.now();
|
|
1348
|
+
const lineage = {
|
|
1349
|
+
sessionId: sourceSessionId,
|
|
1350
|
+
mode: "summary",
|
|
1351
|
+
fromEventId: options.fromEventId,
|
|
1352
|
+
throughEventId: options.toEventId,
|
|
1353
|
+
sourceEventCount: options.sourceEventCount,
|
|
1354
|
+
createdAt,
|
|
1355
|
+
};
|
|
1356
|
+
const state = buildForkState(source.state, targetSessionId, lineage, createdAt);
|
|
1357
|
+
const [meta] = buildForkTranscript([], state);
|
|
1358
|
+
const summaryEvent = {
|
|
1359
|
+
id: nanoid(12),
|
|
1360
|
+
type: "context_transfer",
|
|
1361
|
+
timestamp: createdAt,
|
|
1362
|
+
turnNumber: 0,
|
|
1363
|
+
data: {
|
|
1364
|
+
summary: options.summary,
|
|
1365
|
+
sourceRange: {
|
|
1366
|
+
sessionId: sourceSessionId,
|
|
1367
|
+
fromEventId: options.fromEventId,
|
|
1368
|
+
toEventId: options.toEventId,
|
|
1369
|
+
},
|
|
1370
|
+
sourceEventCount: options.sourceEventCount,
|
|
1371
|
+
estimatedTokens: options.estimatedTokens,
|
|
1372
|
+
summaryVersion: 1,
|
|
1373
|
+
summaryHash: createHash("sha256").update(options.summary).digest("hex"),
|
|
1374
|
+
},
|
|
1375
|
+
};
|
|
1376
|
+
const bundle = this.publishSessionAtomically(targetSessionId, state, [meta, summaryEvent]);
|
|
1377
|
+
return { bundle, lineage, copiedEventCount: 0 };
|
|
1378
|
+
}
|
|
1379
|
+
readForkSnapshot(sourceSessionId, throughEventId, snapshotMode) {
|
|
618
1380
|
const sessionDir = join(this.sessionsDir, sourceSessionId);
|
|
619
1381
|
const stateFile = join(sessionDir, "state.json");
|
|
620
1382
|
const transcriptFile = join(sessionDir, "transcript.jsonl");
|
|
@@ -633,12 +1395,26 @@ export class SessionManager {
|
|
|
633
1395
|
}
|
|
634
1396
|
const sourceEvents = structuredClone(parsed.events);
|
|
635
1397
|
let frozen = sourceEvents;
|
|
636
|
-
|
|
1398
|
+
const effectiveCursor = snapshotMode === "completed" ? sourceState.completedThroughEventId : throughEventId;
|
|
1399
|
+
if (snapshotMode === "completed" && effectiveCursor === undefined) {
|
|
1400
|
+
// Legacy sessions predate completedThroughEventId. Their tail is a safe
|
|
1401
|
+
// completed snapshot only when the persisted lifecycle status says the
|
|
1402
|
+
// last run completed naturally. A modern schema marker without a cursor
|
|
1403
|
+
// means persistence was degraded, so even status=completed must not use
|
|
1404
|
+
// the tail. Active/error/abort tails may be partial as well.
|
|
1405
|
+
frozen =
|
|
1406
|
+
sourceState.completedSnapshotVersion === undefined && sourceState.status === "completed"
|
|
1407
|
+
? sourceEvents
|
|
1408
|
+
: [];
|
|
1409
|
+
}
|
|
1410
|
+
else if (effectiveCursor !== undefined) {
|
|
637
1411
|
const matches = sourceEvents
|
|
638
|
-
.map((event, index) => (event.id ===
|
|
1412
|
+
.map((event, index) => (event.id === effectiveCursor ? index : -1))
|
|
639
1413
|
.filter((index) => index >= 0);
|
|
640
1414
|
if (matches.length !== 1) {
|
|
641
|
-
throw new SessionError(
|
|
1415
|
+
throw new SessionError(snapshotMode === "completed"
|
|
1416
|
+
? `Completed fork cursor must identify exactly one source event`
|
|
1417
|
+
: `Fork cursor must identify exactly one source event`);
|
|
642
1418
|
}
|
|
643
1419
|
const cursor = sourceEvents[matches[0]];
|
|
644
1420
|
if (cursor.type === "session_meta") {
|
|
@@ -687,11 +1463,16 @@ export class SessionManager {
|
|
|
687
1463
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
688
1464
|
}
|
|
689
1465
|
}
|
|
690
|
-
list(limit = 20) {
|
|
1466
|
+
list(limit = 20, opts) {
|
|
691
1467
|
if (!existsSync(this.sessionsDir))
|
|
692
1468
|
return [];
|
|
1469
|
+
// Extension-owned session kinds stay out of generic lists. Defaults to
|
|
1470
|
+
// hiding "pet" so default-arg callers keep today's behavior.
|
|
1471
|
+
// TODO(pet-out-of-core): drop the default once every caller passes the
|
|
1472
|
+
// host's hidden-kind union explicitly.
|
|
1473
|
+
const excludeKinds = opts?.excludeKinds ?? ["pet"];
|
|
693
1474
|
const dirs = readdirSync(this.sessionsDir, { withFileTypes: true })
|
|
694
|
-
.filter((d) => d.isDirectory() && !d.name.startsWith(".pending-"))
|
|
1475
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith(".pending-") && !d.name.startsWith("qchat-"))
|
|
695
1476
|
.map((d) => d.name);
|
|
696
1477
|
const candidates = [];
|
|
697
1478
|
for (const dir of dirs) {
|
|
@@ -718,11 +1499,17 @@ export class SessionManager {
|
|
|
718
1499
|
candidates.push({ dir, lastActiveAt, transcriptFile, stateFile, transcriptExists });
|
|
719
1500
|
}
|
|
720
1501
|
candidates.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
721
|
-
const top = candidates.slice(0, limit);
|
|
722
1502
|
const sessions = [];
|
|
723
|
-
for (const c of
|
|
1503
|
+
for (const c of candidates) {
|
|
1504
|
+
if (sessions.length >= limit)
|
|
1505
|
+
break;
|
|
724
1506
|
try {
|
|
725
1507
|
const state = JSON.parse(readFileSync(c.stateFile, "utf-8"));
|
|
1508
|
+
if (isEphemeralSessionState(state))
|
|
1509
|
+
continue;
|
|
1510
|
+
state.kind = normalizedSessionKind(state.kind);
|
|
1511
|
+
if (excludeKinds.includes(state.kind))
|
|
1512
|
+
continue;
|
|
726
1513
|
const preview = c.transcriptExists ? readLastUserMessage(c.transcriptFile) : undefined;
|
|
727
1514
|
sessions.push({ ...state, preview, lastActiveAt: c.lastActiveAt });
|
|
728
1515
|
}
|
|
@@ -739,6 +1526,8 @@ export function buildForkState(source, targetSessionId, lineage, startedAt = Dat
|
|
|
739
1526
|
: { root: source.cwd, kind: "main" };
|
|
740
1527
|
return {
|
|
741
1528
|
sessionId: targetSessionId,
|
|
1529
|
+
kind: "work",
|
|
1530
|
+
stateRevision: 0,
|
|
742
1531
|
cwd: source.cwd,
|
|
743
1532
|
workspace,
|
|
744
1533
|
startedAt,
|
|
@@ -880,15 +1669,6 @@ function validateForkToolPairs(events) {
|
|
|
880
1669
|
throw new SessionError(`Fork tool metadata does not match provider history`);
|
|
881
1670
|
}
|
|
882
1671
|
}
|
|
883
|
-
function newestGoalTerminal(incoming, persisted) {
|
|
884
|
-
if (!incoming)
|
|
885
|
-
return persisted;
|
|
886
|
-
if (!persisted)
|
|
887
|
-
return incoming;
|
|
888
|
-
const incomingAt = incoming.terminatedAtMs ?? Number.NEGATIVE_INFINITY;
|
|
889
|
-
const persistedAt = persisted.terminatedAtMs ?? Number.NEGATIVE_INFINITY;
|
|
890
|
-
return incomingAt > persistedAt ? incoming : persisted;
|
|
891
|
-
}
|
|
892
1672
|
/**
|
|
893
1673
|
* Scan a transcript.jsonl for the LAST user message and return a short
|
|
894
1674
|
* preview, reading the file from the END in 64 KiB chunks.
|