@@ -264,93 +264,137 @@ async function tryRecoverRunnerCommandAfterTransportError(
264264 lifecycleState,
265265 } ,
266266 } ) ;
267+ return handleRunnerCommandStatusRecovery (
268+ status ,
269+ lifecycleState ,
270+ command ,
271+ transportError ,
272+ options ,
273+ ) ;
274+ }
267275
276+ function handleRunnerCommandStatusRecovery (
277+ status : Record < string , unknown > ,
278+ lifecycleState : string ,
279+ command : RunnerCommand ,
280+ transportError : AppError ,
281+ options : AppleRunnerCommandOptions ,
282+ ) : Record < string , unknown > | undefined {
268283 if ( lifecycleState === 'completed' ) {
269- const recovered = parseLifecycleResponseJson ( status . lifecycleResponseJson ) ;
270- if ( recovered ) return recovered ;
271- if ( isReadOnlyRunnerCommand ( command . command ) ) {
272- throw transportError ;
273- }
274- throw new AppError (
275- 'COMMAND_FAILED' ,
276- `Runner command "${ command . command } " completed after the transport response was lost, but no recoverable response was retained.` ,
277- {
278- command : command . command ,
279- commandId : command . commandId ,
280- lifecycleState,
281- recovery : 'completed_without_retained_response' ,
282- hint : completedWithoutRetainedResponseHint ( command . command ) ,
283- logPath : options . logPath ,
284- transportError : transportError . message ,
285- } ,
286- transportError ,
287- ) ;
284+ return handleCompletedRunnerStatus ( status , command , transportError , options ) ;
288285 }
289286
290287 if ( lifecycleState === 'failed' ) {
291- const errorCode =
292- typeof status . lifecycleErrorCode === 'string' ? status . lifecycleErrorCode : undefined ;
293- const errorMessage =
294- typeof status . lifecycleErrorMessage === 'string'
295- ? status . lifecycleErrorMessage
296- : 'Runner command failed' ;
297- const hint =
298- typeof status . lifecycleErrorHint === 'string' ? status . lifecycleErrorHint : undefined ;
299- throw new AppError (
300- toAppErrorCode ( errorCode ) ,
301- errorMessage ,
302- {
303- command : command . command ,
304- commandId : command . commandId ,
305- lifecycleState,
306- recovery : 'runner_reported_failure' ,
307- hint : hint ?? runnerReportedFailureHint ( command . command ) ,
308- logPath : options . logPath ,
309- transportError : transportError . message ,
310- } ,
311- transportError ,
312- ) ;
288+ throw runnerStatusFailureError ( status , command , transportError , options ) ;
313289 }
314290
315291 if ( lifecycleState === 'accepted' || lifecycleState === 'started' ) {
316- if ( isReadOnlyRunnerCommand ( command . command ) ) {
317- throw transportError ;
318- }
319- throw new AppError (
320- 'COMMAND_FAILED' ,
321- `Runner command "${ command . command } " is still ${ lifecycleState } after the transport response was lost.` ,
322- {
323- command : command . command ,
324- commandId : command . commandId ,
325- lifecycleState,
326- recovery : 'command_still_in_flight' ,
327- hint : inFlightAfterLostResponseHint ( command . command ) ,
328- logPath : options . logPath ,
329- transportError : transportError . message ,
330- } ,
331- transportError ,
332- ) ;
292+ throw runnerStatusInFlightError ( lifecycleState , command , transportError , options ) ;
333293 }
334294
335295 return undefined ;
336296}
337297
298+ function handleCompletedRunnerStatus (
299+ status : Record < string , unknown > ,
300+ command : RunnerCommand ,
301+ transportError : AppError ,
302+ options : AppleRunnerCommandOptions ,
303+ ) : Record < string , unknown > | undefined {
304+ const recovered = parseLifecycleResponseJson ( status . lifecycleResponseJson ) ;
305+ if ( recovered ) return recovered ;
306+ if ( isReadOnlyRunnerCommand ( command . command ) ) {
307+ throw transportError ;
308+ }
309+ throw new AppError (
310+ 'COMMAND_FAILED' ,
311+ `Runner command "${ command . command } " completed after the transport response was lost, but no recoverable response was retained.` ,
312+ {
313+ command : command . command ,
314+ commandId : command . commandId ,
315+ lifecycleState : 'completed' ,
316+ recovery : 'completed_without_retained_response' ,
317+ hint : completedWithoutRetainedResponseHint ( command . command ) ,
318+ logPath : options . logPath ,
319+ transportError : transportError . message ,
320+ } ,
321+ transportError ,
322+ ) ;
323+ }
324+
325+ function runnerStatusFailureError (
326+ status : Record < string , unknown > ,
327+ command : RunnerCommand ,
328+ transportError : AppError ,
329+ options : AppleRunnerCommandOptions ,
330+ ) : AppError {
331+ const errorCode =
332+ typeof status . lifecycleErrorCode === 'string' ? status . lifecycleErrorCode : undefined ;
333+ const errorMessage =
334+ typeof status . lifecycleErrorMessage === 'string'
335+ ? status . lifecycleErrorMessage
336+ : 'Runner command failed' ;
337+ const hint =
338+ typeof status . lifecycleErrorHint === 'string' ? status . lifecycleErrorHint : undefined ;
339+ return new AppError (
340+ toAppErrorCode ( errorCode ) ,
341+ errorMessage ,
342+ {
343+ command : command . command ,
344+ commandId : command . commandId ,
345+ lifecycleState : 'failed' ,
346+ recovery : 'runner_reported_failure' ,
347+ hint : hint ?? runnerReportedFailureHint ( command . command ) ,
348+ logPath : options . logPath ,
349+ transportError : transportError . message ,
350+ } ,
351+ transportError ,
352+ ) ;
353+ }
354+
355+ function runnerStatusInFlightError (
356+ lifecycleState : string ,
357+ command : RunnerCommand ,
358+ transportError : AppError ,
359+ options : AppleRunnerCommandOptions ,
360+ ) : AppError {
361+ if ( isReadOnlyRunnerCommand ( command . command ) ) {
362+ return transportError ;
363+ }
364+ return new AppError (
365+ 'COMMAND_FAILED' ,
366+ `Runner command "${ command . command } " is still ${ lifecycleState } after the transport response was lost.` ,
367+ {
368+ command : command . command ,
369+ commandId : command . commandId ,
370+ lifecycleState,
371+ recovery : 'command_still_in_flight' ,
372+ hint : inFlightAfterLostResponseHint ( command . command ) ,
373+ logPath : options . logPath ,
374+ transportError : transportError . message ,
375+ } ,
376+ transportError ,
377+ ) ;
378+ }
379+
338380function parseLifecycleResponseJson ( value : unknown ) : Record < string , unknown > | undefined {
339381 if ( typeof value !== 'string' || value . trim ( ) . length === 0 ) return undefined ;
340- let parsed : LifecycleResponsePayload ;
341- try {
342- const raw : unknown = JSON . parse ( value ) ;
343- parsed = raw && typeof raw === 'object' ? ( raw as LifecycleResponsePayload ) : { } ;
344- } catch {
345- return undefined ;
346- }
382+ const parsed = parseLifecycleResponsePayload ( value ) ;
347383 if ( ! parsed . ok ) return undefined ;
348384 if ( parsed . data && typeof parsed . data === 'object' && ! Array . isArray ( parsed . data ) ) {
349385 return parsed . data as Record < string , unknown > ;
350386 }
351387 return { } ;
352388}
353389
390+ function parseLifecycleResponsePayload ( value : string ) : LifecycleResponsePayload {
391+ try {
392+ const raw : unknown = JSON . parse ( value ) ;
393+ if ( raw && typeof raw === 'object' ) return raw as LifecycleResponsePayload ;
394+ } catch { }
395+ return { } ;
396+ }
397+
354398function completedWithoutRetainedResponseHint ( command : string ) : string {
355399 return `The runner reports "${ command } " already completed, so agent-device will not replay it. Run snapshot -i to inspect the current UI, then continue from that observed state. If the session is stale, close and reopen the session before retrying.` ;
356400}
0 commit comments