@vellumai/assistant 0.8.8 → 0.8.9-staging.2
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/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { basename } from "node:path";
|
|
7
8
|
|
|
8
|
-
import { inArray } from "drizzle-orm";
|
|
9
|
+
import { eq, inArray } from "drizzle-orm";
|
|
9
10
|
|
|
10
|
-
import { findConversation } from "../daemon/conversation-
|
|
11
|
+
import { findConversation } from "../daemon/conversation-registry.js";
|
|
11
12
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
12
13
|
import type { AcpSessionUpdate } from "../daemon/message-types/acp.js";
|
|
13
14
|
import { getDb } from "../memory/db-connection.js";
|
|
@@ -15,11 +16,41 @@ import { acpSessionHistory } from "../memory/schema.js";
|
|
|
15
16
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
16
17
|
import { getLogger } from "../util/logger.js";
|
|
17
18
|
import { AcpAgentProcess } from "./agent-process.js";
|
|
19
|
+
import { resolveAgentWithAutoInstall } from "./auto-install.js";
|
|
18
20
|
import { VellumAcpClientHandler } from "./client-handler.js";
|
|
21
|
+
import { prepareAgentEnv } from "./prepare-agent-env.js";
|
|
22
|
+
import { formatResolveFailure } from "./resolve-agent.js";
|
|
23
|
+
import { claudeResumeHint } from "./resume-hint.js";
|
|
19
24
|
import type { AcpAgentConfig, AcpSessionState } from "./types.js";
|
|
20
25
|
|
|
21
26
|
const log = getLogger("acp:session-manager");
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The manager's "unknown session id" error. Thrown whenever an operation
|
|
30
|
+
* references an acpSessionId with no in-memory entry (and, for resume, no
|
|
31
|
+
* persisted history row). Callers (acp_steer tool, /v1/acp/:id/steer route)
|
|
32
|
+
* use `instanceof` checks to map this to their transport's not-found shape.
|
|
33
|
+
*/
|
|
34
|
+
export class AcpSessionNotFoundError extends Error {
|
|
35
|
+
constructor(public readonly acpSessionId: string) {
|
|
36
|
+
super(`ACP session "${acpSessionId}" not found`);
|
|
37
|
+
this.name = "AcpSessionNotFoundError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Wraps failures from the resume-then-steer phase of `steerOrResume` so
|
|
43
|
+
* transport callers can distinguish them (HTTP 424 with the actionable
|
|
44
|
+
* resume hint) from plain steer failures (404). The message mirrors the
|
|
45
|
+
* underlying error's message; the original error rides on `cause`.
|
|
46
|
+
*/
|
|
47
|
+
export class AcpResumeError extends Error {
|
|
48
|
+
constructor(cause: unknown) {
|
|
49
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
50
|
+
this.name = "AcpResumeError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
/** Maximum number of update events kept in a session's ring buffer. */
|
|
24
55
|
const MAX_BUFFER_EVENTS = 200;
|
|
25
56
|
/** Maximum aggregate JSON size of a session's ring buffer, in bytes. */
|
|
@@ -41,11 +72,21 @@ interface SessionEntry {
|
|
|
41
72
|
currentPrompt: Promise<unknown> | null;
|
|
42
73
|
parentConversationId: string;
|
|
43
74
|
cwd: string;
|
|
44
|
-
/**
|
|
45
|
-
* the only adapter (claude-agent-acp) whose CLI
|
|
75
|
+
/** Resolved adapter command basename (e.g. "claude-agent-acp"). Used to
|
|
76
|
+
* gate resume hints to the only adapter (claude-agent-acp) whose CLI
|
|
77
|
+
* accepts `--resume`. */
|
|
46
78
|
command: string;
|
|
47
79
|
}
|
|
48
80
|
|
|
81
|
+
/**
|
|
82
|
+
* An `acp_session_history` row that passed resumeFromHistory's validation
|
|
83
|
+
* guards: `cwd` (nullable in the schema for pre-resume-support rows) is
|
|
84
|
+
* guaranteed present.
|
|
85
|
+
*/
|
|
86
|
+
type ResumableHistoryRow = typeof acpSessionHistory.$inferSelect & {
|
|
87
|
+
cwd: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
49
90
|
export class AcpSessionManager {
|
|
50
91
|
private sessions = new Map<string, SessionEntry>();
|
|
51
92
|
/**
|
|
@@ -55,6 +96,25 @@ export class AcpSessionManager {
|
|
|
55
96
|
* `acp_session_history` on terminal transition, then cleared.
|
|
56
97
|
*/
|
|
57
98
|
private eventBuffers = new Map<string, BufferedAcpUpdate[]>();
|
|
99
|
+
/**
|
|
100
|
+
* In-flight resumes by session id, keyed to the promise of the resume's
|
|
101
|
+
* async body. Reserved SYNCHRONOUSLY before the first await so concurrent
|
|
102
|
+
* resumes of the same id cannot both pass the guards (the loser would
|
|
103
|
+
* overwrite the winner's map entry and leak its child process), and so N
|
|
104
|
+
* concurrent resumes of distinct ids cannot exceed maxConcurrent. The
|
|
105
|
+
* entry lives until the resume settles so `steerOrResume` can await a
|
|
106
|
+
* concurrent caller's resume instead of failing the already-active guard.
|
|
107
|
+
* The spawn path needs no such reservation: its check-then-register is
|
|
108
|
+
* synchronous.
|
|
109
|
+
*/
|
|
110
|
+
private pendingResumes = new Map<string, Promise<void>>();
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set by dispose() (the daemon-shutdown path). Resumes that are mid-await
|
|
114
|
+
* when the manager is disposed re-check this flag before spawning a child
|
|
115
|
+
* process nothing would ever kill.
|
|
116
|
+
*/
|
|
117
|
+
private disposed = false;
|
|
58
118
|
|
|
59
119
|
constructor(private readonly maxConcurrent: number) {
|
|
60
120
|
this.cleanupStaleRunningRows();
|
|
@@ -91,6 +151,33 @@ export class AcpSessionManager {
|
|
|
91
151
|
}
|
|
92
152
|
}
|
|
93
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Ids that must be treated as live: registered sessions plus in-flight
|
|
156
|
+
* resume reservations (deduped, since a resuming session appears in both maps
|
|
157
|
+
* between registration and settle). Delete guards use this so a history
|
|
158
|
+
* row cannot be removed out from under a resume that is still awaiting
|
|
159
|
+
* env preparation; the later terminal upsert would resurrect it.
|
|
160
|
+
*/
|
|
161
|
+
getActiveAndPendingIds(): string[] {
|
|
162
|
+
return [
|
|
163
|
+
...new Set([...this.sessions.keys(), ...this.pendingResumes.keys()]),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Concurrency guard shared by spawn() and resumeFromHistory(). Counts
|
|
169
|
+
* both registered sessions and in-flight resume reservations so the cap
|
|
170
|
+
* holds even while a resume is still awaiting prepareAgentEnv.
|
|
171
|
+
*/
|
|
172
|
+
private assertCapacity(): void {
|
|
173
|
+
if (this.getActiveAndPendingIds().length >= this.maxConcurrent) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`ACP concurrency limit reached (max ${this.maxConcurrent}). ` +
|
|
176
|
+
`Close an existing session before spawning a new one.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
94
181
|
/**
|
|
95
182
|
* Spawns a new ACP agent session. Returns the generated acpSessionId.
|
|
96
183
|
*
|
|
@@ -105,12 +192,7 @@ export class AcpSessionManager {
|
|
|
105
192
|
parentConversationId: string,
|
|
106
193
|
sendToVellum: (msg: ServerMessage) => void,
|
|
107
194
|
): Promise<{ acpSessionId: string; protocolSessionId: string }> {
|
|
108
|
-
|
|
109
|
-
throw new Error(
|
|
110
|
-
`ACP concurrency limit reached (max ${this.maxConcurrent}). ` +
|
|
111
|
-
`Close an existing session before spawning a new one.`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
195
|
+
this.assertCapacity();
|
|
114
196
|
|
|
115
197
|
const acpSessionId = randomUUID();
|
|
116
198
|
log.info(
|
|
@@ -124,40 +206,103 @@ export class AcpSessionManager {
|
|
|
124
206
|
"ACP spawn requested",
|
|
125
207
|
);
|
|
126
208
|
|
|
209
|
+
const entry = this.registerSession({
|
|
210
|
+
acpSessionId,
|
|
211
|
+
agentId,
|
|
212
|
+
agentConfig,
|
|
213
|
+
parentConversationId,
|
|
214
|
+
cwd,
|
|
215
|
+
startedAt: Date.now(),
|
|
216
|
+
sendToVellum,
|
|
217
|
+
});
|
|
218
|
+
const { process: agentProcess, state } = entry;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
log.info({ acpSessionId, agentId }, "ACP spawning child process");
|
|
222
|
+
agentProcess.spawn(cwd);
|
|
223
|
+
log.info(
|
|
224
|
+
{ acpSessionId, agentId },
|
|
225
|
+
"ACP initializing protocol connection",
|
|
226
|
+
);
|
|
227
|
+
await agentProcess.initialize();
|
|
228
|
+
log.info({ acpSessionId, agentId }, "ACP creating session");
|
|
229
|
+
const acpProtocolSessionId = await agentProcess.createSession(cwd);
|
|
230
|
+
state.acpSessionId = acpProtocolSessionId;
|
|
231
|
+
state.status = "running";
|
|
232
|
+
log.info(
|
|
233
|
+
{ acpSessionId, agentId, acpProtocolSessionId },
|
|
234
|
+
"ACP session running",
|
|
235
|
+
);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
log.error({ acpSessionId, agentId, err }, "ACP spawn failed");
|
|
238
|
+
// No prompt has fired yet, so no permissions can be pending.
|
|
239
|
+
this.teardownSession(acpSessionId, entry);
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.sendSpawnedEvent(acpSessionId, entry);
|
|
244
|
+
|
|
245
|
+
// Fire prompt in the background — don't await
|
|
246
|
+
entry.currentPrompt = this.firePromptInBackground(
|
|
247
|
+
acpSessionId,
|
|
248
|
+
entry,
|
|
249
|
+
state.acpSessionId,
|
|
250
|
+
task,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
return { acpSessionId, protocolSessionId: state.acpSessionId };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Wires up the in-memory plumbing shared by spawn() and
|
|
258
|
+
* resumeFromHistory(): the per-session ring buffer, the buffer-mirroring
|
|
259
|
+
* sender, the client handler, the agent process, and the SessionEntry.
|
|
260
|
+
* Registers the entry in the session map (reserving a concurrency slot
|
|
261
|
+
* before any async work) and returns it. Does NOT start the process.
|
|
262
|
+
*/
|
|
263
|
+
private registerSession(opts: {
|
|
264
|
+
acpSessionId: string;
|
|
265
|
+
agentId: string;
|
|
266
|
+
agentConfig: AcpAgentConfig;
|
|
267
|
+
parentConversationId: string;
|
|
268
|
+
cwd: string;
|
|
269
|
+
startedAt: number;
|
|
270
|
+
sendToVellum: (msg: ServerMessage) => void;
|
|
271
|
+
}): SessionEntry {
|
|
272
|
+
const { acpSessionId } = opts;
|
|
273
|
+
|
|
127
274
|
// Initialize the per-session ring buffer before any update can fire.
|
|
128
275
|
this.eventBuffers.set(acpSessionId, []);
|
|
129
276
|
|
|
130
277
|
// Wrap the sender so every emitted message is mirrored into the buffer
|
|
131
278
|
// when it's an `acp_session_update`. The wrapper preserves the original
|
|
132
|
-
// call semantics
|
|
279
|
+
// call semantics: it forwards every message unchanged.
|
|
133
280
|
const wrappedSend = (msg: ServerMessage) => {
|
|
134
281
|
if (msg.type === "acp_session_update") {
|
|
135
282
|
this.appendToBuffer(acpSessionId, msg);
|
|
136
283
|
}
|
|
137
|
-
sendToVellum(msg);
|
|
284
|
+
opts.sendToVellum(msg);
|
|
138
285
|
};
|
|
139
286
|
|
|
140
287
|
const clientHandler = new VellumAcpClientHandler(
|
|
141
288
|
acpSessionId,
|
|
142
289
|
wrappedSend,
|
|
143
|
-
parentConversationId,
|
|
290
|
+
opts.parentConversationId,
|
|
144
291
|
);
|
|
145
292
|
|
|
146
293
|
const agentProcess = new AcpAgentProcess(
|
|
147
|
-
agentId,
|
|
148
|
-
agentConfig,
|
|
294
|
+
opts.agentId,
|
|
295
|
+
opts.agentConfig,
|
|
149
296
|
(_agent) => clientHandler,
|
|
150
297
|
);
|
|
151
298
|
|
|
152
|
-
// Reserve a slot in the map before any async work to enforce the
|
|
153
|
-
// concurrency limit even when multiple spawn() calls race.
|
|
154
299
|
const state: AcpSessionState = {
|
|
155
300
|
id: acpSessionId,
|
|
156
|
-
agentId,
|
|
157
|
-
acpSessionId: "", // placeholder until createSession resolves
|
|
158
|
-
parentConversationId,
|
|
301
|
+
agentId: opts.agentId,
|
|
302
|
+
acpSessionId: "", // placeholder until createSession/resume resolves
|
|
303
|
+
parentConversationId: opts.parentConversationId,
|
|
159
304
|
status: "initializing",
|
|
160
|
-
startedAt:
|
|
305
|
+
startedAt: opts.startedAt,
|
|
161
306
|
};
|
|
162
307
|
|
|
163
308
|
const entry: SessionEntry = {
|
|
@@ -166,54 +311,223 @@ export class AcpSessionManager {
|
|
|
166
311
|
clientHandler,
|
|
167
312
|
sendToVellum: wrappedSend,
|
|
168
313
|
currentPrompt: null,
|
|
169
|
-
parentConversationId,
|
|
170
|
-
cwd,
|
|
171
|
-
command: agentConfig.command,
|
|
314
|
+
parentConversationId: opts.parentConversationId,
|
|
315
|
+
cwd: opts.cwd,
|
|
316
|
+
command: basename(opts.agentConfig.command),
|
|
172
317
|
};
|
|
173
318
|
|
|
174
319
|
this.sessions.set(acpSessionId, entry);
|
|
320
|
+
return entry;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Notifies connected clients that a session is live. Shared by spawn()
|
|
325
|
+
* and the resume path (resumed sessions reuse the spawned event so
|
|
326
|
+
* clients render them; a dedicated acp_session_resumed event is a
|
|
327
|
+
* possible follow-up, not in scope here).
|
|
328
|
+
*/
|
|
329
|
+
private sendSpawnedEvent(acpSessionId: string, entry: SessionEntry): void {
|
|
330
|
+
entry.sendToVellum({
|
|
331
|
+
type: "acp_session_spawned",
|
|
332
|
+
acpSessionId,
|
|
333
|
+
agent: entry.state.agentId,
|
|
334
|
+
parentConversationId: entry.parentConversationId,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Resumes a terminal-state session from its persisted
|
|
340
|
+
* `acp_session_history` row, reattaching to the agent's stored
|
|
341
|
+
* conversation via ACP `session/resume` (preferred: no history replay) or
|
|
342
|
+
* `session/load` (replayed history is suppressed; see
|
|
343
|
+
* VellumAcpClientHandler.beginReplaySuppression).
|
|
344
|
+
*
|
|
345
|
+
* The resumed session reuses the original vellum session id,
|
|
346
|
+
* parentConversationId, and startedAt, and re-seeds its ring buffer from
|
|
347
|
+
* the persisted event log so the terminal upsert after the resumed run
|
|
348
|
+
* merges new events into the original row instead of losing them.
|
|
349
|
+
*
|
|
350
|
+
* Throws with an actionable message when the row is missing, was recorded
|
|
351
|
+
* before resume support (no cwd), the agent cannot be resolved or
|
|
352
|
+
* auto-installed, or the agent advertises neither resume capability.
|
|
353
|
+
*/
|
|
354
|
+
async resumeFromHistory(
|
|
355
|
+
acpSessionId: string,
|
|
356
|
+
sendToVellum: (msg: ServerMessage) => void,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
if (
|
|
359
|
+
this.sessions.has(acpSessionId) ||
|
|
360
|
+
this.pendingResumes.has(acpSessionId)
|
|
361
|
+
) {
|
|
362
|
+
throw new Error(`ACP session "${acpSessionId}" is already active`);
|
|
363
|
+
}
|
|
364
|
+
this.assertCapacity();
|
|
365
|
+
|
|
366
|
+
const row = getDb()
|
|
367
|
+
.select()
|
|
368
|
+
.from(acpSessionHistory)
|
|
369
|
+
.where(eq(acpSessionHistory.id, acpSessionId))
|
|
370
|
+
.get();
|
|
371
|
+
if (!row) {
|
|
372
|
+
throw new AcpSessionNotFoundError(acpSessionId);
|
|
373
|
+
}
|
|
374
|
+
if (!row.cwd) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`ACP session "${acpSessionId}" was recorded before resume support ` +
|
|
377
|
+
`(no working directory persisted) and cannot be resumed. ` +
|
|
378
|
+
`Spawn a new session instead.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (!row.acpSessionId) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`ACP session "${acpSessionId}" has no protocol session id ` +
|
|
384
|
+
`persisted and cannot be resumed. Spawn a new session instead.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Everything up to here is synchronous. Reserve the id + concurrency
|
|
389
|
+
// slot BEFORE the first await so a concurrent resume of the same id
|
|
390
|
+
// (or a spawn racing the cap) fails the guards above instead of
|
|
391
|
+
// double-registering and leaking the first child process. The
|
|
392
|
+
// reservation holds the resume's promise until it settles so
|
|
393
|
+
// steerOrResume can await a concurrent caller's in-flight resume, and
|
|
394
|
+
// so the delete guards see the id as live while the row's terminal
|
|
395
|
+
// status still reflects the previous run. Agent resolution (which may
|
|
396
|
+
// auto-install a missing adapter) is therefore deferred into
|
|
397
|
+
// performResume, after the slot is reserved.
|
|
398
|
+
const resumePromise = this.performResume(
|
|
399
|
+
acpSessionId,
|
|
400
|
+
row as ResumableHistoryRow,
|
|
401
|
+
sendToVellum,
|
|
402
|
+
);
|
|
403
|
+
this.pendingResumes.set(acpSessionId, resumePromise);
|
|
404
|
+
try {
|
|
405
|
+
await resumePromise;
|
|
406
|
+
} finally {
|
|
407
|
+
this.pendingResumes.delete(acpSessionId);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The async body of resumeFromHistory, split out so the caller can store
|
|
413
|
+
* its promise in `pendingResumes` synchronously before the first await.
|
|
414
|
+
* All guards and row validation have already passed; this resolves the
|
|
415
|
+
* adapter (auto-installing a missing allowlisted binary), prepares its
|
|
416
|
+
* env, and reattaches.
|
|
417
|
+
*/
|
|
418
|
+
private async performResume(
|
|
419
|
+
acpSessionId: string,
|
|
420
|
+
row: ResumableHistoryRow,
|
|
421
|
+
sendToVellum: (msg: ServerMessage) => void,
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
// Resolve the adapter, silently auto-installing a missing allowlisted
|
|
424
|
+
// binary via the same sandboxed `bun` path as spawn (see
|
|
425
|
+
// acp/auto-install.ts). A fresh container, or a session created under
|
|
426
|
+
// the old bunx fallback, may have no adapter on PATH; resume installs it
|
|
427
|
+
// transparently instead of failing with binary_not_found. The token is
|
|
428
|
+
// NEVER in scope here — resolveAgentWithAutoInstall strips it from the
|
|
429
|
+
// installer env; it is injected only by prepareAgentEnv below, at spawn
|
|
430
|
+
// time, on the real installed binary.
|
|
431
|
+
const { resolved, failureMessage } = await resolveAgentWithAutoInstall(
|
|
432
|
+
row.agentId,
|
|
433
|
+
);
|
|
434
|
+
if (failureMessage) {
|
|
435
|
+
throw new Error(failureMessage);
|
|
436
|
+
}
|
|
437
|
+
if (!resolved.ok) {
|
|
438
|
+
throw new Error(formatResolveFailure(row.agentId, resolved));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const agentConfig = await prepareAgentEnv(resolved.agent);
|
|
442
|
+
|
|
443
|
+
// The daemon may have shut down while prepareAgentEnv was pending.
|
|
444
|
+
// Registering now would spawn a child process on a disposed manager
|
|
445
|
+
// that nothing would ever kill.
|
|
446
|
+
if (this.disposed) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`ACP session manager is disposed; cannot resume session "${acpSessionId}"`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const entry = this.registerSession({
|
|
453
|
+
acpSessionId,
|
|
454
|
+
agentId: row.agentId,
|
|
455
|
+
agentConfig,
|
|
456
|
+
parentConversationId: row.parentConversationId,
|
|
457
|
+
cwd: row.cwd,
|
|
458
|
+
startedAt: row.startedAt,
|
|
459
|
+
sendToVellum,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
log.info(
|
|
463
|
+
{ acpSessionId, agentId: row.agentId, cwd: row.cwd },
|
|
464
|
+
"ACP resume from history requested",
|
|
465
|
+
);
|
|
466
|
+
const { process: agentProcess, state } = entry;
|
|
467
|
+
|
|
468
|
+
// Re-seed the ring buffer from the persisted event log, routed through
|
|
469
|
+
// appendToBuffer so the count/byte caps still apply. The terminal
|
|
470
|
+
// upsert then persists the merged (old + new) log.
|
|
471
|
+
try {
|
|
472
|
+
const persisted = JSON.parse(row.eventLogJson) as unknown;
|
|
473
|
+
if (Array.isArray(persisted)) {
|
|
474
|
+
for (const update of persisted) {
|
|
475
|
+
this.appendToBuffer(acpSessionId, update as AcpSessionUpdate);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
log.warn(
|
|
480
|
+
{ acpSessionId, err },
|
|
481
|
+
"Failed to re-seed ACP event buffer from persisted history",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
175
484
|
|
|
176
485
|
try {
|
|
177
|
-
log.info({ acpSessionId, agentId }, "ACP spawning child process");
|
|
178
|
-
agentProcess.spawn(cwd);
|
|
179
486
|
log.info(
|
|
180
|
-
{ acpSessionId, agentId },
|
|
181
|
-
"ACP
|
|
487
|
+
{ acpSessionId, agentId: row.agentId },
|
|
488
|
+
"ACP spawning child process for resume",
|
|
182
489
|
);
|
|
490
|
+
agentProcess.spawn(row.cwd);
|
|
183
491
|
await agentProcess.initialize();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
492
|
+
if (agentProcess.supportsSessionResume) {
|
|
493
|
+
// session/resume reattaches without replaying history.
|
|
494
|
+
await agentProcess.resumeSession(row.acpSessionId, row.cwd);
|
|
495
|
+
} else if (agentProcess.supportsLoadSession) {
|
|
496
|
+
// session/load replays the full history as session/update
|
|
497
|
+
// notifications before resolving; suppress forwarding so the
|
|
498
|
+
// conversation and ring buffer don't receive duplicates.
|
|
499
|
+
entry.clientHandler.beginReplaySuppression();
|
|
500
|
+
try {
|
|
501
|
+
await agentProcess.loadSession(row.acpSessionId, row.cwd);
|
|
502
|
+
} finally {
|
|
503
|
+
entry.clientHandler.endReplaySuppression();
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`ACP agent "${row.agentId}" does not support session resume`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
state.acpSessionId = row.acpSessionId;
|
|
187
511
|
state.status = "running";
|
|
188
512
|
log.info(
|
|
189
|
-
{
|
|
190
|
-
|
|
513
|
+
{
|
|
514
|
+
acpSessionId,
|
|
515
|
+
agentId: row.agentId,
|
|
516
|
+
protocolSessionId: row.acpSessionId,
|
|
517
|
+
},
|
|
518
|
+
"ACP session resumed",
|
|
191
519
|
);
|
|
192
520
|
} catch (err) {
|
|
193
|
-
log.error(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
521
|
+
log.error(
|
|
522
|
+
{ acpSessionId, agentId: row.agentId, err },
|
|
523
|
+
"ACP resume failed",
|
|
524
|
+
);
|
|
525
|
+
// No prompt has fired yet, so no permissions can be pending.
|
|
526
|
+
this.teardownSession(acpSessionId, entry);
|
|
198
527
|
throw err;
|
|
199
528
|
}
|
|
200
529
|
|
|
201
|
-
|
|
202
|
-
type: "acp_session_spawned",
|
|
203
|
-
acpSessionId,
|
|
204
|
-
agent: agentId,
|
|
205
|
-
parentConversationId,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Fire prompt in the background — don't await
|
|
209
|
-
entry.currentPrompt = this.firePromptInBackground(
|
|
210
|
-
acpSessionId,
|
|
211
|
-
entry,
|
|
212
|
-
state.acpSessionId,
|
|
213
|
-
task,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
return { acpSessionId, protocolSessionId: state.acpSessionId };
|
|
530
|
+
this.sendSpawnedEvent(acpSessionId, entry);
|
|
217
531
|
}
|
|
218
532
|
|
|
219
533
|
/**
|
|
@@ -225,7 +539,7 @@ export class AcpSessionManager {
|
|
|
225
539
|
async steer(acpSessionId: string, instruction: string): Promise<void> {
|
|
226
540
|
const entry = this.sessions.get(acpSessionId);
|
|
227
541
|
if (!entry) {
|
|
228
|
-
throw new
|
|
542
|
+
throw new AcpSessionNotFoundError(acpSessionId);
|
|
229
543
|
}
|
|
230
544
|
|
|
231
545
|
if (entry.state.status !== "running") {
|
|
@@ -258,22 +572,123 @@ export class AcpSessionManager {
|
|
|
258
572
|
);
|
|
259
573
|
}
|
|
260
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Steers a session, transparently resuming it from persisted history
|
|
577
|
+
* first when it is no longer in memory (it completed, or the daemon
|
|
578
|
+
* restarted). The resume and the instruction prompt are atomic from the
|
|
579
|
+
* caller's perspective: a successfully resumed session immediately gets
|
|
580
|
+
* the instruction fired, so it never sits running-idle with no in-flight
|
|
581
|
+
* prompt (and therefore no teardown owner). If the post-resume steer
|
|
582
|
+
* fails, the freshly resumed session is closed (process killed, terminal
|
|
583
|
+
* row persisted, maps cleared) instead of being leaked.
|
|
584
|
+
*
|
|
585
|
+
* When a concurrent caller's resume of the same id is already in flight,
|
|
586
|
+
* this call awaits that resume and then retries the plain steer, so both
|
|
587
|
+
* callers' instructions land on the single resumed session.
|
|
588
|
+
*
|
|
589
|
+
* Error contract for transport callers (acp_steer tool, steer route):
|
|
590
|
+
* - `AcpSessionNotFoundError`: no in-memory session AND no history row.
|
|
591
|
+
* - `AcpResumeError`: the resume (own or a concurrent caller's awaited
|
|
592
|
+
* one, or the steer immediately after an own resume) failed; the
|
|
593
|
+
* message carries the actionable hint.
|
|
594
|
+
* - any other error: the plain steer on an in-memory session failed.
|
|
595
|
+
*/
|
|
596
|
+
async steerOrResume(
|
|
597
|
+
acpSessionId: string,
|
|
598
|
+
instruction: string,
|
|
599
|
+
sendToVellum: (msg: ServerMessage) => void,
|
|
600
|
+
): Promise<{ resumed: boolean }> {
|
|
601
|
+
try {
|
|
602
|
+
await this.steer(acpSessionId, instruction);
|
|
603
|
+
return { resumed: false };
|
|
604
|
+
} catch (err) {
|
|
605
|
+
// Fall through to the in-flight-resume handling both when the session
|
|
606
|
+
// is entirely unknown and when a concurrent resume has already
|
|
607
|
+
// registered its entry but is still initializing: steer rejects with a
|
|
608
|
+
// plain not-running error in that window, yet the resume reservation
|
|
609
|
+
// is live and the retry below will land once it settles.
|
|
610
|
+
if (
|
|
611
|
+
!(err instanceof AcpSessionNotFoundError) &&
|
|
612
|
+
!this.pendingResumes.has(acpSessionId)
|
|
613
|
+
) {
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Another caller's resume of this id may already be in flight (the
|
|
619
|
+
// session is not in memory yet, but its slot is reserved). Await that
|
|
620
|
+
// resume and retry the plain steer once instead of failing
|
|
621
|
+
// resumeFromHistory's already-active guard, which would surface a
|
|
622
|
+
// misleading resume error and drop this instruction.
|
|
623
|
+
const inFlightResume = this.pendingResumes.get(acpSessionId);
|
|
624
|
+
if (inFlightResume) {
|
|
625
|
+
try {
|
|
626
|
+
await inFlightResume;
|
|
627
|
+
} catch (err) {
|
|
628
|
+
throw new AcpResumeError(err);
|
|
629
|
+
}
|
|
630
|
+
// The resumed session is owned by the concurrent caller (its own
|
|
631
|
+
// post-resume steer handles teardown on failure), so a failure here
|
|
632
|
+
// propagates as a plain steer error without closing the session.
|
|
633
|
+
await this.steer(acpSessionId, instruction);
|
|
634
|
+
return { resumed: true };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await this.resumeFromHistory(acpSessionId, sendToVellum);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
// A missing history row keeps its not-found shape; everything else
|
|
641
|
+
// (legacy row without cwd, resolver failure, capability missing)
|
|
642
|
+
// is a resume failure with an actionable message.
|
|
643
|
+
if (err instanceof AcpSessionNotFoundError) throw err;
|
|
644
|
+
throw new AcpResumeError(err);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
await this.steer(acpSessionId, instruction);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
// Tear down the just-resumed session rather than leaving it
|
|
651
|
+
// running-idle with no prompt handler to own its cleanup.
|
|
652
|
+
try {
|
|
653
|
+
this.close(acpSessionId);
|
|
654
|
+
} catch (closeErr) {
|
|
655
|
+
log.warn(
|
|
656
|
+
{ acpSessionId, err: closeErr },
|
|
657
|
+
"Failed to close ACP session after post-resume steer failure",
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
throw new AcpResumeError(err);
|
|
661
|
+
}
|
|
662
|
+
return { resumed: true };
|
|
663
|
+
}
|
|
664
|
+
|
|
261
665
|
/**
|
|
262
666
|
* Cancels an ongoing prompt in the specified session.
|
|
263
667
|
*
|
|
264
|
-
*
|
|
265
|
-
* catch handler in `firePromptInBackground` performs the
|
|
266
|
-
* persistence + teardown
|
|
267
|
-
* preserves "cancelled" instead of overwriting with "failed".
|
|
668
|
+
* When a prompt is in flight, its `prompt()` call rejects in response
|
|
669
|
+
* and the catch handler in `firePromptInBackground` performs the
|
|
670
|
+
* terminal persistence + teardown; we just flip the status here so that
|
|
671
|
+
* handler preserves "cancelled" instead of overwriting with "failed".
|
|
672
|
+
*
|
|
673
|
+
* When NO prompt is in flight there is no handler to own cleanup, so
|
|
674
|
+
* cancel persists and tears down the session itself. (Sessions normally
|
|
675
|
+
* always have a prompt in flight, but a cancel can race the window in
|
|
676
|
+
* steer() between clearing the old prompt and firing the new one.)
|
|
268
677
|
*/
|
|
269
678
|
async cancel(acpSessionId: string): Promise<void> {
|
|
270
679
|
const entry = this.sessions.get(acpSessionId);
|
|
271
680
|
if (!entry) {
|
|
272
|
-
throw new
|
|
681
|
+
throw new AcpSessionNotFoundError(acpSessionId);
|
|
273
682
|
}
|
|
274
683
|
await entry.process.cancel(entry.state.acpSessionId);
|
|
275
684
|
entry.state.status = "cancelled";
|
|
276
685
|
entry.state.completedAt = Date.now();
|
|
686
|
+
// Re-check the map after the await: the in-flight prompt's handler may
|
|
687
|
+
// have already torn the session down while process.cancel was pending.
|
|
688
|
+
if (!entry.currentPrompt && this.sessions.get(acpSessionId) === entry) {
|
|
689
|
+
this.persistTerminal(acpSessionId, entry);
|
|
690
|
+
this.teardownSession(acpSessionId, entry);
|
|
691
|
+
}
|
|
277
692
|
}
|
|
278
693
|
|
|
279
694
|
/**
|
|
@@ -288,7 +703,7 @@ export class AcpSessionManager {
|
|
|
288
703
|
close(acpSessionId: string): void {
|
|
289
704
|
const entry = this.sessions.get(acpSessionId);
|
|
290
705
|
if (!entry) {
|
|
291
|
-
throw new
|
|
706
|
+
throw new AcpSessionNotFoundError(acpSessionId);
|
|
292
707
|
}
|
|
293
708
|
if (
|
|
294
709
|
entry.state.status === "running" ||
|
|
@@ -335,7 +750,7 @@ export class AcpSessionManager {
|
|
|
335
750
|
if (acpSessionId) {
|
|
336
751
|
const entry = this.sessions.get(acpSessionId);
|
|
337
752
|
if (!entry) {
|
|
338
|
-
throw new
|
|
753
|
+
throw new AcpSessionNotFoundError(acpSessionId);
|
|
339
754
|
}
|
|
340
755
|
return entry.state;
|
|
341
756
|
}
|
|
@@ -368,15 +783,18 @@ export class AcpSessionManager {
|
|
|
368
783
|
|
|
369
784
|
/**
|
|
370
785
|
* Persists the session's final state + buffered event log to
|
|
371
|
-
* `acp_session_history`, then frees the buffer entry.
|
|
372
|
-
*
|
|
373
|
-
*
|
|
786
|
+
* `acp_session_history`, then frees the buffer entry. Upserts on id:
|
|
787
|
+
* resumed runs reuse the original vellum session id, so their terminal
|
|
788
|
+
* write must update the existing row (status, event log, etc.) instead of
|
|
789
|
+
* being silently skipped. Best-effort: a DB failure is logged but does
|
|
790
|
+
* not propagate, since the session has already reached a terminal state
|
|
791
|
+
* and clients have been notified.
|
|
374
792
|
*/
|
|
375
793
|
private persistTerminal(acpSessionId: string, entry: SessionEntry): void {
|
|
376
794
|
const buffer = this.eventBuffers.get(acpSessionId) ?? [];
|
|
377
795
|
// Serialize only the wire-shaped updates — drop the byte-size accounting
|
|
378
796
|
// metadata so persisted rows match the protocol shape clients receive.
|
|
379
|
-
const
|
|
797
|
+
const eventLogJson = JSON.stringify(buffer.map((b) => b.update));
|
|
380
798
|
try {
|
|
381
799
|
getDb()
|
|
382
800
|
.insert(acpSessionHistory)
|
|
@@ -390,9 +808,20 @@ export class AcpSessionManager {
|
|
|
390
808
|
status: entry.state.status,
|
|
391
809
|
stopReason: entry.state.stopReason ?? null,
|
|
392
810
|
error: entry.state.error ?? null,
|
|
393
|
-
eventLogJson
|
|
811
|
+
eventLogJson,
|
|
812
|
+
cwd: entry.cwd,
|
|
813
|
+
})
|
|
814
|
+
.onConflictDoUpdate({
|
|
815
|
+
target: acpSessionHistory.id,
|
|
816
|
+
set: {
|
|
817
|
+
status: entry.state.status,
|
|
818
|
+
completedAt: entry.state.completedAt ?? null,
|
|
819
|
+
stopReason: entry.state.stopReason ?? null,
|
|
820
|
+
error: entry.state.error ?? null,
|
|
821
|
+
eventLogJson,
|
|
822
|
+
cwd: entry.cwd,
|
|
823
|
+
},
|
|
394
824
|
})
|
|
395
|
-
.onConflictDoNothing()
|
|
396
825
|
.run();
|
|
397
826
|
} catch (err) {
|
|
398
827
|
log.error(
|
|
@@ -451,10 +880,12 @@ export class AcpSessionManager {
|
|
|
451
880
|
const agentLabel = current.state.agentId;
|
|
452
881
|
const responseText = current.clientHandler.responseText;
|
|
453
882
|
const sessionId = current.state.acpSessionId;
|
|
454
|
-
const
|
|
455
|
-
current.command
|
|
456
|
-
|
|
457
|
-
|
|
883
|
+
const hint = claudeResumeHint(
|
|
884
|
+
current.command,
|
|
885
|
+
current.cwd,
|
|
886
|
+
sessionId,
|
|
887
|
+
);
|
|
888
|
+
const resumeHint = hint ? `\n\n${hint}` : "";
|
|
458
889
|
const notifyMessage = `[ACP agent "${agentLabel}" completed]\n\n${responseText}${resumeHint}`;
|
|
459
890
|
const parentConversation = findConversation(
|
|
460
891
|
current.parentConversationId,
|
|
@@ -517,9 +948,12 @@ export class AcpSessionManager {
|
|
|
517
948
|
}
|
|
518
949
|
|
|
519
950
|
/**
|
|
520
|
-
* Kills all processes on shutdown.
|
|
951
|
+
* Kills all processes on shutdown. Also flags the manager as disposed so
|
|
952
|
+
* resumes that are mid-await when shutdown happens abort before spawning
|
|
953
|
+
* a child process nothing would ever kill.
|
|
521
954
|
*/
|
|
522
955
|
dispose(): void {
|
|
956
|
+
this.disposed = true;
|
|
523
957
|
this.closeAll();
|
|
524
958
|
}
|
|
525
959
|
}
|