@posthog/agent 2.3.67 → 2.3.73

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 (33) hide show
  1. package/dist/adapters/claude/permissions/permission-options.js +12 -2
  2. package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
  3. package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -1
  4. package/dist/agent.js +239 -70
  5. package/dist/agent.js.map +1 -1
  6. package/dist/claude-cli/cli.js +4002 -2916
  7. package/dist/claude-cli/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
  8. package/dist/claude-cli/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
  9. package/dist/claude-cli/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
  10. package/dist/claude-cli/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
  11. package/dist/claude-cli/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
  12. package/dist/claude-cli/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
  13. package/dist/claude-cli/vendor/tree-sitter-bash/arm64-darwin/tree-sitter-bash.node +0 -0
  14. package/dist/claude-cli/vendor/tree-sitter-bash/arm64-linux/tree-sitter-bash.node +0 -0
  15. package/dist/claude-cli/vendor/tree-sitter-bash/arm64-win32/tree-sitter-bash.node +0 -0
  16. package/dist/claude-cli/vendor/tree-sitter-bash/x64-darwin/tree-sitter-bash.node +0 -0
  17. package/dist/claude-cli/vendor/tree-sitter-bash/x64-linux/tree-sitter-bash.node +0 -0
  18. package/dist/claude-cli/vendor/tree-sitter-bash/x64-win32/tree-sitter-bash.node +0 -0
  19. package/dist/posthog-api.js +3 -3
  20. package/dist/posthog-api.js.map +1 -1
  21. package/dist/server/agent-server.js +239 -70
  22. package/dist/server/agent-server.js.map +1 -1
  23. package/dist/server/bin.cjs +239 -70
  24. package/dist/server/bin.cjs.map +1 -1
  25. package/package.json +3 -3
  26. package/src/adapters/base-acp-agent.ts +11 -2
  27. package/src/adapters/claude/UPSTREAM.md +3 -4
  28. package/src/adapters/claude/claude-agent.ts +217 -35
  29. package/src/adapters/claude/conversion/sdk-to-acp.ts +2 -25
  30. package/src/adapters/claude/permissions/permission-handlers.ts +5 -7
  31. package/src/adapters/claude/permissions/permission-options.ts +17 -2
  32. package/src/adapters/claude/session/models.ts +94 -4
  33. package/src/adapters/claude/types.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.3.67",
3
+ "version": "2.3.73",
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": {
@@ -78,8 +78,8 @@
78
78
  "@posthog/git": "1.0.0"
79
79
  },
