@posthog/agent 2.3.735 → 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.
Files changed (62) hide show
  1. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +2 -10
  2. package/dist/adapters/claude/conversion/tool-use-to-acp.js +24 -10
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
  4. package/dist/adapters/claude/permissions/permission-options.js +6 -4
  5. package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
  6. package/dist/adapters/claude/session/jsonl-hydration.js +1 -0
  7. package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -1
  8. package/dist/adapters/claude/session/models.js +20 -1
  9. package/dist/adapters/claude/session/models.js.map +1 -1
  10. package/dist/adapters/claude/tools.js +4 -2
  11. package/dist/adapters/claude/tools.js.map +1 -1
  12. package/dist/adapters/reasoning-effort.js +3 -1
  13. package/dist/adapters/reasoning-effort.js.map +1 -1
  14. package/dist/agent.js +524 -72
  15. package/dist/agent.js.map +1 -1
  16. package/dist/posthog-api.js +4 -4
  17. package/dist/posthog-api.js.map +1 -1
  18. package/dist/server/agent-server.js +524 -72
  19. package/dist/server/agent-server.js.map +1 -1
  20. package/dist/server/bin.cjs +524 -72
  21. package/dist/server/bin.cjs.map +1 -1
  22. package/package.json +6 -6
  23. package/src/adapters/claude/UPSTREAM.md +80 -3
  24. package/src/adapters/claude/claude-agent.ts +201 -18
  25. package/src/adapters/claude/conversion/sdk-to-acp.ts +182 -16
  26. package/src/adapters/claude/conversion/task-state.test.ts +338 -0
  27. package/src/adapters/claude/conversion/task-state.ts +178 -0
  28. package/src/adapters/claude/conversion/tool-use-to-acp.ts +29 -19
  29. package/src/adapters/claude/hooks.test.ts +162 -0
  30. package/src/adapters/claude/hooks.ts +44 -3
  31. package/src/adapters/claude/permissions/permission-options.ts +7 -2
  32. package/src/adapters/claude/session/commands.ts +1 -0
  33. package/src/adapters/claude/session/mcp-config.ts +23 -7
  34. package/src/adapters/claude/session/model-config.test.ts +60 -0
  35. package/src/adapters/claude/session/model-config.ts +56 -0
  36. package/src/adapters/claude/session/models.test.ts +119 -0
  37. package/src/adapters/claude/session/models.ts +31 -0
  38. package/src/adapters/claude/session/options.test.ts +1 -0
  39. package/src/adapters/claude/session/options.ts +29 -1
  40. package/src/adapters/claude/session/settings.test.ts +102 -1
  41. package/src/adapters/claude/session/settings.ts +33 -6
  42. package/src/adapters/claude/tools.ts +4 -2
  43. package/src/adapters/claude/types.ts +8 -0
  44. package/src/adapters/codex/codex-agent.ts +2 -2
  45. package/src/server/agent-server.test.ts +173 -0
  46. package/src/test/mocks/claude-sdk.ts +4 -0
  47. package/src/test/native-binary.test.ts +27 -0
  48. package/dist/claude-cli/cli.js +0 -18412
  49. package/dist/claude-cli/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
  50. package/dist/claude-cli/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
  51. package/dist/claude-cli/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
  52. package/dist/claude-cli/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
  53. package/dist/claude-cli/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
  54. package/dist/claude-cli/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
  55. package/dist/claude-cli/vendor/ripgrep/COPYING +0 -3
  56. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  57. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  58. package/dist/claude-cli/vendor/ripgrep/arm64-win32/rg.exe +0 -0
  59. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  60. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  61. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  62. 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.735",
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": {
@@ -107,13 +107,13 @@
107
107
  "typescript": "^5.5.0",
108
108
  "vitest": "^2.1.8",
109
109
  "@posthog/shared": "1.0.0",
110
- "@posthog/enricher": "1.0.0",
111
- "@posthog/git": "1.0.0"
110
+ "@posthog/git": "1.0.0",
111
+ "@posthog/enricher": "1.0.0"
112
112
  },
