1- import { promises as fs } from 'node:fs' ;
2- import os from 'node:os' ;
3- import path from 'node:path' ;
41import type { AgentDeviceBackend , BackendSnapshotResult } from '../backend.ts' ;
52import type { CommandSessionRecord } from '../runtime.ts' ;
63import { createAgentDevice } from '../runtime.ts' ;
74import { isCommandSupportedOnDevice } from '../core/capabilities.ts' ;
8- import { dispatchCommand } from '../core/dispatch.ts' ;
9- import { AppError , normalizeError } from '../utils/errors.ts' ;
10- import { emitDiagnostic } from '../utils/diagnostics.ts' ;
5+ import { AppError } from '../utils/errors.ts' ;
116import type { SnapshotDiffSummary } from '../utils/snapshot-diff.ts' ;
127import type { DaemonRequest , DaemonResponse , DaemonResponseData , SessionState } from './types.ts' ;
138import { SessionStore } from './session-store.ts' ;
@@ -18,10 +13,9 @@ import {
1813 resolveSessionDevice ,
1914 withSessionlessRunnerCleanup ,
2015} from './handlers/snapshot-session.ts' ;
21- import { contextFromFlags } from './context.ts' ;
2216import { createDaemonRuntimePolicy } from './runtime-policy.ts' ;
2317import { createDaemonRuntimeSessionStore } from './runtime-session.ts' ;
24- import { annotateScreenshotWithRefs } from './screenshot-overlay .ts' ;
18+ import { maybeBuildAndroidSnapshotTimeoutFailure } from './android-snapshot-timeout-evidence .ts' ;
2519
2620export async function dispatchSnapshotViaRuntime ( params : {
2721 req : DaemonRequest ;
@@ -141,25 +135,15 @@ async function dispatchSnapshotRuntimeCommand(
141135 snapshotScope : resolvedScope . scope ,
142136 } ) ;
143137 } catch ( error ) {
144- const timeoutEvidence = await maybeCaptureAndroidSnapshotTimeoutEvidence ( {
138+ const timeoutResponse = await maybeBuildAndroidSnapshotTimeoutFailure ( {
145139 error,
146140 command : params . command ,
147141 logPath,
148142 session,
149143 device,
150144 } ) ;
151- if ( ! timeoutEvidence ) throw error ;
152- const normalized = normalizeError ( error ) ;
153- return {
154- ok : false ,
155- error : {
156- ...normalized ,
157- details : {
158- ...( normalized . details ?? { } ) ,
159- androidSnapshotTimeoutScreenshot : timeoutEvidence ,
160- } ,
161- } ,
162- } ;
145+ if ( ! timeoutResponse ) throw error ;
146+ return timeoutResponse ;
163147 }
164148 recordSnapshotRuntimeAction ( {
165149 req,
@@ -174,125 +158,6 @@ async function dispatchSnapshotRuntimeCommand(
174158 } ) ;
175159}
176160
177- async function maybeCaptureAndroidSnapshotTimeoutEvidence ( params : {
178- error : unknown ;
179- command : SnapshotRuntimeCommandParams [ 'command' ] ;
180- logPath : string ;
181- session : SessionState | undefined ;
182- device : SessionState [ 'device' ] ;
183- } ) : Promise < Record < string , unknown > | undefined > {
184- if ( params . command !== 'snapshot' ) return undefined ;
185- if ( params . device . platform !== 'android' ) return undefined ;
186- if ( ! isAndroidSnapshotTimeoutError ( params . error ) ) return undefined ;
187-
188- try {
189- const tempDir = await fs . mkdtemp (
190- path . join ( os . tmpdir ( ) , 'agent-device-android-snapshot-timeout-' ) ,
191- ) ;
192- const screenshotPath = path . join ( tempDir , 'snapshot-timeout-overlay-refs.png' ) ;
193- const data = await dispatchCommand ( params . device , 'screenshot' , [ screenshotPath ] , undefined , {
194- ...contextFromFlags (
195- params . logPath ,
196- { screenshotNoStabilize : true } ,
197- params . session ?. appBundleId ,
198- params . session ?. trace ?. outPath ,
199- ) ,
200- surface : params . session ?. surface ,
201- } ) ;
202- const resolvedPath =
203- typeof data === 'object' &&
204- data !== null &&
205- typeof ( data as Record < string , unknown > ) . path === 'string'
206- ? ( ( data as Record < string , unknown > ) . path as string )
207- : screenshotPath ;
208- const evidence : Record < string , unknown > = {
209- path : resolvedPath ,
210- overlayRefsRequested : true ,
211- overlayRefsAnnotated : false ,
212- } ;
213-
214- if ( params . session ?. snapshot ) {
215- try {
216- const overlayRefs = await annotateScreenshotWithRefs ( {
217- screenshotPath : resolvedPath ,
218- snapshot : params . session . snapshot ,
219- } ) ;
220- evidence . overlayRefsAnnotated = overlayRefs . length > 0 ;
221- evidence . overlayRefCount = overlayRefs . length ;
222- evidence . overlayRefSource = 'session-snapshot' ;
223- evidence . overlayRefs = overlayRefs ;
224- } catch ( error ) {
225- const normalized = normalizeError ( error ) ;
226- evidence . overlayAnnotationError = normalized . message ;
227- emitDiagnostic ( {
228- level : 'warn' ,
229- phase : 'android_snapshot_timeout_screenshot_overlay_failed' ,
230- data : { path : resolvedPath , error : normalized . message } ,
231- } ) ;
232- }
233- } else {
234- evidence . overlayRefSource = 'unavailable' ;
235- evidence . overlayRefCount = 0 ;
236- }
237-
238- emitDiagnostic ( {
239- level : 'warn' ,
240- phase : 'android_snapshot_timeout_screenshot_captured' ,
241- data : {
242- path : resolvedPath ,
243- overlayRefCount : evidence . overlayRefCount ,
244- overlayRefsAnnotated : evidence . overlayRefsAnnotated ,
245- } ,
246- } ) ;
247- return evidence ;
248- } catch ( error ) {
249- const normalized = normalizeError ( error ) ;
250- emitDiagnostic ( {
251- level : 'warn' ,
252- phase : 'android_snapshot_timeout_screenshot_failed' ,
253- data : { error : normalized . message } ,
254- } ) ;
255- return {
256- captureFailed : true ,
257- error : normalized . message ,
258- } ;
259- }
260- }
261-
262- function isAndroidSnapshotTimeoutError ( error : unknown ) : boolean {
263- const normalized = normalizeError ( error ) ;
264- if ( normalized . code !== 'COMMAND_FAILED' ) return false ;
265-
266- const text = `${ normalized . message } \n${ normalized . hint ?? '' } ` ;
267- if ( / A n d r o i d U I h i e r a r c h y d u m p t i m e d o u t / i. test ( text ) ) return true ;
268- if ( / S t o c k U I A u t o m a t o r f a l l b a c k w a s s k i p p e d / i. test ( text ) ) return true ;
269- if ( / A n d r o i d a c c e s s i b i l i t y s n a p s h o t s c a n b e b l o c k e d / i. test ( text ) ) return true ;
270-
271- const details = normalized . details ;
272- const helper = details ?. helper ;
273- if ( helper && typeof helper === 'object' ) {
274- const helperRecord = helper as Record < string , unknown > ;
275- const errorType = String ( helperRecord . errorType ?? '' ) ;
276- const message = String ( helperRecord . message ?? '' ) ;
277- if ( / T i m e o u t E x c e p t i o n / i. test ( errorType ) || / t i m e d o u t / i. test ( message ) ) return true ;
278- }
279-
280- const timeoutMs = details ?. timeoutMs ;
281- const cmd = details ?. cmd ;
282- const rawArgs = details ?. args ;
283- const args = Array . isArray ( rawArgs )
284- ? rawArgs . map ( String )
285- : typeof rawArgs === 'string'
286- ? rawArgs . split ( / \s + / )
287- : [ ] ;
288- return (
289- typeof timeoutMs === 'number' &&
290- cmd === 'adb' &&
291- args . includes ( 'uiautomator' ) &&
292- args . includes ( 'dump' )
293- ) ;
294- }
295-
296161function createSnapshotRuntime ( params : {
297162 req : DaemonRequest ;
298163 sessionName : string ;
0 commit comments