@@ -16,16 +16,32 @@ type AcpToolCallUpdateStatus = NonNullable<Extract<UpdateSessionEvent, {
1616type LegacyFunctionCallUpdate = {
1717 update : AcpToolCallEvent ;
1818 usesTerminal : boolean ;
19+ isExecCommand : boolean ;
1920} ;
2021type ParsedShellCommand = {
2122 tokens : string [ ] ;
2223} ;
2324
25+ function historyFallbackUpdateKey ( update : UpdateSessionEvent ) : string | null {
26+ switch ( update . sessionUpdate ) {
27+ case "user_message_chunk" :
28+ case "agent_message_chunk" :
29+ case "agent_thought_chunk" :
30+ return `${ update . sessionUpdate } :${ JSON . stringify ( update . content ) } ` ;
31+ case "tool_call" :
32+ return `tool_call:${ update . toolCallId } :start` ;
33+ case "tool_call_update" :
34+ return `tool_call:${ update . toolCallId } :update` ;
35+ default :
36+ return null ;
37+ }
38+ }
39+
2440export async function createResponseItemHistoryFallbackUpdates (
2541 thread : Thread ,
2642 terminalOutputMode : TerminalOutputMode ,
2743) : Promise < UpdateSessionEvent [ ] | null > {
28- if ( ! thread . path || threadHasToolItems ( thread ) ) {
44+ if ( ! thread . path ) {
2945 return null ;
3046 }
3147
@@ -36,16 +52,32 @@ export async function createResponseItemHistoryFallbackUpdates(
3652 return null ;
3753 }
3854
39- return parseResponseItemHistoryFallback ( contents , terminalOutputMode ) ;
55+ return parseResponseItemHistoryFallback ( contents , terminalOutputMode , toolCallIdsFromThread ( thread ) ) ;
4056}
4157
4258export function parseResponseItemHistoryFallback (
4359 contents : string ,
4460 terminalOutputMode : TerminalOutputMode ,
61+ existingToolCallIds : Set < string > = new Set ( ) ,
4562) : UpdateSessionEvent [ ] | null {
4663 const updates : UpdateSessionEvent [ ] = [ ] ;
4764 const terminalToolCallIds = new Set < string > ( ) ;
48- let sawFunctionCall = false ;
65+ const execToolCallIds = new Set < string > ( ) ;
66+ const skippedToolCallIds = new Set < string > ( ) ;
67+ const emittedToolCallIds = new Set < string > ( ) ;
68+ let recoveredFunctionCall = false ;
69+ let lastUpdateKey : string | null = null ;
70+
71+ const pushUpdates = ( nextUpdates : UpdateSessionEvent [ ] ) => {
72+ for ( const update of nextUpdates ) {
73+ const key = historyFallbackUpdateKey ( update ) ;
74+ if ( key && key === lastUpdateKey ) {
75+ continue ;
76+ }
77+ updates . push ( update ) ;
78+ lastUpdateKey = key ;
79+ }
80+ } ;
4981
5082 for ( const line of contents . split ( / \r ? \n / ) ) {
5183 const record = parseJsonRecord ( line ) ;
@@ -55,7 +87,7 @@ export function parseResponseItemHistoryFallback(
5587
5688 const eventMsgUpdates = createEventMsgUpdates ( record ) ;
5789 if ( eventMsgUpdates ) {
58- updates . push ( ... eventMsgUpdates ) ;
90+ pushUpdates ( eventMsgUpdates ) ;
5991 continue ;
6092 }
6193
@@ -66,27 +98,48 @@ export function parseResponseItemHistoryFallback(
6698
6799 switch ( item [ "type" ] ) {
68100 case "message" :
69- updates . push ( ... createMessageUpdates ( item ) ) ;
101+ pushUpdates ( createMessageUpdates ( item ) ) ;
70102 break ;
71103 case "reasoning" :
72- updates . push ( ... createReasoningUpdates ( item ) ) ;
104+ pushUpdates ( createReasoningUpdates ( item ) ) ;
73105 break ;
74106 case "function_call" : {
107+ const toolCallId = stringValue ( item [ "call_id" ] ) ;
108+ if ( toolCallId && existingToolCallIds . has ( toolCallId ) ) {
109+ skippedToolCallIds . add ( toolCallId ) ;
110+ break ;
111+ }
112+ if ( toolCallId && emittedToolCallIds . has ( toolCallId ) ) {
113+ break ;
114+ }
75115 const result = createFunctionCallUpdate ( item ) ;
76116 if ( ! result ) {
77117 break ;
78118 }
79- sawFunctionCall = true ;
119+ recoveredFunctionCall = true ;
120+ emittedToolCallIds . add ( result . update . toolCallId ) ;
80121 if ( result . usesTerminal ) {
81122 terminalToolCallIds . add ( result . update . toolCallId ) ;
82123 }
83- updates . push ( result . update ) ;
124+ if ( result . isExecCommand ) {
125+ execToolCallIds . add ( result . update . toolCallId ) ;
126+ }
127+ pushUpdates ( [ result . update ] ) ;
84128 break ;
85129 }
86130 case "function_call_output" : {
87- const update = createFunctionCallOutputUpdate ( item , terminalOutputMode , terminalToolCallIds ) ;
131+ const toolCallId = stringValue ( item [ "call_id" ] ) ;
132+ if ( toolCallId && skippedToolCallIds . has ( toolCallId ) ) {
133+ break ;
134+ }
135+ const update = createFunctionCallOutputUpdate (
136+ item ,
137+ terminalOutputMode ,
138+ terminalToolCallIds ,
139+ execToolCallIds ,
140+ ) ;
88141 if ( update ) {
89- updates . push ( update ) ;
142+ pushUpdates ( [ update ] ) ;
90143 }
91144 break ;
92145 }
@@ -95,14 +148,23 @@ export function parseResponseItemHistoryFallback(
95148 }
96149 }
97150
98- return sawFunctionCall ? updates : null ;
151+ return recoveredFunctionCall ? updates : null ;
99152}
100153
101- function threadHasToolItems ( thread : Thread ) : boolean {
102- return thread . turns . some ( ( turn ) => turn . items . some ( isToolThreadItem ) ) ;
154+ function toolCallIdsFromThread ( thread : Thread ) : Set < string > {
155+ const ids = new Set < string > ( ) ;
156+ for ( const turn of thread . turns ) {
157+ for ( const item of turn . items ) {
158+ const id = toolCallIdFromThreadItem ( item ) ;
159+ if ( id ) {
160+ ids . add ( id ) ;
161+ }
162+ }
163+ }
164+ return ids ;
103165}
104166
105- function isToolThreadItem ( item : ThreadItem ) : boolean {
167+ function toolCallIdFromThreadItem ( item : ThreadItem ) : string | null {
106168 switch ( item . type ) {
107169 case "commandExecution" :
108170 case "fileChange" :
@@ -112,7 +174,7 @@ function isToolThreadItem(item: ThreadItem): boolean {
112174 case "webSearch" :
113175 case "imageView" :
114176 case "imageGeneration" :
115- return true ;
177+ return item . id ;
116178 case "userMessage" :
117179 case "hookPrompt" :
118180 case "agentMessage" :
@@ -122,7 +184,7 @@ function isToolThreadItem(item: ThreadItem): boolean {
122184 case "enteredReviewMode" :
123185 case "exitedReviewMode" :
124186 case "contextCompaction" :
125- return false ;
187+ return null ;
126188 }
127189}
128190
@@ -312,6 +374,7 @@ function createFunctionCallUpdate(item: JsonRecord): LegacyFunctionCallUpdate |
312374 return {
313375 update : createCommandActionEvent ( toolCallId , "inProgress" , cwd , commandAction ) ,
314376 usesTerminal : false ,
377+ isExecCommand : true ,
315378 } ;
316379 }
317380
@@ -325,28 +388,30 @@ function createFunctionCallUpdate(item: JsonRecord): LegacyFunctionCallUpdate |
325388 } ;
326389
327390 if ( ! functionCallUsesTerminal ( item ) ) {
328- return { update, usesTerminal : false } ;
391+ return { update, usesTerminal : false , isExecCommand : false } ;
329392 }
330393
331394 return {
332395 update : withTerminalContent ( update , toolCallId , cwd ) ,
333396 usesTerminal : true ,
397+ isExecCommand : true ,
334398 } ;
335399}
336400
337401function createFunctionCallOutputUpdate (
338402 item : JsonRecord ,
339403 terminalOutputMode : TerminalOutputMode ,
340404 terminalToolCallIds : Set < string > ,
405+ execToolCallIds : Set < string > ,
341406) : UpdateSessionEvent | null {
342407 const toolCallId = stringValue ( item [ "call_id" ] ) ;
343408 if ( ! toolCallId ) {
344409 return null ;
345410 }
346411
347412 const output = outputText ( item [ "output" ] ) ;
348- const exitCode = parseExitCode ( output ) ;
349- const status = statusFromExitCode ( exitCode ) ;
413+ const exitCode = parseExitCode ( item [ "output" ] , output ) ;
414+ const status = statusFromExitCode ( exitCode , output , execToolCallIds . has ( toolCallId ) ) ;
350415 if ( ! terminalToolCallIds . has ( toolCallId ) ) {
351416 return {
352417 sessionUpdate : "tool_call_update" ,
@@ -1004,7 +1069,15 @@ function outputText(output: unknown): string {
10041069 } ) . join ( "\n" ) ;
10051070}
10061071
1007- function parseExitCode ( output : string ) : number | null {
1072+ function parseExitCode ( rawOutput : unknown , output : string ) : number | null {
1073+ const record = asRecord ( rawOutput ) ;
1074+ if ( record ) {
1075+ const exitCode = numberValue ( record [ "exit_code" ] ) ?? numberValue ( record [ "exitCode" ] ) ;
1076+ if ( exitCode !== null ) {
1077+ return exitCode ;
1078+ }
1079+ }
1080+
10081081 const match = output . match ( / P r o c e s s e x i t e d w i t h c o d e ( - ? \d + ) / ) ;
10091082 if ( ! match ) {
10101083 return null ;
@@ -1019,8 +1092,25 @@ function parseExitCode(output: string): number | null {
10191092 return Number . isFinite ( exitCode ) ? exitCode : null ;
10201093}
10211094
1022- function statusFromExitCode ( exitCode : number | null ) : AcpToolCallUpdateStatus {
1023- return exitCode === null || exitCode === 0 ? "completed" : "failed" ;
1095+ function statusFromExitCode (
1096+ exitCode : number | null ,
1097+ output : string ,
1098+ isExecCommand : boolean ,
1099+ ) : AcpToolCallUpdateStatus {
1100+ if ( exitCode !== null ) {
1101+ return exitCode === 0 ? "completed" : "failed" ;
1102+ }
1103+
1104+ return isExecCommand && looksLikeCommandFailure ( output ) ? "failed" : "completed" ;
1105+ }
1106+
1107+ function looksLikeCommandFailure ( output : string ) : boolean {
1108+ const trimmed = output . trim ( ) ;
1109+ if ( trimmed . length === 0 ) {
1110+ return false ;
1111+ }
1112+
1113+ return / ( ^ | \n ) ( E r r o r | F a i l e d | C o m m a n d f a i l e d | S a n d b o x e r r o r | N o s u c h f i l e o r d i r e c t o r y | P e r m i s s i o n d e n i e d | O p e r a t i o n n o t p e r m i t t e d | E N O E N T | E A C C E S ) ( : | \b ) / i. test ( trimmed ) ;
10241114}
10251115
10261116function asRecord ( value : unknown ) : JsonRecord | null {
@@ -1033,3 +1123,7 @@ function asRecord(value: unknown): JsonRecord | null {
10331123function stringValue ( value : unknown ) : string | null {
10341124 return typeof value === "string" ? value : null ;
10351125}
1126+
1127+ function numberValue ( value : unknown ) : number | null {
1128+ return typeof value === "number" && Number . isFinite ( value ) ? value : null ;
1129+ }
0 commit comments