113
113
  "dependencies": {
114
- "@agentclientprotocol/sdk": "0.19.0",
115
- "@anthropic-ai/claude-agent-sdk": "0.2.112",
116
- "@anthropic-ai/sdk": "0.89.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.30.0, commit `e9dd452`, April 20 2026
9
- - **SDK**: `@anthropic-ai/claude-agent-sdk` 0.2.112 (0.2.114 breaks session init, see agentclientprotocol/claude-agent-acp#575), `@agentclientprotocol/sdk` 0.19.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
 
@@ -67,6 +67,83 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
67
67
  - **Effort level sync** (v0.25.x): `xhigh` level added, `applyFlagSettings` on effort change
68
68
  - **Auto permission mode** (v0.25.0): Added to `CODE_EXECUTION_MODES`, available modes, ExitPlanMode options
69
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
+
70
147
  ## Skipped in v0.30.0 Sync
71
148
 
72
149
  - **Separate auth methods** (v0.25.0): PostHog returns empty authMethods
@@ -75,7 +152,7 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
75
152
 
76
153
  ## Next Sync
77
154
 
78
- 1. Check upstream changelog since v0.30.0
155
+ 1. Check upstream changelog since v0.37.0
79
156
  2. Diff upstream source against PostHog Code using the file mapping above
80
157
  3. Port in phases: bug fixes first, then features
81
158
  4. After each phase: `pnpm --filter agent typecheck && pnpm --filter agent build && pnpm lint`
@@ -84,6 +84,11 @@ import {
84
84
  handleSystemMessage,
85
85
  handleUserAssistantMessage,
86
86
  } from "./conversion/sdk-to-acp";
87
+ import {
88
+ rehydrateTaskState,
89
+ type TaskState,
90
+ taskStateToPlanEntries,
91
+ } from "./conversion/task-state";
87
92
  import type { EnrichedReadCache } from "./hooks";
88
93
  import { createLocalToolsMcpServer } from "./mcp/local-tools";
89
94
  import {
@@ -95,6 +100,10 @@ import {
95
100
  import { canUseTool } from "./permissions/permission-handlers";
96
101
  import { getAvailableSlashCommands } from "./session/commands";
97
102
  import { parseMcpServers } from "./session/mcp-config";
103
+ import {
104
+ applyAvailableModelsAllowlist,
105
+ resolveInitialModelId,
106
+ } from "./session/model-config";
98
107
  import {
99
108
  DEFAULT_MODEL,
100
109
  getEffortOptions,
@@ -237,6 +246,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
237
246
  },
238
247
  loadSession: true,
239
248
  sessionCapabilities: {
249
+ additionalDirectories: {},
240
250
  list: {},
241
251
  fork: {},
242
252
  resume: {},
@@ -269,11 +279,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
269
279
  throw RequestError.authRequired();
270
280
  }
271
281
 
272
- const response = await this.createSession(params, {
273
- // Revisit these meta values once we support resume
274
- resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options
275
- ?.resume as string | undefined,
276
- });
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
+ );
277
295
 
278
296
  return response;
279
297
  }
@@ -285,13 +303,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
285
303
  {
286
304
  cwd: params.cwd,
287
305
  mcpServers: params.mcpServers ?? [],
306
+ additionalDirectories: params.additionalDirectories,
288
307
  _meta: params._meta,
289
308
  },
290
309
  { resume: params.sessionId, forkSession: true },
291
310
  );
292
311
  }
293
312
 
294
- async unstable_resumeSession(
313
+ async resumeSession(
295
314
  params: ResumeSessionRequest,
296
315
  ): Promise<ResumeSessionResponse> {
297
316
  // Reuse existing session if it matches
@@ -302,6 +321,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
302
321
  {
303
322
  cwd: params.cwd,
304
323
  mcpServers: params.mcpServers ?? [],
324
+ additionalDirectories: params.additionalDirectories,
305
325
  _meta: params._meta,
306
326
  },
307
327
  {
@@ -309,6 +329,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
309
329
  },
310
330
  );
311
331
 
332
+ await this.rehydrateTaskStateFromJsonl(params.sessionId);
333
+
312
334
  return response;
313
335
  }
314
336
 
@@ -321,6 +343,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
321
343
  {
322
344
  cwd: params.cwd,
323
345
  mcpServers: params.mcpServers ?? [],
346
+ additionalDirectories: params.additionalDirectories,
324
347
  _meta: params._meta,
325
348
  },
326
349
  { resume: params.sessionId, skipBackgroundFetches: true },
@@ -421,6 +444,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
421
444
 
422
445
  this.session.promptRunning = true;
423
446
  let handedOff = false;
447
+ let errored = false;
424
448
  let lastAssistantTotalUsage: number | null = null;
425
449
  let lastStreamUsage = {
426
450
  input_tokens: 0,
@@ -428,6 +452,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
428
452
  cache_read_input_tokens: 0,
429
453
  cache_creation_input_tokens: 0,
430
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;
431
462
  if (this.session.lastContextWindowSize == null) {
432
463
  this.session.lastContextWindowSize = this.getContextWindowForModel(
433
464
  this.session.modelId ?? "",
@@ -503,6 +534,54 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
503
534
  if (message.subtype === "local_command_output") {
504
535
  promptReplayed = true;
505
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
+ }
506
585
  if (
507
586
  message.subtype === "session_state_changed" &&
508
587
  (message as Record<string, unknown>).state === "idle"
@@ -564,7 +643,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
564
643
  },
565
644
  });
566
645
 
567
- return { stopReason: "end_turn" };
646
+ return {
647
+ stopReason: this.session.cancelled ? "cancelled" : "end_turn",
648
+ };
568
649
  }
