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