Skip to content

Commit 12cfd51

Browse files
authored
chore(agent): sync claude adapter with upstream v0.54.1 (#3076)
1 parent 3c1598c commit 12cfd51

22 files changed

Lines changed: 1661 additions & 511 deletions

packages/agent/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@
129129
"vitest": "^4.1.8"
130130
},
131131
"dependencies": {
132-
"@agentclientprotocol/sdk": "0.25.0",
133-
"@anthropic-ai/claude-agent-sdk": "0.3.170",
134-
"@anthropic-ai/sdk": "0.104.1",
132+
"@agentclientprotocol/sdk": "1.1.0",
133+
"@anthropic-ai/claude-agent-sdk": "0.3.197",
134+
"@anthropic-ai/sdk": "0.109.0",
135135
"@hono/node-server": "^1.19.9",
136136
"@openai/codex": "0.140.0",
137137
"@opentelemetry/api-logs": "^0.208.0",

packages/agent/src/acp-extensions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export const POSTHOG_NOTIFICATIONS = {
2525
/** Agent finished processing a turn (prompt returned, waiting for next input) */
2626
TURN_COMPLETE: "_posthog/turn_complete",
2727

28+
/** Background/task-notification-triggered reply finished. Same rendering
29+
* effect as TURN_COMPLETE (closes out the current turn) without touching
30+
* the tracked prompt lifecycle that TURN_COMPLETE drives on the agent side. */
31+
BACKGROUND_TURN_COMPLETE: "_posthog/background_turn_complete",
32+
2833
/** Error occurred during task execution */
2934
ERROR: "_posthog/error",
3035

packages/agent/src/adapters/claude/SKILL.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ pnpm --filter code test
152152

153153
- **Single session.** The agent owns one `this.session` (from `BaseAcpAgent`), not a `sessions` map.
154154
Upstream's per-session refactors usually collapse to "just use `this.session`".
155+
- **Prompt loop is a persistent consumer** (since the v0.54.1 sync, upstream #780): `prompt()`
156+
enqueues a `Turn` deferred; `runConsumer` drains the query stream for the session's life, settles
157+
turns at their terminal `result`, and captures `query` + `session.queryGeneration` so the
158+
fork-only `refreshSession()` can retire it (bump generation → abort wake-up → end input). Steer
159+
mode, `interruptReason`, per-turn broadcast-at-activation and the unsupported-slash-command gate
160+
all live inside it — port upstream prompt-loop changes into the consumer, not a per-prompt loop.
161+
- **ACP connection classes are the deprecated ones on purpose.** The fork stays on
162+
`AgentSideConnection`/`ClientSideConnection` (still shipped in ACP 1.x) because they carry the
163+
`extMethod`/`extNotification` surface `_posthog/*` uses; permission requests reach the client via
164+
the class's generic `request(..., { cancellationSignal })`. Don't port the `agent()` builder
165+
without a plan for the extension surface.
155166
- **Renderer uses config options only.** Model/mode/effort selection is `SessionConfigOption` end to
156167
end; the renderer never reads the legacy `models` response field or calls `unstable_setSessionModel`.
157168
That's why upstream's ACP-0.24/0.25 model-state removals are safe to follow.

packages/agent/src/adapters/claude/UPSTREAM.md

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
55
## Fork Point
66

77
- **Forked**: v0.10.9, commit `5411e0f4`, Dec 2 2025
8-
- **Last sync**: v0.44.0, commit `7de5e4b`, Jun 11 2026
9-
- **SDK**: `@anthropic-ai/claude-agent-sdk` 0.3.170, `@agentclientprotocol/sdk` 0.25.0, `@anthropic-ai/sdk` 0.104.1
8+
- **Last sync**: v0.54.1, commit `8d5febf`, Jul 1 2026
9+
- **SDK**: `@anthropic-ai/claude-agent-sdk` 0.3.197, `@agentclientprotocol/sdk` 1.1.0, `@anthropic-ai/sdk` 0.109.0
1010

1111
## File Mapping
1212

@@ -60,8 +60,118 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
6060
| Auth methods | `claude-ai-login` + `console-login` | Returns empty `authMethods` | Auth handled externally |
6161
| Session fingerprinting | Implicit teardown on cwd/mcp change | Explicit `refreshSession()` | Caller-initiated is more predictable |
6262
| Shutdown on ACP close | Process exits | No standalone process | Agent is embedded in server |
63-
| Unsupported slash commands | Loops silently on early idle | Emits "Unsupported slash command" chunk, gated on `initializationResult().commands` so plugin/skill commands (e.g. `/skills-store`) whose echoes use a fresh uuid are not false-flagged | The SDK consumes some slash commands without producing output (e.g. `/plugin` in non-interactive mode); without this we hang. The known-commands gate avoids racing plugin/skill loads where idle can arrive before the transformed user-message echo. |
64-
| Prompt-loop cancel race | `Promise.race([query.next(), cancelWake])` each iteration (#742) | `withAbort(query.next(), cancelController.signal)` helper in `utils/common.ts`, also guarding the `compact_boundary` `getContextUsage` fetch | The classic `Promise.race` leak (nodejs/node#17469): each race call parks a reaction on the turn-lived `cancelWake` promise that retains that iteration's settled value, so every yielded message (and every stream event, since `includePartialMessages` is on) stays reachable until the turn ends. Long high-reasoning turns could pin tens of MB. `withAbort` removes its abort listener as soon as `next()` settles, so nothing accumulates. Cancel semantics are unchanged, including the force-cancel backstop. |
63+
| Unsupported slash commands | Loops silently on early idle | Emits "Unsupported slash command" chunk, gated on `initializationResult().commands` so plugin/skill commands (e.g. `/skills-store`) whose echoes use a fresh uuid are not false-flagged. Lives in the consumer's idle handler: fires only when idle arrives with no active turn, an unsettled head turn whose leading command is unknown, and no pending orphan results. | The SDK consumes some slash commands without producing output (e.g. `/plugin` in non-interactive mode); without this we hang. The known-commands gate avoids racing plugin/skill loads where idle can arrive before the transformed user-message echo. |
64+
| Prompt-loop cancel race | Per-iteration `addEventListener`/`removeEventListener` race in the consumer (#780) | `withAbort(query.next(), cancelController.signal)` helper in `utils/common.ts`, also guarding the `compact_boundary` `getContextUsage` fetch | Same effect (no listener/reaction accumulation on the long-lived wake-up promise), different helper. `withAbort` removes its abort listener as soon as `next()` settles; the consumer re-arms a fresh controller after each abort fire, matching upstream's re-arm. |
65+
| ACP connection wiring | `agent({name}).onRequest(...).connect(stream)` builder + narrow `AcpClient` interface (#790) | Keeps `AgentSideConnection` / `ClientSideConnection` (deprecated but fully functional in ACP 1.1.0) in `acp-connection.ts` / `base-acp-agent.ts` / codex | The fork is embedded (in-process streams, `extMethod`/`extNotification` extension surface) and the deprecated classes still route optional `extMethod`/`extNotification` to the Agent/Client. Revisit when ACP removes them; permission cancellation already uses the class's generic `request(..., { cancellationSignal })`. |
66+
| Consumer ownership | Per-session map; consumer keyed by `sessions[id]` | Single `this.session`; consumer captures `query` + `queryGeneration` and exits quietly on mismatch | `refreshSession()` (fork-only) swaps `query`/`input` in place on the same session object; the generation guard keeps a retired consumer from tearing down the refreshed session. |
67+
68+
## Changes Ported in v0.54.1 Sync
69+
70+
- **SDK bumps**: claude-agent-sdk 0.3.170 -> 0.3.197, ACP SDK 0.25.0 -> 1.1.0, anthropic SDK
71+
0.104.1 -> 0.109.0. The ACP 1.x major is source-compatible for the fork: the deprecated
72+
`AgentSideConnection`/`ClientSideConnection` classes are still shipped and still route
73+
`extMethod`/`extNotification` (see Intentional Divergences). Only in-repo break was the SDK
74+
`Query` interface gaining `setMcpPermissionModeOverride` and `reinitialize` (test mock updated).
75+
- **Persistent consumer + turn queue** (#780, 4f273a2): The per-prompt message loop became a
76+
single long-lived consumer per session. `prompt()` now enqueues a `Turn` (deferred) and returns;
77+
the consumer drains the query stream for the session's whole life, activates turns via their
78+
user-message echoes (promoting the queue head for echo-less local-only/compaction results, with
79+
orphan-result accounting after cancels), settles turns at their terminal `result` instead of
80+
waiting for the SDK's trailing `idle` (which can lag behind background tasks — upstream issues
81+
#773/#679/#688), forwards between-turn/background output live, and rejects turns with a clear
82+
"session has ended" error once the stream dies (`queryClosed`). Upstream's fixes folded in:
83+
fresh-abort-listener per iteration (kept as `withAbort` + re-armed controller), error results
84+
via `failActive` without killing the consumer (replaces the drain-after-error loop, #706's
85+
successor), process-death teardown via `failAllTurns` + `closeQueryStream`. Fork adaptations:
86+
single-session, steer mode untouched (mid-turn push + benign end_turn), `interruptReason`
87+
carried on every cancelled settle, per-turn broadcast fired at activation (preserves the old
88+
"broadcast when the turn takes over" timing), the unsupported-slash-command gate re-anchored on
89+
"idle with an unactivated head turn", `toolUseStreamCache` cleared on cancel/error settles, and
90+
a `queryGeneration` guard so `refreshSession()` retires the old consumer cleanly.
91+
- **Content-based streamed-block dedupe** (#785 12d34e6, #789 1c80bf8, #800 960f62d — ported as
92+
the final #800 state): `StreamedAssistantBlocks` switched from per-message-id
93+
`textIds`/`thinkingIds` sets to an ordered accumulated-text record; the consolidated assistant
94+
message prefix-diffs each block against what streamed and forwards only the un-streamed
95+
remainder (nothing / whole block / cut-short tail). Robust to gateways whose consolidated
96+
message id doesn't match the stream. Record cleared at each top-level `message_start` and after
97+
consumption; consumer-lived so mid-message turn activation can't drop it. New unit tests cover
98+
tail-forwarding, id-mismatch dedupe, residue clearing and empty-delta stalls.
99+
- **Skip empty thinking chunks** (#793, 15fdf26): `handleThinkingChunk` drops signature-only
100+
(empty) thinking blocks that models with `thinking.display: "omitted"` stream; empty deltas are
101+
also excluded from the streamed-block record so they can't stall the diff cursor.
102+
- **Emit tool_call before permission request** (#820, c95fc88): New agent-lived
103+
`emittedToolCalls` set shared between the streamed tool_use path and the permission flow.
104+
`requestPermissionFromClient` eagerly emits the referenced `tool_call` (Task*/TodoWrite
105+
excluded; Bash carries `terminal_info`) so the client has it before being asked to approve;
106+
whichever side runs second emits a `tool_call_update` instead of a duplicate. Pruned at
107+
`tool_result` alongside `toolUseCache`.
108+
- **Permission request cancellation** (#801, 9013d1d): All five permission-request sites now go
109+
through `client.request(methods.client.session.requestPermission, params, { cancellationSignal:
110+
signal })`, so cancelling a turn sends `$/cancel_request` and the client can dismiss the open
111+
dialog; an abort-time rejection maps to the existing "Tool use aborted".
112+
- **Terminal error rendering** (#776, db6eaaf): Bash `is_error` results keep flowing through the
113+
terminal-output `_meta` channel (when the client supports it) instead of short-circuiting to
114+
plain error content.
115+
- **Bash image output** (#617, a759e64): Array tool_result content that isn't text-only (e.g. an
116+
image from a piped data URI) bypasses the terminal channel and surfaces as ACP content blocks
117+
instead of being silently dropped.
118+
- **`informational` system subtype** (rode in with SDK 0.3.178, #777 58549ff): Surfaced as an
119+
`agent_message_chunk` (level folded into the text for non-info levels) so hook-blocked stops are
120+
no longer silent. `worker_shutting_down` no-ops via the existing `default: break`.
121+
- **Sonnet 5 model-version matching** (#826, ef42c46): `MODEL_FAMILY_VERSION_PATTERN` accepts
122+
single-number generations (`5`) and `extractModelFamilyVersion` strips `[1m]`-style context
123+
hints before matching, so `sonnet 5` resolves and `claude-sonnet-4-6` can't cross-match a
124+
Sonnet 5 alias. Unit tests added.
125+
- **Session title push at turn end** (#812, 1fe7ec0): `maybeUpdateSessionTitle` polls
126+
`getSessionInfo` at each `idle` and pushes a `session_info_update` (ACP 1.1) when the
127+
SDK-generated `customTitle`/`summary` changes.
128+
- **Fast mode session config** (#828, fa949a2, adapted to gateway models): New `fast` on/off
129+
select config option, surfaced only for models in `MODELS_WITH_FAST_MODE`
130+
(claude-opus-4-8/-4-7). Toggling calls `query.applyFlagSettings({ fastMode })`; the intent is
131+
retained across model switches (`session.fastModeEnabled`), seeded from
132+
`initializationResult.fast_mode_state`, and reconciled with SDK-reported `fast_mode_state` on
133+
init and user-turn results (`cooldown` never flaps the toggle). Boolean-typed config options
134+
were not adopted — the renderer consumes selects; revisit if it advertises
135+
`sessionConfigOptions.boolean`.
136+
- **ReportFindings tool rendering** (#826, ef42c46): Not ported to `toolInfoFromToolUse` — see
137+
Skipped (the fork renders unknown tools generically and PostHog Code has no code-review
138+
ReportFindings flow); re-evaluate if the SDK starts emitting it in our sessions.
139+
- **Test mock**: added `setMcpPermissionModeOverride` and `reinitialize` to the SDK `MockQuery`
140+
(new methods on the SDK `Query` interface by 0.3.197).
141+
142+
## Skipped in v0.54.1 Sync
143+
144+
- **ACP builder-pattern migration** (#790, 2554c7b): Kept the deprecated connection classes —
145+
recorded as an Intentional Divergence (they still ship in 1.1.0 and carry the
146+
`extMethod`/`extNotification` surface the fork's `_posthog/*` extensions rely on).
147+
- **Elicitation fixes** (#774 d58004a, #779 b364059): Upstream's AskUserQuestion runs through
148+
ACP's unstable elicitation API; ours uses its own `questions/` machinery behind the permission
149+
flow and the renderer does not advertise elicitation. Same standing skip as the v0.44 sync.
150+
- **ACP logout support** (#816, 0a0468c): Fork returns empty `authMethods` (auth handled
151+
externally by PostHog); there is no CLI credential store to clear from the embedded agent.
152+
- **Version flag handling** (#813, 9616bda): `src/index.ts` CLI-entrypoint concern; the fork is
153+
embedded in the agent server and has no standalone binary.
154+
- **Agent selection dropdown** (#794, 5729c47): Surfaces custom main-thread agent personas
155+
(`supportedAgents()` minus built-ins) as an `agent` config option. PostHog Code drives its own
156+
agent concepts; defer until product wants persona selection in the picker.
157+
- **availableModels allowlist fixes** (#768 cc2885f, #827 98c284b) and **1M inference from model
158+
descriptions** (#799, 508453c): All operate on upstream's SDK-settings model pipeline
159+
(`ANTHROPIC_CUSTOM_MODEL_OPTION`, `modelOverrides`, `ModelInfo.description` scans). The fork's
160+
models and context windows come from the PostHog gateway (`fetchGatewayModels`,
161+
`getContextWindowForModel`), which has none of those inputs.
162+
- **ReportFindings rendering** (#826): See above — no ReportFindings flow reaches the fork today;
163+
the generic tool_call rendering is acceptable if it ever does.
164+
- **`model_refusal_no_fallback` status subtype** (SDK 0.3.193, #818 5dd8746): Our
165+
`handleSystemMessage` status handling is non-exhaustive, so the new subtype already no-ops
166+
(same precedent as `thinking_tokens` / `model_refusal_fallback`).
167+
- **Idle-time `usage_update`**: Dropped along with the #780 port (upstream removed it when turns
168+
began settling at their terminal result). The mid-stream and result-time usage updates remain;
169+
the idle-time emission double-counted cumulative loop usage in rare paths anyway.
170+
- **Test-only upstream changes** (#769 41cde99 CLAUDE_CONFIG_DIR isolation, #792 9f38cb6 tmp
171+
dirs): Upstream test-harness hygiene; our tests use their own fixtures.
172+
- **Release / CI / dep-group bumps** (#772, #775, #778, #784, #788, #795, #802, #803, #808,
173+
#811, #817, #821, #822, #823, #829, #831 and the pure SDK-bump commits #771, #783, #791, #798,
174+
#806, #807, #810, #818 beyond the versions captured above): No fork-relevant code.
65175

66176
## Changes Ported in v0.44.0 Sync
67177

@@ -276,7 +386,7 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
276386

277387
## Next Sync
278388

279-
1. Check upstream changelog since v0.44.0
389+
1. Check upstream changelog since v0.54.1
280390
2. Diff upstream source against PostHog Code using the file mapping above
281391
3. Port in phases: bug fixes first, then features
282392
4. After each phase: `pnpm --filter agent typecheck && pnpm --filter agent build && pnpm lint`

packages/agent/src/adapters/claude/claude-agent.refresh.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ function installFakeSession(
128128
},
129129
sessionResources: new Set(),
130130
configOptions: [],
131-
promptRunning: false,
132-
pendingMessages: new Map(),
133-
nextPendingOrder: 0,
131+
turnQueue: [],
132+
activeTurn: null,
133+
pendingOrphanResults: 0,
134+
queryGeneration: 0,
134135
cwd: "/tmp/repo",
135136
notificationHistory: [{ foo: "bar" }],
136137
taskRunId: "run-1",
@@ -201,7 +202,9 @@ describe("ClaudeAcpAgent.extMethod refresh_session", () => {
201202
it("rejects refresh while a prompt is in flight", async () => {
202203
const agent = makeAgent();
203204
const { session } = installFakeSession(agent, "s-1");
204-
(session as unknown as { promptRunning: boolean }).promptRunning = true;
205+
(session as unknown as { turnQueue: unknown[] }).turnQueue = [
206+
{ promptUuid: "u-1", settled: false },
207+
];
205208

206209
await expect(
207210
agent.extMethod(POSTHOG_METHODS.REFRESH_SESSION, {

0 commit comments

Comments
 (0)