569
650
  await handleSystemMessage(message, context);
570
651
  break;
@@ -838,6 +919,37 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
838
919
  }
839
920
  throw new Error("Session did not end in result");
840
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
+
841
953
  if (error instanceof RequestError || !(error instanceof Error)) {
842
954
  throw error;
843
955
  }
@@ -868,10 +980,25 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
868
980
  this.toolUseStreamCache.clear();
869
981
  if (!handedOff) {
870
982
  this.session.promptRunning = false;
871
- // Resolve all remaining pending prompts so no callers get stuck.
872
- for (const [key, pending] of this.session.pendingMessages) {
873
- pending.resolve(true);
874
- this.session.pendingMessages.delete(key);
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
+ }
875
1002
  }
876
1003
  }
877
1004
  }
@@ -933,6 +1060,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
933
1060
 
934
1061
  const mcpServers = parseMcpServers(
935
1062
  params as Pick<NewSessionRequest, "mcpServers">,
1063
+ this.logger,
936
1064
  );
937
1065
  await this.refreshSession(mcpServers);
938
1066
  return { refreshed: true };
@@ -1166,6 +1294,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1166
1294
  params: {
1167
1295
  cwd: string;
1168
1296
  mcpServers: NewSessionRequest["mcpServers"];
1297
+ additionalDirectories?: NewSessionRequest["additionalDirectories"];
1169
1298
  _meta?: unknown;
1170
1299
  },
1171
1300
  creationOpts: {
@@ -1205,7 +1334,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1205
1334
  const earlyModelId =
1206
1335
  settingsManager.getSettings().model || meta?.model || "";
1207
1336
  const mcpServers = supportsMcpInjection(earlyModelId)
1208
- ? parseMcpServers(params)
1337
+ ? parseMcpServers(params, this.logger)
1209
1338
  : {};
1210
1339
 
1211
1340
  // Register the in-process general local-tools MCP server. Tools self-gate
@@ -1250,6 +1379,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1250
1379
  ? (meta.permissionMode as CodeExecutionMode)
1251
1380
  : "default";
1252
1381
 
1382
+ const taskState: TaskState = new Map();
1253
1383
  const options = buildSessionOptions({
1254
1384
  cwd,
1255
1385
  mcpServers,
@@ -1263,7 +1393,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1263
1393
  forkSession,
1264
1394
  additionalDirectories: [
1265
1395
  ...(meta?.claudeCode?.options?.additionalDirectories ?? []),
1266
- ...(meta?.additionalRoots ?? []),
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 ?? []),
1267
1400
  ],
1268
1401
  disableBuiltInTools: meta?.disableBuiltInTools,
1269
1402
  outputFormat,
@@ -1275,6 +1408,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1275
1408
  enrichmentDeps: this.enrichment?.deps,
1276
1409
  enrichedReadCache: this.enrichedReadCache,
1277
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
+ },
1278
1421
  });
1279
1422
 
1280
1423
  // Use the same abort controller that buildSessionOptions gave to the query
@@ -1307,6 +1450,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1307
1450
  systemPrompt: estimateSystemPrompt(systemPrompt),
1308
1451
  rules: estimateRulesTokens(readClaudeMdQuietly(cwd, this.logger)),
1309
1452
  },
1453
+ taskState,
1310
1454
 
1311
1455
  // Custom properties
1312
1456
  cwd,
@@ -1359,7 +1503,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1359
1503
  ? withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS)
1360
1504
  : undefined;
1361
1505
 
1362
- const [modelOptions] = await Promise.all([
1506
+ const [rawModelOptions] = await Promise.all([
1363
1507
  this.getModelConfigOptions(
1364
1508
  settingsManager.getSettings().model || meta?.model || undefined,
1365
1509
  ),
@@ -1374,6 +1518,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1374
1518
  : []),
1375
1519
  ]);
1376
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
+
1377
1531
  if (initPromise) {
1378
1532
  try {
1379
1533
  const initResult = await initPromise;
@@ -1398,10 +1552,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1398
1552
  }
1399
1553
  }
1400
1554
 
1401
- const settingsModel = settingsManager.getSettings().model;
1402
- const metaModel = meta?.model;
1403
- const resolvedModelId =
1404
- settingsModel || metaModel || modelOptions.currentModelId;
1555
+ const resolvedModelId = resolveInitialModelId(modelOptions, [
1556
+ settingsManager.getSettings().model,
1557
+ meta?.model,
1558
+ ]);
1405
1559
  session.modelId = resolvedModelId;
1406
1560
  session.lastContextWindowSize =
1407
1561
  this.getContextWindowForModel(resolvedModelId);
@@ -1665,6 +1819,35 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1665
1819
  };
1666
1820
  }
1667
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
+
1668
1851
  private async replaySessionHistory(sessionId: string): Promise<void> {
1669
1852
  try {
1670
1853
  const messages = await getSessionMessages(sessionId, {