@@ -42,6 +42,37 @@ interface SessionUpdate {
4242 sessionUpdate : string ;
4343 content ?: ContentBlock | ContentBlock [ ] ;
4444 _meta ?: { claudeCode ?: ClaudeCodeMeta } ;
45+ // ACP puts these on the update itself; _meta.claudeCode only reliably
46+ // carries toolName (and sometimes toolResponse).
47+ toolCallId ?: string ;
48+ rawInput ?: unknown ;
49+ rawOutput ?: unknown ;
50+ }
51+
52+ // Individual tool payloads can be huge (whole-file Write inputs, full test
53+ // output). Cap each one so a single call can't dominate the resume budget.
54+ const MAX_TOOL_PAYLOAD_CHARS = 10_000 ;
55+
56+ function capToolPayload ( value : unknown ) : unknown {
57+ const text = typeof value === "string" ? value : JSON . stringify ( value ) ;
58+ if ( typeof text !== "string" || text . length <= MAX_TOOL_PAYLOAD_CHARS ) {
59+ return value ;
60+ }
61+ const preview = `${ text . slice ( 0 , MAX_TOOL_PAYLOAD_CHARS ) } … [truncated ${ text . length - MAX_TOOL_PAYLOAD_CHARS } chars]` ;
62+ // tool_use.input must stay an object per the Claude API schema — wrap
63+ // instead of replacing with a bare string.
64+ return typeof value === "string"
65+ ? preview
66+ : { _truncated : true , preview, originalSize : text . length } ;
67+ }
68+
69+ function isEmptyRecord ( value : unknown ) : boolean {
70+ return (
71+ typeof value === "object" &&
72+ value !== null &&
73+ ! Array . isArray ( value ) &&
74+ Object . keys ( value ) . length === 0
75+ ) ;
4576}
4677
4778const MAX_PROJECT_KEY_LENGTH = 200 ;
@@ -148,36 +179,47 @@ export function rebuildConversation(
148179 case "tool_call" :
149180 case "tool_call_update" : {
150181 const meta = update . _meta ?. claudeCode ;
151- if ( meta ) {
152- const { toolCallId, toolName, toolInput, toolResponse } = meta ;
153-
154- if ( toolCallId && toolName ) {
155- let toolCall = currentToolCalls . find (
156- ( tc ) => tc . toolCallId === toolCallId ,
157- ) ;
158- if ( ! toolCall ) {
159- toolCall = { toolCallId, toolName, input : toolInput } ;
160- currentToolCalls . push ( toolCall ) ;
161- }
162- if ( toolResponse !== undefined ) {
163- toolCall . result = toolResponse ;
164- }
165- }
182+ const toolCallId = update . toolCallId ?? meta ?. toolCallId ;
183+ if ( ! toolCallId ) break ;
184+
185+ let toolCall = currentToolCalls . find (
186+ ( tc ) => tc . toolCallId === toolCallId ,
187+ ) ;
188+ if ( ! toolCall ) {
189+ const toolName = meta ?. toolName ;
190+ // Bare streaming updates carry no name; the opening tool_call
191+ // always does, so the call exists by the time they arrive.
192+ if ( ! toolName ) break ;
193+ toolCall = { toolCallId, toolName, input : undefined } ;
194+ currentToolCalls . push ( toolCall ) ;
195+ }
196+
197+ const input = update . rawInput ?? meta ?. toolInput ;
198+ // The opening tool_call ships rawInput: {} — don't clobber an
199+ // already-streamed input with it.
200+ if (
201+ input !== undefined &&
202+ ! ( isEmptyRecord ( input ) && toolCall . input !== undefined )
203+ ) {
204+ toolCall . input = capToolPayload ( input ) ;
205+ }
206+ const result = update . rawOutput ?? meta ?. toolResponse ;
207+ if ( result !== undefined ) {
208+ toolCall . result = capToolPayload ( result ) ;
166209 }
167210 break ;
168211 }
169212
170213 case "tool_result" : {
171214 const meta = update . _meta ?. claudeCode ;
172- if ( meta ) {
173- const { toolCallId, toolResponse } = meta ;
174- if ( toolCallId ) {
175- const toolCall = currentToolCalls . find (
176- ( tc ) => tc . toolCallId === toolCallId ,
177- ) ;
178- if ( toolCall && toolResponse !== undefined ) {
179- toolCall . result = toolResponse ;
180- }
215+ const toolCallId = update . toolCallId ?? meta ?. toolCallId ;
216+ if ( toolCallId ) {
217+ const toolCall = currentToolCalls . find (
218+ ( tc ) => tc . toolCallId === toolCallId ,
219+ ) ;
220+ const result = update . rawOutput ?? meta ?. toolResponse ;
221+ if ( toolCall && result !== undefined ) {
222+ toolCall . result = capToolPayload ( result ) ;
181223 }
182224 }
183225 break ;
@@ -236,6 +278,15 @@ export function selectRecentTurns(
236278 startIndex = i ;
237279 }
238280
281+ if ( startIndex === turns . length && turns . length > 0 ) {
282+ // Even the most recent turn alone exceeds the budget — typical for a
283+ // single-prompt run, where everything after the prompt is one giant
284+ // assistant turn. Resuming with nothing loses all context, so keep the
285+ // nearest user turn (the task intent) and shed the assistant turn's
286+ // oldest tool calls until it fits.
287+ return selectOversizedTailFallback ( turns , maxTokens ) ;
288+ }
289+
239290 // Ensure we start on a user turn so the conversation is well-formed
240291 while ( startIndex < turns . length && turns [ startIndex ] . role !== "user" ) {
241292 startIndex ++ ;
@@ -244,6 +295,45 @@ export function selectRecentTurns(
244295 return turns . slice ( startIndex ) ;
245296}
246297
298+ function selectOversizedTailFallback (
299+ turns : ConversationTurn [ ] ,
300+ maxTokens : number ,
301+ ) : ConversationTurn [ ] {
302+ const last = turns [ turns . length - 1 ] ;
303+
304+ let userIndex = turns . length - 1 ;
305+ while ( userIndex >= 0 && turns [ userIndex ] . role !== "user" ) {
306+ userIndex -- ;
307+ }
308+
309+ const selected : ConversationTurn [ ] = [ ] ;
310+ let budget = maxTokens ;
311+ if ( userIndex >= 0 ) {
312+ selected . push ( turns [ userIndex ] ) ;
313+ budget -= estimateTurnTokens ( turns [ userIndex ] ) ;
314+ }
315+ if ( userIndex !== turns . length - 1 ) {
316+ selected . push ( dropOldestToolCalls ( last , Math . max ( budget , 0 ) ) ) ;
317+ }
318+ return selected ;
319+ }
320+
321+ function dropOldestToolCalls (
322+ turn : ConversationTurn ,
323+ budget : number ,
324+ ) : ConversationTurn {
325+ if ( ! turn . toolCalls ?. length ) return turn ;
326+ const toolCalls = [ ...turn . toolCalls ] ;
327+ const trimmed : ConversationTurn = { ...turn , toolCalls } ;
328+ while ( toolCalls . length > 0 && estimateTurnTokens ( trimmed ) > budget ) {
329+ toolCalls . shift ( ) ;
330+ }
331+ if ( toolCalls . length === 0 ) {
332+ trimmed . toolCalls = undefined ;
333+ }
334+ return trimmed ;
335+ }
336+
247337const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ;
248338
249339function generateMessageId ( ) : string {
0 commit comments