@agentclientprotocol/claude-agent-acp 0.40.0 → 0.42.0

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.
@@ -1,4 +1,4 @@
1
- import { Agent, AgentSideConnection, AuthenticateRequest, CancelNotification, ClientCapabilities, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, TerminalHandle, TerminalOutputResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
1
+ import { Agent, AgentSideConnection, AuthenticateRequest, CancelNotification, ClientCapabilities, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
2
2
  import { CanUseTool, ModelInfo, Options, PermissionMode, PermissionUpdate, Query, SDKMessageOrigin, SDKPartialAssistantMessage, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
3
3
  import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
4
4
  import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
@@ -52,6 +52,17 @@ type Session = {
52
52
  }>;
53
53
  nextPendingOrder: number;
54
54
  abortController: AbortController;
55
+ /** Per-turn signal the active prompt loop races `query.next()` against.
56
+ * Aborted by cancel() (after a grace period) to force the loop to return
57
+ * "cancelled" when the SDK is wedged and `query.next()` never yields again
58
+ * (issue #680). Distinct from `abortController`: this only wakes the loop;
59
+ * it does NOT touch the SDK query/subprocess. Undefined when no prompt is
60
+ * actively consuming the query. */
61
+ cancelController?: AbortController;
62
+ /** Pending grace-period timer that aborts `cancelController`. Cleared when
63
+ * the loop returns normally so the backstop never fires after a clean
64
+ * cancel. */
65
+ forceCancelTimer?: ReturnType<typeof setTimeout>;
55
66
  emitRawSDKMessages: boolean | SDKMessageFilter[];
56
67
  /** Context window size of the last top-level assistant model, carried across
57
68
  * prompts so mid-stream usage_update notifications report a correct `size`
@@ -62,14 +73,30 @@ type Session = {
62
73
  /** Accumulated task list for the session, keyed by task ID. Task IDs are
63
74
  * per-session, so this state must not be shared across sessions. */
64
75
  taskState: TaskState;
65
- };
66
- type BackgroundTerminal = {
67
- handle: TerminalHandle;
68
- status: "started";
69
- lastOutput: TerminalOutputResponse | null;
70
- } | {
71
- status: "aborted" | "exited" | "killed" | "timedOut";
72
- pendingOutput: TerminalOutputResponse;
76
+ /** Caches `tool_use` blocks by id so the matching `tool_result` can recover
77
+ * the tool name/input when mapping it to a `tool_call_update`. Per-session
78
+ * (tool_use ids are only unique within a session) and pruned at
79
+ * `tool_result` time so a long-running session doesn't accumulate every
80
+ * tool call for its whole lifetime. */
81
+ toolUseCache: ToolUseCache;
82
+ /** Maps the ACP `messageId` we expose to clients (see `messageIdForGrouping`)
83
+ * to the SDK message uuid that the Agent SDK's rewind/resume APIs key on
84
+ * (`Query.rewindFiles` takes a user-message uuid; `resumeSessionAt` takes an
85
+ * `SDKAssistantMessage.uuid`). For assistant turns the two differ — the ACP
86
+ * id is the Anthropic API message id (`msg_…`), available at `message_start`
87
+ * so streamed chunks can carry it, while the uuid only arrives on the
88
+ * consolidated message — so a client can only ask to rewind/fork by the id it
89
+ * was given, and we need this table to translate it back.
90
+ *
91
+ * Populated as a byproduct of the message loop (the consolidated message
92
+ * carries both ids) and of `replaySessionHistory` on load, so no extra
93
+ * `getSessionMessages` read is needed at rewind time. Last-write-wins
94
+ * naturally yields the turn-boundary uuid when one `msg_…` spans several
95
+ * content-block messages.
96
+ *
97
+ * NOT READ YET — recorded now so the mapping exists if/when we wire up
98
+ * fork/rewind. */
99
+ messageIdToUuid: Map<string, string>;
73
100
  };
74
101
  export type SDKMessageFilter = {
75
102
  type: string;
@@ -177,13 +204,13 @@ export declare class ClaudeAcpAgent implements Agent {
177
204
  [key: string]: Session;
178
205
  };
179
206
  client: AgentSideConnection;
180
- toolUseCache: ToolUseCache;
181
- backgroundTerminals: {
182
- [key: string]: BackgroundTerminal;
183
- };
184
207
  clientCapabilities?: ClientCapabilities;
185
208
  logger: Logger;
186
209
  gatewayAuthRequest?: GatewayAuthRequest;
210
+ /** Grace period before a `session/cancel` forces a wedged prompt loop to
211
+ * return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
212
+ * tests can shrink it. */
213
+ forceCancelGraceMs: number;
187
214
  constructor(client: AgentSideConnection, logger?: Logger);
188
215
  initialize(request: InitializeRequest): Promise<InitializeResponse>;
189
216
  newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
@@ -215,6 +242,22 @@ export declare class ClaudeAcpAgent implements Agent {
215
242
  private createSession;
216
243
  }
217
244
  export declare function promptToClaude(prompt: PromptRequest): SDKUserMessage;
245
+ /**
246
+ * Resolves the ACP `messageId` for a Claude SDK message (live) or a persisted
247
+ * transcript message (replay) so chunk grouping is identical in both views.
248
+ *
249
+ * Assistant turns are keyed by the Anthropic API message id (`message.id`),
250
+ * which is identical at `message_start`, on the consolidated assistant message,
251
+ * and in the persisted transcript — unlike the per-`stream_event` uuid, which is
252
+ * unique per event and never persisted. User messages have no API id, but they
253
+ * are never streamed, so their (stable) SDK uuid is used instead. ACP message
254
+ * ids are opaque strings, so no particular format is required.
255
+ */
256
+ export declare function messageIdForGrouping(message: {
257
+ type?: string;
258
+ uuid?: string | null;
259
+ message?: unknown;
260
+ }): string | undefined;
218
261
  /**
219
262
  * Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
220
263
  * Only handles text, image, and thinking chunks for now.
@@ -225,11 +268,13 @@ export declare function toAcpNotifications(content: string | ContentBlockParam[]
225
268
  parentToolUseId?: string | null;
226
269
  cwd?: string;
227
270
  taskState?: TaskState;
271
+ messageId?: string;
228
272
  }): SessionNotification[];
229
273
  export declare function streamEventToAcpNotifications(message: SDKPartialAssistantMessage, sessionId: string, toolUseCache: ToolUseCache, client: AgentSideConnection, logger: Logger, options?: {
230
274
  clientCapabilities?: ClientCapabilities;
231
275
  cwd?: string;
232
276
  taskState?: TaskState;
277
+ messageId?: string;
233
278
  }): SessionNotification[];
234
279
  export declare function runAcp(): {
235
280
  connection: AgentSideConnection;
@@ -1 +1 @@
1
- {"version":3,"file":"acp-agent.d.ts","sourceRoot":"","sources":["../src/acp-agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"acp-agent.d.ts","sourceRoot":"","sources":["../src/acp-agent.ts"],"names":[],"mappings":"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"}
package/dist/acp-agent.js CHANGED
@@ -27,6 +27,16 @@ const ZERO_USAGE = Object.freeze({
27
27
  cache_creation_input_tokens: 0,
28
28
  });
29
29
  const DEFAULT_CONTEXT_WINDOW = 200000;
30
+ /** Floor after `session/cancel` before the adapter forces the active prompt
31
+ * loop to return "cancelled". `query.interrupt()` normally makes the SDK
32
+ * yield a trailing idle within milliseconds, and the loop returns through its
33
+ * usual path — so this timer is armed and cleared, never fired, on healthy
34
+ * cancels. It only trips when the SDK is genuinely wedged (e.g. a
35
+ * `TaskOutput { block: true }` poll against a hung background task — issue
36
+ * #680) and never yields. The value is deliberately loose: it's an
37
+ * "obviously stuck" ceiling, not a guess at interrupt latency, so it can't
38
+ * pre-empt a slow-but-healthy interrupt. */
39
+ const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
30
40
  /** Compute a stable fingerprint of the session-defining params so we can
31
41
  * detect when a loadSession/resumeSession call requires tearing down and
32
42
  * recreating the underlying Query process. MCP servers are sorted by name
@@ -233,10 +243,12 @@ export function describeAlwaysAllow(suggestions, toolName) {
233
243
  // Implement the ACP Agent interface
234
244
  export class ClaudeAcpAgent {
235
245
  constructor(client, logger) {
236
- this.backgroundTerminals = {};
246
+ /** Grace period before a `session/cancel` forces a wedged prompt loop to
247
+ * return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
248
+ * tests can shrink it. */
249
+ this.forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
237
250
  this.sessions = {};
238
251
  this.client = client;
239
- this.toolUseCache = {};
240
252
  this.logger = logger ?? console;
241
253
  }
242
254
  async initialize(request) {
@@ -463,6 +475,12 @@ export class ClaudeAcpAgent {
463
475
  // forward it to clients as structured `data`, sparing them from
464
476
  // pattern-matching on the human-readable message text.
465
477
  let lastAssistantError;
478
+ // When a streaming classifier refuses a turn, the assistant message carries
479
+ // stop_reason "refusal" and structured stop_details. We capture the
480
+ // human-readable explanation here so the terminal `result` can surface it
481
+ // to the user (the refused assistant message itself usually has no content)
482
+ // and report ACP's dedicated `refusal` stop reason.
483
+ let lastRefusalExplanation = null;
466
484
  // Tracks whether we're inside a compaction. The SDK emits the terminal
467
485
  // `status` (compact_result success/failed) twice for a single failed
468
486
  // compaction, and the two messages are indistinguishable — so we report the
@@ -470,6 +488,17 @@ export class ClaudeAcpAgent {
470
488
  // `compacting` status sets it again, so every distinct compaction (e.g.
471
489
  // repeated auto-compactions in a long turn) is still shown.
472
490
  let compactionInProgress = false;
491
+ // Holds the Anthropic API message id of the assistant message currently
492
+ // being streamed, captured from `message_start` so every streamed chunk can
493
+ // be tagged with it. We use the API message id rather than the
494
+ // per-`stream_event` uuid because the same id is also present on the
495
+ // consolidated assistant message and in the persisted transcript — so a turn
496
+ // keeps the same ACP `messageId` whether it is streamed live or replayed
497
+ // from history. The per-event uuid is unique per event and never persisted.
498
+ // A single value suffices because every streaming partial arrives with
499
+ // `parent_tool_use_id === null` (subagent work is folded into tool-result
500
+ // messages, never surfaced as partial streams).
501
+ let currentStreamMessageId;
473
502
  const userMessage = promptToClaude(params);
474
503
  const promptUuid = randomUUID();
475
504
  userMessage.uuid = promptUuid;
@@ -495,9 +524,26 @@ export class ClaudeAcpAgent {
495
524
  let handedOff = false;
496
525
  let errored = false;
497
526
  let stopReason = "end_turn";
527
+ // Wake-up channel so cancel() can force this loop to return "cancelled"
528
+ // even when query.next() is wedged and never yields again (issue #680).
529
+ const cancelController = new AbortController();
530
+ session.cancelController = cancelController;
531
+ const cancelled = new Promise((resolve) => {
532
+ cancelController.signal.addEventListener("abort", () => resolve(), { once: true });
533
+ });
498
534
  try {
499
535
  while (true) {
500
- const { value: message, done } = await session.query.next();
536
+ const nextMessage = session.query.next();
537
+ const next = await Promise.race([nextMessage, cancelled]);
538
+ if (cancelController.signal.aborted) {
539
+ // The SDK never yielded after interrupt() (e.g. a wedged TaskOutput
540
+ // block). Abandon the in-flight next() — swallowing any later
541
+ // rejection so it can't surface as an unhandled rejection — and
542
+ // honor the cancel per the ACP contract.
543
+ void nextMessage.catch(() => { });
544
+ return { stopReason: "cancelled" };
545
+ }
546
+ const { value: message, done } = next;
501
547
  if (done || !message) {
502
548
  if (session.cancelled) {
503
549
  return { stopReason: "cancelled" };
@@ -554,28 +600,35 @@ export class ClaudeAcpAgent {
554
600
  break;
555
601
  }
556
602
  case "compact_boundary": {
557
- // Send used:0 immediately so the client doesn't keep showing
558
- // the stale pre-compaction context size until the next turn.
603
+ // Refresh the displayed usage immediately so the client doesn't
604
+ // keep showing the stale pre-compaction size (e.g. "944k/1m")
605
+ // right after the user sees "Compacting completed", which is
606
+ // confusing and wrong.
607
+ //
608
+ // Prefer the SDK's authoritative post-compaction `used` via
609
+ // getContextUsage — it reflects the real retained context
610
+ // (system prompt + tools + surviving messages), which the
611
+ // per-message API usage numbers can't give us until the next
612
+ // turn's result. If the control request fails, fall back to the
613
+ // used:0 approximation: directionally correct (context just
614
+ // dropped dramatically) and replaced within seconds by the next
615
+ // result message.
559
616
  //
560
- // This is a deliberate approximation: we don't know the exact
561
- // post-compaction token count (only the SDK's next API call
562
- // reveals that). But used:0 is directionally correct — context
563
- // just dropped dramatically — and the real value replaces it
564
- // within seconds when the next result message arrives.
565
- // The alternative (no update) leaves the client showing e.g.
566
- // "944k/1m" right after the user sees "Compacting completed",
567
- // which is confusing and wrong.
617
+ // `size` keeps coming from session.contextWindowSize (learned
618
+ // from modelUsage / the model heuristic) — getContextUsage's
619
+ // window field under-reports extended 1M windows.
568
620
  //
569
621
  // The "Compacting completed." text is emitted from the `status`
570
622
  // handler (keyed on `compact_result`), not here, so the failure
571
623
  // path gets a message too.
572
- lastAssistantTotalUsage = 0;
624
+ const usedTokens = await fetchContextUsedTokens(session.query, this.logger);
573
625
  lastAssistantUsage = null;
626
+ lastAssistantTotalUsage = usedTokens ?? 0;
574
627
  await this.client.sessionUpdate({
575
628
  sessionId: message.session_id,
576
629
  update: {
577
630
  sessionUpdate: "usage_update",
578
- used: 0,
631
+ used: lastAssistantTotalUsage,
579
632
  size: session.contextWindowSize,
580
633
  },
581
634
  });
@@ -637,6 +690,63 @@ export class ClaudeAcpAgent {
637
690
  });
638
691
  break;
639
692
  }
693
+ case "commands_changed": {
694
+ // Push the full slash-command list after a mid-session change
695
+ // (e.g. skills discovered dynamically as the agent works in a
696
+ // subdirectory). The client should REPLACE its cached command
697
+ // list with this payload: supportedCommands() is captured once
698
+ // at initialize and never reflects mid-session changes, so we
699
+ // forward message.commands directly rather than re-querying.
700
+ await this.client.sessionUpdate({
701
+ sessionId: message.session_id,
702
+ update: {
703
+ sessionUpdate: "available_commands_update",
704
+ availableCommands: getAvailableSlashCommands(message.commands),
705
+ },
706
+ });
707
+ break;
708
+ }
709
+ case "mirror_error": {
710
+ // The SDK failed to persist session history (SessionStore
711
+ // append rejected/timed out after retry) — potential data loss
712
+ // the user should know about rather than a silent gap on
713
+ // resume. Log it and surface a warning in the conversation.
714
+ this.logger.error(`Session ${message.session_id}: failed to persist history: ${message.error}`);
715
+ break;
716
+ }
717
+ case "permission_denied": {
718
+ // A tool call was auto-denied (by a rule, the classifier,
719
+ // dontAsk mode, etc.) before running. The tool_use block was
720
+ // already emitted as a `tool_call`, so mark it failed with the
721
+ // rejection reason — otherwise the client shows a tool call
722
+ // that silently never resolves.
723
+ const reason = message.decision_reason ?? message.message;
724
+ await this.client.sessionUpdate({
725
+ sessionId: message.session_id,
726
+ update: {
727
+ sessionUpdate: "tool_call_update",
728
+ toolCallId: message.tool_use_id,
729
+ status: "failed",
730
+ content: [
731
+ {
732
+ type: "content",
733
+ content: { type: "text", text: `Permission denied: ${reason}` },
734
+ },
735
+ ],
736
+ _meta: {
737
+ claudeCode: {
738
+ toolName: message.tool_name,
739
+ toolResponse: {
740
+ decisionReasonType: message.decision_reason_type,
741
+ decisionReason: message.decision_reason,
742
+ message: message.message,
743
+ },
744
+ },
745
+ },
746
+ },
747
+ });
748
+ break;
749
+ }
640
750
  case "hook_started":
641
751
  case "hook_progress":
642
752
  case "hook_response":
@@ -649,8 +759,6 @@ export class ClaudeAcpAgent {
649
759
  case "plugin_install":
650
760
  case "notification":
651
761
  case "api_retry":
652
- case "mirror_error":
653
- case "permission_denied":
654
762
  case "thinking_tokens":
655
763
  // Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
656
764
  break;
@@ -704,6 +812,25 @@ export class ClaudeAcpAgent {
704
812
  }
705
813
  break;
706
814
  }
815
+ // A refusal can arrive on any result subtype (and may even set
816
+ // is_error), so handle it before the subtype switch — otherwise the
817
+ // is_error throw below would surface it as an internal error. The
818
+ // refused assistant message carries no visible content, so surface
819
+ // the classifier's explanation (when available) and report ACP's
820
+ // dedicated `refusal` stop reason.
821
+ if (message.stop_reason === "refusal" && !isTaskNotification) {
822
+ if (lastRefusalExplanation) {
823
+ await this.client.sessionUpdate({
824
+ sessionId: params.sessionId,
825
+ update: {
826
+ sessionUpdate: "agent_message_chunk",
827
+ content: { type: "text", text: lastRefusalExplanation },
828
+ },
829
+ });
830
+ }
831
+ stopReason = "refusal";
832
+ break;
833
+ }
707
834
  switch (message.subtype) {
708
835
  case "success": {
709
836
  if (message.result.includes("Please run /login")) {
@@ -723,7 +850,7 @@ export class ClaudeAcpAgent {
723
850
  // Task-notification followups never originate from a user
724
851
  // slash command, so skip the forwarding for them.
725
852
  if (isLocalOnlyCommand && !isTaskNotification) {
726
- for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId, this.toolUseCache, this.client, this.logger)) {
853
+ for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId, session.toolUseCache, this.client, this.logger)) {
727
854
  await this.client.sessionUpdate(notification);
728
855
  }
729
856
  }
@@ -761,6 +888,12 @@ export class ClaudeAcpAgent {
761
888
  break;
762
889
  }
763
890
  case "stream_event": {
891
+ // `message_start` carries the Anthropic API message id; capture it
892
+ // so the streamed chunks that follow (whose delta events don't carry
893
+ // it) can all be tagged with the same, replay-stable id.
894
+ if (message.event.type === "message_start") {
895
+ currentStreamMessageId = message.event.message.id || undefined;
896
+ }
764
897
  if (message.parent_tool_use_id === null &&
765
898
  (message.event.type === "message_start" || message.event.type === "message_delta")) {
766
899
  if (message.event.type === "message_start") {
@@ -808,10 +941,11 @@ export class ClaudeAcpAgent {
808
941
  });
809
942
  }
810
943
  }
811
- for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger, {
944
+ for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
812
945
  clientCapabilities: this.clientCapabilities,
813
946
  cwd: session.cwd,
814
947
  taskState: session.taskState,
948
+ messageId: currentStreamMessageId,
815
949
  })) {
816
950
  await this.client.sessionUpdate(notification);
817
951
  }
@@ -822,6 +956,14 @@ export class ClaudeAcpAgent {
822
956
  if (session.cancelled) {
823
957
  break;
824
958
  }
959
+ // Record the ACP messageId -> SDK uuid mapping for this message. The
960
+ // consolidated message carries both ids, so this is where we learn
961
+ // the uuid that the SDK's rewind/resume APIs key on for the id we
962
+ // hand clients. Not read yet (see Session.messageIdToUuid).
963
+ const mappedMessageId = messageIdForGrouping(message);
964
+ if (mappedMessageId && typeof message.uuid === "string" && message.uuid.length > 0) {
965
+ session.messageIdToUuid.set(mappedMessageId, message.uuid);
966
+ }
825
967
  // Check for prompt replay
826
968
  if (message.type === "user" && "uuid" in message && message.uuid) {
827
969
  if (message.uuid === promptUuid) {
@@ -854,6 +996,9 @@ export class ClaudeAcpAgent {
854
996
  if (message.error) {
855
997
  lastAssistantError = message.error;
856
998
  }
999
+ if (message.message.stop_reason === "refusal") {
1000
+ lastRefusalExplanation = message.message.stop_details?.explanation ?? null;
1001
+ }
857
1002
  }
858
1003
  // Strip <command-*>/<local-command-stdout> markers and render any
859
1004
  // remaining prose. Skill bodies and built-in slash commands (e.g.
@@ -865,11 +1010,12 @@ export class ClaudeAcpAgent {
865
1010
  message.message.content.includes("<local-command-stdout>")) {
866
1011
  const stripped = stripLocalCommandMetadata(message.message.content);
867
1012
  if (typeof stripped === "string") {
868
- for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
1013
+ for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, this.client, this.logger, {
869
1014
  clientCapabilities: this.clientCapabilities,
870
1015
  parentToolUseId: message.parent_tool_use_id,
871
1016
  cwd: session.cwd,
872
1017
  taskState: session.taskState,
1018
+ messageId: messageIdForGrouping(message),
873
1019
  })) {
874
1020
  await this.client.sessionUpdate(notification);
875
1021
  }
@@ -907,21 +1053,51 @@ export class ClaudeAcpAgent {
907
1053
  ? // Handled by stream events above
908
1054
  message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
909
1055
  : message.message.content;
910
- for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
1056
+ for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, this.client, this.logger, {
911
1057
  clientCapabilities: this.clientCapabilities,
912
1058
  parentToolUseId: message.parent_tool_use_id,
913
1059
  cwd: session.cwd,
914
1060
  taskState: session.taskState,
1061
+ messageId: messageIdForGrouping(message),
915
1062
  })) {
916
1063
  await this.client.sessionUpdate(notification);
917
1064
  }
918
1065
  break;
919
1066
  }
920
- case "tool_progress":
1067
+ case "tool_progress": {
1068
+ await this.client.sessionUpdate({
1069
+ sessionId: message.session_id,
1070
+ update: {
1071
+ sessionUpdate: "tool_call_update",
1072
+ toolCallId: message.tool_use_id,
1073
+ status: "in_progress",
1074
+ _meta: {
1075
+ claudeCode: {
1076
+ toolName: message.tool_name,
1077
+ toolResponse: { elapsedTimeSeconds: message.elapsed_time_seconds },
1078
+ },
1079
+ },
1080
+ },
1081
+ });
1082
+ break;
1083
+ }
1084
+ case "rate_limit_event": {
1085
+ if (lastAssistantTotalUsage !== null) {
1086
+ await this.client.sessionUpdate({
1087
+ sessionId: message.session_id,
1088
+ update: {
1089
+ sessionUpdate: "usage_update",
1090
+ used: lastAssistantTotalUsage,
1091
+ size: session.contextWindowSize,
1092
+ _meta: { "_claude/rateLimit": message.rate_limit_info },
1093
+ },
1094
+ });
1095
+ }
1096
+ break;
1097
+ }
921
1098
  case "tool_use_summary":
922
1099
  case "auth_status":
923
1100
  case "prompt_suggestion":
924
- case "rate_limit_event":
925
1101
  break;
926
1102
  default:
927
1103
  unreachable(message);
@@ -973,6 +1149,16 @@ export class ClaudeAcpAgent {
973
1149
  throw error;
974
1150
  }
975
1151
  finally {
1152
+ // The loop is returning — interrupt() succeeded or the prompt finished
1153
+ // — so disarm the force-cancel backstop and release the wake-up channel
1154
+ // (only if we still own it; a handoff installs the next prompt's).
1155
+ if (session.forceCancelTimer) {
1156
+ clearTimeout(session.forceCancelTimer);
1157
+ session.forceCancelTimer = undefined;
1158
+ }
1159
+ if (session.cancelController === cancelController) {
1160
+ session.cancelController = undefined;
1161
+ }
976
1162
  if (!handedOff) {
977
1163
  session.promptRunning = false;
978
1164
  if (errored) {
@@ -1008,6 +1194,27 @@ export class ClaudeAcpAgent {
1008
1194
  pending.resolve(true);
1009
1195
  }
1010
1196
  session.pendingMessages.clear();
1197
+ // Arm a backstop before interrupting: if a prompt is actively consuming
1198
+ // the query and interrupt() doesn't make the SDK yield (e.g. a wedged
1199
+ // TaskOutput block — issue #680), force the loop to return "cancelled"
1200
+ // after the floor elapses so the pending session/prompt still resolves per
1201
+ // the ACP cancellation contract instead of hanging forever. The loop's
1202
+ // `finally` clears this timer when interrupt() works and it returns through
1203
+ // the normal idle path, so on healthy cancels it is armed but never fires.
1204
+ //
1205
+ // Arm at most once per turn: the floor is an absolute ceiling from the
1206
+ // first cancel, so a client that re-sends cancel (each call still retries
1207
+ // interrupt() below) can't keep pushing the deadline out.
1208
+ if (session.promptRunning &&
1209
+ session.cancelController &&
1210
+ !session.cancelController.signal.aborted &&
1211
+ !session.forceCancelTimer) {
1212
+ const cancelController = session.cancelController;
1213
+ session.forceCancelTimer = setTimeout(() => {
1214
+ this.logger.error(`Session ${params.sessionId}: cancel floor elapsed without the SDK yielding; forcing "cancelled". The underlying query may still be wedged — a new session may be required.`);
1215
+ cancelController.abort();
1216
+ }, this.forceCancelGraceMs);
1217
+ }
1011
1218
  await session.query.interrupt();
1012
1219
  }
1013
1220
  /** Cleanly tear down a session: cancel in-flight work, dispose resources,
@@ -1018,6 +1225,18 @@ export class ClaudeAcpAgent {
1018
1225
  return;
1019
1226
  }
1020
1227
  await this.cancel({ sessionId });
1228
+ // cancel() arms the force-cancel floor and interrupts gracefully, but a
1229
+ // wedged prompt loop only wakes when `cancelController` aborts — closing
1230
+ // the query/abortController below doesn't touch it. Since we're tearing the
1231
+ // session down anyway, wake the loop now so the in-flight prompt() resolves
1232
+ // immediately instead of after the floor, and clear the timer so it can't
1233
+ // outlive the deleted session (it isn't unref'd and would otherwise keep
1234
+ // the event loop alive until it fires).
1235
+ if (session.forceCancelTimer) {
1236
+ clearTimeout(session.forceCancelTimer);
1237
+ session.forceCancelTimer = undefined;
1238
+ }
1239
+ session.cancelController?.abort();
1021
1240
  session.settingsManager.dispose();
1022
1241
  session.abortController.abort();
1023
1242
  session.query.close();
@@ -1145,6 +1364,15 @@ export class ClaudeAcpAgent {
1145
1364
  const toolUseCache = {};
1146
1365
  const messages = await getSessionMessages(sessionId);
1147
1366
  for (const message of messages) {
1367
+ // Backfill the ACP messageId -> SDK uuid mapping for messages we didn't
1368
+ // observe live (resumed/loaded sessions), so rewind/resume can translate
1369
+ // a client-supplied id without an extra getSessionMessages read. Not read
1370
+ // yet (see Session.messageIdToUuid).
1371
+ const replayMessageId = messageIdForGrouping(message);
1372
+ const replaySession = this.sessions[sessionId];
1373
+ if (replaySession && replayMessageId && message.uuid) {
1374
+ replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
1375
+ }
1148
1376
  // @ts-expect-error - untyped in SDK but we handle all of these
1149
1377
  let content = message.message.content;
1150
1378
  // @ts-expect-error - untyped in SDK but we handle all of these
@@ -1162,6 +1390,7 @@ export class ClaudeAcpAgent {
1162
1390
  clientCapabilities: this.clientCapabilities,
1163
1391
  cwd: this.sessions[sessionId]?.cwd,
1164
1392
  taskState: this.sessions[sessionId]?.taskState,
1393
+ messageId: replayMessageId,
1165
1394
  })) {
1166
1395
  await this.client.sessionUpdate(notification);
1167
1396
  }
@@ -1513,10 +1742,8 @@ export class ClaudeAcpAgent {
1513
1742
  // Extract options from _meta if provided
1514
1743
  const sessionMeta = params._meta;
1515
1744
  const userProvidedOptions = sessionMeta?.claudeCode?.options;
1516
- // Configure thinking tokens from environment variable
1517
- const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
1518
- ? parseInt(process.env.MAX_THINKING_TOKENS, 10)
1519
- : undefined;
1745
+ // Configure thinking behavior from environment variable
1746
+ const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
1520
1747
  // Parse model configuration from environment (e.g. Bedrock model overrides)
1521
1748
  const modelConfig = parseModelConfig(process.env.CLAUDE_MODEL_CONFIG);
1522
1749
  // Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
@@ -1535,7 +1762,7 @@ export class ClaudeAcpAgent {
1535
1762
  const options = {
1536
1763
  systemPrompt,
1537
1764
  settingSources: ["user", "project", "local"],
1538
- ...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
1765
+ ...(thinking !== undefined && { thinking }),
1539
1766
  ...userProvidedOptions,
1540
1767
  // CLAUDE_MODEL_CONFIG env var is a fallback for model
1541
1768
  // configuration (e.g. Bedrock model ID overrides). When the caller
@@ -1752,6 +1979,8 @@ export class ClaudeAcpAgent {
1752
1979
  emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
1753
1980
  contextWindowSize: inferContextWindowFromModel(models.currentModelId) ?? DEFAULT_CONTEXT_WINDOW,
1754
1981
  taskState,
1982
+ toolUseCache: {},
1983
+ messageIdToUuid: new Map(),
1755
1984
  };
1756
1985
  return {
1757
1986
  sessionId,
@@ -1993,11 +2222,16 @@ function tokenizeModelPreference(model) {
1993
2222
  function scoreModelMatch(model, tokens, contextHint) {
1994
2223
  const haystack = `${model.value} ${model.displayName}`.toLowerCase();
1995
2224
  let score = 0;
2225
+ let nonHintMatched = false;
1996
2226
  for (const token of tokens) {
1997
2227
  if (haystack.includes(token)) {
2228
+ if (token !== contextHint)
2229
+ nonHintMatched = true;
1998
2230
  score += token === contextHint ? 3 : 1;
1999
2231
  }
2000
2232
  }
2233
+ if (contextHint && !nonHintMatched)
2234
+ return 0;
2001
2235
  return score;
2002
2236
  }
2003
2237
  function resolveModelPreference(models, preference) {
@@ -2265,6 +2499,42 @@ export function promptToClaude(prompt) {
2265
2499
  parent_tool_use_id: null,
2266
2500
  };
2267
2501
  }
2502
+ /**
2503
+ * Resolves the ACP `messageId` for a Claude SDK message (live) or a persisted
2504
+ * transcript message (replay) so chunk grouping is identical in both views.
2505
+ *
2506
+ * Assistant turns are keyed by the Anthropic API message id (`message.id`),
2507
+ * which is identical at `message_start`, on the consolidated assistant message,
2508
+ * and in the persisted transcript — unlike the per-`stream_event` uuid, which is
2509
+ * unique per event and never persisted. User messages have no API id, but they
2510
+ * are never streamed, so their (stable) SDK uuid is used instead. ACP message
2511
+ * ids are opaque strings, so no particular format is required.
2512
+ */
2513
+ export function messageIdForGrouping(message) {
2514
+ if (message.type === "assistant") {
2515
+ const inner = message.message;
2516
+ const apiId = inner && typeof inner === "object" && "id" in inner
2517
+ ? inner.id
2518
+ : undefined;
2519
+ if (typeof apiId === "string" && apiId.length > 0) {
2520
+ return apiId;
2521
+ }
2522
+ }
2523
+ return typeof message.uuid === "string" && message.uuid.length > 0 ? message.uuid : undefined;
2524
+ }
2525
+ /**
2526
+ * Stamps an ACP `messageId` onto a session update, but only on the message/
2527
+ * thought chunk variants that carry one — tool_call/plan/etc. updates never do.
2528
+ * No-op when `messageId` is falsy, so callers can pass it through unconditionally.
2529
+ */
2530
+ function applyMessageId(update, messageId) {
2531
+ if (messageId &&
2532
+ (update.sessionUpdate === "agent_message_chunk" ||
2533
+ update.sessionUpdate === "user_message_chunk" ||
2534
+ update.sessionUpdate === "agent_thought_chunk")) {
2535
+ update.messageId = messageId;
2536
+ }
2537
+ }
2268
2538
  /**
2269
2539
  * Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
2270
2540
  * Only handles text, image, and thinking chunks for now.
@@ -2281,6 +2551,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
2281
2551
  text: content,
2282
2552
  },
2283
2553
  };
2554
+ applyMessageId(update, options?.messageId);
2284
2555
  if (options?.parentToolUseId) {
2285
2556
  update._meta = {
2286
2557
  ...update._meta,
@@ -2353,40 +2624,40 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
2353
2624
  else {
2354
2625
  // Only register hooks on first encounter to avoid double-firing
2355
2626
  if (registerHooks && !alreadyCached) {
2627
+ // Capture the tool name in the closure rather than re-reading the
2628
+ // cache when the hook fires. The cache entry is pruned at
2629
+ // tool_result time, and a PostToolUse hook can fire after that, so
2630
+ // closing over the name keeps the diff working without depending on
2631
+ // (or pinning) the cache entry's lifetime.
2632
+ const toolName = chunk.name;
2356
2633
  registerHookCallback(chunk.id, {
2357
2634
  onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
2358
- const toolUse = toolUseCache[toolUseId];
2359
- if (toolUse) {
2360
- // Both `Edit` and `Write` produce a structuredPatch in their
2361
- // PostToolUse tool_response. For Edit the diff replaces the
2362
- // optimistic content built at tool_use time. For Write the
2363
- // optimistic content (built from `input.content` alone with
2364
- // `oldText: null`) shows "creation" semantics regardless of
2365
- // whether the file existed; the structuredPatch from the
2366
- // hook lets us emit the real diff for `type: "update"`. The
2367
- // helper returns `{}` if the response shape isn't usable.
2368
- const editDiff = toolUse.name === "Edit" || toolUse.name === "Write"
2369
- ? toolUpdateFromDiffToolResponse(toolResponse)
2370
- : {};
2371
- const update = {
2372
- _meta: {
2373
- claudeCode: {
2374
- toolResponse,
2375
- toolName: toolUse.name,
2376
- },
2635
+ // Both `Edit` and `Write` produce a structuredPatch in their
2636
+ // PostToolUse tool_response. For Edit the diff replaces the
2637
+ // optimistic content built at tool_use time. For Write the
2638
+ // optimistic content (built from `input.content` alone with
2639
+ // `oldText: null`) shows "creation" semantics regardless of
2640
+ // whether the file existed; the structuredPatch from the
2641
+ // hook lets us emit the real diff for `type: "update"`. The
2642
+ // helper returns `{}` if the response shape isn't usable.
2643
+ const editDiff = toolName === "Edit" || toolName === "Write"
2644
+ ? toolUpdateFromDiffToolResponse(toolResponse)
2645
+ : {};
2646
+ const update = {
2647
+ _meta: {
2648
+ claudeCode: {
2649
+ toolResponse,
2650
+ toolName,
2377
2651
  },
2378
- toolCallId: toolUseId,
2379
- sessionUpdate: "tool_call_update",
2380
- ...editDiff,
2381
- };
2382
- await client.sessionUpdate({
2383
- sessionId,
2384
- update,
2385
- });
2386
- }
2387
- else {
2388
- logger.error(`[claude-agent-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
2389
- }
2652
+ },
2653
+ toolCallId: toolUseId,
2654
+ sessionUpdate: "tool_call_update",
2655
+ ...editDiff,
2656
+ };
2657
+ await client.sessionUpdate({
2658
+ sessionId,
2659
+ update,
2660
+ });
2390
2661
  },
2391
2662
  });
2392
2663
  }
@@ -2509,6 +2780,11 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
2509
2780
  ...toolUpdate,
2510
2781
  };
2511
2782
  }
2783
+ // The tool_use is fully resolved now — drop it so a long session doesn't
2784
+ // retain every tool call. The PostToolUse hook (Edit/Write diffs) closes
2785
+ // over the tool name and no longer reads the cache, so pruning here is
2786
+ // safe regardless of hook/result ordering.
2787
+ delete toolUseCache[chunk.tool_use_id];
2512
2788
  break;
2513
2789
  }
2514
2790
  case "document":
@@ -2537,6 +2813,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
2537
2813
  },
2538
2814
  };
2539
2815
  }
2816
+ applyMessageId(update, options?.messageId);
2540
2817
  output.push({ sessionId, update });
2541
2818
  }
2542
2819
  }
@@ -2551,6 +2828,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
2551
2828
  parentToolUseId: message.parent_tool_use_id,
2552
2829
  cwd: options?.cwd,
2553
2830
  taskState: options?.taskState,
2831
+ messageId: options?.messageId,
2554
2832
  });
2555
2833
  case "content_block_delta":
2556
2834
  return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, {
@@ -2558,6 +2836,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
2558
2836
  parentToolUseId: message.parent_tool_use_id,
2559
2837
  cwd: options?.cwd,
2560
2838
  taskState: options?.taskState,
2839
+ messageId: options?.messageId,
2561
2840
  });
2562
2841
  // No content. `ping` is a Messages-API keep-alive event that the SDK's
2563
2842
  // `BetaRawMessageStreamEvent` union doesn't include even though the
@@ -2593,7 +2872,8 @@ function commonPrefixLength(a, b) {
2593
2872
  return i;
2594
2873
  }
2595
2874
  /** Best-effort first guess of a model's context window from its ID, used only
2596
- * until a `result` message arrives with the authoritative `modelUsage` value.
2875
+ * as a fallback when the SDK's authoritative `getContextUsage` is unavailable
2876
+ * (and until a `result` message arrives with the `modelUsage` value).
2597
2877
  * Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
2598
2878
  * model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
2599
2879
  * matching things like "10m" or embedded substrings. */
@@ -2602,6 +2882,42 @@ function inferContextWindowFromModel(model) {
2602
2882
  return 1_000_000;
2603
2883
  return null;
2604
2884
  }
2885
+ /** Fetch the SDK's authoritative context-window occupancy via the
2886
+ * `getContextUsage` control request. Unlike the per-message API usage numbers
2887
+ * (which only count message tokens), this `totalTokens` includes the system
2888
+ * prompt, tool schemas, MCP tools, and memory-file overhead — the real
2889
+ * occupancy the user sees. Returns `null` on any control-request failure.
2890
+ *
2891
+ * Note: we deliberately do NOT use this response's window fields for `size`.
2892
+ * They have been observed to under-report extended (1M) context windows, so
2893
+ * the window keeps coming from `modelUsage` / `inferContextWindowFromModel`,
2894
+ * which handle the 1M variants correctly. */
2895
+ async function fetchContextUsedTokens(query, logger) {
2896
+ try {
2897
+ const usage = await query.getContextUsage();
2898
+ return usage.totalTokens;
2899
+ }
2900
+ catch (error) {
2901
+ logger.error("Failed to fetch context usage from SDK:", error);
2902
+ return null;
2903
+ }
2904
+ }
2905
+ /** Translate the legacy `MAX_THINKING_TOKENS` env var into the SDK's `thinking`
2906
+ * option. The `maxThinkingTokens` option it used to feed is deprecated and
2907
+ * reduced to on/off on current models, so map the value to explicit thinking
2908
+ * config instead: unset → `undefined` (SDK default, adaptive on models that
2909
+ * support it); `0` → disabled; a positive integer → a fixed token budget.
2910
+ * Anything else is ignored with a warning. */
2911
+ function resolveThinkingConfig(raw, logger) {
2912
+ if (raw === undefined)
2913
+ return undefined;
2914
+ const parsed = Number.parseInt(raw, 10);
2915
+ if (Number.isNaN(parsed) || parsed < 0) {
2916
+ logger.error(`Ignoring MAX_THINKING_TOKENS: expected a non-negative integer, got '${raw}'.`);
2917
+ return undefined;
2918
+ }
2919
+ return parsed === 0 ? { type: "disabled" } : { type: "enabled", budgetTokens: parsed };
2920
+ }
2605
2921
  function parseModelConfig(raw) {
2606
2922
  if (!raw)
2607
2923
  return undefined;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.40.0",
6
+ "version": "0.42.0",
7
7
  "description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
8
8
  "main": "dist/lib.js",
9
9
  "types": "dist/lib.d.ts",
@@ -61,7 +61,7 @@
61
61
  "license": "Apache-2.0",
62
62
  "dependencies": {
63
63
  "@agentclientprotocol/sdk": "0.24.0",
64
- "@anthropic-ai/claude-agent-sdk": "0.3.160",
64
+ "@anthropic-ai/claude-agent-sdk": "0.3.165",
65
65
  "zod": "^3.25.0 || ^4.0.0"
66
66
  },
67
67
  "devDependencies": {