@posthog/agent 2.3.727 → 2.3.736
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/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +2 -10
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +24 -10
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/permissions/permission-options.js +6 -4
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/session/jsonl-hydration.js +1 -0
- package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -1
- package/dist/adapters/claude/session/models.js +20 -1
- package/dist/adapters/claude/session/models.js.map +1 -1
- package/dist/adapters/claude/tools.js +4 -2
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/adapters/reasoning-effort.js +3 -1
- package/dist/adapters/reasoning-effort.js.map +1 -1
- package/dist/agent.js +544 -74
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +4 -4
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +544 -74
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +544 -74
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +4 -4
- package/src/adapters/claude/UPSTREAM.md +81 -3
- package/src/adapters/claude/claude-agent.slash-command.test.ts +22 -2
- package/src/adapters/claude/claude-agent.ts +232 -19
- package/src/adapters/claude/conversion/sdk-to-acp.ts +182 -16
- package/src/adapters/claude/conversion/task-state.test.ts +338 -0
- package/src/adapters/claude/conversion/task-state.ts +178 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +29 -19
- package/src/adapters/claude/hooks.test.ts +162 -0
- package/src/adapters/claude/hooks.ts +44 -3
- package/src/adapters/claude/permissions/permission-options.ts +7 -2
- package/src/adapters/claude/session/commands.ts +1 -0
- package/src/adapters/claude/session/mcp-config.ts +23 -7
- package/src/adapters/claude/session/model-config.test.ts +60 -0
- package/src/adapters/claude/session/model-config.ts +56 -0
- package/src/adapters/claude/session/models.test.ts +119 -0
- package/src/adapters/claude/session/models.ts +31 -0
- package/src/adapters/claude/session/options.test.ts +1 -0
- package/src/adapters/claude/session/options.ts +29 -1
- package/src/adapters/claude/session/settings.test.ts +102 -1
- package/src/adapters/claude/session/settings.ts +33 -6
- package/src/adapters/claude/tools.ts +4 -2
- package/src/adapters/claude/types.ts +15 -0
- package/src/adapters/codex/codex-agent.ts +2 -2
- package/src/server/agent-server.test.ts +173 -0
- package/src/test/mocks/claude-sdk.ts +4 -0
- package/src/test/native-binary.test.ts +27 -0
- package/dist/claude-cli/cli.js +0 -18412
- package/dist/claude-cli/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/COPYING +0 -3
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/yoga.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.736",
|
|
4
4
|
"repository": "https://github.com/PostHog/code",
|
|
5
5
|
"description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
6
6
|
"exports": {
|
|
@@ -111,9 +111,9 @@
|
|
|
111
111
|
"@posthog/enricher": "1.0.0"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
|
-
"@agentclientprotocol/sdk": "0.
|
|
115
|
-
"@anthropic-ai/claude-agent-sdk": "0.
|
|
116
|
-
"@anthropic-ai/sdk": "0.
|
|
114
|
+
"@agentclientprotocol/sdk": "0.22.1",
|
|
115
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.154",
|
|
116
|
+
"@anthropic-ai/sdk": "0.100.0",
|
|
117
117
|
"@hono/node-server": "^1.19.9",
|
|
118
118
|
"@opentelemetry/api-logs": "^0.208.0",
|
|
119
119
|
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
|
@@ -5,8 +5,8 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
|
|
|
5
5
|
## Fork Point
|
|
6
6
|
|
|
7
7
|
- **Forked**: v0.10.9, commit `5411e0f4`, Dec 2 2025
|
|
8
|
-
- **Last sync**: v0.
|
|
9
|
-
- **SDK**: `@anthropic-ai/claude-agent-sdk` 0.
|
|
8
|
+
- **Last sync**: v0.38.0 + #716, commit `61ebda2`, May 28 2026
|
|
9
|
+
- **SDK**: `@anthropic-ai/claude-agent-sdk` 0.3.154, `@agentclientprotocol/sdk` 0.22.1, `@anthropic-ai/sdk` 0.100.0
|
|
10
10
|
|
|
11
11
|
## File Mapping
|
|
12
12
|
|
|
@@ -53,6 +53,7 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
|
|
|
53
53
|
| Auth methods | `claude-ai-login` + `console-login` | Returns empty `authMethods` | Auth handled externally |
|
|
54
54
|
| Session fingerprinting | Implicit teardown on cwd/mcp change | Explicit `refreshSession()` | Caller-initiated is more predictable |
|
|
55
55
|
| Shutdown on ACP close | Process exits | No standalone process | Agent is embedded in server |
|
|
56
|
+
| Unsupported slash commands | Loops silently on early idle | Emits "Unsupported slash command" chunk, gated on `initializationResult().commands` so plugin/skill commands (e.g. `/skills-store`) whose echoes use a fresh uuid are not false-flagged | The SDK consumes some slash commands without producing output (e.g. `/plugin` in non-interactive mode); without this we hang. The known-commands gate avoids racing plugin/skill loads where idle can arrive before the transformed user-message echo. |
|
|
56
57
|
|
|
57
58
|
## Changes Ported in v0.30.0 Sync
|
|
58
59
|
|
|
@@ -66,6 +67,83 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
|
|
|
66
67
|
- **Effort level sync** (v0.25.x): `xhigh` level added, `applyFlagSettings` on effort change
|
|
67
68
|
- **Auto permission mode** (v0.25.0): Added to `CODE_EXECUTION_MODES`, available modes, ExitPlanMode options
|
|
68
69
|
|
|
70
|
+
## Changes Ported in v0.38.0 Sync
|
|
71
|
+
|
|
72
|
+
- **SDK bumps**: claude-agent-sdk 0.3.144 -> 0.3.154, anthropic SDK 0.96.0 -> 0.100.0 (ACP SDK
|
|
73
|
+
unchanged at 0.22.1).
|
|
74
|
+
- **Compaction state-flag fix** (#716, a172885): SDK 0.3.154 emits the terminal `status` carrying
|
|
75
|
+
`compact_result` twice for failed compactions. Added a per-turn `compactionInProgress` flag in
|
|
76
|
+
`prompt()` so the user sees a single `Compacting completed.` / `Compacting failed: <reason>`
|
|
77
|
+
chunk. Manual `/compact` outcomes now surface here rather than via `compact_boundary` (which only
|
|
78
|
+
fires when there's content to compact).
|
|
79
|
+
- **System-role guard on user/assistant handler** (#716, a172885): Added an early return in
|
|
80
|
+
`handleUserAssistantMessage` for `message.message.role === "system"`, covering both upstream's
|
|
81
|
+
`<local-command-stdout>` strip branch guard and the broader assistant-handler guard. Avoids
|
|
82
|
+
rendering SDK-injected system reminders as user-visible chunks.
|
|
83
|
+
- **New no-op content block types** (#716, a172885): Added `advisor_tool_result` and
|
|
84
|
+
`mid_conv_system` cases to `processContentChunk` so unknown content blocks don't trip the
|
|
85
|
+
`unreachable` default.
|
|
86
|
+
- **Opus 4.8 model entries** (#718, 98b54a0): Added `claude-opus-4-8` to gateway model maps with
|
|
87
|
+
1M context, effort and xhigh-effort support. MCP injection auto-included (Haiku exclusion only).
|
|
88
|
+
|
|
89
|
+
## Skipped in v0.38.0 Sync
|
|
90
|
+
|
|
91
|
+
- **Remove hide Claude auth flag** (#707, 7ed1daf): Our fork already returns `authMethods: []`
|
|
92
|
+
unconditionally; no flag to remove.
|
|
93
|
+
- **`thinking_tokens` status case** (#716, a172885): Our `handleSystemMessage` switch on
|
|
94
|
+
`status === "compacting"` is non-exhaustive (no default `unreachable`), so unknown status values
|
|
95
|
+
already no-op harmlessly.
|
|
96
|
+
- **Empty CI-retry commit** (#718, 98b54a0): No code change in the commit itself; the model entries
|
|
97
|
+
it carried are ported above.
|
|
98
|
+
- **`MessageDisplay` hook + `SessionStart` reloadSkills/sessionTitle** (SDK 0.3.152): Available in
|
|
99
|
+
the bumped SDK but not wired into our fork; upstream doesn't consume them in #716 either. Defer
|
|
100
|
+
to a focused PR if we want the capability.
|
|
101
|
+
|
|
102
|
+
## Changes Ported in v0.37.0 Sync
|
|
103
|
+
|
|
104
|
+
- **SDK bumps**: claude-agent-sdk 0.2.114 -> 0.3.144, ACP SDK 0.19.0 -> 0.22.1, anthropic SDK 0.89.0 -> 0.96.0
|
|
105
|
+
- **TodoWrite -> Task tools migration** (SDK 0.3.142): Replaced TodoWrite snapshot tool with incremental
|
|
106
|
+
TaskCreate/TaskUpdate/TaskGet/TaskList. Added `conversion/task-state.ts` and `createTaskHook` to mirror the
|
|
107
|
+
SDK `TaskCreated`/`TaskCompleted` hook events into a per-session task map; plan entries are derived from
|
|
108
|
+
Map insertion order (preserves upstream ordering semantics).
|
|
109
|
+
- **MCP_CONNECTION_NONBLOCKING=0** (SDK 0.3.142): SDK changed MCP servers to background-connect by default;
|
|
110
|
+
set env to restore blocking-connect behavior so MCP tools are available on first prompt.
|
|
111
|
+
- **ACP SDK 0.22 breaking changes**: Renamed `unstable_resumeSession` -> `resumeSession`; new
|
|
112
|
+
`McpSdkServerConfig` variant (`type: "sdk"`) in the `McpServerConfig` union. Our
|
|
113
|
+
`parseMcpServers` only accepts `http`/`sse`/stdio entries, so `sdk` falls through and is
|
|
114
|
+
implicitly dropped (no explicit filter needed).
|
|
115
|
+
- **Skills option** (SDK 0.2.133): `'Skill'` in `allowedTools` deprecated; replaced with `skills` option.
|
|
116
|
+
- **Memory recall tool calls** (#703, a0bfb98): Emit a `tool_call` for SDK `memory_recall` events so the
|
|
117
|
+
UI shows what memories were surfaced; addresses phantom MEMORY.md read attempts.
|
|
118
|
+
- **Write diff fix** (#618, 8d7e220): `toolUpdateFromEditToolResponse` now also processes `Write` tool
|
|
119
|
+
responses so overwrites show real diffs instead of optimistic "creation" diffs.
|
|
120
|
+
- **Local-command-stdout render** (#649, 3b9b7d5): Strip marker tags from `<local-command-stdout>` content
|
|
121
|
+
and render remaining prose so custom slash commands and skill expansions reach the UI.
|
|
122
|
+
- **Cancelled vs end_turn** (#694, 2414a6f): `session_state_changed: idle` handler now reports
|
|
123
|
+
`stopReason: "cancelled"` when the session was interrupted.
|
|
124
|
+
- **Recover prompt stream** (#706, 2711f50): After a failed turn, drain the trailing
|
|
125
|
+
`session_state_changed: idle` so the next prompt's first `query.next()` doesn't short-circuit.
|
|
126
|
+
- **additionalDirectories field** (#684, f37e9a0): Accept the official ACP field on session lifecycle
|
|
127
|
+
requests; advertise via `sessionCapabilities.additionalDirectories`. Legacy `_meta.additionalRoots` still
|
|
128
|
+
honored as fallback.
|
|
129
|
+
- **availableModels allowlist** (#637, 867a3a0): `ClaudeCodeSettings.availableModels` array merged-and-deduped
|
|
130
|
+
across settings sources, then applied to gateway model options via `applyAvailableModelsAllowlist`.
|
|
131
|
+
- **Model alias version match** (#702, e1e1c69): Refuse cross-version alias matches in `resolveModelPreference`
|
|
132
|
+
so `claude-opus-4-6` doesn't get copied onto the `opus` alias when it resolves to 4.7.
|
|
133
|
+
- **Hide /clear** (#705, cfce130): `/clear` removed from advertised commands; clients should use
|
|
134
|
+
`session/new` for the same effect.
|
|
135
|
+
- **No-op ping events** (#698, 694221a): `streamEventToAcpNotifications` no-ops `ping` keep-alive events
|
|
136
|
+
instead of falling through to `unreachable` and spamming stderr.
|
|
137
|
+
|
|
138
|
+
## Skipped in v0.37.0 Sync
|
|
139
|
+
|
|
140
|
+
- **Avoid redundant initial model sync** (#704, b275f6f): Our flow already guards `setModel` behind
|
|
141
|
+
`!isResume && resolvedSdkModel !== DEFAULT_MODEL`, so the upstream optimization is redundant.
|
|
142
|
+
- **Default effort option** (#701, 9e259d1): Our effort options are model-class-based rather than
|
|
143
|
+
SDK-supplied; the implicit no-override path already covers the "let SDK decide" case.
|
|
144
|
+
- **Gate auto mode on model support** (#604, ec47d34): Our `auto` mode is gated behind `ALLOW_BYPASS`,
|
|
145
|
+
not per-model `supportsAutoMode`. Per-model gating would be a larger refactor.
|
|
146
|
+
|
|
69
147
|
## Skipped in v0.30.0 Sync
|
|
70
148
|
|
|
71
149
|
- **Separate auth methods** (v0.25.0): PostHog returns empty authMethods
|
|
@@ -74,7 +152,7 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
|
|
|
74
152
|
|
|
75
153
|
## Next Sync
|
|
76
154
|
|
|
77
|
-
1. Check upstream changelog since v0.
|
|
155
|
+
1. Check upstream changelog since v0.37.0
|
|
78
156
|
2. Diff upstream source against PostHog Code using the file mapping above
|
|
79
157
|
3. Port in phases: bug fixes first, then features
|
|
80
158
|
4. After each phase: `pnpm --filter agent typecheck && pnpm --filter agent build && pnpm lint`
|
|
@@ -34,7 +34,11 @@ function makeAgent(): { agent: Agent; client: ClientMocks } {
|
|
|
34
34
|
return { agent, client };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function installFakeSession(
|
|
37
|
+
function installFakeSession(
|
|
38
|
+
agent: Agent,
|
|
39
|
+
sessionId: string,
|
|
40
|
+
knownSlashCommands?: Set<string>,
|
|
41
|
+
): MockQuery {
|
|
38
42
|
const query = createMockQuery();
|
|
39
43
|
const input = new Pushable();
|
|
40
44
|
const abortController = new AbortController();
|
|
@@ -63,6 +67,7 @@ function installFakeSession(agent: Agent, sessionId: string): MockQuery {
|
|
|
63
67
|
taskRunId: "run-1",
|
|
64
68
|
lastContextWindowSize: 200_000,
|
|
65
69
|
modelId: "claude-sonnet-4-6",
|
|
70
|
+
knownSlashCommands,
|
|
66
71
|
};
|
|
67
72
|
|
|
68
73
|
(agent as unknown as { session: typeof session }).session = session;
|
|
@@ -99,6 +104,7 @@ describe("ClaudeAcpAgent.prompt — early idle handling", () => {
|
|
|
99
104
|
label: "unsupported slash command surfaces error and ends turn",
|
|
100
105
|
sessionId: "s-slash",
|
|
101
106
|
prompt: "/plugin install slack",
|
|
107
|
+
knownCommands: undefined,
|
|
102
108
|
expectsUnsupportedChunk: true,
|
|
103
109
|
commandInMessage: "/plugin",
|
|
104
110
|
},
|
|
@@ -106,6 +112,16 @@ describe("ClaudeAcpAgent.prompt — early idle handling", () => {
|
|
|
106
112
|
label: "non-slash prompt with early idle is silently skipped",
|
|
107
113
|
sessionId: "s-regular",
|
|
108
114
|
prompt: "hello",
|
|
115
|
+
knownCommands: undefined,
|
|
116
|
+
expectsUnsupportedChunk: false,
|
|
117
|
+
commandInMessage: null,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
label:
|
|
121
|
+
"known plugin/skill command with early idle is not flagged as unsupported",
|
|
122
|
+
sessionId: "s-skill",
|
|
123
|
+
prompt: "/skills-store use my address pr review skill",
|
|
124
|
+
knownCommands: new Set(["skills-store"]),
|
|
109
125
|
expectsUnsupportedChunk: false,
|
|
110
126
|
commandInMessage: null,
|
|
111
127
|
},
|
|
@@ -113,7 +129,11 @@ describe("ClaudeAcpAgent.prompt — early idle handling", () => {
|
|
|
113
129
|
|
|
114
130
|
it.each(cases)("$label", async (tc) => {
|
|
115
131
|
const { agent, client } = makeAgent();
|
|
116
|
-
const query = installFakeSession(
|
|
132
|
+
const query = installFakeSession(
|
|
133
|
+
agent,
|
|
134
|
+
tc.sessionId,
|
|
135
|
+
tc.knownCommands as Set<string> | undefined,
|
|
136
|
+
);
|
|
117
137
|
|
|
118
138
|
const promptPromise = agent.prompt({
|
|
119
139
|
sessionId: tc.sessionId,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type Query,
|
|
44
44
|
query,
|
|
45
45
|
type SDKUserMessage,
|
|
46
|
+
type SlashCommand,
|
|
46
47
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
47
48
|
import { v7 as uuidv7 } from "uuid";
|
|
48
49
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
@@ -83,6 +84,11 @@ import {
|
|
|
83
84
|
handleSystemMessage,
|
|
84
85
|
handleUserAssistantMessage,
|
|
85
86
|
} from "./conversion/sdk-to-acp";
|
|
87
|
+
import {
|
|
88
|
+
rehydrateTaskState,
|
|
89
|
+
type TaskState,
|
|
90
|
+
taskStateToPlanEntries,
|
|
91
|
+
} from "./conversion/task-state";
|
|
86
92
|
import type { EnrichedReadCache } from "./hooks";
|
|
87
93
|
import { createLocalToolsMcpServer } from "./mcp/local-tools";
|
|
88
94
|
import {
|
|
@@ -94,6 +100,10 @@ import {
|
|
|
94
100
|
import { canUseTool } from "./permissions/permission-handlers";
|
|
95
101
|
import { getAvailableSlashCommands } from "./session/commands";
|
|
96
102
|
import { parseMcpServers } from "./session/mcp-config";
|
|
103
|
+
import {
|
|
104
|
+
applyAvailableModelsAllowlist,
|
|
105
|
+
resolveInitialModelId,
|
|
106
|
+
} from "./session/model-config";
|
|
97
107
|
import {
|
|
98
108
|
DEFAULT_MODEL,
|
|
99
109
|
getEffortOptions,
|
|
@@ -143,6 +153,17 @@ function readClaudeMdQuietly(cwd: string, logger: Logger): string | undefined {
|
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
155
|
|
|
156
|
+
function collectKnownSlashCommands(
|
|
157
|
+
commands: SlashCommand[] | undefined,
|
|
158
|
+
): Set<string> {
|
|
159
|
+
const names = new Set<string>();
|
|
160
|
+
if (!commands) return names;
|
|
161
|
+
for (const cmd of commands) {
|
|
162
|
+
if (cmd.name) names.add(cmd.name);
|
|
163
|
+
}
|
|
164
|
+
return names;
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
function sanitizeTitle(text: string): string {
|
|
147
168
|
const sanitized = text
|
|
148
169
|
.replace(/[\r\n]+/g, " ")
|
|
@@ -225,6 +246,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
225
246
|
},
|
|
226
247
|
loadSession: true,
|
|
227
248
|
sessionCapabilities: {
|
|
249
|
+
additionalDirectories: {},
|
|
228
250
|
list: {},
|
|
229
251
|
fork: {},
|
|
230
252
|
resume: {},
|
|
@@ -257,11 +279,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
257
279
|
throw RequestError.authRequired();
|
|
258
280
|
}
|
|
259
281
|
|
|
260
|
-
const response = await this.createSession(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
const response = await this.createSession(
|
|
283
|
+
{
|
|
284
|
+
cwd: params.cwd,
|
|
285
|
+
mcpServers: params.mcpServers ?? [],
|
|
286
|
+
additionalDirectories: params.additionalDirectories,
|
|
287
|
+
_meta: params._meta,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
// Revisit these meta values once we support resume
|
|
291
|
+
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode
|
|
292
|
+
?.options?.resume as string | undefined,
|
|
293
|
+
},
|
|
294
|
+
);
|
|
265
295
|
|
|
266
296
|
return response;
|
|
267
297
|
}
|
|
@@ -273,13 +303,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
273
303
|
{
|
|
274
304
|
cwd: params.cwd,
|
|
275
305
|
mcpServers: params.mcpServers ?? [],
|
|
306
|
+
additionalDirectories: params.additionalDirectories,
|
|
276
307
|
_meta: params._meta,
|
|
277
308
|
},
|
|
278
309
|
{ resume: params.sessionId, forkSession: true },
|
|
279
310
|
);
|
|
280
311
|
}
|
|
281
312
|
|
|
282
|
-
async
|
|
313
|
+
async resumeSession(
|
|
283
314
|
params: ResumeSessionRequest,
|
|
284
315
|
): Promise<ResumeSessionResponse> {
|
|
285
316
|
// Reuse existing session if it matches
|
|
@@ -290,6 +321,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
290
321
|
{
|
|
291
322
|
cwd: params.cwd,
|
|
292
323
|
mcpServers: params.mcpServers ?? [],
|
|
324
|
+
additionalDirectories: params.additionalDirectories,
|
|
293
325
|
_meta: params._meta,
|
|
294
326
|
},
|
|
295
327
|
{
|
|
@@ -297,6 +329,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
297
329
|
},
|
|
298
330
|
);
|
|
299
331
|
|
|
332
|
+
await this.rehydrateTaskStateFromJsonl(params.sessionId);
|
|
333
|
+
|
|
300
334
|
return response;
|
|
301
335
|
}
|
|
302
336
|
|
|
@@ -309,6 +343,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
309
343
|
{
|
|
310
344
|
cwd: params.cwd,
|
|
311
345
|
mcpServers: params.mcpServers ?? [],
|
|
346
|
+
additionalDirectories: params.additionalDirectories,
|
|
312
347
|
_meta: params._meta,
|
|
313
348
|
},
|
|
314
349
|
{ resume: params.sessionId, skipBackgroundFetches: true },
|
|
@@ -409,6 +444,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
409
444
|
|
|
410
445
|
this.session.promptRunning = true;
|
|
411
446
|
let handedOff = false;
|
|
447
|
+
let errored = false;
|
|
412
448
|
let lastAssistantTotalUsage: number | null = null;
|
|
413
449
|
let lastStreamUsage = {
|
|
414
450
|
input_tokens: 0,
|
|
@@ -416,6 +452,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
416
452
|
cache_read_input_tokens: 0,
|
|
417
453
|
cache_creation_input_tokens: 0,
|
|
418
454
|
};
|
|
455
|
+
// Tracks whether we're inside a compaction. The SDK emits the terminal
|
|
456
|
+
// `status` (compact_result success/failed) twice for a single failed
|
|
457
|
+
// compaction, and the two messages are indistinguishable, so we report the
|
|
458
|
+
// outcome only while a compaction is in progress, then clear this. A fresh
|
|
459
|
+
// `compacting` status sets it again, so every distinct compaction (e.g.
|
|
460
|
+
// repeated auto-compactions in a long turn) is still shown.
|
|
461
|
+
let compactionInProgress = false;
|
|
419
462
|
if (this.session.lastContextWindowSize == null) {
|
|
420
463
|
this.session.lastContextWindowSize = this.getContextWindowForModel(
|
|
421
464
|
this.session.modelId ?? "",
|
|
@@ -491,6 +534,54 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
491
534
|
if (message.subtype === "local_command_output") {
|
|
492
535
|
promptReplayed = true;
|
|
493
536
|
}
|
|
537
|
+
if (message.subtype === "status") {
|
|
538
|
+
// The SDK signals manual `/compact` completion with a status
|
|
539
|
+
// message carrying `compact_result`, not the `compact_boundary`
|
|
540
|
+
// message (which only fires when there's content to compact).
|
|
541
|
+
// Gate the user-facing outcome on `compactionInProgress` to
|
|
542
|
+
// dedupe the duplicate terminal status the SDK emits for failed
|
|
543
|
+
// compactions.
|
|
544
|
+
if (message.status === "compacting") {
|
|
545
|
+
compactionInProgress = true;
|
|
546
|
+
// Fall through to handleSystemMessage so the COMPACTING
|
|
547
|
+
// extNotification still fires.
|
|
548
|
+
} else if (
|
|
549
|
+
message.compact_result === "success" &&
|
|
550
|
+
compactionInProgress
|
|
551
|
+
) {
|
|
552
|
+
compactionInProgress = false;
|
|
553
|
+
await this.client.sessionUpdate({
|
|
554
|
+
sessionId: params.sessionId,
|
|
555
|
+
update: {
|
|
556
|
+
sessionUpdate: "agent_message_chunk",
|
|
557
|
+
content: {
|
|
558
|
+
type: "text",
|
|
559
|
+
text: "\n\nCompacting completed.",
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
} else if (
|
|
565
|
+
message.compact_result === "failed" &&
|
|
566
|
+
compactionInProgress
|
|
567
|
+
) {
|
|
568
|
+
compactionInProgress = false;
|
|
569
|
+
const reason = message.compact_error
|
|
570
|
+
? `: ${message.compact_error}`
|
|
571
|
+
: ".";
|
|
572
|
+
await this.client.sessionUpdate({
|
|
573
|
+
sessionId: params.sessionId,
|
|
574
|
+
update: {
|
|
575
|
+
sessionUpdate: "agent_message_chunk",
|
|
576
|
+
content: {
|
|
577
|
+
type: "text",
|
|
578
|
+
text: `\n\nCompacting failed${reason}`,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
494
585
|
if (
|
|
495
586
|
message.subtype === "session_state_changed" &&
|
|
496
587
|
(message as Record<string, unknown>).state === "idle"
|
|
@@ -500,7 +591,17 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
500
591
|
// and produced no output (e.g. /plugin in a non-interactive
|
|
501
592
|
// context). Without this branch we would loop forever waiting
|
|
502
593
|
// for an echo that never comes; surface a clear error instead.
|
|
503
|
-
|
|
594
|
+
//
|
|
595
|
+
// Only fire for commands the SDK does NOT recognize. Plugin
|
|
596
|
+
// and skill commands (e.g. /skills-store) produce a fresh
|
|
597
|
+
// user-message echo with a new uuid that our replay check
|
|
598
|
+
// can't match, so an early idle here is a race, not a real
|
|
599
|
+
// "unsupported" — fall through and let the loop continue.
|
|
600
|
+
const cmdName = commandMatch?.[1].slice(1);
|
|
601
|
+
const known =
|
|
602
|
+
cmdName !== undefined &&
|
|
603
|
+
this.session.knownSlashCommands?.has(cmdName) === true;
|
|
604
|
+
if (commandMatch && !known) {
|
|
504
605
|
const cmd = commandMatch[1];
|
|
505
606
|
this.logger.warn(
|
|
506
607
|
"Slash command produced no output; treating as unsupported",
|
|
@@ -520,6 +621,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
520
621
|
}
|
|
521
622
|
this.logger.debug("Skipping idle state before prompt replay", {
|
|
522
623
|
sessionId: params.sessionId,
|
|
624
|
+
command: commandMatch?.[1],
|
|
625
|
+
known,
|
|
523
626
|
});
|
|
524
627
|
break;
|
|
525
628
|
}
|
|
@@ -540,7 +643,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
540
643
|
},
|
|
541
644
|
});
|
|
542
645
|
|
|
543
|
-
return {
|
|
646
|
+
return {
|
|
647
|
+
stopReason: this.session.cancelled ? "cancelled" : "end_turn",
|
|
648
|
+
};
|
|
544
649
|
}
|
|
545
650
|
await handleSystemMessage(message, context);
|
|
546
651
|
break;
|
|
@@ -814,6 +919,37 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
814
919
|
}
|
|
815
920
|
throw new Error("Session did not end in result");
|
|
816
921
|
} catch (error) {
|
|
922
|
+
errored = true;
|
|
923
|
+
// A failed turn typically leaves a trailing `session_state_changed: idle`
|
|
924
|
+
// (and possibly more) in the query iterator. If we don't drain it here,
|
|
925
|
+
// the next prompt's first `query.next()` consumes that stale idle and
|
|
926
|
+
// short-circuits to end_turn with zero usage.
|
|
927
|
+
try {
|
|
928
|
+
await this.session.query.interrupt();
|
|
929
|
+
const MAX_DRAIN = 100;
|
|
930
|
+
for (let i = 0; i < MAX_DRAIN; i++) {
|
|
931
|
+
const { value: m, done } = await this.session.query.next();
|
|
932
|
+
if (done || !m) break;
|
|
933
|
+
if (
|
|
934
|
+
m.type === "system" &&
|
|
935
|
+
m.subtype === "session_state_changed" &&
|
|
936
|
+
(m as Record<string, unknown>).state === "idle"
|
|
937
|
+
) {
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
if (i === MAX_DRAIN - 1) {
|
|
941
|
+
this.logger.error(
|
|
942
|
+
`Session ${params.sessionId}: drained ${MAX_DRAIN} messages after error without observing idle`,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} catch (drainErr) {
|
|
947
|
+
this.logger.error(
|
|
948
|
+
`Session ${params.sessionId}: failed to drain query after prompt error`,
|
|
949
|
+
{ error: drainErr },
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
817
953
|
if (error instanceof RequestError || !(error instanceof Error)) {
|
|
818
954
|
throw error;
|
|
819
955
|
}
|
|
@@ -844,10 +980,25 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
844
980
|
this.toolUseStreamCache.clear();
|
|
845
981
|
if (!handedOff) {
|
|
846
982
|
this.session.promptRunning = false;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
983
|
+
if (errored) {
|
|
984
|
+
// The query stream was just drained — handing pending prompts off
|
|
985
|
+
// onto it would let them race with the recovery. Cancel them so
|
|
986
|
+
// each waiting prompt() returns stopReason "cancelled" and the
|
|
987
|
+
// client can decide whether to retry.
|
|
988
|
+
for (const pending of this.session.pendingMessages.values()) {
|
|
989
|
+
pending.resolve(true);
|
|
990
|
+
}
|
|
991
|
+
this.session.pendingMessages.clear();
|
|
992
|
+
} else if (this.session.pendingMessages.size > 0) {
|
|
993
|
+
// Clean exit with queued prompts: hand off the lowest-order one
|
|
994
|
+
// so it can proceed. The rest stay queued for their own turn.
|
|
995
|
+
const next = [...this.session.pendingMessages.entries()].sort(
|
|
996
|
+
(a, b) => a[1].order - b[1].order,
|
|
997
|
+
)[0];
|
|
998
|
+
if (next) {
|
|
999
|
+
next[1].resolve(false);
|
|
1000
|
+
this.session.pendingMessages.delete(next[0]);
|
|
1001
|
+
}
|
|
851
1002
|
}
|
|
852
1003
|
}
|
|
853
1004
|
}
|
|
@@ -909,6 +1060,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
909
1060
|
|
|
910
1061
|
const mcpServers = parseMcpServers(
|
|
911
1062
|
params as Pick<NewSessionRequest, "mcpServers">,
|
|
1063
|
+
this.logger,
|
|
912
1064
|
);
|
|
913
1065
|
await this.refreshSession(mcpServers);
|
|
914
1066
|
return { refreshed: true };
|
|
@@ -1142,6 +1294,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1142
1294
|
params: {
|
|
1143
1295
|
cwd: string;
|
|
1144
1296
|
mcpServers: NewSessionRequest["mcpServers"];
|
|
1297
|
+
additionalDirectories?: NewSessionRequest["additionalDirectories"];
|
|
1145
1298
|
_meta?: unknown;
|
|
1146
1299
|
},
|
|
1147
1300
|
creationOpts: {
|
|
@@ -1181,7 +1334,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1181
1334
|
const earlyModelId =
|
|
1182
1335
|
settingsManager.getSettings().model || meta?.model || "";
|
|
1183
1336
|
const mcpServers = supportsMcpInjection(earlyModelId)
|
|
1184
|
-
? parseMcpServers(params)
|
|
1337
|
+
? parseMcpServers(params, this.logger)
|
|
1185
1338
|
: {};
|
|
1186
1339
|
|
|
1187
1340
|
// Register the in-process general local-tools MCP server. Tools self-gate
|
|
@@ -1226,6 +1379,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1226
1379
|
? (meta.permissionMode as CodeExecutionMode)
|
|
1227
1380
|
: "default";
|
|
1228
1381
|
|
|
1382
|
+
const taskState: TaskState = new Map();
|
|
1229
1383
|
const options = buildSessionOptions({
|
|
1230
1384
|
cwd,
|
|
1231
1385
|
mcpServers,
|
|
@@ -1239,7 +1393,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1239
1393
|
forkSession,
|
|
1240
1394
|
additionalDirectories: [
|
|
1241
1395
|
...(meta?.claudeCode?.options?.additionalDirectories ?? []),
|
|
1242
|
-
|
|
1396
|
+
// Prefer the official ACP `additionalDirectories` field. Fall back
|
|
1397
|
+
// to the legacy `_meta.additionalRoots` extension for clients that
|
|
1398
|
+
// haven't been updated yet.
|
|
1399
|
+
...(params.additionalDirectories ?? meta?.additionalRoots ?? []),
|
|
1243
1400
|
],
|
|
1244
1401
|
disableBuiltInTools: meta?.disableBuiltInTools,
|
|
1245
1402
|
outputFormat,
|
|
@@ -1251,6 +1408,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1251
1408
|
enrichmentDeps: this.enrichment?.deps,
|
|
1252
1409
|
enrichedReadCache: this.enrichedReadCache,
|
|
1253
1410
|
cloudMode: cloudRun,
|
|
1411
|
+
taskState,
|
|
1412
|
+
onTaskStateChange: async () => {
|
|
1413
|
+
await this.client.sessionUpdate({
|
|
1414
|
+
sessionId,
|
|
1415
|
+
update: {
|
|
1416
|
+
sessionUpdate: "plan",
|
|
1417
|
+
entries: taskStateToPlanEntries(taskState),
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
},
|
|
1254
1421
|
});
|
|
1255
1422
|
|
|
1256
1423
|
// Use the same abort controller that buildSessionOptions gave to the query
|
|
@@ -1283,6 +1450,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1283
1450
|
systemPrompt: estimateSystemPrompt(systemPrompt),
|
|
1284
1451
|
rules: estimateRulesTokens(readClaudeMdQuietly(cwd, this.logger)),
|
|
1285
1452
|
},
|
|
1453
|
+
taskState,
|
|
1286
1454
|
|
|
1287
1455
|
// Custom properties
|
|
1288
1456
|
cwd,
|
|
@@ -1305,6 +1473,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1305
1473
|
`Session ${forkSession ? "fork" : "resumption"} timed out for sessionId=${sessionId}`,
|
|
1306
1474
|
);
|
|
1307
1475
|
}
|
|
1476
|
+
session.knownSlashCommands = collectKnownSlashCommands(
|
|
1477
|
+
result.value.commands,
|
|
1478
|
+
);
|
|
1308
1479
|
} catch (err) {
|
|
1309
1480
|
settingsManager.dispose();
|
|
1310
1481
|
if (
|
|
@@ -1332,7 +1503,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1332
1503
|
? withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS)
|
|
1333
1504
|
: undefined;
|
|
1334
1505
|
|
|
1335
|
-
const [
|
|
1506
|
+
const [rawModelOptions] = await Promise.all([
|
|
1336
1507
|
this.getModelConfigOptions(
|
|
1337
1508
|
settingsManager.getSettings().model || meta?.model || undefined,
|
|
1338
1509
|
),
|
|
@@ -1347,6 +1518,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1347
1518
|
: []),
|
|
1348
1519
|
]);
|
|
1349
1520
|
|
|
1521
|
+
// Restrict the model list to the user's `availableModels` allowlist
|
|
1522
|
+
// from settings.json so config UI and downstream resolution stay
|
|
1523
|
+
// consistent with what the user configured. The Default option is
|
|
1524
|
+
// always preserved per the Claude Code docs.
|
|
1525
|
+
const settingsAvailableModels =
|
|
1526
|
+
settingsManager.getSettings().availableModels;
|
|
1527
|
+
const modelOptions = Array.isArray(settingsAvailableModels)
|
|
1528
|
+
? applyAvailableModelsAllowlist(rawModelOptions, settingsAvailableModels)
|
|
1529
|
+
: rawModelOptions;
|
|
1530
|
+
|
|
1350
1531
|
if (initPromise) {
|
|
1351
1532
|
try {
|
|
1352
1533
|
const initResult = await initPromise;
|
|
@@ -1356,6 +1537,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1356
1537
|
`Session initialization timed out for sessionId=${sessionId}`,
|
|
1357
1538
|
);
|
|
1358
1539
|
}
|
|
1540
|
+
session.knownSlashCommands = collectKnownSlashCommands(
|
|
1541
|
+
initResult.value.commands,
|
|
1542
|
+
);
|
|
1359
1543
|
} catch (err) {
|
|
1360
1544
|
settingsManager.dispose();
|
|
1361
1545
|
this.logger.error("Session initialization failed", {
|
|
@@ -1368,10 +1552,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1368
1552
|
}
|
|
1369
1553
|
}
|
|
1370
1554
|
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1555
|
+
const resolvedModelId = resolveInitialModelId(modelOptions, [
|
|
1556
|
+
settingsManager.getSettings().model,
|
|
1557
|
+
meta?.model,
|
|
1558
|
+
]);
|
|
1375
1559
|
session.modelId = resolvedModelId;
|
|
1376
1560
|
session.lastContextWindowSize =
|
|
1377
1561
|
this.getContextWindowForModel(resolvedModelId);
|
|
@@ -1635,6 +1819,35 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1635
1819
|
};
|
|
1636
1820
|
}
|
|
1637
1821
|
|
|
1822
|
+
/**
|
|
1823
|
+
* Rebuild the in-memory taskState from JSONL and push a plan update so the
|
|
1824
|
+
* client's plan panel reflects pre-resume tasks. `loadSession` already covers
|
|
1825
|
+
* this via the full `replaySessionHistory` notification stream; resume
|
|
1826
|
+
* deliberately stays quiet (the client keeps its own message history) so we
|
|
1827
|
+
* walk the transcript here for state only.
|
|
1828
|
+
*/
|
|
1829
|
+
private async rehydrateTaskStateFromJsonl(sessionId: string): Promise<void> {
|
|
1830
|
+
try {
|
|
1831
|
+
const messages = await getSessionMessages(sessionId, {
|
|
1832
|
+
dir: this.session.cwd,
|
|
1833
|
+
});
|
|
1834
|
+
rehydrateTaskState(messages, this.session.taskState);
|
|
1835
|
+
if (this.session.taskState.size === 0) return;
|
|
1836
|
+
await this.client.sessionUpdate({
|
|
1837
|
+
sessionId,
|
|
1838
|
+
update: {
|
|
1839
|
+
sessionUpdate: "plan",
|
|
1840
|
+
entries: taskStateToPlanEntries(this.session.taskState),
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
this.logger.warn("Failed to rehydrate task state", {
|
|
1845
|
+
sessionId,
|
|
1846
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1638
1851
|
private async replaySessionHistory(sessionId: string): Promise<void> {
|
|
1639
1852
|
try {
|
|
1640
1853
|
const messages = await getSessionMessages(sessionId, {
|