80
80
  "dependencies": {
81
- "@agentclientprotocol/sdk": "0.15.0",
82
- "@anthropic-ai/claude-agent-sdk": "0.2.71",
81
+ "@agentclientprotocol/sdk": "0.16.1",
82
+ "@anthropic-ai/claude-agent-sdk": "0.2.76",
83
83
  "@anthropic-ai/sdk": "^0.78.0",
84
84
  "@hono/node-server": "^1.19.9",
85
85
  "@opentelemetry/api-logs": "^0.208.0",
@@ -20,6 +20,7 @@ import {
20
20
  DEFAULT_GATEWAY_MODEL,
21
21
  fetchGatewayModels,
22
22
  formatGatewayModelName,
23
+ type GatewayModel,
23
24
  isAnthropicModel,
24
25
  } from "../gateway-models";
25
26
  import { Logger } from "../utils/logger";
@@ -33,6 +34,8 @@ export interface BaseSession {
33
34
  settingsManager: SettingsManager;
34
35
  }
35
36
 
37
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
38
+
36
39
  export abstract class BaseAcpAgent implements Agent {
37
40
  abstract readonly adapterName: string;
38
41
  protected session!: BaseSession;
@@ -40,6 +43,7 @@ export abstract class BaseAcpAgent implements Agent {
40
43
  client: AgentSideConnection;
41
44
  logger: Logger;
42
45
  fileContentCache: { [key: string]: string } = {};
46
+ protected gatewayModels: GatewayModel[] = [];
43
47
 
44
48
  constructor(client: AgentSideConnection) {
45
49
  this.client = client;
@@ -119,9 +123,9 @@ export abstract class BaseAcpAgent implements Agent {
119
123
  currentModelId: string;
120
124
  options: SessionConfigSelectOption[];
121
125
  }> {
122
- const gatewayModels = await fetchGatewayModels();
126
+ this.gatewayModels = await fetchGatewayModels();
123
127
 
124
- const options = gatewayModels
128
+ const options = this.gatewayModels
125
129
  .filter((model) => isAnthropicModel(model))
126
130
  .map((model) => ({
127
131
  value: model.id,
@@ -150,4 +154,9 @@ export abstract class BaseAcpAgent implements Agent {
150
154
 
151
155
  return { currentModelId, options };
152
156
  }
157
+
158
+ getContextWindowForModel(modelId: string): number {
159
+ const match = this.gatewayModels.find((m) => m.id === modelId);
160
+ return match?.context_window ?? DEFAULT_CONTEXT_WINDOW;
161
+ }
153
162
  }
@@ -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.21.0, commit `c13edf3b02ddaf49f8e8b9e11b02cbf17869b57d`, March 9 2026
9
- - **SDK**: `@anthropic-ai/claude-agent-sdk` 0.2.71, `@agentclientprotocol/sdk` 0.15.0
8
+ - **Last sync**: v0.22.2, commit `07db59e`, March 25 2026
9
+ - **SDK**: `@anthropic-ai/claude-agent-sdk` 0.2.76, `@agentclientprotocol/sdk` 0.16.1
10
10
 
11
11
  ## File Mapping
12
12
 
@@ -49,13 +49,12 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
49
49
  | Model resolution | `initializationResult.models` from SDK | `fetchGatewayModels()` from gateway API | Different model backend |
50
50
  | permissionMode | Hardcoded `"default"` | Reads from `meta.permissionMode` | More flexible mode selection |
51
51
  | Session storage | `this.sessions[sessionId]` (multi) | `this.session` (single) | Architectural choice |
52
- | ExitPlanMode denial | `interrupt: true` | `interrupt: false` | Better UX — lets Claude refine plan |
53
52
  | bypassPermissions | `updatedPermissions` with `destination: "session"` | No `updatedPermissions` | Different permission persistence |
54
53
  | Auth methods | Always returns `claude-login` auth method | Returns empty `authMethods` | Auth handled externally |
55
54
 
56
55
  ## Next Sync
57
56
 
58
- 1. Check upstream changelog since v0.21.0
57
+ 1. Check upstream changelog since v0.22.2
59
58
  2. Diff upstream source against PostHog Code using the file mapping above
60
59
  3. Port in phases: bug fixes first, then features
61
60
  4. After each phase: `pnpm --filter agent typecheck && pnpm --filter agent build && pnpm lint`
@@ -64,8 +64,8 @@ import { getAvailableSlashCommands } from "./session/commands";
64
64
  import { parseMcpServers } from "./session/mcp-config";
65
65
  import {
66
66
  DEFAULT_MODEL,
67
- getDefaultContextWindow,
68
67
  getEffortOptions,
68
+ resolveModelPreference,
69
69
  toSdkModelId,
70
70
  } from "./session/models";
71
71
  import {
@@ -89,6 +89,7 @@ import type {
89
89
 
90
90
  const SESSION_VALIDATION_TIMEOUT_MS = 10_000;
91
91
  const MAX_TITLE_LENGTH = 256;
92
+ const LOCAL_ONLY_COMMANDS = new Set(["/context", "/heapdump", "/extra-usage"]);
92
93
 
93
94
  function sanitizeTitle(text: string): string {
94
95
  const sanitized = text
@@ -141,6 +142,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
141
142
  list: {},
142
143
  fork: {},
143
144
  resume: {},
145
+ close: {},
144
146
  },
145
147
  _meta: {
146
148
  posthog: {
@@ -195,6 +197,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
195
197
  async unstable_resumeSession(
196
198
  params: ResumeSessionRequest,
197
199
  ): Promise<ResumeSessionResponse> {
200
+ // Reuse existing session if it matches
201
+ const existing = this.getExistingSessionState(params.sessionId);
202
+ if (existing) return existing;
203
+
198
204
  const response = await this.createSession(
199
205
  {
200
206
  cwd: params.cwd,
@@ -210,6 +216,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
210
216
  }
211
217
 
212
218
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
219
+ // Reuse existing session if it matches
220
+ const existing = this.getExistingSessionState(params.sessionId);
221
+ if (existing) return existing;
222
+
213
223
  const response = await this.createSession(
214
224
  {
215
225
  cwd: params.cwd,
@@ -231,7 +241,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
231
241
  };
232
242
  }
233
243
 
234
- async unstable_listSessions(
244
+ async listSessions(
235
245
  params: ListSessionsRequest,
236
246
  ): Promise<ListSessionsResponse> {
237
247
  const sdkSessions = await listSessions({ dir: params.cwd ?? undefined });
@@ -251,6 +261,12 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
251
261
  };
252
262
  }
253
263
 
264
+ async unstable_listSessions(
265
+ params: ListSessionsRequest,
266
+ ): Promise<ListSessionsResponse> {
267
+ return this.listSessions(params);
268
+ }
269
+
254
270
  async prompt(params: PromptRequest): Promise<PromptResponse> {
255
271
  this.session.cancelled = false;
256
272
  this.session.interruptReason = undefined;
@@ -262,18 +278,40 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
262
278
  };
263
279
 
264
280
  const userMessage = promptToClaude(params);
281
+ const promptUuid = randomUUID();
282
+ userMessage.uuid = promptUuid;
283
+ let promptReplayed = false;
284
+ let isLocalOnlyCommand = false;
285
+
286
+ // Detect local-only slash commands that return results without model invocation
287
+ const msgContent = userMessage.message.content;
288
+ let firstTextPart = "";
289
+ if (typeof msgContent === "string") {
290
+ firstTextPart = msgContent;
291
+ } else if (Array.isArray(msgContent)) {
292
+ for (const block of msgContent) {
293
+ if ("type" in block && block.type === "text" && "text" in block) {
294
+ firstTextPart = block.text as string;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+ const commandMatch = firstTextPart.match(/^(\/\S+)/);
300
+ if (commandMatch && LOCAL_ONLY_COMMANDS.has(commandMatch[1])) {
301
+ isLocalOnlyCommand = true;
302
+ promptReplayed = true;
303
+ }
265
304
 
266
305
  if (this.session.promptRunning) {
267
- const uuid = randomUUID();
268
- userMessage.uuid = uuid;
269
306
  this.session.input.push(userMessage);
270
307
  const order = this.session.nextPendingOrder++;
271
308
  const cancelled = await new Promise<boolean>((resolve) => {
272
- this.session.pendingMessages.set(uuid, { resolve, order });
309
+ this.session.pendingMessages.set(promptUuid, { resolve, order });
273
310
  });
274
311
  if (cancelled) {
275
312
  return { stopReason: "cancelled" };
276
313
  }
314
+ promptReplayed = true;
277
315
  } else {
278
316
  this.session.input.push(userMessage);
279
317
  }
@@ -284,6 +322,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
284
322
  this.session.promptRunning = true;
285
323
  let handedOff = false;
286
324
  let lastAssistantTotalUsage: number | null = null;
325
+ if (this.session.lastContextWindowSize == null) {
326
+ this.session.lastContextWindowSize = this.getContextWindowForModel(
327
+ this.session.modelId ?? "",
328
+ );
329
+ this.logger.debug("Initial context window size from gateway", {
330
+ modelId: this.session.modelId,
331
+ contextWindowSize: this.session.lastContextWindowSize,
332
+ });
333
+ }
334
+ let lastContextWindowSize = this.session.lastContextWindowSize;
287
335
 
288
336
  const supportsTerminalOutput =
289
337
  (
@@ -322,11 +370,24 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
322
370
  case "system":
323
371
  if (message.subtype === "compact_boundary") {
324
372
  lastAssistantTotalUsage = 0;
373
+ promptReplayed = true;
374
+ }
375
+ if (message.subtype === "local_command_output") {
376
+ promptReplayed = true;
325
377
  }
326
378
  await handleSystemMessage(message, context);
327
379
  break;
328
380
 
329
381
  case "result": {
382
+ // Skip results from background tasks that finished after our prompt started
383
+ if (!promptReplayed) {
384
+ this.logger.debug(
385
+ "Skipping background task result before prompt replay",
386
+ { sessionId: params.sessionId },
387
+ );
388
+ break;
389
+ }
390
+
330
391
  if (this.session.cancelled) {
331
392
  return { stopReason: "cancelled" };
332
393
  }
@@ -341,16 +402,25 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
341
402
  this.session.accumulatedUsage.cachedWriteTokens +=
342
403
  message.usage.cache_creation_input_tokens;
343
404
 
344
- // Calculate context window size from modelUsage (minimum across all models used)
405
+ // SDK can underreport context window (e.g. 200k for 1M models).
406
+ // Use SDK value only if it's larger than what gateway reported.
345
407
  const contextWindows = Object.values(message.modelUsage).map(
346
408
  (m) => m.contextWindow,
347
409
  );
348
- const contextWindowSize =
349
- contextWindows.length > 0
350
- ? Math.min(...contextWindows)
351
- : getDefaultContextWindow(this.session.modelId ?? "");
410
+ if (contextWindows.length > 0) {
411
+ const sdkContextWindow = Math.min(...contextWindows);
412
+ if (sdkContextWindow > lastContextWindowSize) {
413
+ lastContextWindowSize = sdkContextWindow;
414
+ }
415
+ }
416
+ this.session.lastContextWindowSize = lastContextWindowSize;
417
+ this.logger.debug("Context window size from result", {
418
+ sdkReported: contextWindows,
419
+ resolved: lastContextWindowSize,
420
+ modelId: this.session.modelId,
421
+ });
352
422
 
353
- this.session.contextSize = contextWindowSize;
423
+ this.session.contextSize = lastContextWindowSize;
354
424
  if (lastAssistantTotalUsage !== null) {
355
425
  this.session.contextUsed = lastAssistantTotalUsage;
356
426
  }
@@ -362,7 +432,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
362
432
  update: {
363
433
  sessionUpdate: "usage_update",
364
434
  used: lastAssistantTotalUsage,
365
- size: contextWindowSize,
435
+ size: lastContextWindowSize,
366
436
  cost: {
367
437
  amount: message.total_cost_usd,
368
438
  currency: "USD",
@@ -398,6 +468,21 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
398
468
  const result = handleResultMessage(message);
399
469
  if (result.error) throw result.error;
400
470
 
471
+ // For local-only commands, forward the result text to the client
472
+ if (
473
+ isLocalOnlyCommand &&
474
+ message.subtype === "success" &&
475
+ message.result
476
+ ) {
477
+ await this.client.sessionUpdate({
478
+ sessionId: params.sessionId,
479
+ update: {
480
+ sessionUpdate: "agent_message_chunk",
481
+ content: { type: "text", text: message.result },
482
+ },
483
+ });
484
+ }
485
+
401
486
  return { stopReason: result.stopReason ?? "end_turn", usage };
402
487
  }
403
488
 
@@ -411,8 +496,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
411
496
  break;
412
497
  }
413
498
 
414
- // Check for queued prompt replay
499
+ // Check for prompt replay (our own message echoed back)
415
500
  if (message.type === "user" && "uuid" in message && message.uuid) {
501
+ if (message.uuid === promptUuid) {
502
+ promptReplayed = true;
503
+ break;
504
+ }
505
+
416
506
  const pending = this.session.pendingMessages.get(
417
507
  message.uuid as string,
418
508
  );
@@ -449,9 +539,18 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
449
539
  };
450
540
  lastAssistantTotalUsage =
451
541
  usage.input_tokens +
452
- usage.output_tokens +
453
542
  usage.cache_read_input_tokens +
454
543
  usage.cache_creation_input_tokens;
544
+
545
+ await this.client.sessionUpdate({
546
+ sessionId: params.sessionId,
547
+ update: {
548
+ sessionUpdate: "usage_update",
549
+ used: lastAssistantTotalUsage,
550
+ size: lastContextWindowSize,
551
+ cost: null,
552
+ },
553
+ });
455
554
  }
456
555
 
457
556
  const result = await handleUserAssistantMessage(message, context);
@@ -490,6 +589,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
490
589
  this.logger.error(`Process died: ${msg}`, {
491
590
  sessionId: this.sessionId,
492
591
  });
592
+ this.session.settingsManager.dispose();
493
593
  this.session.input.end();
494
594
  throw RequestError.internalError(
495
595
  undefined,
@@ -522,9 +622,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
522
622
  async unstable_setSessionModel(
523
623
  params: SetSessionModelRequest,
524
624
  ): Promise<SetSessionModelResponse | undefined> {
525
- const sdkModelId = toSdkModelId(params.modelId);
526
- await this.session.query.setModel(sdkModelId);
625
+ await this.session.query.setModel(toSdkModelId(params.modelId));
527
626
  this.session.modelId = params.modelId;
627
+ this.session.lastContextWindowSize = this.getContextWindowForModel(
628
+ params.modelId,
629
+ );
528
630
  this.rebuildEffortConfigOption(params.modelId);
529
631
  await this.updateConfigOption("model", params.modelId);
530
632
  return {};
@@ -548,43 +650,72 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
548
650
  throw new Error(`Unknown config option: ${params.configId}`);
549
651
  }
550
652
 
551
- const allValues: { value: string }[] =
653
+ if (typeof params.value !== "string") {
654
+ throw new Error(
655
+ `Invalid value type for config option ${params.configId}`,
656
+ );
657
+ }
658
+
659
+ const allValues: { value: string; name?: string; description?: string }[] =
552
660
  "options" in option && Array.isArray(option.options)
553
661
  ? (option.options as Array<Record<string, unknown>>).flatMap((o) =>
554
662
  "options" in o && Array.isArray(o.options)
555
- ? (o.options as { value: string }[])
556
- : [o as { value: string }],
663
+ ? (o.options as {
664
+ value: string;
665
+ name?: string;
666
+ description?: string;
667
+ }[])
668
+ : [o as { value: string; name?: string; description?: string }],
557
669
  )
558
670
  : [];
559
- const validValue = allValues.find((o) => o.value === params.value);
671
+ let validValue = allValues.find((o) => o.value === params.value);
672
+
673
+ // For model options, fall back to alias resolution when exact match fails.
674
+ // This lets callers use human-friendly aliases like "opus" or "sonnet"
675
+ // instead of full model IDs like "claude-opus-4-6".
676
+ if (!validValue && params.configId === "model") {
677
+ const resolved = resolveModelPreference(params.value, allValues);
678
+ if (resolved) {
679
+ validValue = allValues.find((o) => o.value === resolved);
680
+ }
681
+ }
682
+
560
683
  if (!validValue) {
561
684
  throw new Error(
562
685
  `Invalid value for config option ${params.configId}: ${params.value}`,
563
686
  );
564
687
  }
565
688
 
689
+ // Use the canonical option value so downstream code always receives the
690
+ // model ID rather than the caller-supplied alias.
691
+ const resolvedValue = validValue.value;
692
+
566
693
  if (params.configId === "mode") {
567
- await this.applySessionMode(params.value);
694
+ await this.applySessionMode(resolvedValue);
568
695
  await this.client.sessionUpdate({
569
696
  sessionId: this.sessionId,
570
697
  update: {
571
698
  sessionUpdate: "current_mode_update",
572
- currentModeId: params.value,
699
+ currentModeId: resolvedValue,
573
700
  },
574
701
  });
575
702
  } else if (params.configId === "model") {
576
- const sdkModelId = toSdkModelId(params.value);
703
+ const sdkModelId = toSdkModelId(resolvedValue);
577
704
  await this.session.query.setModel(sdkModelId);
578
- this.session.modelId = params.value;
579
- this.rebuildEffortConfigOption(params.value);
705
+ this.session.modelId = resolvedValue;
706
+ this.session.lastContextWindowSize =
707
+ this.getContextWindowForModel(resolvedValue);
708
+ this.rebuildEffortConfigOption(resolvedValue);
580
709
  } else if (params.configId === "effort") {
581
- const newEffort = params.value as EffortLevel;
710
+ const newEffort = resolvedValue as EffortLevel;
582
711
  this.session.effort = newEffort;
583
712
  this.session.queryOptions.effort = newEffort;
584
713
  }
585
714
 
586
715
  this.session.configOptions = this.session.configOptions.map((o) =>
587
- o.id === params.configId ? { ...o, currentValue: params.value } : o,
716
+ o.id === params.configId && typeof o.currentValue === "string"
717
+ ? { ...o, currentValue: resolvedValue }
718
+ : o,
588
719
  );
589
720
 
590
721
  return { configOptions: this.session.configOptions };
@@ -595,7 +726,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
595
726
  value: string,
596
727
  ): Promise<void> {
597
728
  this.session.configOptions = this.session.configOptions.map((o) =>
598
- o.id === configId ? { ...o, currentValue: value } : o,
729
+ o.id === configId && typeof o.currentValue === "string"
730
+ ? { ...o, currentValue: value }
731
+ : o,
599
732
  );
600
733
 
601
734
  await this.client.sessionUpdate({
@@ -691,7 +824,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
691
824
  sessionId,
692
825
  isResume,
693
826
  forkSession,
694
- additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
827
+ additionalDirectories: [
828
+ ...(meta?.claudeCode?.options?.additionalDirectories ?? []),
829
+ ...(meta?.additionalRoots ?? []),
830
+ ],
695
831
  disableBuiltInTools: meta?.disableBuiltInTools,
696
832
  settingsManager,
697
833
  onModeChange: this.createOnModeChange(),
@@ -788,12 +924,12 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
788
924
  const modelOptions = await this.getModelConfigOptions();
789
925
  const resolvedModelId = settingsModel || modelOptions.currentModelId;
790
926
  session.modelId = resolvedModelId;
927
+ session.lastContextWindowSize =
928
+ this.getContextWindowForModel(resolvedModelId);
791
929
 
792
- if (!isResume) {
793
- const resolvedSdkModel = toSdkModelId(resolvedModelId);
794
- if (resolvedSdkModel !== DEFAULT_MODEL) {
795
- await this.session.query.setModel(resolvedSdkModel);
796
- }
930
+ const resolvedSdkModel = toSdkModelId(resolvedModelId);
931
+ if (!isResume && resolvedSdkModel !== DEFAULT_MODEL) {
932
+ await this.session.query.setModel(resolvedSdkModel);
797
933
  }
798
934
 
799
935
  const availableModes = getAvailableModes();
@@ -869,6 +1005,50 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
869
1005
  };
870
1006
  }
871
1007
 
1008
+ private getExistingSessionState(
1009
+ sessionId: string,
1010
+ ): NewSessionResponse | null {
1011
+ if (this.sessionId !== sessionId || !this.session) return null;
1012
+
1013
+ const availableModes = getAvailableModes();
1014
+ const modes: SessionModeState = {
1015
+ currentModeId: this.session.permissionMode,
1016
+ availableModes: availableModes.map((mode) => ({
1017
+ id: mode.id,
1018
+ name: mode.name,
1019
+ description: mode.description ?? undefined,
1020
+ })),
1021
+ };
1022
+
1023
+ const modelOptions = this.session.configOptions.find(
1024
+ (o) => o.id === "model",
1025
+ );
1026
+ const models: SessionModelState = {
1027
+ currentModelId: this.session.modelId ?? DEFAULT_MODEL,
1028
+ availableModels:
1029
+ modelOptions && "options" in modelOptions
1030
+ ? (
1031
+ modelOptions.options as Array<{
1032
+ value: string;
1033
+ name: string;
1034
+ description?: string;
1035
+ }>
1036
+ ).map((opt) => ({
1037
+ modelId: opt.value,
1038
+ name: opt.name,
1039
+ description: opt.description,
1040
+ }))
1041
+ : [],
1042
+ };
1043
+
1044
+ return {
1045
+ sessionId,
1046
+ modes,
1047
+ models,
1048
+ configOptions: this.session.configOptions,
1049
+ };
1050
+ }
1051
+
872
1052
  private buildConfigOptions(
873
1053
  currentModeId: string,
874
1054
  modelOptions: {
@@ -938,7 +1118,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
938
1118
  return;
939
1119
  }
940
1120
 
941
- const currentValue = existingEffort?.currentValue ?? "high";
1121
+ const rawCurrentValue = existingEffort?.currentValue;
1122
+ const currentValue =
1123
+ typeof rawCurrentValue === "string" ? rawCurrentValue : "high";
942
1124
  const isValidValue = effortOptions.some((o) => o.value === currentValue);
943
1125
  const resolvedValue = isValidValue ? currentValue : "high";
944
1126
 
@@ -544,7 +544,7 @@ export async function handleSystemMessage(
544
544
  message: Extract<SDKMessage, { type: "system" }>,
545
545
  context: MessageHandlerContext,
546
546
  ): Promise<void> {
547
- const { sessionId, client, logger } = context;
547
+ const { session, sessionId, client, logger } = context;
548
548
 
549
549
  switch (message.subtype) {
550
550
  case "init":
@@ -554,6 +554,7 @@ export async function handleSystemMessage(
554
554
  sessionId,
555
555
  trigger: message.compact_metadata.trigger,
556
556
  preTokens: message.compact_metadata.pre_tokens,
557
+ contextSize: session.contextSize,
557
558
  });
558
559
  break;
559
560
  case "hook_response":
@@ -797,30 +798,6 @@ export async function handleUserAssistantMessage(
797
798
  context;
798
799
 
799
800
  if (shouldSkipUserAssistantMessage(message)) {
800
- const content = message.message.content;
801
-
802
- // Handle /context by sending its reply as a regular agent message
803
- if (
804
- typeof content === "string" &&
805
- hasLocalCommandStdout(content) &&
806
- content.includes("Context Usage")
807
- ) {
808
- const stripped = content
809
- .replace("<local-command-stdout>", "")
810
- .replace("</local-command-stdout>", "");
811
- for (const notification of toAcpNotifications(
812
- stripped,
813
- "assistant",
814
- sessionId,
815
- toolUseCache,
816
- fileContentCache,
817
- client,
818
- logger,
819
- )) {
820
- await client.sessionUpdate(notification);
821
- }
822
- }
823
-
824
801
  logSpecialMessages(message, logger);
825
802
 
826
803
  if (isLoginRequiredMessage(message)) {
@@ -34,7 +34,7 @@ export type ToolPermissionResult =
34
34
  | {
35
35
  behavior: "deny";
36
36
  message: string;
37
- interrupt: boolean;
37
+ interrupt?: boolean;
38
38
  };
39
39
 
40
40
  interface ToolHandlerContext {
@@ -164,9 +164,11 @@ async function applyPlanApproval(
164
164
  if (
165
165
  response.outcome?.outcome === "selected" &&
166
166
  (response.outcome.optionId === "default" ||
167
- response.outcome.optionId === "acceptEdits")
167
+ response.outcome.optionId === "acceptEdits" ||
168
+ response.outcome.optionId === "bypassPermissions")
168
169
  ) {
169
- session.permissionMode = response.outcome.optionId;
170
+ session.permissionMode = response.outcome
171
+ .optionId as typeof session.permissionMode;
170
172
  await session.query.setPermissionMode(response.outcome.optionId);
171
173
  await context.client.sessionUpdate({
172
174
  sessionId: context.sessionId,
@@ -261,7 +263,6 @@ async function handleAskUserQuestionTool(
261
263
  return {
262
264
  behavior: "deny",
263
265
  message: "No questions provided",
264
- interrupt: true,
265
266
  };
266
267
  }
267
268
 
@@ -303,7 +304,6 @@ async function handleAskUserQuestionTool(
303
304
  typeof customMessage === "string"
304
305
  ? customMessage
305
306
  : "User cancelled the questions",
306
- interrupt: true,
307
307
  };
308
308
  }
309
309
 
@@ -312,7 +312,6 @@ async function handleAskUserQuestionTool(
312
312
  return {
313
313
  behavior: "deny",
314
314
  message: "User did not provide answers",
315
- interrupt: true,
316
315
  };
317
316
  }
318
317
 
@@ -393,7 +392,6 @@ async function handleDefaultPermissionFlow(
393
392
  return {
394
393
  behavior: "deny",
395
394
  message,
396
- interrupt: true,
397
395
  };
398
396
  }
399
397
  }
@@ -1,4 +1,5 @@
1
1
  import type { PermissionUpdate } from "@anthropic-ai/claude-agent-sdk";
2
+ import { IS_ROOT } from "../../../utils/common";
2
3
  import { BASH_TOOLS, READ_TOOLS, SEARCH_TOOLS, WRITE_TOOLS } from "../tools";
3
4
 
4
5
  export interface PermissionOption {
@@ -91,8 +92,20 @@ export function buildPermissionOptions(
91
92
  return permissionOptions("Yes, always allow");
92
93
  }
93
94
 
95
+ const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
96
+
94
97
  export function buildExitPlanModePermissionOptions(): PermissionOption[] {
95
- return [
98
+ const options: PermissionOption[] = [];
99
+
100
+ if (ALLOW_BYPASS) {
101
+ options.push({
102
+ kind: "allow_always",
103
+ name: "Yes, bypass all permissions",
104
+ optionId: "bypassPermissions",
105
+ });
106
+ }
107
+
108
+ options.push(
96
109
  {
97
110
  kind: "allow_always",
98
111
  name: "Yes, and auto-accept edits",
@@ -109,5 +122,7 @@ export function buildExitPlanModePermissionOptions(): PermissionOption[] {
109
122
  optionId: "reject_with_feedback",
110
123
  _meta: { customInput: true },
111
124
  },
112
- ];
125
+ );
126
+
127
+ return options;
113
128
  }