@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
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ getEffortOptions,
4
+ resolveModelPreference,
5
+ supports1MContext,
6
+ supportsEffort,
7
+ supportsMcpInjection,
8
+ supportsXhighEffort,
9
+ toSdkModelId,
10
+ } from "./models";
11
+
12
+ describe("toSdkModelId", () => {
13
+ it("maps known gateway IDs to SDK aliases", () => {
14
+ expect(toSdkModelId("claude-opus-4-7")).toBe("opus");
15
+ expect(toSdkModelId("claude-opus-4-8")).toBe("opus");
16
+ expect(toSdkModelId("claude-sonnet-4-6")).toBe("sonnet");
17
+ expect(toSdkModelId("claude-haiku-4-5")).toBe("haiku");
18
+ });
19
+
20
+ it("passes unknown IDs through unchanged", () => {
21
+ expect(toSdkModelId("custom-model")).toBe("custom-model");
22
+ });
23
+ });
24
+
25
+ describe("model capability flags", () => {
26
+ it("flags 1M context support", () => {
27
+ expect(supports1MContext("claude-opus-4-7")).toBe(true);
28
+ expect(supports1MContext("claude-sonnet-4-6")).toBe(true);
29
+ expect(supports1MContext("claude-haiku-4-5")).toBe(false);
30
+ });
31
+
32
+ it("flags effort support and xhigh-effort support", () => {
33
+ expect(supportsEffort("claude-opus-4-5")).toBe(true);
34
+ expect(supportsXhighEffort("claude-opus-4-7")).toBe(true);
35
+ expect(supportsXhighEffort("claude-opus-4-5")).toBe(false);
36
+ expect(supportsEffort("claude-haiku-4-5")).toBe(false);
37
+ });
38
+
39
+ it("excludes MCP injection only for Haiku", () => {
40
+ expect(supportsMcpInjection("claude-opus-4-7")).toBe(true);
41
+ expect(supportsMcpInjection("claude-haiku-4-5")).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe("getEffortOptions", () => {
46
+ it("returns null for models without effort support", () => {
47
+ expect(getEffortOptions("claude-haiku-4-5")).toBeNull();
48
+ });
49
+
50
+ it("returns low/medium/high for effort-supporting models", () => {
51
+ const opts = getEffortOptions("claude-opus-4-5");
52
+ expect(opts?.map((o) => o.value)).toEqual(["low", "medium", "high"]);
53
+ });
54
+
55
+ it("appends xhigh and max for xhigh-supporting models", () => {
56
+ const opts = getEffortOptions("claude-opus-4-7");
57
+ expect(opts?.map((o) => o.value)).toEqual([
58
+ "low",
59
+ "medium",
60
+ "high",
61
+ "xhigh",
62
+ "max",
63
+ ]);
64
+ });
65
+ });
66
+
67
+ describe("resolveModelPreference", () => {
68
+ const options = [
69
+ { value: "claude-opus-4-8", name: "Claude Opus 4.8" },
70
+ { value: "claude-opus-4-7", name: "Claude Opus 4.7" },
71
+ { value: "claude-opus-4-6", name: "Claude Opus 4.6" },
72
+ { value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
73
+ { value: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
74
+ ];
75
+
76
+ it("returns null for empty preference", () => {
77
+ expect(resolveModelPreference("", options)).toBeNull();
78
+ expect(resolveModelPreference(" ", options)).toBeNull();
79
+ });
80
+
81
+ it("matches an exact value", () => {
82
+ expect(resolveModelPreference("claude-opus-4-7", options)).toBe(
83
+ "claude-opus-4-7",
84
+ );
85
+ });
86
+
87
+ it("matches case-insensitively on display name", () => {
88
+ expect(resolveModelPreference("claude haiku 4.5", options)).toBe(
89
+ "claude-haiku-4-5",
90
+ );
91
+ });
92
+
93
+ it("matches by substring", () => {
94
+ expect(resolveModelPreference("sonnet", options)).toBe("claude-sonnet-4-6");
95
+ });
96
+
97
+ it("matches by token alias", () => {
98
+ expect(resolveModelPreference("opus[1m]", options)).toBe("claude-opus-4-8");
99
+ });
100
+
101
+ it("refuses cross-version alias matches", () => {
102
+ const optionsWithAlias = [
103
+ { value: "opus", name: "Claude Opus 4.7" },
104
+ { value: "claude-opus-4-6", name: "Claude Opus 4.6" },
105
+ ];
106
+ expect(resolveModelPreference("claude-opus-4-6", optionsWithAlias)).toBe(
107
+ "claude-opus-4-6",
108
+ );
109
+ });
110
+
111
+ it("returns null when nothing matches", () => {
112
+ expect(resolveModelPreference("gpt-5", options)).toBeNull();
113
+ });
114
+
115
+ it("treats `best` and `default` as wildcards (no tokens contribute)", () => {
116
+ expect(resolveModelPreference("best", options)).toBeNull();
117
+ expect(resolveModelPreference("default", options)).toBeNull();
118
+ });
119
+ });
@@ -4,6 +4,7 @@ const GATEWAY_TO_SDK_MODEL: Record<string, string> = {
4
4
  "claude-opus-4-5": "opus",
5
5
  "claude-opus-4-6": "opus",
6
6
  "claude-opus-4-7": "opus",
7
+ "claude-opus-4-8": "opus",
7
8
  "claude-sonnet-4-5": "sonnet",
8
9
  "claude-sonnet-4-6": "sonnet",
9
10
  "claude-haiku-4-5": "haiku",
@@ -16,6 +17,7 @@ export function toSdkModelId(modelId: string): string {
16
17
  const MODELS_WITH_1M_CONTEXT = new Set([
17
18
  "claude-opus-4-6",
18
19
  "claude-opus-4-7",
20
+ "claude-opus-4-8",
19
21
  "claude-sonnet-4-6",
20
22
  ]);
21
23
 
@@ -27,12 +29,14 @@ const MODELS_WITH_EFFORT = new Set([
27
29
  "claude-opus-4-5",
28
30
  "claude-opus-4-6",
29
31
  "claude-opus-4-7",
32
+ "claude-opus-4-8",
30
33
  "claude-sonnet-4-6",
31
34
  ]);
32
35
 
33
36
  const MODELS_WITH_XHIGH_EFFORT = new Set([
34
37
  "claude-opus-4-6",
35
38
  "claude-opus-4-7",
39
+ "claude-opus-4-8",
36
40
  ]);
37
41
 
38
42
  export function supportsEffort(modelId: string): boolean {
@@ -107,6 +111,31 @@ interface ModelOption {
107
111
  description?: string;
108
112
  }
109
113
 
114
+ // Captures a model family version such as `4-6` or `4.7` so we can keep
115
+ // `claude-opus-4-6` from being copied onto the SDK's `opus` alias when that
116
+ // alias currently resolves to a different family version (e.g. Opus 4.7).
117
+ const MODEL_FAMILY_VERSION_PATTERN = /\b(\d+)[-.](\d+)\b/;
118
+
119
+ function extractModelFamilyVersion(s: string | undefined): string | null {
120
+ if (!s) return null;
121
+ const match = s.match(MODEL_FAMILY_VERSION_PATTERN);
122
+ return match ? `${match[1]}.${match[2]}` : null;
123
+ }
124
+
125
+ function modelVersionsCompatible(
126
+ preference: string,
127
+ candidate: ModelOption,
128
+ ): boolean {
129
+ const preferred = extractModelFamilyVersion(preference);
130
+ if (!preferred) return true;
131
+ const candidateVersion =
132
+ extractModelFamilyVersion(candidate.value) ??
133
+ extractModelFamilyVersion(candidate.name) ??
134
+ extractModelFamilyVersion(candidate.description);
135
+ if (!candidateVersion) return true;
136
+ return preferred === candidateVersion;
137
+ }
138
+
110
139
  function scoreModelMatch(
111
140
  model: ModelOption,
112
141
  tokens: string[],
@@ -142,6 +171,7 @@ export function resolveModelPreference(
142
171
 
143
172
  // Substring match
144
173
  const includesMatch = options.find((o) => {
174
+ if (!modelVersionsCompatible(trimmed, o)) return false;
145
175
  const value = o.value.toLowerCase();
146
176
  const display = (o.name ?? "").toLowerCase();
147
177
  return (
@@ -157,6 +187,7 @@ export function resolveModelPreference(
157
187
  let bestMatch: ModelOption | null = null;
158
188
  let bestScore = 0;
159
189
  for (const model of options) {
190
+ if (!modelVersionsCompatible(trimmed, model)) continue;
160
191
  const score = scoreModelMatch(model, tokens, contextHint);
161
192
  if (0 < score && (!bestMatch || bestScore < score)) {
162
193
  bestMatch = model;
@@ -17,6 +17,7 @@ function makeParams() {
17
17
  sessionId: "test-session",
18
18
  isResume: false,
19
19
  settingsManager: new SettingsManager(cwd),
20
+ taskState: new Map(),
20
21
  };
21
22
  }
22
23
 
@@ -13,12 +13,14 @@ import type {
13
13
  import type { FileEnrichmentDeps } from "../../../enrichment/file-enricher";
14
14
  import { IS_ROOT } from "../../../utils/common";
15
15
  import type { Logger } from "../../../utils/logger";
16
+ import type { TaskState } from "../conversion/task-state";
16
17
  import {
17
18
  createPostToolUseHook,
18
19
  createPreToolUseHook,
19
20
  createReadEnrichmentHook,
20
21
  createSignedCommitGuardHook,
21
22
  createSubagentRewriteHook,
23
+ createTaskHook,
22
24
  type EnrichedReadCache,
23
25
  type OnModeChange,
24
26
  } from "../hooks";
@@ -58,6 +60,11 @@ export interface BuildOptionsParams {
58
60
  enrichedReadCache?: EnrichedReadCache;
59
61
  /** Cloud task session — enables the signed-commit guard. */
60
62
  cloudMode?: boolean;
63
+ /** Per-session task state populated by createTaskHook from SDK Task* events. */
64
+ taskState: TaskState;
65
+ /** Called after createTaskHook mutates taskState so callers can emit a plan
66
+ * sessionUpdate to the client. */
67
+ onTaskStateChange?: () => Promise<void>;
61
68
  }
62
69
 
63
70
  export function buildSystemPrompt(
@@ -111,6 +118,13 @@ function buildEnvironment(): Record<string, string> {
111
118
  ? `${existingCustomHeaders}\n${bedrockFallbackHeader}`
112
119
  : bedrockFallbackHeader;
113
120
 
121
+ // SDK 0.3.142 made MCP servers connect in the background by default. That
122
+ // default is what we want: a slow or unreachable user MCP server (PostHog
123
+ // MCP, custom stdio servers) would otherwise stall turn 1 by up to ~5s per
124
+ // server. We honor an explicit override from the caller's environment for
125
+ // sessions that genuinely need MCP tools available on turn 1.
126
+ const mcpNonblocking = process.env.MCP_CONNECTION_NONBLOCKING;
127
+
114
128
  return {
115
129
  ...process.env,
116
130
  ELECTRON_RUN_AS_NODE: "1",
@@ -119,6 +133,9 @@ function buildEnvironment(): Record<string, string> {
119
133
  ENABLE_TOOL_SEARCH: "auto:0",
120
134
  // Enable idle state as end-of-turn signal (required for SDK 0.2.114+)
121
135
  CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
136
+ ...(mcpNonblocking !== undefined && {
137
+ MCP_CONNECTION_NONBLOCKING: mcpNonblocking,
138
+ }),
122
139
  // Route to AWS Bedrock as a fallback when Anthropic returns 5xx
123
140
  ANTHROPIC_CUSTOM_HEADERS: customHeaders,
124
141
  };
@@ -133,6 +150,8 @@ function buildHooks(
133
150
  enrichedReadCache: EnrichedReadCache | undefined,
134
151
  registeredAgents: ReadonlySet<string>,
135
152
  cloudMode: boolean,
153
+ taskState: TaskState,
154
+ onTaskStateChange: (() => Promise<void>) | undefined,
136
155
  ): Options["hooks"] {
137
156
  const postToolUseHooks = [createPostToolUseHook({ onModeChange })];
138
157
  if (enrichmentDeps && enrichedReadCache) {
@@ -149,6 +168,8 @@ function buildHooks(
149
168
  preToolUseHooks.push(createSignedCommitGuardHook(logger));
150
169
  }
151
170
 
171
+ const taskHook = createTaskHook(taskState, onTaskStateChange);
172
+
152
173
  return {
153
174
  ...userHooks,
154
175
  PostToolUse: [
@@ -156,6 +177,8 @@ function buildHooks(
156
177
  { hooks: postToolUseHooks },
157
178
  ],
158
179
  PreToolUse: [...(userHooks?.PreToolUse || []), { hooks: preToolUseHooks }],
180
+ TaskCreated: [...(userHooks?.TaskCreated || []), { hooks: [taskHook] }],
181
+ TaskCompleted: [...(userHooks?.TaskCompleted || []), { hooks: [taskHook] }],
159
182
  };
160
183
  }
161
184
 
@@ -195,7 +218,10 @@ Rules:
195
218
  "WebFetch",
196
219
  "WebSearch",
197
220
  "NotebookRead",
198
- "TodoWrite",
221
+ "TaskCreate",
222
+ "TaskUpdate",
223
+ "TaskGet",
224
+ "TaskList",
199
225
  ],
200
226
  };
201
227
 
@@ -357,6 +383,8 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
357
383
  params.enrichedReadCache,
358
384
  registeredAgentNames,
359
385
  params.cloudMode ?? false,
386
+ params.taskState,
387
+ params.onTaskStateChange,
360
388
  ),
361
389
  outputFormat: params.outputFormat,
362
390
  abortController: getAbortController(
@@ -4,7 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
6
  import { resolveMainRepoPath } from "./repo-path";
7
- import { SettingsManager } from "./settings";
7
+ import { mergeAvailableModels, SettingsManager } from "./settings";
8
8
 
9
9
  function runGit(cwd: string, args: string[]): void {
10
10
  execFileSync("git", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -207,3 +207,104 @@ describe("resolveMainRepoPath", () => {
207
207
  }
208
208
  });
209
209
  });
210
+
211
+ describe("availableModels merge", () => {
212
+ let tmpRoot: string;
213
+ let cwd: string;
214
+ let configDir: string;
215
+ let originalConfigDir: string | undefined;
216
+
217
+ beforeEach(async () => {
218
+ tmpRoot = await fs.promises.realpath(
219
+ await fs.promises.mkdtemp(path.join(os.tmpdir(), "available-models-")),
220
+ );
221
+ cwd = path.join(tmpRoot, "repo");
222
+ configDir = path.join(tmpRoot, "user");
223
+ await fs.promises.mkdir(cwd, { recursive: true });
224
+ await fs.promises.mkdir(configDir, { recursive: true });
225
+ runGit(cwd, ["init", "-b", "main"]);
226
+ runGit(cwd, ["config", "user.email", "test@example.com"]);
227
+ runGit(cwd, ["config", "user.name", "test"]);
228
+ runGit(cwd, ["commit", "--allow-empty", "-m", "init"]);
229
+
230
+ originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
231
+ process.env.CLAUDE_CONFIG_DIR = configDir;
232
+ });
233
+
234
+ afterEach(async () => {
235
+ if (originalConfigDir === undefined) {
236
+ delete process.env.CLAUDE_CONFIG_DIR;
237
+ } else {
238
+ process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
239
+ }
240
+ await fs.promises.rm(tmpRoot, { recursive: true, force: true });
241
+ });
242
+
243
+ async function writeUserSettings(settings: object): Promise<void> {
244
+ await fs.promises.writeFile(
245
+ path.join(configDir, "settings.json"),
246
+ JSON.stringify(settings),
247
+ );
248
+ }
249
+
250
+ async function writeProjectSettings(settings: object): Promise<void> {
251
+ const projectDir = path.join(cwd, ".claude");
252
+ await fs.promises.mkdir(projectDir, { recursive: true });
253
+ await fs.promises.writeFile(
254
+ path.join(projectDir, "settings.json"),
255
+ JSON.stringify(settings),
256
+ );
257
+ }
258
+
259
+ it("merges and dedupes availableModels across user and project layers", async () => {
260
+ await writeUserSettings({ availableModels: ["model-a", "model-b"] });
261
+ await writeProjectSettings({ availableModels: ["model-b", "model-c"] });
262
+
263
+ const manager = new SettingsManager(cwd);
264
+ await manager.initialize();
265
+
266
+ expect(manager.getSettings().availableModels).toEqual([
267
+ "model-a",
268
+ "model-b",
269
+ "model-c",
270
+ ]);
271
+ });
272
+
273
+ it("passes through a single layer unchanged", async () => {
274
+ await writeProjectSettings({ availableModels: ["only-one"] });
275
+
276
+ const manager = new SettingsManager(cwd);
277
+ await manager.initialize();
278
+
279
+ expect(manager.getSettings().availableModels).toEqual(["only-one"]);
280
+ });
281
+
282
+ it("leaves availableModels undefined when no layer defines it", async () => {
283
+ const manager = new SettingsManager(cwd);
284
+ await manager.initialize();
285
+
286
+ expect(manager.getSettings().availableModels).toBeUndefined();
287
+ });
288
+ });
289
+
290
+ describe("mergeAvailableModels", () => {
291
+ it("merges and dedupes non-enterprise layers", () => {
292
+ expect(
293
+ mergeAvailableModels(
294
+ ["model-a", "model-b"],
295
+ ["model-b", "model-c"],
296
+ "project",
297
+ ),
298
+ ).toEqual(["model-a", "model-b", "model-c"]);
299
+ });
300
+
301
+ it("lets enterprise settings replace lower-precedence allowlists", () => {
302
+ expect(
303
+ mergeAvailableModels(
304
+ ["model-a", "model-b"],
305
+ ["managed-a", "managed-a"],
306
+ "enterprise",
307
+ ),
308
+ ).toEqual(["managed-a"]);
309
+ });
310
+ });
@@ -196,9 +196,12 @@ export interface ClaudeCodeSettings {
196
196
  permissions?: PermissionSettings;
197
197
  env?: Record<string, string>;
198
198
  model?: string;
199
+ availableModels?: string[];
199
200
  posthogApprovedExecTools?: string[];
200
201
  }
201
202
 
203
+ type SettingsLayer = "user" | "project" | "local" | "enterprise";
204
+
202
205
  export type PermissionDecision = "allow" | "deny" | "ask";
203
206
 
204
207
  export interface PermissionCheckResult {
@@ -220,6 +223,22 @@ export function getManagedSettingsPath(): string {
220
223
  }
221
224
  }
222
225
 
226
+ export function mergeAvailableModels(
227
+ existing: string[] | undefined,
228
+ incoming: string[] | undefined,
229
+ layer: SettingsLayer,
230
+ ): string[] | undefined {
231
+ if (incoming === undefined) {
232
+ return existing;
233
+ }
234
+
235
+ if (layer === "enterprise") {
236
+ return Array.from(new Set(incoming));
237
+ }
238
+
239
+ return Array.from(new Set([...(existing ?? []), ...incoming]));
240
+ }
241
+
223
242
  export class SettingsManager {
224
243
  private cwd: string;
225
244
  private repoRoot: string;
@@ -283,11 +302,14 @@ export class SettingsManager {
283
302
  }
284
303
 
285
304
  private mergeAllSettings(): void {
286
- const allSettings = [
287
- this.userSettings,
288
- this.projectSettings,
289
- this.localSettings,
290
- this.enterpriseSettings,
305
+ const allSettings: Array<{
306
+ layer: SettingsLayer;
307
+ settings: ClaudeCodeSettings;
308
+ }> = [
309
+ { layer: "user", settings: this.userSettings },
310
+ { layer: "project", settings: this.projectSettings },
311
+ { layer: "local", settings: this.localSettings },
312
+ { layer: "enterprise", settings: this.enterpriseSettings },
291
313
  ];
292
314
 
293
315
  const permissions: PermissionSettings = {
@@ -298,7 +320,7 @@ export class SettingsManager {
298
320
  const merged: ClaudeCodeSettings = { permissions };
299
321
  const posthogApprovedExecTools = new Set<string>();
300
322
 
301
- for (const settings of allSettings) {
323
+ for (const { layer, settings } of allSettings) {
302
324
  if (settings.permissions) {
303
325
  if (settings.permissions.allow) {
304
326
  permissions.allow?.push(...settings.permissions.allow);
@@ -325,6 +347,11 @@ export class SettingsManager {
325
347
  if (settings.model) {
326
348
  merged.model = settings.model;
327
349
  }
350
+ merged.availableModels = mergeAvailableModels(
351
+ merged.availableModels,
352
+ settings.availableModels,
353
+ layer,
354
+ );
328
355
  if (settings.posthogApprovedExecTools) {
329
356
  for (const tool of settings.posthogApprovedExecTools) {
330
357
  posthogApprovedExecTools.add(tool);
@@ -29,8 +29,10 @@ export const WEB_TOOLS: Set<string> = new Set(["WebSearch", "WebFetch"]);
29
29
  export const AGENT_TOOLS: Set<string> = new Set([
30
30
  "Task",
31
31
  "Agent",
32
- "TodoWrite",
33
- "Skill",
32
+ "TaskCreate",
33
+ "TaskUpdate",
34
+ "TaskGet",
35
+ "TaskList",
34
36
  ]);
35
37
 
36
38
  const BASE_ALLOWED_TOOLS = [
@@ -11,6 +11,7 @@ import type {
11
11
  import type { Pushable } from "../../utils/streams";
12
12
  import type { BaseSession } from "../base-acp-agent";
13
13
  import type { ContextBreakdownBaseline } from "./context-breakdown";
14
+ import type { TaskState } from "./conversion/task-state";
14
15
  import type { McpToolApprovals } from "./mcp/tool-metadata";
15
16
  import type { SettingsManager } from "./session/settings";
16
17
  import type { CodeExecutionMode } from "./tools";
@@ -75,6 +76,13 @@ export type Session = BaseSession & {
75
76
  * "command is genuinely unknown" when the session goes idle without an echo.
76
77
  */
77
78
  knownSlashCommands?: Set<string>;
79
+ /**
80
+ * Per-session task list accumulated from Task* tool calls.
81
+ * SDK >=0.3.142 replaced TodoWrite (snapshot) with TaskCreate/TaskUpdate
82
+ * (incremental, keyed by task id). Map iteration preserves insertion order
83
+ * which we use for plan entry ordering.
84
+ */
85
+ taskState: TaskState;
78
86
  };
79
87
 
80
88
  export type ToolUseCache = {
@@ -466,7 +466,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
466
466
 
467
467
  // Carry taskRunId/taskId across load so prompt() still emits cloud
468
468
  // notifications (TURN_COMPLETE, USAGE_UPDATE) after a reload. newSession
469
- // and unstable_resumeSession both do this; loadSession historically did
469
+ // and resumeSession both do this; loadSession historically did
470
470
  // not, which silently broke task-completion tracking on re-attach.
471
471
  resetSessionState(this.sessionState, params.sessionId, params.cwd, {
472
472
  taskRunId: meta?.taskRunId,
@@ -489,7 +489,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
489
489
  return response;
490
490
  }
491
491
 
492
- async unstable_resumeSession(
492
+ async resumeSession(
493
493
  params: ResumeSessionRequest,
494
494
  ): Promise<ResumeSessionResponse> {
495
495
  const meta = params._meta as NewSessionMeta | undefined;