@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea
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/Dockerfile +20 -4
- package/bun.lock +2 -2
- package/docker-entrypoint.sh +4 -2
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/examples/plugins/echo/README.md +61 -66
- 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/server-message.ts +3 -3
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +3378 -335
- package/package.json +2 -2
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +37 -87
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -3
- package/src/__tests__/anthropic-provider.test.ts +95 -2
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
- package/src/__tests__/btw-routes.test.ts +62 -3
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- package/src/__tests__/catalog-files.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__/clawhub-files.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.benchmark.test.ts +0 -30
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
- package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-history-web-search.test.ts +11 -1
- package/src/__tests__/conversation-pairing.test.ts +4 -31
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
- package/src/__tests__/conversation-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
- package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
- package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
- package/src/__tests__/conversation-sync-tags.test.ts +27 -15
- package/src/__tests__/conversation-title-service.test.ts +135 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
- package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -0
- 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__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -0
- 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__/identity-intro-cache.test.ts +12 -100
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
- package/src/__tests__/injector-background-turn.test.ts +3 -9
- package/src/__tests__/injector-chain.test.ts +139 -275
- package/src/__tests__/injector-disk-pressure.test.ts +75 -41
- package/src/__tests__/injector-document-comments.test.ts +3 -3
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +31 -37
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/list-messages-page-latest.test.ts +60 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
- package/src/__tests__/llm-usage-store.test.ts +223 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- package/src/__tests__/native-web-search.test.ts +191 -0
- package/src/__tests__/onboarding-template-contract.test.ts +2 -0
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +31 -1
- package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-api-shim.test.ts +3 -6
- package/src/__tests__/plugin-bootstrap.test.ts +14 -40
- package/src/__tests__/plugin-registry.test.ts +3 -76
- package/src/__tests__/plugin-types.test.ts +0 -193
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
- package/src/__tests__/schedule-routes.test.ts +603 -2
- package/src/__tests__/schedule-store.test.ts +41 -0
- package/src/__tests__/schedule-tools.test.ts +35 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/server-history-render.test.ts +314 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/skillssh-files.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- 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__/system-prompt.test.ts +20 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +6 -1
- package/src/__tests__/title-generate-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -2
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- 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 +101 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +196 -0
- package/src/acp/auto-install.ts +177 -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 +83 -29
- package/src/acp/resolve-agent.test.ts +320 -7
- package/src/acp/resolve-agent.ts +182 -18
- package/src/acp/resume-hint.ts +25 -0
- package/src/acp/session-manager.ts +495 -73
- package/src/acp/types.ts +8 -0
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +362 -485
- package/src/api/events/assistant-thinking-delta.ts +33 -0
- package/src/api/events/tool-output-chunk.ts +45 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/trace-event.ts +69 -0
- package/src/api/index.ts +48 -13
- package/src/api/responses/conversation-message.ts +374 -0
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- 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/assistant-feature-flags.ts +22 -11
- package/src/config/bundled-skills/acp/SKILL.md +83 -31
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -398
- package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
- 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/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -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-skills/messaging/SKILL.md +0 -7
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +48 -7
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-v2.ts +8 -0
- package/src/config/schemas/memory-v3.ts +8 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +1 -1
- package/src/context/strip-injections.ts +128 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +5 -7
- package/src/credential-execution/executable-discovery.ts +16 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
- package/src/daemon/conversation-agent-loop.ts +301 -997
- package/src/daemon/conversation-history.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +3 -4
- package/src/daemon/conversation-messaging.ts +7 -6
- package/src/daemon/conversation-process.ts +11 -16
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +218 -398
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +32 -20
- package/src/daemon/external-plugins-bootstrap.ts +17 -18
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +42 -10
- package/src/daemon/lifecycle.ts +25 -0
- package/src/daemon/message-types/apps.ts +1 -29
- package/src/daemon/message-types/messages.ts +9 -57
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +136 -3
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/overflow-reduction-loop.ts +230 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- package/src/daemon/server.ts +2 -0
- package/src/daemon/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +19 -0
- package/src/daemon/wake-target-adapter.ts +3 -1
- 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/home/home-greeting-cache.ts +24 -1
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.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/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
- package/src/memory/auth-fallback-events-store.ts +94 -0
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/memory/graph/conversation-graph-memory.ts +65 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +32 -5
- package/src/memory/llm-usage-store.ts +224 -50
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
- package/src/memory/v2/consolidation-job.ts +14 -5
- package/src/notifications/conversation-pairing.ts +8 -15
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/home-feed-side-effect.ts +12 -1
- package/src/permissions/prompter.ts +4 -0
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +7 -5
- package/src/plugin-api/types.ts +151 -1
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/plugins/defaults/compaction/package.json +1 -1
- package/src/plugins/defaults/compaction/register.ts +8 -19
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/register.ts +8 -13
- package/src/plugins/defaults/index.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
- 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__/health.test.ts +16 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
- 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 +48 -12
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
- 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 +104 -32
- 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 +96 -30
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- 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/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
- package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
- package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
- 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 +51 -45
- 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 +61 -46
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/title-generate/package.json +1 -1
- package/src/plugins/defaults/title-generate/register.ts +18 -18
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +1 -1
- package/src/plugins/defaults/tool-error/register.ts +9 -21
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
- package/src/plugins/external-api.ts +2 -2
- package/src/plugins/pipeline.ts +6 -305
- package/src/plugins/registry.ts +10 -55
- package/src/plugins/types.ts +62 -797
- package/src/plugins/user-loader.ts +30 -127
- package/src/proactive-artifact/aux-message-injector.ts +4 -4
- package/src/proactive-artifact/job.test.ts +8 -13
- package/src/prompts/__tests__/system-prompt.test.ts +42 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +15 -0
- package/src/providers/anthropic/client.ts +37 -29
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -0
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/placeholder-sentinels.ts +35 -0
- package/src/runtime/__tests__/agent-wake.test.ts +10 -6
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +2 -5
- package/src/runtime/assistant-event-hub.ts +37 -7
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
- 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/__tests__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- 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 +3 -1
- package/src/runtime/routes/conversation-routes.ts +372 -185
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/documents-routes.ts +4 -0
- package/src/runtime/routes/domain-routes.ts +51 -37
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +28 -34
- package/src/runtime/routes/gateway-log-routes.ts +26 -4
- package/src/runtime/routes/heartbeat-routes.ts +32 -12
- 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-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +224 -18
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/a2a.ts +12 -10
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +4 -0
- package/src/runtime/routes/integrations/slack/share.ts +27 -6
- package/src/runtime/routes/integrations/telegram.ts +6 -0
- package/src/runtime/routes/integrations/twilio.ts +42 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
- package/src/runtime/routes/log-export-routes.ts +8 -0
- package/src/runtime/routes/memory-v2-routes.ts +15 -8
- package/src/runtime/routes/memory-v3-routes.ts +66 -34
- package/src/runtime/routes/oauth-apps.ts +66 -12
- package/src/runtime/routes/oauth-providers.ts +44 -5
- package/src/runtime/routes/platform-routes.ts +81 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/schedule-routes.ts +152 -42
- package/src/runtime/routes/secret-routes.ts +14 -2
- package/src/runtime/routes/skills-routes.ts +43 -14
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trust-rules-routes.ts +26 -2
- package/src/runtime/routes/tts-routes.ts +35 -0
- package/src/runtime/routes/types.ts +66 -8
- package/src/runtime/routes/usage-routes.ts +47 -39
- package/src/runtime/routes/webhook-routes.ts +41 -2
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +4 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
- package/src/runtime/services/analyze-conversation.ts +2 -2
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- package/src/signals/cancel.ts +2 -4
- package/src/skills/catalog-files.ts +2 -2
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -2
- package/src/skills/skillssh-files.ts +1 -2
- package/src/subagent/manager.ts +17 -5
- package/src/telemetry/types.ts +29 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
- package/src/telemetry/usage-telemetry-reporter.ts +57 -2
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +7 -1
- package/src/tools/acp/spawn.test.ts +158 -55
- package/src/tools/acp/spawn.ts +47 -72
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +13 -8
- package/src/tools/executor.ts +1 -53
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +11 -3
- package/src/tools/network/web-search-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +207 -48
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +34 -5
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- package/src/util/mutex.ts +47 -0
- package/src/workspace/git-service.ts +1 -42
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/docs/plugins.md +0 -836
- package/examples/plugins/echo/register.ts +0 -184
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- 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__/empty-response-pipeline.test.ts +0 -423
- package/src/__tests__/llm-call-pipeline.test.ts +0 -287
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -503
- package/src/__tests__/pipeline-runner.test.ts +0 -564
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
- package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- package/src/home/feature-gate.ts +0 -22
- package/src/memory/v3/provider-blocks.ts +0 -16
- 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/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
- package/src/plugins/defaults/empty-response/terminal.ts +0 -106
- package/src/plugins/defaults/injectors/package.json +0 -15
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
- package/src/plugins/defaults/llm-call/register.ts +0 -45
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
- package/src/plugins/defaults/memory-retrieval/package.json +0 -15
- package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
- 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/persistence/middlewares/persistence.ts +0 -19
- package/src/plugins/defaults/persistence/package.json +0 -15
- package/src/plugins/defaults/persistence/register.ts +0 -38
- package/src/plugins/defaults/persistence/terminal.ts +0 -83
- package/src/plugins/defaults/title-generate/terminal.ts +0 -31
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
- package/src/plugins/defaults/token-estimate/package.json +0 -15
- package/src/plugins/defaults/token-estimate/register.ts +0 -34
- package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
- package/src/plugins/defaults/tool-error/terminal.ts +0 -47
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
- package/src/plugins/defaults/tool-execute/package.json +0 -15
- package/src/plugins/defaults/tool-execute/register.ts +0 -49
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
- package/src/skills/category-inference.ts +0 -111
- /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__/needle.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__/types.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}/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
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AcpSessionManager.resumeFromHistory: reattaching a terminal
|
|
3
|
+
* persisted session via ACP session/resume (preferred, no replay) or
|
|
4
|
+
* session/load (replayed history suppressed), plus the terminal upsert
|
|
5
|
+
* that lets a resumed run update its original history row.
|
|
6
|
+
*
|
|
7
|
+
* The agent process is replaced with a fake whose capabilities and replay
|
|
8
|
+
* behavior are scripted per test; the client handler is the real
|
|
9
|
+
* VellumAcpClientHandler so replay suppression is exercised end to end.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
mock.module("../../util/logger.js", () => ({
|
|
15
|
+
getLogger: () =>
|
|
16
|
+
new Proxy({} as Record<string, unknown>, {
|
|
17
|
+
get: () => () => {},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Fake AcpAgentProcess with scriptable capabilities and history replay.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
interface FakeClient {
|
|
26
|
+
sessionUpdate(params: unknown): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fakeCaps = { loadSession: false, resume: false };
|
|
30
|
+
/** Chunks the fake replays through the client handler during loadSession. */
|
|
31
|
+
let replayChunks: string[] = [];
|
|
32
|
+
/** When set, prompt() throws synchronously (steerOrResume teardown tests). */
|
|
33
|
+
let promptThrowsSync = false;
|
|
34
|
+
/**
|
|
35
|
+
* When set, resumeSession() stalls on this gate. Lets tests observe the
|
|
36
|
+
* post-registration "initializing" window where the SessionEntry exists but
|
|
37
|
+
* the resume has not yet settled.
|
|
38
|
+
*/
|
|
39
|
+
let resumeSessionGate: Promise<void> | null = null;
|
|
40
|
+
const fakeInstances: FakeAcpAgentProcess[] = [];
|
|
41
|
+
|
|
42
|
+
class FakeAcpAgentProcess {
|
|
43
|
+
killed = false;
|
|
44
|
+
spawnedCwd: string | null = null;
|
|
45
|
+
loadSessionCalls: Array<{ sessionId: string; cwd: string }> = [];
|
|
46
|
+
resumeSessionCalls: Array<{ sessionId: string; cwd: string }> = [];
|
|
47
|
+
promptCalls: Array<{ sessionId: string; text: string }> = [];
|
|
48
|
+
resolvePrompt: ((v: { stopReason: string }) => void) | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
public readonly agentId: string,
|
|
52
|
+
public readonly config: {
|
|
53
|
+
command: string;
|
|
54
|
+
args: string[];
|
|
55
|
+
adapterCommand?: string;
|
|
56
|
+
},
|
|
57
|
+
private readonly clientFactory: (agent: unknown) => FakeClient,
|
|
58
|
+
) {
|
|
59
|
+
fakeInstances.push(this);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
spawn(cwd: string): void {
|
|
63
|
+
this.spawnedCwd = cwd;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize(): Promise<void> {}
|
|
67
|
+
|
|
68
|
+
get supportsLoadSession(): boolean {
|
|
69
|
+
return fakeCaps.loadSession;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get supportsSessionResume(): boolean {
|
|
73
|
+
return fakeCaps.resume;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async createSession(_cwd: string): Promise<string> {
|
|
77
|
+
return "proto-new";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async loadSession(sessionId: string, cwd: string): Promise<void> {
|
|
81
|
+
this.loadSessionCalls.push({ sessionId, cwd });
|
|
82
|
+
// Replay history through the client handler before resolving, exactly
|
|
83
|
+
// as a real agent does per the ACP spec for session/load.
|
|
84
|
+
for (const text of replayChunks) {
|
|
85
|
+
await this.emitChunk(text);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async resumeSession(sessionId: string, cwd: string): Promise<void> {
|
|
90
|
+
if (resumeSessionGate) await resumeSessionGate;
|
|
91
|
+
this.resumeSessionCalls.push({ sessionId, cwd });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Drives an agent_message_chunk through the real client handler. */
|
|
95
|
+
async emitChunk(text: string): Promise<void> {
|
|
96
|
+
await this.clientFactory(this).sessionUpdate({
|
|
97
|
+
sessionId: "proto-old",
|
|
98
|
+
update: {
|
|
99
|
+
sessionUpdate: "agent_message_chunk",
|
|
100
|
+
content: { type: "text", text },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
prompt(sessionId: string, text: string): Promise<{ stopReason: string }> {
|
|
106
|
+
if (promptThrowsSync) {
|
|
107
|
+
throw new Error("prompt transport dead");
|
|
108
|
+
}
|
|
109
|
+
this.promptCalls.push({ sessionId, text });
|
|
110
|
+
return new Promise((res) => {
|
|
111
|
+
this.resolvePrompt = res;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async cancel(): Promise<void> {}
|
|
116
|
+
|
|
117
|
+
kill(): void {
|
|
118
|
+
this.killed = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
mock.module("../agent-process.js", () => ({
|
|
123
|
+
AcpAgentProcess: FakeAcpAgentProcess,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// Identity env-prep: credential-broker plumbing has its own suite. Tests
|
|
127
|
+
// that need to observe the manager mid-resume (dispose, pending-id
|
|
128
|
+
// visibility) set `prepareAgentEnvGate` to stall the resume here.
|
|
129
|
+
let prepareAgentEnvGate: Promise<void> | null = null;
|
|
130
|
+
mock.module("../prepare-agent-env.js", () => ({
|
|
131
|
+
prepareAgentEnv: async (agentConfig: unknown) => {
|
|
132
|
+
if (prepareAgentEnvGate) await prepareAgentEnvGate;
|
|
133
|
+
return agentConfig;
|
|
134
|
+
},
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Resolver stub: defaults to resolving every id to the claude adapter.
|
|
138
|
+
type ResolveResult =
|
|
139
|
+
| {
|
|
140
|
+
ok: true;
|
|
141
|
+
agent: { command: string; args: string[]; adapterCommand?: string };
|
|
142
|
+
}
|
|
143
|
+
| { ok: false; reason: "binary_not_found"; hint: string; command: string };
|
|
144
|
+
let resolveImpl: (id: string) => ResolveResult = () => ({
|
|
145
|
+
ok: true,
|
|
146
|
+
agent: { command: "claude-agent-acp", args: [] },
|
|
147
|
+
});
|
|
148
|
+
const realResolveModule = await import("../resolve-agent.js");
|
|
149
|
+
mock.module("../resolve-agent.js", () => ({
|
|
150
|
+
...realResolveModule,
|
|
151
|
+
resolveAcpAgent: (id: string) => resolveImpl(id),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
import type { ServerMessage } from "../../daemon/message-protocol.js";
|
|
155
|
+
import type { AcpSessionUpdate } from "../../daemon/message-types/acp.js";
|
|
156
|
+
import { getSqlite } from "../../memory/db-connection.js";
|
|
157
|
+
import { initializeDb } from "../../memory/db-init.js";
|
|
158
|
+
import type { AcpSessionState } from "../types.js";
|
|
159
|
+
import {
|
|
160
|
+
clearHistory,
|
|
161
|
+
insertHistoryRow,
|
|
162
|
+
readHistoryRow,
|
|
163
|
+
} from "./helpers/acp-history-db.js";
|
|
164
|
+
|
|
165
|
+
const { AcpResumeError, AcpSessionManager, AcpSessionNotFoundError } =
|
|
166
|
+
await import("../session-manager.js");
|
|
167
|
+
|
|
168
|
+
initializeDb();
|
|
169
|
+
|
|
170
|
+
function countHistoryRows(): number {
|
|
171
|
+
const row = getSqlite()
|
|
172
|
+
.query("SELECT COUNT(*) AS n FROM acp_session_history")
|
|
173
|
+
.get() as { n: number };
|
|
174
|
+
return row.n;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Manager internals accessors (mirrors session-manager-persistence.test.ts)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
type ManagerInternals = {
|
|
182
|
+
sessions: Map<string, { clientHandler: FakeClient; command: string }>;
|
|
183
|
+
eventBuffers: Map<string, Array<{ update: AcpSessionUpdate }>>;
|
|
184
|
+
pendingResumes: Map<string, Promise<void>>;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function internals(
|
|
188
|
+
manager: InstanceType<typeof AcpSessionManager>,
|
|
189
|
+
): ManagerInternals {
|
|
190
|
+
return manager as unknown as ManagerInternals;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
clearHistory();
|
|
195
|
+
fakeInstances.length = 0;
|
|
196
|
+
fakeCaps.loadSession = false;
|
|
197
|
+
fakeCaps.resume = false;
|
|
198
|
+
replayChunks = [];
|
|
199
|
+
promptThrowsSync = false;
|
|
200
|
+
prepareAgentEnvGate = null;
|
|
201
|
+
resumeSessionGate = null;
|
|
202
|
+
resolveImpl = () => ({
|
|
203
|
+
ok: true,
|
|
204
|
+
agent: { command: "claude-agent-acp", args: [] },
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const PERSISTED_EVENT: AcpSessionUpdate = {
|
|
209
|
+
type: "acp_session_update",
|
|
210
|
+
acpSessionId: "resume-1",
|
|
211
|
+
updateType: "agent_message_chunk",
|
|
212
|
+
content: "original-run-chunk",
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
describe("AcpSessionManager.resumeFromHistory", () => {
|
|
216
|
+
test("resumes via session/load with replay suppressed, then steers the loaded session", async () => {
|
|
217
|
+
fakeCaps.loadSession = true;
|
|
218
|
+
replayChunks = ["replayed-1", "replayed-2"];
|
|
219
|
+
insertHistoryRow({
|
|
220
|
+
id: "resume-1",
|
|
221
|
+
eventLogJson: JSON.stringify([PERSISTED_EVENT]),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const manager = new AcpSessionManager(4);
|
|
225
|
+
const sent: ServerMessage[] = [];
|
|
226
|
+
await manager.resumeFromHistory("resume-1", (msg) => sent.push(msg));
|
|
227
|
+
|
|
228
|
+
const fake = fakeInstances[0]!;
|
|
229
|
+
expect(fake.spawnedCwd).toBe("/tmp/proj");
|
|
230
|
+
expect(fake.loadSessionCalls).toEqual([
|
|
231
|
+
{ sessionId: "proto-old", cwd: "/tmp/proj" },
|
|
232
|
+
]);
|
|
233
|
+
expect(fake.resumeSessionCalls).toEqual([]);
|
|
234
|
+
|
|
235
|
+
// Replayed history was never forwarded to the sender; only the
|
|
236
|
+
// spawned event went out.
|
|
237
|
+
expect(sent.filter((m) => m.type === "acp_session_update")).toEqual([]);
|
|
238
|
+
expect(sent).toEqual([
|
|
239
|
+
{
|
|
240
|
+
type: "acp_session_spawned",
|
|
241
|
+
acpSessionId: "resume-1",
|
|
242
|
+
agent: "claude",
|
|
243
|
+
parentConversationId: "conv-1",
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
// State reuses the row's identity and is running again.
|
|
248
|
+
const state = manager.getStatus("resume-1") as AcpSessionState;
|
|
249
|
+
expect(state).toMatchObject({
|
|
250
|
+
id: "resume-1",
|
|
251
|
+
agentId: "claude",
|
|
252
|
+
acpSessionId: "proto-old",
|
|
253
|
+
parentConversationId: "conv-1",
|
|
254
|
+
status: "running",
|
|
255
|
+
startedAt: 1234,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Ring buffer was re-seeded from the persisted log only; the replay
|
|
259
|
+
// never reached it.
|
|
260
|
+
const buffer = internals(manager).eventBuffers.get("resume-1")!;
|
|
261
|
+
expect(buffer.map((b) => b.update)).toEqual([PERSISTED_EVENT]);
|
|
262
|
+
|
|
263
|
+
// Updates after the load flow normally (suppression ended).
|
|
264
|
+
await fake.emitChunk("live-after-load");
|
|
265
|
+
expect(sent.filter((m) => m.type === "acp_session_update")).toHaveLength(1);
|
|
266
|
+
expect(internals(manager).eventBuffers.get("resume-1")).toHaveLength(2);
|
|
267
|
+
|
|
268
|
+
// The agent receives the new prompt in the loaded session.
|
|
269
|
+
await manager.steer("resume-1", "follow up please");
|
|
270
|
+
expect(fake.promptCalls).toEqual([
|
|
271
|
+
{ sessionId: "proto-old", text: "follow up please" },
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("prefers session/resume when advertised and never calls loadSession", async () => {
|
|
276
|
+
fakeCaps.loadSession = true;
|
|
277
|
+
fakeCaps.resume = true;
|
|
278
|
+
insertHistoryRow({ id: "resume-2" });
|
|
279
|
+
|
|
280
|
+
const manager = new AcpSessionManager(4);
|
|
281
|
+
await manager.resumeFromHistory("resume-2", () => {});
|
|
282
|
+
|
|
283
|
+
const fake = fakeInstances[0]!;
|
|
284
|
+
expect(fake.resumeSessionCalls).toEqual([
|
|
285
|
+
{ sessionId: "proto-old", cwd: "/tmp/proj" },
|
|
286
|
+
]);
|
|
287
|
+
expect(fake.loadSessionCalls).toEqual([]);
|
|
288
|
+
const state = manager.getStatus("resume-2") as AcpSessionState;
|
|
289
|
+
expect(state.status).toBe("running");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("legacy row with null cwd raises an actionable error", async () => {
|
|
293
|
+
insertHistoryRow({ id: "legacy-1", cwd: null });
|
|
294
|
+
|
|
295
|
+
const manager = new AcpSessionManager(4);
|
|
296
|
+
await expect(
|
|
297
|
+
manager.resumeFromHistory("legacy-1", () => {}),
|
|
298
|
+
).rejects.toThrow(/recorded before resume support/);
|
|
299
|
+
expect(fakeInstances).toHaveLength(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("missing row raises the spawn-style not-found error", async () => {
|
|
303
|
+
const manager = new AcpSessionManager(4);
|
|
304
|
+
await expect(manager.resumeFromHistory("nope-1", () => {})).rejects.toThrow(
|
|
305
|
+
'ACP session "nope-1" not found',
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("row with an empty protocol session id raises an actionable error", async () => {
|
|
310
|
+
insertHistoryRow({ id: "no-proto-1", acpSessionId: "" });
|
|
311
|
+
|
|
312
|
+
const manager = new AcpSessionManager(4);
|
|
313
|
+
await expect(
|
|
314
|
+
manager.resumeFromHistory("no-proto-1", () => {}),
|
|
315
|
+
).rejects.toThrow(/no protocol session id/);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("agent without either capability errors and kills the process", async () => {
|
|
319
|
+
insertHistoryRow({ id: "no-caps-1" });
|
|
320
|
+
|
|
321
|
+
const manager = new AcpSessionManager(4);
|
|
322
|
+
await expect(
|
|
323
|
+
manager.resumeFromHistory("no-caps-1", () => {}),
|
|
324
|
+
).rejects.toThrow('ACP agent "claude" does not support session resume');
|
|
325
|
+
|
|
326
|
+
const fake = fakeInstances[0]!;
|
|
327
|
+
expect(fake.killed).toBe(true);
|
|
328
|
+
expect(internals(manager).sessions.has("no-caps-1")).toBe(false);
|
|
329
|
+
expect(internals(manager).eventBuffers.has("no-caps-1")).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("bunx-rewritten resolver output flows through resume with the canonical adapter command", async () => {
|
|
333
|
+
// resolveAcpAgent rewrites a missing claude-agent-acp binary to run via
|
|
334
|
+
// `bun x --bun <pkg>`; resumeFromHistory re-resolves through it, so the
|
|
335
|
+
// rewritten config must reach the agent process while the SessionEntry
|
|
336
|
+
// keeps the canonical adapter command (resume hints gate on it).
|
|
337
|
+
fakeCaps.resume = true;
|
|
338
|
+
insertHistoryRow({ id: "bunx-resume-1" });
|
|
339
|
+
resolveImpl = () => ({
|
|
340
|
+
ok: true,
|
|
341
|
+
agent: {
|
|
342
|
+
command: "bun",
|
|
343
|
+
args: ["x", "--bun", "@agentclientprotocol/claude-agent-acp"],
|
|
344
|
+
adapterCommand: "claude-agent-acp",
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const manager = new AcpSessionManager(4);
|
|
349
|
+
await manager.resumeFromHistory("bunx-resume-1", () => {});
|
|
350
|
+
|
|
351
|
+
const fake = fakeInstances[0]!;
|
|
352
|
+
expect(fake.config.command).toBe("bun");
|
|
353
|
+
expect(fake.config.args).toEqual([
|
|
354
|
+
"x",
|
|
355
|
+
"--bun",
|
|
356
|
+
"@agentclientprotocol/claude-agent-acp",
|
|
357
|
+
]);
|
|
358
|
+
expect(fake.resumeSessionCalls).toEqual([
|
|
359
|
+
{ sessionId: "proto-old", cwd: "/tmp/proj" },
|
|
360
|
+
]);
|
|
361
|
+
expect(
|
|
362
|
+
internals(manager).sessions.get("bunx-resume-1")!.command,
|
|
363
|
+
).toBe("claude-agent-acp");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("resolver failures surface the actionable hint", async () => {
|
|
367
|
+
insertHistoryRow({ id: "no-bin-1" });
|
|
368
|
+
resolveImpl = () => ({
|
|
369
|
+
ok: false,
|
|
370
|
+
reason: "binary_not_found",
|
|
371
|
+
hint: "npm i -g @agentclientprotocol/claude-agent-acp",
|
|
372
|
+
command: "claude-agent-acp",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const manager = new AcpSessionManager(4);
|
|
376
|
+
await expect(
|
|
377
|
+
manager.resumeFromHistory("no-bin-1", () => {}),
|
|
378
|
+
).rejects.toThrow(
|
|
379
|
+
"claude-agent-acp is not on PATH. npm i -g @agentclientprotocol/claude-agent-acp",
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("already-active id and concurrency limit reuse spawn's guards", async () => {
|
|
384
|
+
fakeCaps.resume = true;
|
|
385
|
+
insertHistoryRow({ id: "active-1" });
|
|
386
|
+
|
|
387
|
+
const manager = new AcpSessionManager(4);
|
|
388
|
+
await manager.resumeFromHistory("active-1", () => {});
|
|
389
|
+
await expect(
|
|
390
|
+
manager.resumeFromHistory("active-1", () => {}),
|
|
391
|
+
).rejects.toThrow('ACP session "active-1" is already active');
|
|
392
|
+
|
|
393
|
+
const full = new AcpSessionManager(0);
|
|
394
|
+
await expect(full.resumeFromHistory("active-1", () => {})).rejects.toThrow(
|
|
395
|
+
/ACP concurrency limit reached \(max 0\)/,
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("terminal persistence after a resumed run upserts the original row with the merged event log", async () => {
|
|
400
|
+
fakeCaps.resume = true;
|
|
401
|
+
insertHistoryRow({
|
|
402
|
+
id: "resume-up-1",
|
|
403
|
+
eventLogJson: JSON.stringify([PERSISTED_EVENT]),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const manager = new AcpSessionManager(4);
|
|
407
|
+
await manager.resumeFromHistory("resume-up-1", () => {});
|
|
408
|
+
await manager.steer("resume-up-1", "continue the work");
|
|
409
|
+
|
|
410
|
+
const fake = fakeInstances[0]!;
|
|
411
|
+
expect(fake.promptCalls).toEqual([
|
|
412
|
+
{ sessionId: "proto-old", text: "continue the work" },
|
|
413
|
+
]);
|
|
414
|
+
|
|
415
|
+
// A new update lands during the resumed run.
|
|
416
|
+
await fake.emitChunk("resumed-run-chunk");
|
|
417
|
+
|
|
418
|
+
// Drive the prompt to completion; yield twice to flush the .then()
|
|
419
|
+
// and the persist call queued behind it.
|
|
420
|
+
fake.resolvePrompt!({ stopReason: "end_turn" });
|
|
421
|
+
await Promise.resolve();
|
|
422
|
+
await Promise.resolve();
|
|
423
|
+
|
|
424
|
+
expect(countHistoryRows()).toBe(1);
|
|
425
|
+
const row = readHistoryRow("resume-up-1")!;
|
|
426
|
+
expect(row.status).toBe("completed");
|
|
427
|
+
expect(row.stop_reason).toBe("end_turn");
|
|
428
|
+
expect(row.started_at).toBe(1234);
|
|
429
|
+
expect(row.cwd).toBe("/tmp/proj");
|
|
430
|
+
|
|
431
|
+
const log = JSON.parse(row.event_log_json) as Array<{ content?: string }>;
|
|
432
|
+
expect(log).toHaveLength(2);
|
|
433
|
+
expect(log[0]!.content).toBe("original-run-chunk");
|
|
434
|
+
expect(log[1]!.content).toBe("resumed-run-chunk");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("concurrent resumes of the same id: one wins, the loser fails cleanly without leaking a process", async () => {
|
|
438
|
+
fakeCaps.resume = true;
|
|
439
|
+
insertHistoryRow({ id: "race-1" });
|
|
440
|
+
|
|
441
|
+
const manager = new AcpSessionManager(4);
|
|
442
|
+
const sent: ServerMessage[] = [];
|
|
443
|
+
const [a, b] = await Promise.allSettled([
|
|
444
|
+
manager.resumeFromHistory("race-1", (msg) => sent.push(msg)),
|
|
445
|
+
manager.resumeFromHistory("race-1", (msg) => sent.push(msg)),
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
// Exactly one resume wins; the other fails the synchronous guard.
|
|
449
|
+
expect([a.status, b.status].sort()).toEqual(["fulfilled", "rejected"]);
|
|
450
|
+
const rejected = (a.status === "rejected" ? a : b) as PromiseRejectedResult;
|
|
451
|
+
expect(String(rejected.reason)).toContain("is already active");
|
|
452
|
+
|
|
453
|
+
// Only one child process was ever constructed and it is still alive
|
|
454
|
+
// (the loser never got far enough to spawn-and-leak a second one).
|
|
455
|
+
expect(fakeInstances).toHaveLength(1);
|
|
456
|
+
expect(fakeInstances[0]!.killed).toBe(false);
|
|
457
|
+
expect((manager.getStatus("race-1") as AcpSessionState).status).toBe(
|
|
458
|
+
"running",
|
|
459
|
+
);
|
|
460
|
+
// Exactly one spawned event went out (single stream, not doubled).
|
|
461
|
+
expect(sent.filter((m) => m.type === "acp_session_spawned")).toHaveLength(
|
|
462
|
+
1,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("concurrent resumes of distinct ids cannot exceed maxConcurrent", async () => {
|
|
467
|
+
fakeCaps.resume = true;
|
|
468
|
+
insertHistoryRow({ id: "cap-1" });
|
|
469
|
+
insertHistoryRow({ id: "cap-2" });
|
|
470
|
+
|
|
471
|
+
const manager = new AcpSessionManager(1);
|
|
472
|
+
const [a, b] = await Promise.allSettled([
|
|
473
|
+
manager.resumeFromHistory("cap-1", () => {}),
|
|
474
|
+
manager.resumeFromHistory("cap-2", () => {}),
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
expect([a.status, b.status].sort()).toEqual(["fulfilled", "rejected"]);
|
|
478
|
+
const rejected = (a.status === "rejected" ? a : b) as PromiseRejectedResult;
|
|
479
|
+
expect(String(rejected.reason)).toMatch(
|
|
480
|
+
/ACP concurrency limit reached \(max 1\)/,
|
|
481
|
+
);
|
|
482
|
+
expect(fakeInstances).toHaveLength(1);
|
|
483
|
+
expect(manager.getStatus() as AcpSessionState[]).toHaveLength(1);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("getActiveAndPendingIds surfaces a resume still awaiting env prep", async () => {
|
|
487
|
+
fakeCaps.resume = true;
|
|
488
|
+
insertHistoryRow({ id: "pending-vis-1" });
|
|
489
|
+
let release!: () => void;
|
|
490
|
+
prepareAgentEnvGate = new Promise((res) => {
|
|
491
|
+
release = res;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const manager = new AcpSessionManager(4);
|
|
495
|
+
const promise = manager.resumeFromHistory("pending-vis-1", () => {});
|
|
496
|
+
|
|
497
|
+
// Mid-resume: no SessionEntry yet (getStatus throws not-found), but the
|
|
498
|
+
// id must already be visible to the delete guards.
|
|
499
|
+
expect(() => manager.getStatus("pending-vis-1")).toThrow(/not found/);
|
|
500
|
+
expect(manager.getActiveAndPendingIds()).toEqual(["pending-vis-1"]);
|
|
501
|
+
|
|
502
|
+
release();
|
|
503
|
+
await promise;
|
|
504
|
+
|
|
505
|
+
// After the resume lands the id comes from the sessions map and the
|
|
506
|
+
// reservation is gone (no duplicate).
|
|
507
|
+
expect(manager.getActiveAndPendingIds()).toEqual(["pending-vis-1"]);
|
|
508
|
+
expect(internals(manager).pendingResumes.size).toBe(0);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("dispose during prepareAgentEnv aborts the resume before any process spawns", async () => {
|
|
512
|
+
fakeCaps.resume = true;
|
|
513
|
+
insertHistoryRow({ id: "dispose-1" });
|
|
514
|
+
let release!: () => void;
|
|
515
|
+
prepareAgentEnvGate = new Promise((res) => {
|
|
516
|
+
release = res;
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const manager = new AcpSessionManager(4);
|
|
520
|
+
const promise = manager.resumeFromHistory("dispose-1", () => {});
|
|
521
|
+
manager.dispose();
|
|
522
|
+
release();
|
|
523
|
+
|
|
524
|
+
await expect(promise).rejects.toThrow(/disposed/);
|
|
525
|
+
// No child process was ever constructed on the disposed manager, and
|
|
526
|
+
// the reservation was released.
|
|
527
|
+
expect(fakeInstances).toHaveLength(0);
|
|
528
|
+
expect(internals(manager).pendingResumes.size).toBe(0);
|
|
529
|
+
expect(internals(manager).sessions.size).toBe(0);
|
|
530
|
+
expect(internals(manager).eventBuffers.size).toBe(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("cancel on a resumed session with no in-flight prompt persists and tears down", async () => {
|
|
534
|
+
fakeCaps.resume = true;
|
|
535
|
+
insertHistoryRow({
|
|
536
|
+
id: "idle-1",
|
|
537
|
+
eventLogJson: JSON.stringify([PERSISTED_EVENT]),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const manager = new AcpSessionManager(4);
|
|
541
|
+
await manager.resumeFromHistory("idle-1", () => {});
|
|
542
|
+
|
|
543
|
+
// No prompt is in flight, so cancel() itself must own the terminal
|
|
544
|
+
// persistence + teardown (there is no prompt handler to do it).
|
|
545
|
+
await manager.cancel("idle-1");
|
|
546
|
+
|
|
547
|
+
expect(fakeInstances[0]!.killed).toBe(true);
|
|
548
|
+
expect(internals(manager).sessions.has("idle-1")).toBe(false);
|
|
549
|
+
expect(internals(manager).eventBuffers.has("idle-1")).toBe(false);
|
|
550
|
+
|
|
551
|
+
const row = readHistoryRow("idle-1")!;
|
|
552
|
+
expect(row.status).toBe("cancelled");
|
|
553
|
+
expect(row.completed_at).not.toBeNull();
|
|
554
|
+
// The re-seeded event log survived the cancel-side persistence.
|
|
555
|
+
const log = JSON.parse(row.event_log_json) as Array<{ content?: string }>;
|
|
556
|
+
expect(log).toHaveLength(1);
|
|
557
|
+
expect(log[0]!.content).toBe("original-run-chunk");
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("AcpSessionManager.steerOrResume", () => {
|
|
562
|
+
test("steers an in-memory session directly without a resume", async () => {
|
|
563
|
+
fakeCaps.resume = true;
|
|
564
|
+
insertHistoryRow({ id: "sor-live-1" });
|
|
565
|
+
|
|
566
|
+
const manager = new AcpSessionManager(4);
|
|
567
|
+
await manager.resumeFromHistory("sor-live-1", () => {});
|
|
568
|
+
expect(fakeInstances).toHaveLength(1);
|
|
569
|
+
|
|
570
|
+
const result = await manager.steerOrResume(
|
|
571
|
+
"sor-live-1",
|
|
572
|
+
"go faster",
|
|
573
|
+
() => {},
|
|
574
|
+
);
|
|
575
|
+
expect(result).toEqual({ resumed: false });
|
|
576
|
+
// No second process was constructed.
|
|
577
|
+
expect(fakeInstances).toHaveLength(1);
|
|
578
|
+
expect(fakeInstances[0]!.promptCalls).toEqual([
|
|
579
|
+
{ sessionId: "proto-old", text: "go faster" },
|
|
580
|
+
]);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("session not in memory: resumes from history and fires the instruction atomically", async () => {
|
|
584
|
+
fakeCaps.resume = true;
|
|
585
|
+
insertHistoryRow({ id: "sor-resume-1" });
|
|
586
|
+
|
|
587
|
+
const manager = new AcpSessionManager(4);
|
|
588
|
+
const sent: ServerMessage[] = [];
|
|
589
|
+
const result = await manager.steerOrResume(
|
|
590
|
+
"sor-resume-1",
|
|
591
|
+
"continue the work",
|
|
592
|
+
(msg) => sent.push(msg),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
expect(result).toEqual({ resumed: true });
|
|
596
|
+
const fake = fakeInstances[0]!;
|
|
597
|
+
expect(fake.resumeSessionCalls).toEqual([
|
|
598
|
+
{ sessionId: "proto-old", cwd: "/tmp/proj" },
|
|
599
|
+
]);
|
|
600
|
+
// The instruction prompt fired immediately after the resume - the
|
|
601
|
+
// session never sat running-idle with no in-flight prompt.
|
|
602
|
+
expect(fake.promptCalls).toEqual([
|
|
603
|
+
{ sessionId: "proto-old", text: "continue the work" },
|
|
604
|
+
]);
|
|
605
|
+
expect(sent.filter((m) => m.type === "acp_session_spawned")).toHaveLength(
|
|
606
|
+
1,
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("missing history row keeps the typed not-found error", async () => {
|
|
611
|
+
const manager = new AcpSessionManager(4);
|
|
612
|
+
const promise = manager.steerOrResume("sor-missing-1", "go", () => {});
|
|
613
|
+
await expect(promise).rejects.toBeInstanceOf(AcpSessionNotFoundError);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("resume failure surfaces as AcpResumeError with the actionable hint", async () => {
|
|
617
|
+
insertHistoryRow({ id: "sor-legacy-1", cwd: null });
|
|
618
|
+
|
|
619
|
+
const manager = new AcpSessionManager(4);
|
|
620
|
+
const promise = manager.steerOrResume("sor-legacy-1", "go", () => {});
|
|
621
|
+
await expect(promise).rejects.toBeInstanceOf(AcpResumeError);
|
|
622
|
+
await expect(promise).rejects.toThrow(/recorded before resume support/);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("concurrent steerOrResume on the same id: the loser awaits the in-flight resume and steers", async () => {
|
|
626
|
+
fakeCaps.resume = true;
|
|
627
|
+
insertHistoryRow({ id: "sor-race-1" });
|
|
628
|
+
|
|
629
|
+
const manager = new AcpSessionManager(4);
|
|
630
|
+
const sent: ServerMessage[] = [];
|
|
631
|
+
const [a, b] = await Promise.all([
|
|
632
|
+
manager.steerOrResume("sor-race-1", "first instruction", (msg) =>
|
|
633
|
+
sent.push(msg),
|
|
634
|
+
),
|
|
635
|
+
manager.steerOrResume("sor-race-1", "second instruction", (msg) =>
|
|
636
|
+
sent.push(msg),
|
|
637
|
+
),
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
// Neither caller got the misleading "already active" resume error;
|
|
641
|
+
// both landed their instruction on the single resumed session.
|
|
642
|
+
expect(a).toEqual({ resumed: true });
|
|
643
|
+
expect(b).toEqual({ resumed: true });
|
|
644
|
+
expect(fakeInstances).toHaveLength(1);
|
|
645
|
+
const fake = fakeInstances[0]!;
|
|
646
|
+
expect(fake.resumeSessionCalls).toHaveLength(1);
|
|
647
|
+
expect(fake.promptCalls.map((c) => c.text).sort()).toEqual([
|
|
648
|
+
"first instruction",
|
|
649
|
+
"second instruction",
|
|
650
|
+
]);
|
|
651
|
+
// Single spawned event: one resume happened, not two.
|
|
652
|
+
expect(sent.filter((m) => m.type === "acp_session_spawned")).toHaveLength(
|
|
653
|
+
1,
|
|
654
|
+
);
|
|
655
|
+
expect((manager.getStatus("sor-race-1") as AcpSessionState).status).toBe(
|
|
656
|
+
"running",
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("steerOrResume arriving in the initializing window awaits the in-flight resume and steers", async () => {
|
|
661
|
+
fakeCaps.resume = true;
|
|
662
|
+
insertHistoryRow({ id: "sor-init-1" });
|
|
663
|
+
let release!: () => void;
|
|
664
|
+
resumeSessionGate = new Promise((res) => {
|
|
665
|
+
release = res;
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const manager = new AcpSessionManager(4);
|
|
669
|
+
const sent: ServerMessage[] = [];
|
|
670
|
+
const first = manager.steerOrResume("sor-init-1", "first instruction", (msg) =>
|
|
671
|
+
sent.push(msg),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// Advance microtasks until the first call's resume has registered its
|
|
675
|
+
// SessionEntry but is still gated inside resumeSession: the session
|
|
676
|
+
// exists in memory with status "initializing".
|
|
677
|
+
while (!internals(manager).sessions.has("sor-init-1")) {
|
|
678
|
+
await Promise.resolve();
|
|
679
|
+
}
|
|
680
|
+
expect((manager.getStatus("sor-init-1") as AcpSessionState).status).toBe(
|
|
681
|
+
"initializing",
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// A steerOrResume in this window previously rethrew the plain
|
|
685
|
+
// "is not running (status: initializing)" error and dropped the
|
|
686
|
+
// instruction. It must instead await the in-flight resume and retry.
|
|
687
|
+
const second = manager.steerOrResume(
|
|
688
|
+
"sor-init-1",
|
|
689
|
+
"second instruction",
|
|
690
|
+
(msg) => sent.push(msg),
|
|
691
|
+
);
|
|
692
|
+
// Let the second call hit the initializing steer failure and park on
|
|
693
|
+
// the in-flight resume before releasing the gate.
|
|
694
|
+
await Promise.resolve();
|
|
695
|
+
await Promise.resolve();
|
|
696
|
+
release();
|
|
697
|
+
|
|
698
|
+
const [a, b] = await Promise.all([first, second]);
|
|
699
|
+
expect(a).toEqual({ resumed: true });
|
|
700
|
+
expect(b).toEqual({ resumed: true });
|
|
701
|
+
|
|
702
|
+
// Exactly one process was constructed and one resume happened; both
|
|
703
|
+
// instructions landed on the single resumed session after it settled.
|
|
704
|
+
expect(fakeInstances).toHaveLength(1);
|
|
705
|
+
const fake = fakeInstances[0]!;
|
|
706
|
+
expect(fake.resumeSessionCalls).toHaveLength(1);
|
|
707
|
+
expect(fake.promptCalls.map((c) => c.text).sort()).toEqual([
|
|
708
|
+
"first instruction",
|
|
709
|
+
"second instruction",
|
|
710
|
+
]);
|
|
711
|
+
expect(sent.filter((m) => m.type === "acp_session_spawned")).toHaveLength(
|
|
712
|
+
1,
|
|
713
|
+
);
|
|
714
|
+
expect((manager.getStatus("sor-init-1") as AcpSessionState).status).toBe(
|
|
715
|
+
"running",
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("post-resume steer failure tears the resumed session down instead of leaving it idle", async () => {
|
|
720
|
+
fakeCaps.resume = true;
|
|
721
|
+
insertHistoryRow({ id: "sor-fail-1" });
|
|
722
|
+
|
|
723
|
+
const manager = new AcpSessionManager(4);
|
|
724
|
+
promptThrowsSync = true;
|
|
725
|
+
const promise = manager.steerOrResume("sor-fail-1", "go", () => {});
|
|
726
|
+
await expect(promise).rejects.toBeInstanceOf(AcpResumeError);
|
|
727
|
+
await expect(promise).rejects.toThrow("prompt transport dead");
|
|
728
|
+
|
|
729
|
+
// The resumed session did not leak: process killed, maps cleared,
|
|
730
|
+
// terminal row persisted.
|
|
731
|
+
expect(fakeInstances[0]!.killed).toBe(true);
|
|
732
|
+
expect(internals(manager).sessions.has("sor-fail-1")).toBe(false);
|
|
733
|
+
expect(internals(manager).eventBuffers.has("sor-fail-1")).toBe(false);
|
|
734
|
+
expect(readHistoryRow("sor-fail-1")!.status).toBe("cancelled");
|
|
735
|
+
});
|
|
736
|
+
});
|