JevRuntime.java

原始文件

1package com.android.commands.monkey.ape.jev;
2
3import java.io.*;
4import java.util.*;
5import java.util.concurrent.*;
6import org.json.*;
7import android.graphics.Rect;
8import android.graphics.Bitmap;
9import com.android.commands.monkey.ape.model.*;
10import com.android.commands.monkey.ape.tree.*;
11import com.android.commands.monkey.ape.utils.Config;
12import com.android.commands.monkey.ape.utils.ApeSnapshot;
13import com.android.commands.monkey.ape.naming.NameManager;
14import com.android.commands.monkey.MonkeyEvent;
15import com.android.commands.monkey.MonkeySourceApe;
16import com.android.commands.monkey.ape.Subsequence;
17import com.android.commands.monkey.ape.ActionFilter;
18
19/** External advice and receipts. APE alone owns its graph and exploration model. */
20public final class JevRuntime {
21    public interface Validator { boolean valid(ModelAction action); }
22    private final JevTiming timing=new JevTiming();
23    private final JevOutput output=new JevOutput(timing);
24    private final JevEvents events;
25    private final JevRegistry registry;
26    private JevTransport probeTransport;
27    private JevSemanticReview semanticReview;
28    private final JevTransport regionTransport;
29    private final String endpoint,token;
30    private final int queueCapacity;
31    private final File receipts,graphSnapshotFile,screenshotDirectory;
32    private final MonkeySourceApe ape;
33    private boolean observeOnly,probeEnabled,snapshotResumed;
34    private final boolean regionShadowAudit,regionContextAudit;
35    private final boolean semanticReviewEnabled;
36    private final JevRegion region;
37    private final int maxBytes,probeTimeoutMs;
38    private final String providerModel;
39    private final Map<String,String> stateScreenshotPaths=new ConcurrentHashMap<String,String>();
40    private final Set<String> screenshotPending=Collections.newSetFromMap(new ConcurrentHashMap<String,Boolean>());
41    private final Map<String,Integer> screenshotFailures=new ConcurrentHashMap<String,Integer>();
42    private final Map<String,String> screenshotFailureReasons=new ConcurrentHashMap<String,String>();
43    private final IdentityHashMap<GUITreeTransition,String> transitionIds=new IdentityHashMap<GUITreeTransition,String>();
44    private final Set<State> knownStates=Collections.newSetFromMap(new IdentityHashMap<State,Boolean>());
45    private final Set<ModelAction> knownActions=Collections.newSetFromMap(new IdentityHashMap<ModelAction,Boolean>());
46    private int indexed;
47    private Model lastModel;
48    private long step,transitions,startNanos=ApeSnapshot.nowNanos();
49    private final String runId="run-"+Long.toHexString(startNanos);
50    private boolean closed,decisionHealthy=true;
51    private boolean probeRequested;
52    private List<JevRegistry.Entry> roundSuggestions=Collections.emptyList();
53    private int suggestionIndex;
54    private JevRegistry.Entry selected;
55    private boolean semanticSelected;
56    private String semanticEntryId,semanticRequestId,semanticCandidateId;
57    private double semanticConfidence;
58    private String semanticChoice;
59    private long selectedVersion;
60    private GUITreeAction selectedAction,originalResolvedAction;
61    // Freeze at the input boundary, before APE resolves another GUI/action.
62    private JSONObject probeSourceScreenshot;
63    private final JevEvidenceAudit evidenceAudit;
64    private GUITreeNode originalResolvedNode;
65    private final IdentityHashMap<MonkeyEvent,InputTrace> inputEvents=new IdentityHashMap<MonkeyEvent,InputTrace>();
66    private final Set<GUITreeAction> confirmedInputs=Collections.newSetFromMap(new IdentityHashMap<GUITreeAction,Boolean>());
67    private final IdentityHashMap<GUITreeAction,String> confirmedInputBranches=new IdentityHashMap<GUITreeAction,String>();
68    private final IdentityHashMap<GUITreeAction,ExecutionContext> executionContexts=new IdentityHashMap<GUITreeAction,ExecutionContext>();
69    private long executionContextSequence,historyAuditSequence;
70    private long historyAuditSelectionStart,historyAuditSelectionStep;
71    // One timing span for the existing review/Native-selection boundary, not a scheduler.
72    private long semanticReviewStart,semanticPrepareEnd,semanticReviewStep,semanticReviewSequence;
73    private String semanticReviewId;
74    private long diagnosticConfirmedSummaries;
75    private static final class ExecutionContext {
76        final String id;final long step;final JSONObject snapshot;
77        ExecutionContext(String id,long step,JSONObject snapshot){this.id=id;this.step=step;this.snapshot=snapshot;}
78    }
79    private InputTrace selectedInput;
80    private static final class InputTrace {
81        final JevRegistry.Entry entry;
82        final GUITreeAction action;
83        final String semanticEntryId,semanticRequestId,semanticCandidateId;
84        final String branch=ApeSnapshot.branch();
85        int total,success,failed,discarded;
86        InputTrace(JevRegistry.Entry e,GUITreeAction a){this(e,a,null,null,null);}
87        InputTrace(JevRegistry.Entry e,GUITreeAction a,String entryId,String requestId,String candidateId){entry=e;action=a;semanticEntryId=entryId;semanticRequestId=requestId;semanticCandidateId=candidateId;}
88        int remaining(){return total-success-failed-discarded;}
89        boolean confirmed(){return total>0 && remaining()==0 && failed==0 && discarded==0;}
90        String phase(){return total==0?"no_input_events":confirmed()?"input_injected":"input_incomplete";}
91    }
92
93    public static JevRuntime create(File output){return create(output,null,false);}
94    public static JevRuntime create(File output,MonkeySourceApe ape){return create(output,ape,false);}
95    public static JevRuntime create(File output,MonkeySourceApe ape,boolean observeOnly) {
96        JevRegion.validateConfiguration(!observeOnly || Config.getBoolean("ape.jev.region_shadow_audit", false));
97        try {return new JevRuntime(output,Config.get("ape.jev.endpoint",JevProtocol.DEFAULT_ENDPOINT),
98            JevProvider.token(Config.get("ape.jev.provider","startlux")),Config.getInteger("ape.jev.timeoutMs",60000),
99            Config.getInteger("ape.jev.maxRequestBytes",32768),Config.get("ape.jev.model",JevProtocol.DEFAULT_MODEL),ape,observeOnly);}
100        catch(Exception e){throw new IllegalStateException("Decision runtime initialization failed",e);}
101    }
102    public JevRuntime(File output,String endpoint,String token,int timeoutMs,int maxBytes,String model) throws IOException {
103        this(output,endpoint,token,timeoutMs,maxBytes,model,null,false);
104    }
105    private JevRuntime(File directory,String endpoint,String token,int timeoutMs,int maxBytes,String model,MonkeySourceApe ape,boolean observeOnly) throws IOException {
106        boolean configuredShadowAudit=Config.getBoolean("ape.jev.region_shadow_audit", false) || Config.getBoolean("ape.jev.region_locator_shadow",false);
107        JevRegion.validateConfiguration(!observeOnly || configuredShadowAudit);
108        receipts=new File(directory,"jev-receipts");receipts.mkdirs();graphSnapshotFile=new File(directory,"jev-graph.json");
109        screenshotDirectory=new File(directory,"ape-state-screenshots");this.ape=ape;
110        observeOnly |= Config.getBoolean("ape.jev.region_locator_shadow",false)
111            || !Config.getBoolean("ape.jev.probe.enabled",true) && !Config.getBoolean("ape.jev.regionBlocking.enabled",false);
112        this.observeOnly=observeOnly;
113        this.regionShadowAudit=configuredShadowAudit;
114        this.regionContextAudit=regionShadowAudit && Config.getBoolean("ape.jev.region_context_audit",false);
115        if(maxBytes<=0)throw new IllegalArgumentException("invalid probe bounds");
116        // The provider contract has a single hard body limit for both sync and
117        // async review. Older callers may still pass a larger profile value.
118        this.maxBytes=Math.min(maxBytes,32768);providerModel=model;
119        probeEnabled=!observeOnly && Config.getBoolean("ape.jev.probe.enabled",true);
120        semanticReviewEnabled=probeEnabled && Config.isJevSemanticReviewEnabled();
121        probeTimeoutMs=Config.getInteger("ape.jev.probeTimeoutMs",5000);
122        if(probeTimeoutMs<=0)throw new IllegalArgumentException("invalid probe bounds");
123        double confidence=Double.parseDouble(Config.get("ape.jev.localProbeConfidence","0.5"));
124        if(confidence!=0.5)throw new IllegalArgumentException("probe confidence threshold must be 0.5");
125        events=new JevEvents(new File(directory,"jev-events.jsonl"),this.output);registry=new JevRegistry(events,confidence);
126        queueCapacity=Config.getInteger("ape.jev.queueCapacity",4);
127        this.endpoint=endpoint;this.token=token;
128        evidenceAudit=new JevEvidenceAudit(directory,runId,timing,imageSlots);
129        boolean regionEnabled=!observeOnly && Config.getBoolean("ape.jev.regionBlocking.enabled",false);
130        // Semantic mode owns its asynchronous page-review transport; do not start
131        // the synchronous probe worker that it does not use.
132        probeTransport=probeEnabled && !semanticReviewEnabled?new JevTransport(endpoint,token,queueCapacity,"probe"):null;
133        if(probeTransport!=null)probeTransport.setSnapshotIdentity(ApeSnapshot.transportIdentity());
134        semanticReview=semanticReviewEnabled?new JevSemanticReview(endpoint,token,queueCapacity,probeTimeoutMs,maxBytes,model,registry,new JevSemanticReview.ReceiptSink(){
135            public boolean save(String name,JSONObject receipt){try{JevRuntime.this.save(name,receipt);return true;}catch(IOException error){receiptFailed(error);return false;}}
136        },Config.isJevSemanticReviewSynchronous()):null;
137        if(semanticReview!=null){semanticReview.setSnapshotIdentity(ApeSnapshot.transportIdentity());semanticReview.setEvidenceAudit(evidenceAudit,ape);}
138        regionTransport=regionEnabled && !configuredShadowAudit && !Config.getBoolean("ape.jev.region_auto",false)?new JevTransport(endpoint,token,queueCapacity,"region"):null;
139        region=(regionEnabled || regionShadowAudit)?
140            new JevRegion(this,timing,events,regionTransport,maxBytes,model,timeoutMs,Config.getLong("ape.jev.regionSeed",0),regionShadowAudit):null;
141        events.emit("PROBE_CONFIG",0,0,null,null,JevRegistry.fields("probe_enabled",probeEnabled,"confidence_threshold",0.5,
142            "timeout_ms",probeTimeoutMs,"probe_receipt_schema",6,"semantic_review_enabled",semanticReviewEnabled,
143            "semantic_review_synchronous",semanticReview!=null && semanticReview.isSynchronous(),"semantic_observability_schema",semanticReview!=null?1:0));
144    }
145    public JevRegistry registry(){return registry;}
146    public JevRegion region(){return region;}
147    public boolean isDecisionEnabled(){return !observeOnly;}
148    public boolean probeEnabled(){return probeEnabled;}
149    public boolean semanticReviewEnabled(){return semanticReview!=null;}
150    public boolean semanticReviewSynchronous(){return semanticReview!=null && semanticReview.isSynchronous();}
151    public boolean regionShadowAudit(){return regionShadowAudit;}
152    /** No observation is invented to settle the last injected action: its input facts
153     * remain in memory until the next real GUI observation after restoration. */
154    public void snapshotDrain() {
155        ApeSnapshot.requireRegionOff();
156        if(closed || probeEnabled || probeTransport!=null || regionTransport!=null || region!=null
157                || selected!=null || selectedInput!=null || !inputEvents.isEmpty() || !roundSuggestions.isEmpty())
158            throw new IllegalStateException("Checkpoint requires idle native observation runtime");
159        events.flush();output.drain();
160        if(!output.isHealthy() || !events.isHealthy())throw new IllegalStateException("Checkpoint evidence incomplete");
161    }
162    public JSONObject snapshotEvidence() {
163        return JevRegistry.fields("confirmed_input_actions",confirmedInputs.size(),"observed_transitions",transitionIds.size(),
164            "indexed_history",indexed,"pending_input_events",inputEvents.size(),"pending_probe",selected!=null,
165            "active_requests",(probeTransport==null?0:probeTransport.outstanding())+(regionTransport==null?0:regionTransport.outstanding()),"region_enabled",region!=null,
166            "state_screenshots_saved",stateScreenshotPaths.size(),"state_screenshot_failures",new JSONObject(screenshotFailures),
167            "registry",registry.historyEvidence());
168    }
169    public void snapshotResume(boolean live,String namespace) throws IOException {
170        if(snapshotResumed)throw new IllegalStateException("Restore checkpoint before another branch");
171        snapshotDrain();
172        registry.requestNamespace(namespace);
173        probeEnabled=live;observeOnly=!live;
174        // Rebind retained concrete executions against the current APE action objects.
175        // Replaying history emits no new transition and consumes no APE RNG.
176        if(live && lastModel!=null) {
177            registry.rebuildHistory(lastModel.getVersion(),step);
178            for(GUITreeTransition transition:lastModel.getGraph().getTreeHistory())index(transition,true);
179            indexed=lastModel.getGraph().getTreeHistory().size();
180        }
181        if(live){probeTransport=new JevTransport(endpoint,token,queueCapacity,"probe");probeTransport.setSnapshotIdentity(ApeSnapshot.transportIdentity());}
182        snapshotResumed=true;
183        events.emit("PROBE_CONFIG",step,registry.version(),null,null,JevRegistry.fields("probe_enabled",live,
184            "confidence_threshold",0.5,"timeout_ms",probeTimeoutMs,"probe_receipt_schema",6));
185    }
186    boolean regionContextAuditEnabled(){return regionContextAudit;}
187    JSONObject contextSnapshot(GUITree tree,State state,ModelAction action,GUITreeNode node) {
188        int before=NameManager.registeredNameCount();long start=System.nanoTime();
189        try{return JevEvidence.contextSnapshot(tree,state,action,node);}
190        finally{timing.record("context_snapshot_build",System.nanoTime()-start);
191            timing.count("context_name_registry_growth",NameManager.registeredNameCount()-before);}
192    }
193    long nowNanos(){return System.nanoTime();}
194    private java.util.function.LongSupplier regionClock=new java.util.function.LongSupplier(){public long getAsLong(){return System.nanoTime();}};
195    long regionNowNanos(){return regionClock.getAsLong();}
196    void save(String name,JSONObject receipt) throws IOException {
197        long start=System.nanoTime();
198        try{output.save(new File(receipts,name.replace(':','-')+".json"),ApeSnapshot.identify(receipt),true);}
199        finally{timing.record("receipt_enqueue",System.nanoTime()-start);}
200    }
201    String saveDiagnostic(String name,JSONObject receipt) {
202        if(!regionContextAudit)return null;
203        try {save(name,receipt);return name;}
204        catch(IOException error){receiptFailed(error);return null;}
205    }
206    void receiptFailed(Exception error){decisionHealthy=false;events.emit("RECEIPT_FAILED",step,registry.version(),null,null,JevRegistry.fields("reason","receipt_write_failed","error",error.getClass().getSimpleName()));}
207    public void regionRoot(State state){if(region!=null)region.root(state);}
208    public void beforeSelection(Model model,State state,GUITree tree,int timestamp){if(region!=null)region.safePoint(model,state,tree,timestamp);}
209    public void shadowSelection(ModelAction action,GUITree tree,int timestamp,String strategy){if(region!=null && regionShadowAudit)region.auditSelection(action,tree,timestamp,strategy);}
210    JevRegion.Fact explorationFact(State state) {
211        GUITree tree=state.getLatestGUITree();boolean known=tree!=null && tree.getCurrentNames()!=null;
212        boolean saturated=true;
213        for(ModelAction action:state.getActions())if(ActionFilter.ENABLED_VALID.include(action))saturated &= action.isSaturated();
214        return new JevRegion.Fact(known,saturated,true);
215    }
216    public void sync(Model model,long currentStep) {
217        step=currentStep;lastModel=model;
218        registry.confirmationSource(model,confirmedInputs);
219        List<GUITreeTransition> history=model.getGraph().getTreeHistory();
220        if(registry.version()!=model.getVersion()) {
221            roundSuggestions=Collections.emptyList();suggestionIndex=0;
222            registry.reset(model.getVersion(),step);indexed=0;
223            if(region!=null)region.reset(model.getVersion());
224            for(GUITreeTransition transition:history)index(transition,true);
225            indexed=history.size();
226            knownStates.addAll(model.getGraph().getStates());
227            for(State state:model.getGraph().getStates())knownActions.addAll(state.getActions());
228        }else while(indexed<history.size())index(history.get(indexed++),false);
229        if(semanticReview!=null)semanticReview.setModelVersion(registry.version());
230    }
231    private void index(GUITreeTransition transition,boolean rebuilding) {
232        boolean fresh=!transitionIds.containsKey(transition);
233        String id=transitionIds.get(transition);
234        if(fresh){id=transition.getCurrentStateTransition().getGraphId()+":"+(transitionIds.size()+1);transitionIds.put(transition,id);}
235        ModelAction action=transition.getAction().getModelAction();
236        State source=transition.getSource().getCurrentState(),target=transition.getTarget().getCurrentState();
237        boolean probe=selected!=null && selectedAction==transition.getAction();
238        boolean semanticProbe=semanticSelected && selectedAction==transition.getAction();
239        boolean refined=probe && selectedVersion!=registry.version();
240        boolean newState=!rebuilding && !knownStates.contains(target),newAction=false;
241        if(!rebuilding)for(ModelAction candidate:target.getActions())if(!knownActions.contains(candidate))newAction=true;
242        boolean loop=source==target;
243        boolean guiUnchanged=JevEvidence.guiIdentity(transition.getSource()).equals(JevEvidence.guiIdentity(transition.getTarget()));
244        boolean inputConfirmed=confirmedInputs.contains(transition.getAction());
245        JSONObject probeTargetScreenshot=null;
246        if(fresh && !rebuilding && inputConfirmed && (probe || semanticProbe))
247            probeTargetScreenshot=captureProbeScreenshot(transition.getTarget(),null,null,"target");
248        if(fresh)evidenceAudit.transition(rebuilding?null:ape,transition,id,step,registry.version(),inputConfirmed,probe||semanticProbe,probeTargetScreenshot);
249        ExecutionContext executionContext=executionContexts.remove(transition.getAction());
250        if(region!=null)region.transition(transition,id,(regionShadowAudit && !Config.getBoolean("ape.jev.region_locator_shadow",false)) || inputConfirmed,
251            executionContext==null?null:executionContext.id,executionContext==null?null:executionContext.snapshot);
252        GUITreeNode node=transition.getAction().getGUITreeNode();
253        if(probeEnabled && inputConfirmed && node!=null)registry.executed(source.getGraphId(),action.getGraphId(),JevEvidence.member(action.getType().name(),node),
254            transition.getAction(),transition.getTarget(),source.getActivity(),target.getGraphId(),target.getActivity(),JevEvidence.resultLabels(transition.getTarget().getRootNode()),
255            id,step,newState,newAction,refined,false,!fresh,JevEvidence.resultUnsafe(transition.getTarget().getRootNode()),guiUnchanged);
256        else if(fresh)events.emit("NATIVE_TRANSITION",step,registry.version(),source.getGraphId(),action.getGraphId(),
257            JevRegistry.fields("transition_id",id,"new_state",newState,"new_model_action",newAction,"refinement",refined,
258                "self_loop",loop,"target_state",target.getGraphId(),"gui_unchanged",guiUnchanged,"input_confirmed",inputConfirmed,
259                "execution_context_id",executionContext==null?JSONObject.NULL:executionContext.id,
260                "source_gui_id",transition.getSource().getId(),"target_gui_id",transition.getTarget().getId()));
261        if(inputConfirmed && node!=null && semanticReview!=null)semanticReview.markExecuted(source,action,node);
262        if(semanticProbe) {
263            String entryId=semanticEntryId,requestId=semanticRequestId,candidateId=semanticCandidateId;double confidence=semanticConfidence;String choice=semanticChoice;
264            if(inputConfirmed && !rebuilding)events.emit("PROBE_EXECUTED",step,registry.version(),source.getGraphId(),action.getGraphId(),JevRegistry.fields(
265                "entry_id",entryId==null?JSONObject.NULL:entryId,"request_id",requestId==null?JSONObject.NULL:requestId,
266                "candidate_id",candidateId==null?JSONObject.NULL:candidateId,"confidence",confidence,"choice",choice==null?"PROMOTE":choice,
267                "transition_id",id,"target_state",target.getGraphId(),"input_confirmed",true));
268            else if(!inputConfirmed)events.emit("PROBE_EXECUTION_FAILED",step,registry.version(),source.getGraphId(),action.getGraphId(),JevRegistry.fields(
269                "entry_id",entryId==null?JSONObject.NULL:entryId,"request_id",requestId==null?JSONObject.NULL:requestId,
270                "candidate_id",candidateId==null?JSONObject.NULL:candidateId,"reason","native_transition_without_confirmed_input"));
271            semanticSelected=false;selectedAction=null;originalResolvedNode=null;originalResolvedAction=null;
272            semanticEntryId=null;semanticRequestId=null;semanticCandidateId=null;semanticConfidence=0;semanticChoice=null;
273        }
274        if(fresh) {
275            transitions++;
276            JSONObject receipt=JevRegistry.fields("transition_id",id,"native_edge_id",transition.getCurrentStateTransition().getGraphId(),
277                "native_history_position",transitionIds.size()-1,"source_gui_timestamp",transition.getSource().getTimestamp(),
278                "target_gui_timestamp",transition.getTarget().getTimestamp(),"state_id",source.getGraphId(),"model_action_id",action.getGraphId(),
279                "source_gui_id",transition.getSource().getId(),"target_gui_id",transition.getTarget().getId(),
280                "abstraction_version",registry.version(),"source_activity",source.getActivity(),
281                "execution_branch",confirmedInputBranches.get(transition.getAction()),"input_confirmed",inputConfirmed);
282            try {
283                // The immutable source snapshot already has an exclusive execution receipt.
284                if(executionContext!=null)receipt.put("execution_context_id",executionContext.id);
285                if(probe || semanticProbe) {
286                    receipt.put("probe_source_screenshot",probeSourceScreenshot==null?JSONObject.NULL:probeSourceScreenshot);
287                    receipt.put("probe_target_screenshot",probeTargetScreenshot==null?JSONObject.NULL:probeTargetScreenshot);
288                }
289                save("transition-"+id,receipt);
290            } catch(IOException error){receiptFailed(error);
291            } catch(JSONException error){throw new IllegalStateException(error);}
292        }
293        knownStates.add(source);knownStates.add(target);knownActions.addAll(source.getActions());knownActions.addAll(target.getActions());
294        if(probe) {
295            if(selectedInput!=null && selectedInput.confirmed()) {
296                registry.recordProbeExecution(selected,step);
297                entryEvent("PROBE_EXECUTED",selected,JevRegistry.fields("transition_id",id,"new_state",newState,"new_model_action",newAction,
298                    "refinement",refined,"novelty_known",!rebuilding,"self_loop",loop,"gui_unchanged",guiUnchanged,
299                    "target_state",target.getGraphId(),"input_confirmed",true));
300
301            }else {registry.failed(selected,step,"native_transition_without_confirmed_input");}
302            selected=null;selectedAction=null;selectedInput=null;originalResolvedNode=null;originalResolvedAction=null;
303        }
304        if(probe || semanticProbe)probeSourceScreenshot=null;
305    }
306    public void safePoint(Model model,State state,GUITree tree,int timestamp) {
307        long started=System.nanoTime();sync(model,timestamp);
308        if(probeNavigationGlobal)finishProbeNavigation(state,timestamp,"global_target_observed");
309        if(selected!=null || semanticSelected)selectionFailed("no_native_transition");
310        if(region!=null)region.interrupted("no_native_transition");
311
312        captureFirstStateScreenshot(state);
313        knownStates.add(state);knownActions.addAll(state.getActions());
314        if(semanticReview!=null) {
315            semanticReview.setStep(step);
316            semanticReview.poll();
317        }
318        timing.record("safe_point_total",System.nanoTime()-started);
319    }
320    public void prepareSemanticReviews(Model model,State current,GUITree tree) {prepareSemanticReviews(model,current,tree,null);}
321    public void prepareSemanticReviews(Model model,State current,GUITree tree,Validator validator) {
322        if(semanticReview==null)return;
323        if(semanticReviewStart!=0) {
324            // An interrupted prior span must never add an unbounded lock wait before the next review.
325            try(JevDeadline ignored=JevDeadline.enter(System.nanoTime())) {
326                finishSemanticTiming(null,null,"review_reentered_without_selection");
327            } catch(JevDeadline.Expired lost) {
328                semanticReviewStart=0;semanticReviewId=null;
329                output.fail(new IOException("Interrupted semantic timing receipt could not be enqueued without waiting"));
330            }
331        }
332        semanticReviewStart=System.nanoTime();semanticPrepareEnd=0;semanticReviewStep=step;
333        semanticReviewId=runId+":semantic:"+(++semanticReviewSequence);
334        semanticReview.setReviewId(semanticReviewId,semanticReviewStart);
335        if(Config.getBoolean("ape.jev.historyAudit",false)) {
336            long began=System.nanoTime();
337            try {
338                JSONObject audit=JevHistoryAudit.capture(model,current,registry,confirmedInputs,transitionIds,indexed,step);
339                timing.record("history_audit_enumeration_build",System.nanoTime()-began);
340                audit.put("diagnostic_confirmed_summary_events",diagnosticConfirmedSummaries);
341                audit.put("pending_tracked_input_events",inputEvents.size());
342                long copyStart=System.nanoTime();
343                save("history-audit-"+step+"-"+(++historyAuditSequence),audit);
344                timing.record("history_audit_copy_enqueue",System.nanoTime()-copyStart);
345            }
346            catch(IOException error){receiptFailed(error);}
347            catch(JSONException error){throw new IllegalStateException(error);}
348            finally {timing.record("history_audit_diagnostic",System.nanoTime()-began);}
349        }
350        long prepareStart=System.nanoTime();
351        try {semanticReview.prepare(current,tree,step,validator);}
352        finally {semanticPrepareEnd=System.nanoTime();if(Config.getBoolean("ape.jev.historyAudit",false)) {
353            timing.record("history_audit_semantic_prepare",System.nanoTime()-prepareStart);
354            historyAuditSelectionStart=System.nanoTime();historyAuditSelectionStep=step;
355        }}
356        if(semanticReview.isSynchronous() && !semanticReview.isHealthy()) {
357            decisionHealthy=false;
358            // Provider failures are audited as unhealthy, but must not invoke APE bad-state recovery.
359            if(semanticReview.hasLocalFailure())
360                throw new com.android.commands.monkey.ape.BadStateException("synchronous semantic request construction failed");
361        }
362    }
363    public void beginProbeSelection() {
364        probeRequested=false;suggestionIndex=0;roundSuggestions=Collections.emptyList();
365        registry.discardAdvice(selected,step);
366        if(semanticReview!=null)semanticReview.beginSelection();
367    }
368    public void endProbeSelection() {
369        roundSuggestions=Collections.emptyList();suggestionIndex=0;
370        registry.discardAdvice(selected,step);
371        if(semanticReview!=null)semanticReview.endSelection();
372    }
373    private boolean currentWindow(GUITree tree) {
374        if(ape==null)return true;
375        String expected=tree.getJevObservationIdentity();
376        android.view.accessibility.AccessibilityNodeInfo root=ape.getRootInActiveWindow();
377        try {return expected!=null && expected.equals(JevEvidence.accessibilityIdentity(root));}
378        finally {if(root!=null)root.recycle();}
379    }
380    public ModelAction nextProbe(State state,GUITree tree,int timestamp,Validator validator) {
381        if(!probeEnabled || closed)return null;
382        step=timestamp;
383        if(semanticReview!=null)semanticReview.setStep(timestamp);
384        if(semanticReview!=null)return nextSemanticProbe(state,tree,timestamp,validator);
385        if(!probeRequested) {
386            probeRequested=true;
387            if(!requestProbe(state,tree))return null;
388            roundSuggestions=registry.suggestions(state,tree);
389        }
390        if(!currentWindow(tree))
391            throw new com.android.commands.monkey.ape.BadStateException("GUI changed during probe review");
392        while(suggestionIndex<roundSuggestions.size()) {
393            JevRegistry.Entry entry=roundSuggestions.get(suggestionIndex++);
394            if(registry.retireIfExecuted(entry,step))continue;
395            ModelAction action=entry.concreteAction.getModelAction();
396            GUITreeNode node=JevRegistry.correspondingNode(entry.concreteAction,tree,action);
397            if(node==null || !node.isEnabled() || !action.isValid() || !action.isEnabled() || !action.isResolvedAt(timestamp)
398                || action.getResolvedGUITree()!=tree)continue;
399            GUITreeNode previous=action.getResolvedNode();GUITreeAction previousAction=action.getResolvedGUITreeAction();
400            GUITreeNode[] nodes=action.getResolvedNodes();int throttle=action.getThrottle();
401            action.resolveAt(timestamp,throttle,tree,node,nodes);
402            if(!validator.valid(action) || !registry.claimProbe(entry,step)) {
403                action.resolveAt(timestamp,throttle,tree,previous,nodes);action.setResolvedGUITreeAction(previousAction);
404                continue;
405            }
406            originalResolvedNode=previous;originalResolvedAction=previousAction;
407            selected=entry;selectedVersion=registry.version();selectedAction=action.getResolvedGUITreeAction();selectedInput=null;
408            registry.selected(entry,step);
409            entryEvent("PROBE_BOUND",entry,JevRegistry.fields("confidence",entry.confidence,
410                "selection_rank",suggestionIndex,"source","local","changed_node",previous!=node));
411            return action;
412        }
413        return null;
414    }
415    private ModelAction nextSemanticProbe(State state,GUITree tree,int timestamp,Validator validator) {
416        if(semanticSelected || !currentWindow(tree))return null;
417        while(true) {
418            JevSemanticReview.Selection selection=semanticReview.nextSelection(state,tree);
419            if(selection==null)return null;
420            JevSemanticReview.Candidate candidate=selection.candidate;
421            ModelAction action=candidate.action;
422            if(!action.isValid() || !action.isEnabled() || !action.isResolvedAt(timestamp) || action.getResolvedGUITree()!=tree) {
423                semanticSelectionRejected(selection,timestamp,"native_binding_invalid");continue;
424            }
425            GUITreeNode[] nodes=action.getResolvedNodes();
426            if(nodes==null || !candidate.node.isEnabled()){semanticSelectionRejected(selection,timestamp,"resolved_nodes_unavailable_or_disabled");continue;}
427            GUITreeNode previous=action.getResolvedNode();GUITreeAction previousAction=action.getResolvedGUITreeAction();
428            int throttle=action.getThrottle();
429            action.resolveAt(timestamp,throttle,tree,candidate.node,nodes);
430            if(!validator.valid(action)) {
431                action.resolveAt(timestamp,throttle,tree,previous,nodes);action.setResolvedGUITreeAction(previousAction);
432                semanticSelectionRejected(selection,timestamp,"native_validator_rejected");
433                continue;
434            }
435            originalResolvedNode=previous;originalResolvedAction=previousAction;
436            selectedVersion=registry.version();selectedAction=action.getResolvedGUITreeAction();semanticSelected=true;
437            semanticEntryId=selection.entryId;semanticRequestId=selection.requestId;semanticCandidateId=selection.candidateId;
438            semanticConfidence=selection.confidence;semanticChoice="PROMOTE";
439            events.emit("PROBE_BOUND",timestamp,registry.version(),state.getGraphId(),action.getGraphId(),JevRegistry.fields(
440                "entry_id",semanticEntryId==null?JSONObject.NULL:semanticEntryId,"request_id",semanticRequestId==null?JSONObject.NULL:semanticRequestId,
441                "candidate_id",semanticCandidateId,"confidence",semanticConfidence,"choice","PROMOTE","source",semanticReviewSynchronous()?"semantic_sync":"semantic_async",
442                "input_signature",selection.inputSignature,"decision_scope",selection.decisionScope,
443                "concrete_identity",JevEvidence.digest(ConcreteActionIdentity.of(action.getType().name(),action.getResolvedNode()).toString())));
444            events.emit("PROBE_SELECTED",timestamp,registry.version(),state.getGraphId(),action.getGraphId(),JevRegistry.fields(
445                "entry_id",semanticEntryId==null?JSONObject.NULL:semanticEntryId,"request_id",semanticRequestId==null?JSONObject.NULL:semanticRequestId,
446                "candidate_id",semanticCandidateId,"confidence",semanticConfidence,"choice","PROMOTE","source",semanticReviewSynchronous()?"semantic_sync":"semantic_async"));
447            return action;
448        }
449    }
450    private void semanticSelectionRejected(JevSemanticReview.Selection selection,long timestamp,String reason) {
451        ModelAction action=selection.candidate.action;
452        evidenceAudit.navigation(ape,action.getResolvedGUITree(),timestamp,registry.version(),JevRegistry.fields("stage","native_binding_rejected",
453            "entry_id",selection.entryId,"request_id",selection.requestId,"candidate_id",selection.candidateId,"reason",reason,
454            "source_review_path","jev-audit/reviews/"+selection.requestId+".json","concrete",JevEvidenceAudit.concrete(action,selection.candidate.node)),true);
455        events.emit("SEMANTIC_SELECTION_REJECTED",timestamp,registry.version(),action.getState().getGraphId(),action.getGraphId(),
456            JevRegistry.fields("entry_id",selection.entryId,"request_id",selection.requestId,"candidate_id",selection.candidateId,
457                "concrete_identity",selection.concreteIdentity,"decision_scope",selection.decisionScope,
458                "input_signature",selection.inputSignature,"reason",reason,"decision_role","candidate_decision"));
459    }
460    private boolean requestProbe(State state,GUITree tree) {
461        List<JevRegistry.Observation> observations=new ArrayList<JevRegistry.Observation>();
462        for(ModelAction action:state.getActions()) {
463            if(!action.requireTarget() || action.isBack() || !action.isValid() || !action.isEnabled() || action.isUnvisited() || !action.isSaturated()
464                || !action.isResolvedAt((int)step) || action.getResolvedGUITree()!=tree || action.getResolvedNodes()==null || action.getResolvedNodes().length==0)continue;
465            List<JevRegistry.Member> members=new ArrayList<JevRegistry.Member>();List<GUITreeNode> nodes=new ArrayList<GUITreeNode>();
466            for(GUITreeNode node:action.getResolvedNodes()){members.add(JevEvidence.member(action.getType().name(),node));nodes.add(node);}
467            observations.add(new JevRegistry.Observation(state.getGraphId(),action.getGraphId(),action.getType().name(),state.getActivity(),tree,
468                action.getResolvedGUITreeAction(),nodes,true,true,members));
469        }
470        JevRegistry.Batch batch=null;List<JevRegistry.Batch> groups=Collections.emptyList();long started=nowNanos();
471        JevTransport.Result lastResponse=null;boolean anySent=false;
472        try {
473            registry.setSemanticReviewModel(providerModel);
474            groups=registry.discover(observations,step);
475            if(groups.isEmpty()) {
476                events.emit("PROBE_NO_CANDIDATES",step,registry.version(),state.getGraphId(),null,
477                    JevRegistry.fields("reason","no_reviewable_concrete"));
478                return true;
479            }
480            // Assign the page/entry identity before enforcing the wire budget so a
481            // rejected request still has a complete, auditable terminal receipt.
482            batch=registry.page(groups,Integer.MAX_VALUE,providerModel);
483            save("request-"+batch.id,JevRegistry.merge(registry.receipt(batch),JevRegistry.fields("request",new JSONObject(batch.request.bodyJson),
484                "evidence_digest",batch.request.evidenceDigest,"gui_identity",JevEvidence.guiIdentity(tree))));
485            if(registry.reuseFingerprint(batch,step)) {
486                GUITree currentTree=state.getLatestGUITree();registry.bindCurrent(batch,currentTree==null?tree:currentTree);
487                events.emit("PROBE_SYNC_RESULT",step,registry.version(),state.getGraphId(),null,
488                    JevRegistry.fields("request_id",batch.id,"reason","input_fingerprint_hit","input_fingerprint",batch.request.inputFingerprint,"wait_ns",0));
489                return true;
490            }
491            events.emit("PROBE_SYNC_REQUEST",step,registry.version(),state.getGraphId(),null,JevRegistry.fields("request_id",batch.id,
492                "candidate_count",batch.entries.size(),"request_bytes",batch.request.requestBytes,"timeout_ms",probeTimeoutMs));
493            JevProbePackets.Plan plan=JevProbePackets.plan(batch.request,maxBytes);
494            save("packet-map-"+batch.id,plan.receipt());
495            for(JevProtocol.Request packet:plan.packets)save("physical-request-"+packet.requestId,JevRegistry.fields(
496                "logical_request_id",batch.id,"physical_request_id",packet.requestId,"request_bytes",packet.requestBytes,
497                "candidate_ids",new JSONArray(packet.candidateIds),"request",new JSONObject(packet.bodyJson)));
498            // One deadline for the entire logical selection, including packet retries.
499            long networkStart=nowNanos(),deadline=networkStart+TimeUnit.MILLISECONDS.toNanos(probeTimeoutMs),firstSent=0;
500            boolean admitted=false,usageKnown=true;long inputTokens=0,outputTokens=0;
501            JSONObject answers=new JSONObject();
502            for(JevProtocol.Request packet:plan.packets) {
503                for(int attempt=1;attempt<=2;attempt++) {
504                    if(!currentWindow(tree)) {
505                        failRequest(state,batch,"STALE_GUI",started,lastResponse,anySent);
506                        throw new com.android.commands.monkey.ape.BadStateException("GUI changed during probe review");
507                    }
508                    long remaining=TimeUnit.NANOSECONDS.toMillis(deadline-nowNanos());
509                    if(remaining<=0){failRequest(state,batch,"TIMEOUT",started,lastResponse,anySent);return false;}
510                    JevTransport.Call call=probeTransport.submit(packet.requestId,packet.bodyJson,(int)remaining,Math.max(16384,maxBytes));
511                    if(call==null){failRequest(state,batch,"QUEUE_FULL",started,lastResponse,anySent);return false;}
512                    if(!admitted){registry.admitted(batch,step);admitted=true;}
513                    JevTransport.Result response=call.await();lastResponse=response;
514                    if(response.sentNanos>0) {
515                        if(!anySent){firstSent=response.sentNanos;registry.requestEvent("JEV_REQUEST_SENT",batch,step,JevRegistry.fields("physical_packet_count",plan.packets.size()));}
516                        anySent=true;
517                        registry.requestEvent("PROBE_PACKET_SENT",batch,step,JevRegistry.fields("physical_request_id",packet.requestId,"attempt",attempt,"request_bytes",packet.requestBytes));
518                    }
519                    String error=response.error;JSONObject provider=null;
520                    if(error==null)try {
521                        JevProtocol.fromProvider(response.raw,packet);provider=StrictJson.object(response.raw);
522                    }catch(JSONException invalid){error="INVALID_RESPONSE";}
523                    save("physical-response-"+packet.requestId+"-attempt-"+attempt,JevRegistry.fields(
524                        "logical_request_id",batch.id,"physical_request_id",packet.requestId,"attempt",attempt,
525                        "http_status",response.httpStatus,"sent",response.sentNanos>0,"error",error==null?JSONObject.NULL:error,
526                        "wait_ns",response.waitNanos(),"latency_ns",response.latencyNanos(),
527                        "raw_response",response.raw==null?JSONObject.NULL:response.raw));
528                    registry.requestEvent("PROBE_PACKET_RESULT",batch,step,JevRegistry.fields("physical_request_id",packet.requestId,"attempt",attempt,
529                        "reason",error==null?"success":error,"wait_ns",response.waitNanos()));
530                    if(error==null) {
531                        JSONObject received=provider.getJSONObject("answers");
532                        for(String id:packet.candidateIds) {
533                            if(answers.has(id))throw new JSONException("duplicate packet result");
534                            answers.put(id,received.get(id));
535                        }
536                        JSONObject usage=provider.optJSONObject("usage");
537                        if(usage==null || !usage.has("input_tokens") || !usage.has("output_tokens"))usageKnown=false;
538                        else {inputTokens+=usage.getLong("input_tokens");outputTokens+=usage.getLong("output_tokens");}
539                        break;
540                    }
541                    if(attempt<2 && retryablePacket(error) && nowNanos()<deadline)continue;
542                    failRequest(state,batch,error,started,response,anySent);return false;
543                }
544            }
545            JSONObject raw=JevRegistry.fields("model",providerModel,"answers",answers);
546            if(usageKnown)raw.put("usage",JevRegistry.fields("input_tokens",inputTokens,"output_tokens",outputTokens));
547            JevProtocol.Result result=JevProtocol.fromProvider(raw.toString(),batch.request);
548            long latency=firstSent==0?0:nowNanos()-firstSent,wait=nowNanos()-networkStart;
549            save("response-"+batch.id,JevRegistry.fields("request_id",batch.id,"abstraction_version",batch.version,
550                "evidence_digest",batch.request.evidenceDigest,"latency_ns",latency,"wait_ns",wait,
551                "physical_packet_count",plan.packets.size(),"raw_response",raw,"response",result.toJson()));
552            registry.requestEvent("JEV_RESULT_RECEIVED",batch,step,JevRegistry.fields("latency_ns",latency,"wait_ns",wait));
553            if(!currentWindow(tree)) {
554                failRequest(state,batch,"STALE_GUI",started,lastResponse,anySent);
555                throw new com.android.commands.monkey.ape.BadStateException("GUI changed during probe review");
556            }
557            registry.apply(batch,raw.toString(),step);
558            GUITree currentTree=state.getLatestGUITree();registry.bindCurrent(batch,currentTree==null?tree:currentTree);
559            events.emit("PROBE_SYNC_RESULT",step,registry.version(),state.getGraphId(),null,JevRegistry.fields("request_id",batch.id,"reason","success","wait_ns",nowNanos()-started));
560            return true;
561        }catch(JSONException error){
562            events.emit("PROBE_REQUEST_ERROR",step,registry.version(),state.getGraphId(),null,JevRegistry.fields(
563                "exception",error.getClass().getSimpleName(),"message",error.getMessage(),
564                "request_bytes",error instanceof JevProtocol.BudgetExceeded?((JevProtocol.BudgetExceeded)error).request.requestBytes:JSONObject.NULL));
565            failRequest(state,batch,error instanceof JevProtocol.BudgetExceeded?"REQUEST_BUDGET_EXCEEDED":"INVALID_RESPONSE_OR_REQUEST",started,lastResponse,anySent);
566        }
567        catch(IOException error){receiptFailed(error);failRequest(state,batch,"RECEIPT_FAILED",started,lastResponse,anySent);}
568        if(batch==null)for(JevRegistry.Batch group:groups)registry.deliveryFailed(group,step,"REQUEST_CONSTRUCTION_FAILED");
569        return false;
570    }
571    private static boolean retryablePacket(String error) {
572        if("TRANSPORT_FAILURE".equals(error) || "TIMEOUT".equals(error) || "HTTP_408".equals(error) || "HTTP_429".equals(error))return true;
573        return error!=null && error.matches("HTTP_5[0-9][0-9]");
574    }
575    private void failRequest(State state,JevRegistry.Batch batch,String error,long started,JevTransport.Result response,boolean anySent) {
576        if(batch!=null){registry.deliveryFailed(batch,step,error);registry.requestEvent("JEV_REQUEST_FAILED",batch,step,JevRegistry.fields("reason",error,"latency_ns",nowNanos()-started));}
577        events.emit("PROBE_SYNC_RESULT",step,registry.version(),state.getGraphId(),null,
578            JevRegistry.fields("request_id",batch==null?JSONObject.NULL:batch.id,"reason",error,"wait_ns",nowNanos()-started));
579        try {if(batch!=null)save("failure-"+batch.id,JevRegistry.fields("request_id",batch.id,"error",error,"wait_ns",nowNanos()-started,
580            "http_status",response==null?0:response.httpStatus,"sent",anySent));}
581        catch(IOException failure){receiptFailed(failure);}
582    }
583    /** One native navigation's receipt, never a queue of targets. The Ledger
584     * alone owns pending decisions and their one-arrival navigation guard. */
585    private List<JevReviewLedger.PendingProbe> navigatingProbes=Collections.emptyList();
586    private State probeNavigationTarget;
587    private boolean probeNavigationGlobal;
588    private List<JevReviewLedger.PendingProbe> eligibleNavigationProbes(State state,boolean diagnose) {
589        List<JevReviewLedger.PendingProbe> eligible=new ArrayList<JevReviewLedger.PendingProbe>();
590        if(!probeEnabled || observeOnly || semanticReview==null || lastModel==null || state==null)return eligible;
591        Collection<State> live=lastModel.getGraph().getStates();
592        for(JevReviewLedger.PendingProbe probe:semanticReview.pendingNavigationProbes(state)) {
593            JevReviewLedger.Decision d=probe.decision;GUITreeAction source=probe.source;
594            boolean accepted=JevHistoryPairwiseProtocol.PROMOTE.equals(d.representativeModelActionId)
595                && d.requestId!=null && d.candidateId!=null && d.inputFingerprint!=null;
596            boolean sourceValid=accepted && JevHistoryAudit.mappingProblem(source,live)==null
597                && source.getGUITree().getCurrentState()==state && source.getGUITreeNode()!=null
598                && probe.identity.equals(ConcreteActionIdentity.of(source.getActionType().name(),source.getGUITreeNode()))
599                && JevRegistry.correspondingNode(source,source.getGUITree(),source.getModelAction())==source.getGUITreeNode();
600            boolean exact=sourceValid && registry.hasExactExecution(state,source.getModelAction(),source.getGUITree(),source.getGUITreeNode());
601            boolean related=sourceValid && registry.hasRelatedConcreteExecution(probe.identity.actionType(),state.getActivity(),probe.identity.canonicalFullName());
602            // PROMOTE has already passed the semantic pair trust gate. Remote
603            // navigation keeps only identity and confirmed-execution guards;
604            // an independent confidence cutoff here rejected valid probes.
605            boolean confidencePass=true;
606            String reason=!sourceValid?"source_invalid":exact?"same_state_confirmed":
607                related?"related_concrete_confirmed":probe.navigationArrived?"already_navigated_to_source":"eligible";
608            if("eligible".equals(reason))eligible.add(probe);
609            if(diagnose)events.emit("PROBE_NAVIGATION_ELIGIBILITY",step,registry.version(),state.getGraphId(),
610                source==null||source.getModelAction()==null?null:source.getModelAction().getGraphId(),
611                JevRegistry.merge(probeNavigationFields(probe),JevRegistry.fields("reason",reason,"navigation", "eligible".equals(reason),
612                    "confidence_pass",confidencePass,"source_valid",sourceValid,"same_state_confirmed",exact,
613                    "related_execution_suppressed",related,"navigation_arrived",probe.navigationArrived)));
614        }
615        return eligible;
616    }
617    private JSONObject probeNavigationFields(JevReviewLedger.PendingProbe probe) {
618        JevReviewLedger.Decision d=probe.decision;
619        return JevRegistry.fields("entry_id",d.requestId+"-"+d.candidateId,"request_id",d.requestId,"candidate_id",d.candidateId,
620                "concrete_identity",JevEvidence.digest(probe.identity.toString()),"confidence",d.confidence!=null&&Double.isFinite(d.confidence)?d.confidence:JSONObject.NULL,
621                "path_id",activePath);
622    }
623    /** Called by the native Remote Greedy endpoint filter, within its one search. */
624    public boolean pendingProbeNavigationCandidate(State state) {
625        List<JevReviewLedger.PendingProbe> probes=eligibleNavigationProbes(state,true);
626        if(probes.isEmpty())return false;
627        JSONArray entries=new JSONArray();for(JevReviewLedger.PendingProbe p:probes)entries.put(p.decision.requestId+"-"+p.decision.candidateId);
628        events.emit("PROBE_NAVIGATION_CANDIDATE",step,registry.version(),state.getGraphId(),null,
629            JevRegistry.fields("probe_entries",entries.toString(),"candidate_count",probes.size()));
630        return true;
631    }
632    public void probeNavigationSelected(State target,long timestamp,boolean global) {
633        finishProbeNavigation(null,timestamp,"replaced");
634        navigatingProbes=eligibleNavigationProbes(target,false);
635        if(navigatingProbes.isEmpty())return;
636        probeNavigationTarget=target;probeNavigationGlobal=global;
637        for(JevReviewLedger.PendingProbe p:navigatingProbes)
638            events.emit("PROBE_NAVIGATION_SELECTED",timestamp,registry.version(),target.getGraphId(),p.source.getModelAction().getGraphId(),
639                JevRegistry.merge(probeNavigationFields(p),JevRegistry.fields("source",global?"native_global_action":"native_remote_path")));
640    }
641    private void finishProbeNavigation(State actual,long timestamp,String reason) {
642        boolean arrived=actual!=null && actual==probeNavigationTarget;
643        for(JevReviewLedger.PendingProbe p:navigatingProbes) {
644            if(arrived)registry.reviewLedger().navigationArrived(p);
645            String availability="not_arrived";JSONObject binding=new JSONObject();
646            if(arrived) {
647                binding=JevRegistry.bindingDiagnostic(p.source,actual.getLatestGUITree(),p.source.getModelAction());
648                GUITreeNode node=JevRegistry.correspondingNode(p.source,actual.getLatestGUITree(),p.source.getModelAction());
649                availability=node==null?"node_absent_or_ambiguous":!node.isEnabled()?"node_disabled":
650                    !ActionFilter.ENABLED_VALID.include(p.source.getModelAction())?"model_action_unavailable":"awaiting_native_binding";
651            }
652            JSONObject local=evidenceAudit.navigation(ape,arrived?actual.getLatestGUITree():p.source.getGUITree(),timestamp,registry.version(),
653                JevRegistry.merge(probeNavigationFields(p),JevRegistry.fields("stage",arrived?"arrival":"navigation_ended","reason",reason,"binding",binding,
654                    "original_concrete_identity",p.identity.toString(),"source_review_path","jev-audit/reviews/"+p.decision.requestId+".json")),arrived);
655            events.emit(arrived?"PROBE_NAVIGATION_ARRIVED":"PROBE_NAVIGATION_ENDED",timestamp,registry.version(),
656                probeNavigationTarget.getGraphId(),p.source.getModelAction()==null?null:p.source.getModelAction().getGraphId(),
657                JevRegistry.merge(probeNavigationFields(p),JevRegistry.fields("arrived",arrived,"reason",reason,"last_availability",availability,
658                    "binding_reason",binding.optString("reason","not_arrived"),"source_match_count",binding.optInt("source_match_count",-1),
659                    "target_match_count",binding.optInt("target_match_count",-1),"audit_path",local.opt("path"))));
660        }
661        navigatingProbes=Collections.emptyList();probeNavigationTarget=null;probeNavigationGlobal=false;
662    }
663    private long pathSequence,pathStartNanos,pathStartStep;
664    private String activePath,navigationTarget;
665    private boolean navigationProbeOnly;
666    public void navigationStarted(Subsequence path,long timestamp) {
667        navigationEnded(null,timestamp,"replaced");activePath="p"+(++pathSequence);pathStartNanos=ApeSnapshot.nowNanos();pathStartStep=timestamp;
668        State target=path.getLastState();navigationTarget=target.getGraphId();int unsaturated=0;
669        for(ModelAction action:target.getActions())if(ActionFilter.ENABLED_VALID.include(action) && !action.isSaturated())unsaturated++;
670        JSONArray entries=new JSONArray();Set<ModelAction> saturatedProbes=new HashSet<ModelAction>();
671        for(JevReviewLedger.PendingProbe p:eligibleNavigationProbes(target,false)) {
672            entries.put(p.decision.requestId+"-"+p.decision.candidateId);
673            if(p.source.getModelAction().isSaturated())saturatedProbes.add(p.source.getModelAction());
674        }
675        navigationProbeOnly=unsaturated==0 && !saturatedProbes.isEmpty();
676        if(entries.length()>0) {
677            JSONArray edges=new JSONArray();for(StateTransition edge:path.getEdges())edges.put(JevRegistry.fields("edge_id",edge.getGraphId(),
678                "source_state",edge.getSource().getGraphId(),"target_state",edge.getTarget().getGraphId(),"model_action_id",edge.getAction().getGraphId()));
679            evidenceAudit.save("jev-audit/navigation/path-"+activePath+".json",JevRegistry.fields("kind","native_bfs_selected_path","path_id",activePath,
680                "step",timestamp,"abstraction_version",registry.version(),"probe_entries",entries,"edges",edges));
681        }
682        events.emit("NAVIGATION_STARTED",timestamp,registry.version(),target.getGraphId(),null,JevRegistry.fields("path_id",activePath,
683            "source_state",path.getFirstAction().getState().getGraphId(),"path_length",path.size(),"native_unsaturated_actions",unsaturated,
684            "probe_only_actions",saturatedProbes.size(),"probe_entries",entries.toString()));
685    }
686    public void navigationEnded(State actual,long timestamp,String reason) {
687        finishProbeNavigation(actual,timestamp,reason);
688        if(activePath==null)return;
689        events.emit("NAVIGATION_ENDED",timestamp,registry.version(),actual==null?null:actual.getGraphId(),null,JevRegistry.fields("path_id",activePath,
690            "reason",reason,"elapsed_steps",timestamp-pathStartStep,"elapsed_ns",ApeSnapshot.nowNanos()-pathStartNanos,
691            "arrived",actual!=null && actual.getGraphId().equals(navigationTarget),"probe_only",navigationProbeOnly));
692        activePath=null;navigationTarget=null;
693    }
694    private void entryEvent(String name,JevRegistry.Entry entry,JSONObject fields) {
695        events.emit(name,step,entry.version,entry.state,entry.action,JevRegistry.merge(fields,JevRegistry.fields("entry_id",entry.id,"request_id",entry.requestId)));
696    }
697    public void directTextInput(Action action,String text,boolean sent) {
698        if(action instanceof ModelAction)evidenceAudit.directText(((ModelAction)action).getResolvedGUITreeAction(),text,sent,step,registry.version());
699    }
700    public void beforeInput(Action action){
701        if(!(action instanceof ModelAction))evidenceAudit.beforeOtherInput(action,step,registry.version());
702        boolean auditProbe=action instanceof ModelAction && (selected!=null || semanticSelected)
703            && ((ModelAction)action).getResolvedGUITreeAction()==selectedAction;
704        if(auditProbe) {
705            ModelAction modelAction=(ModelAction)action;
706            probeSourceScreenshot=captureProbeScreenshot(modelAction.getResolvedGUITree(),modelAction,modelAction.getResolvedNode(),"source");
707        }
708        if(action instanceof ModelAction)evidenceAudit.beforeInput(ape,(ModelAction)action,step,registry.version(),auditProbe,
709            auditProbe?probeSourceScreenshot:null,auditProbe?(selected!=null?selected.id:semanticEntryId):null,
710            auditProbe?(selected!=null?selected.requestId:semanticRequestId):null,auditProbe?semanticCandidateId:null);
711        if(regionContextAudit && action instanceof ModelAction) {
712            ModelAction modelAction=(ModelAction)action;GUITreeAction concrete=modelAction.getResolvedGUITreeAction();
713            GUITree tree=modelAction.getResolvedGUITree();GUITreeNode node=modelAction.getResolvedNode();
714            if(concrete!=null && tree!=null) {
715                String id="ctx-exec-"+(++executionContextSequence);
716                JSONObject snapshot=contextSnapshot(tree,modelAction.getState(),modelAction,node);
717                executionContexts.put(concrete,new ExecutionContext(id,step,snapshot));
718                JSONObject receipt=new JSONObject();
719                try {receipt.put("schema",1).put("kind","execution_source_context").put("context_id",id).put("step",step)
720                    .put("snapshot",snapshot);} catch(JSONException error){throw new IllegalStateException(error);}
721                saveDiagnostic("region-context-execution-"+executionContextSequence,receipt);
722                events.emit("REGION_CONTEXT_CAPTURED",step,registry.version(),modelAction.getState().getGraphId(),modelAction.getGraphId(),
723                    JevRegistry.fields("context_snapshot_id",id,"region_context_audit",true));
724            }
725        }
726        if(region!=null)region.beforeInput(action);
727    }
728    public boolean gate(ModelAction action,GUITree tree,String strategy) {
729        if(region==null)return true;
730        boolean allow=region.gate(action,tree,strategy,step);
731        if(!allow && selected!=null && action.getResolvedGUITreeAction()==selectedAction)selectionFailed("region_gate_skipped");
732        return allow;
733    }
734    public void eventsGenerated(Action action,List<? extends MonkeyEvent> queue,int first) {
735        evidenceAudit.generated(action,queue,first,step,registry.version());
736        if(!(action instanceof ModelAction))return;
737        GUITreeAction concrete=((ModelAction)action).getResolvedGUITreeAction();
738        InputTrace trace=concrete==selectedAction
739            ?(semanticSelected?new InputTrace(null,concrete,semanticEntryId,semanticRequestId,semanticCandidateId):new InputTrace(selected,concrete))
740            :new InputTrace(null,concrete);int index=0;
741        for(MonkeyEvent event:queue) {
742            if(index++<first)continue;
743            int type=event.getEventType();
744            if(type==MonkeyEvent.EVENT_TYPE_KEY || type==MonkeyEvent.EVENT_TYPE_TOUCH || type==MonkeyEvent.EVENT_TYPE_TRACKBALL){inputEvents.put(event,trace);trace.total++;}
745        }
746        if(trace.entry!=null){selectedInput=trace;entryEvent("PROBE_INPUT_GENERATED",trace.entry,JevRegistry.fields("event_count",trace.total));}
747        else if(trace.semanticEntryId!=null){selectedInput=trace;events.emit("PROBE_INPUT_GENERATED",step,registry.version(),null,null,JevRegistry.fields(
748            "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId,"event_count",trace.total));}
749        evidenceAudit.input(trace.action,step,registry.version(),"generated",trace.total,0,0,0);
750        if(trace.total==0)inputSummary(trace);
751    }
752    public void eventInjected(MonkeyEvent event,int code){inputResult(event,code,false);}
753    public void eventsDiscarded(List<? extends MonkeyEvent> queue){for(MonkeyEvent event:queue)inputResult(event,0,true);}
754    private void inputResult(MonkeyEvent event,int code,boolean discarded) {
755        evidenceAudit.injected(event,code,discarded,step,registry.version());
756        InputTrace trace=inputEvents.remove(event);if(trace==null)return;
757        if(discarded)trace.discarded++;else if(code==MonkeyEvent.INJECT_SUCCESS)trace.success++;else trace.failed++;
758        if(trace.entry!=null && !discarded && code!=MonkeyEvent.INJECT_SUCCESS)entryEvent("PROBE_INPUT_ERROR",trace.entry,JevRegistry.fields("inject_code",code,"event_type",event.getEventType()));
759        else if(trace.semanticEntryId!=null && !discarded && code!=MonkeyEvent.INJECT_SUCCESS)events.emit("PROBE_INPUT_ERROR",step,registry.version(),null,null,JevRegistry.fields(
760            "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId,"inject_code",code,"event_type",event.getEventType()));
761        if(discarded || code!=MonkeyEvent.INJECT_SUCCESS)evidenceAudit.input(trace.action,step,registry.version(),discarded?"discarded":"injection_failed",trace.total,trace.success,trace.failed,trace.discarded);
762        if(trace.remaining()==0)inputSummary(trace);
763    }
764    private void inputSummary(InputTrace trace) {
765        evidenceAudit.input(trace.action,step,registry.version(),trace.phase(),trace.total,trace.success,trace.failed,trace.discarded);
766        boolean added=false;
767        if(trace.confirmed()){
768            added=confirmedInputs.add(trace.action);confirmedInputBranches.put(trace.action,trace.branch);
769            if(semanticReview!=null && trace.action!=null && trace.action.getGUITree()!=null)
770                semanticReview.markExecuted(trace.action.getGUITree().getCurrentState(),trace.action.getModelAction(),trace.action.getGUITreeNode());
771        }
772        if(Config.getBoolean("ape.jev.historyAudit",false)) {
773            if(trace.confirmed())diagnosticConfirmedSummaries++;
774            GUITree tree=trace.action==null?null:trace.action.getGUITree();
775            ModelAction action=trace.action==null?null:trace.action.getModelAction();
776            events.emit("HISTORY_INPUT_SUMMARY",step,registry.version(),action==null?null:action.getState().getGraphId(),
777                action==null?null:action.getGraphId(),JevRegistry.fields("input_confirmed",trace.confirmed(),
778                "new_confirmation_object",added,"confirmation_set_objects",confirmedInputs.size(),
779                "confirmed_summary_events",diagnosticConfirmedSummaries,"event_count",trace.total,
780                "injected_success",trace.success,"injected_failed",trace.failed,"discarded",trace.discarded,
781                "unreported",trace.remaining(),"source_tree",tree==null?JSONObject.NULL:tree.getId(),
782                "source_gui_timestamp",tree==null?JSONObject.NULL:tree.getTimestamp()));
783        }
784        if(trace.entry==null && trace.semanticEntryId==null)return;
785        JSONObject fields=JevRegistry.fields("input_phase",trace.phase(),"event_count",trace.total,
786            "injected_success",trace.success,"injected_failed",trace.failed,"discarded",trace.discarded,"unreported",trace.remaining());
787        if(trace.entry!=null)entryEvent("PROBE_INPUT_COMPLETED",trace.entry,fields);
788        else events.emit("PROBE_INPUT_COMPLETED",step,registry.version(),null,null,JevRegistry.merge(fields,JevRegistry.fields(
789            "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId)));
790    }
791    public void selectionFailed(String reason) {
792        if(region!=null)region.interrupted(reason);
793        if(selected==null && !semanticSelected)return;
794        probeSourceScreenshot=null;
795        if(selected==null) {
796            ModelAction action=selectedAction==null?null:selectedAction.getModelAction();
797            if(action!=null && action.getResolvedGUITreeAction()==selectedAction && action.isResolvedAt((int)step)
798                    && selectedVersion==registry.version()) {
799                action.resolveAt((int)step,action.getThrottle(),selectedAction.getGUITree(),originalResolvedNode,action.getResolvedNodes());
800                action.setResolvedGUITreeAction(originalResolvedAction);
801            }
802            if(semanticEntryId!=null)events.emit("PROBE_EXECUTION_FAILED",step,registry.version(),action==null?null:action.getState().getGraphId(),action==null?null:action.getGraphId(),JevRegistry.fields(
803                "entry_id",semanticEntryId,"request_id",semanticRequestId,"candidate_id",semanticCandidateId,"reason",reason));
804            semanticSelected=false;selectedAction=null;originalResolvedNode=null;originalResolvedAction=null;
805            semanticEntryId=null;semanticRequestId=null;semanticCandidateId=null;semanticConfidence=0;semanticChoice=null;
806            return;
807        }
808        entryEvent("PROBE_INTERRUPTED",selected,JevRegistry.fields("reason",reason,"input_phase",selectedInput==null?"selected":selectedInput.phase()));
809        registry.failed(selected,step,reason);
810        ModelAction action=selectedAction.getModelAction();
811        if(action.getResolvedGUITreeAction()==selectedAction && action.getResolvedGUITree()==selectedAction.getGUITree()
812                && action.isResolvedAt((int)step) && selectedVersion==registry.version()) {
813            action.resolveAt((int)step,action.getThrottle(),selectedAction.getGUITree(),originalResolvedNode,action.getResolvedNodes());
814            action.setResolvedGUITreeAction(originalResolvedAction);
815        }
816        selected=null;selectedAction=null;selectedInput=null;originalResolvedNode=null;originalResolvedAction=null;
817        semanticSelected=false;
818    }
819    public void recordCrash(String process,int pid,String shortMsg,String longMsg,long timestamp,String stack) {
820        evidenceAudit.crash(ape,process,pid,shortMsg,longMsg,timestamp,stack,step,registry.version());
821    }
822    JSONObject performance(){return timing.snapshot();}
823    void resetPerformance(){timing.reset();}
824    public void close() {
825        if(closed)return;
826        if(semanticReview!=null && semanticReview.isSynchronous() && !semanticReview.isHealthy())decisionHealthy=false;
827        finishSemanticTiming(null,null,"run_closed_without_selection");
828        if(region!=null)region.close();
829        navigationEnded(null,step,"run_closed");selectionFailed("run_ended_without_native_transition");endProbeSelection();
830        if(probeTransport!=null)probeTransport.close();
831        if(semanticReview!=null)semanticReview.close();
832        if(regionTransport!=null)regionTransport.close();
833        for(InputTrace trace:new HashSet<InputTrace>(inputEvents.values()))inputSummary(trace);inputEvents.clear();
834        output.drain();recordFinalStateTelemetry();writeGraphSnapshot();registry.finishAccounting(step);registry.counters(step);
835        output.drain();closed=true;timing.count("output_queue_peak",output.peak());
836        try{output.save(new File(graphSnapshotFile.getParentFile(),"jev-performance.json"),timing.snapshot(),true);}catch(IOException error){output.fail(error);}
837        events.emit("RUN_FINISHED",step,registry.version(),null,null,JevRegistry.fields("run_id",runId,"jev_enabled",!observeOnly,
838            "probe_enabled",probeEnabled,"region_enabled",region!=null,"probe_pending",selected==null?0:1,
839            "decision_healthy",decisionHealthy && events.isHealthy(),"evidence_healthy",output.isHealthy(),
840            "final_state_count",lastModel==null?JSONObject.NULL:lastModel.getGraph().getStates().size(),"elapsed_ns",ApeSnapshot.nowNanos()-startNanos,"native_model_actions",transitions));
841        evidenceAudit.close();events.close();output.close();
842        File completion=new File(graphSnapshotFile.getParentFile(),"jev-evidence-complete.json");
843        try {Writer writer=new OutputStreamWriter(new FileOutputStream(completion),"UTF-8");
844            try{writer.write(JevRegistry.fields("healthy",output.isHealthy() && events.isHealthy(),"output_accounting",output.accounting(),"step",step,"abstraction_version",registry.version(),
845                "wall_time_ms",System.currentTimeMillis(),"monotonic_ns",nowNanos()).toString());writer.write('\n');}finally{writer.close();}}
846        catch(IOException error){output.fail(error);}
847    }
848    public void recordSelectionDecision(String strategy,State state,Action selected,int bufferAfter) {
849        finishSemanticTiming(state,selected,selected==null?"no_native_action":"native_action_selected");
850        if(historyAuditSelectionStart!=0) {
851            if(historyAuditSelectionStep==step)timing.record("history_audit_post_prepare_to_selection",System.nanoTime()-historyAuditSelectionStart);
852            historyAuditSelectionStart=0;
853        }
854        if(state==null)return;
855        JSONObject fields=stateTelemetry(state,"selection");
856        JevRegistry.merge(fields,JevRegistry.fields("selection_strategy",strategy==null?"unknown":strategy,
857            "selection_context",activePath!=null?"navigation":"exploration",
858            "selection_buffer_size_after",bufferAfter));
859        if(selected instanceof ModelAction) {
860            ModelAction action=(ModelAction)selected;
861            JevRegistry.merge(fields,JevRegistry.fields("selected_model_action_type",action.getType().name(),
862                "selected_model_action_saturated",action.isSaturated(),"selected_model_action_unvisited",action.isUnvisited(),
863
864                "selected_effective_unvisited",action.isUnvisited(),"selected_navigation_target",!action.isSaturated(),
865                "selected_model_action_visits",action.getVisitedCount(),"selected_action_fingerprint",action.getActionFingerprint(),
866                "selected_resolved_node_count",action.getResolvedNodes()==null?0:action.getResolvedNodes().length));
867            events.emit("SELECTION_DECISION",step,registry.version(),state.getGraphId(),action.getGraphId(),fields);
868            return;
869        }
870        events.emit("SELECTION_DECISION",step,registry.version(),state.getGraphId(),null,fields);
871    }
872
873    private void finishSemanticTiming(State state,Action selected,String reason) {
874        if(semanticReviewStart==0)return;
875        long end=System.nanoTime(),prepared=semanticPrepareEnd==0?end:semanticPrepareEnd;
876        long evidence=semanticReview.localEvidenceNanos(),wait=semanticReview.providerWaitNanos();
877        long prepare=prepared-semanticReviewStart;
878        events.emit("SEMANTIC_REVIEW_TIMING",semanticReviewStep,registry.version(),state==null?null:state.getGraphId(),
879            selected instanceof ModelAction?((ModelAction)selected).getGraphId():null,
880            JevRegistry.fields("review_id",semanticReviewId,"reason",reason,"elapsed_ns",end-semanticReviewStart,
881                "prepare_ns",prepare,"local_evidence_ns",evidence,"provider_wait_ns",wait,
882                "other_prepare_ns",prepare-evidence-wait,"post_prepare_selection_ns",end-prepared));
883        timing.record("semantic_review_to_native_selection",end-semanticReviewStart);
884        semanticReviewStart=0;semanticReviewId=null;
885    }
886
887    private void recordFinalStateTelemetry() {
888        if(lastModel==null)return;
889        int batch=0;
890        for(State state:lastModel.getGraph().getStates()) {
891            events.emit("STATE_FINAL",step,registry.version(),state.getGraphId(),null,stateTelemetry(state,"close"));
892            if(++batch%32==0)output.drain();
893            for(ModelAction action:state.getActions()) {
894                events.emit("MODEL_ACTION_FINAL",step,registry.version(),state.getGraphId(),action.getGraphId(),
895                    JevRegistry.fields("snapshot_reason","close","model_action_type",action.getType().name(),
896                        "model_action_visits",action.getVisitedCount(),"model_action_unvisited",action.isUnvisited(),
897                        "effective_model_action_unvisited",action.isUnvisited(),"navigation_target",!action.isSaturated(),
898                        "model_action_saturated",action.isSaturated(),"native_model_action_saturated",action.isSaturated(),"resolved_node_count",
899                        action.getResolvedNodes()==null?0:action.getResolvedNodes().length,
900                        "action_fingerprint",action.getActionFingerprint()));
901                if(++batch%32==0)output.drain();
902            }
903        }
904        output.drain();
905    }
906
907    private void writeGraphSnapshot() {
908        if(lastModel==null)return;
909        try {
910            Graph graph=lastModel.getGraph();JSONObject snapshot=new JSONObject();
911            snapshot.put("event","graph_snapshot").put("abstraction_version",registry.version()).put("graph_step",step);
912            JSONArray roots=new JSONArray();for(State state:graph.getEntryStates())roots.put(state.getGraphId());snapshot.put("roots",roots);
913            JSONArray states=new JSONArray(),edges=new JSONArray();
914            Set<StateTransition> allEdges=new HashSet<StateTransition>();
915            for(State state:graph.getStates()) {
916                JSONObject item=stateTelemetry(state,"close");item.put("id",state.getGraphId());
917                item.put("activity",String.valueOf(state.getActivity()));item.put("visits",state.getVisitedCount());
918                item.put("first_seen",state.getFirstVisitedTimestamp());item.put("last_seen",state.getLastVisitedTimestamp());
919                JSONArray shots=new JSONArray();
920                String captured=stateScreenshotPaths.get(state.getGraphId());
921                if(captured!=null)shots.put(captured);
922                else for(GUITree tree:state.getGUITrees()) {String path=relativeImagePath(tree.getJevImage());if(path!=null){shots.put(path);break;}}
923                item.put("screenshot_paths",shots).put("screenshot_status",shots.length()>0?"saved":screenshotFailureReasons.containsKey(state.getGraphId())?
924                    screenshotFailureReasons.get(state.getGraphId()):"not_observed_at_safe_point")
925                    .put("screenshot_failure_count",screenshotFailures.containsKey(state.getGraphId())?screenshotFailures.get(state.getGraphId()):0);
926                item.put("model_actions",graphActionDetails(state));states.put(item);
927                allEdges.addAll(graph.getOutStateTransitions(state));
928            }
929            for(StateTransition edge:allEdges) {
930                JSONObject item=new JSONObject();item.put("id",edge.getGraphId()).put("source",edge.getSource().getGraphId())
931                    .put("target",edge.getTarget().getGraphId()).put("action",edge.getAction().getGraphId())
932                    .put("action_fingerprint",edge.getAction().getActionFingerprint()).put("target_activity",String.valueOf(edge.getTarget().getActivity()))
933                    .put("type",String.valueOf(edge.getType())).put("visits",edge.getVisitedCount()).put("is_circle",edge.isCircle())
934                    .put("first_seen",edge.getFirstVisitedTimestamp()).put("last_seen",edge.getLastVisitedTimestamp());edges.put(item);
935            }
936            snapshot.put("states",states).put("edges",edges);
937            output.save(graphSnapshotFile,snapshot,false);
938        } catch(Exception e) {output.fail(e);}
939    }
940
941    private JSONArray graphActionDetails(State state) throws JSONException {
942        JSONArray actions=new JSONArray();
943        for(ModelAction action:state.getActions()) {
944            JSONObject item=new JSONObject().put("id",action.getGraphId()).put("type",action.getType().name())
945                .put("visits",action.getVisitedCount()).put("unvisited",action.isUnvisited())
946                .put("effective_unvisited",action.isUnvisited()).put("navigation_target",!action.isSaturated())
947                .put("saturated",action.isSaturated()).put("native_saturated",action.isSaturated())
948                .put("fingerprint",action.getActionFingerprint());
949            JSONArray concrete=new JSONArray();
950            if(!action.requireTarget())concrete.put(graphConcreteDetails(action,null));
951            else if(action.getResolvedNodes()!=null)for(GUITreeNode node:action.getResolvedNodes())concrete.put(graphConcreteDetails(action,node));
952            item.put("concrete_actions",concrete);actions.put(item);
953        }
954        return actions;
955    }
956
957    private JSONObject graphConcreteDetails(ModelAction action,GUITreeNode node) throws JSONException {
958        JSONObject item=new JSONObject().put("key",action.getGraphId());if(node==null)return item;
959        Rect bounds=node.getBoundsInScreen();JSONArray rect=new JSONArray().put(bounds.left).put(bounds.top).put(bounds.right).put(bounds.bottom);
960        return item.put("key",action.getGraphId()+"|"+String.valueOf(node.getText())).put("text",String.valueOf(node.getText()))
961            .put("resource_id",String.valueOf(node.getResourceID())).put("class",String.valueOf(node.getClassName())).put("bounds",rect);
962    }
963
964    private String relativeImagePath(JevImages.Snapshot image) {
965        if(image==null || image.path==null)return null;
966        String root=graphSnapshotFile.getParentFile().getAbsolutePath(),path=image.path;
967        return path.startsWith(root+File.separator)?path.substring(root.length()+1):path;
968    }
969
970    private final Semaphore imageSlots=new Semaphore(2);
971    /** Take pixels before input or at the first observed native target. Only
972     * compression/IO is deferred; a worker never captures a later live screen. */
973    private JSONObject captureProbeScreenshot(GUITree tree,ModelAction action,GUITreeNode chosen,String side) {
974        JSONObject record=evidenceAudit.capture(ape,tree,step,registry.version(),"source".equals(side)?"before_input_generation":"first_observed_target");
975        try {
976            record=new JSONObject(record.toString());record.put("side",side);
977            if(action!=null) {
978                JSONArray members=new JSONArray();
979                if(action.getResolvedNodes()!=null)for(GUITreeNode node:action.getResolvedNodes())
980                    members.put(JevRegistry.merge(graphConcreteDetails(action,node),JevEvidenceAudit.concrete(action,node)).put("is_probe",node==chosen));
981                record.put("concrete_actions",members).put("resolved_node_count",members.length())
982                    .put("selected_concrete",JevRegistry.merge(graphConcreteDetails(action,chosen),JevEvidenceAudit.concrete(action,chosen)))
983                    .put("execution_fingerprint",action.getActionFingerprint());
984            }
985        }catch(Exception error){ /* Audit failure must not alter the Native action. */ }
986        if(!"capture_queued".equals(record.optString("status"))) {
987            timing.count("probe_screenshot_failed",1);
988            events.emit("PROBE_SCREENSHOT_FAILED",step,registry.version(),tree==null||tree.getCurrentState()==null?null:tree.getCurrentState().getGraphId(),
989                action==null?null:action.getGraphId(),JevRegistry.fields("screenshot_side",side,"reason",record.optString("status")));
990        }
991        return record;
992    }
993
994    private void screenshotFailed(State state,String reason) {
995        Integer count=screenshotFailures.get(state.getGraphId());screenshotFailures.put(state.getGraphId(),count==null?1:count+1);
996        screenshotFailureReasons.put(state.getGraphId(),reason);
997        timing.count("state_screenshot_failed",1);timing.count("state_screenshot_failed_"+reason,1);
998        events.emit("STATE_SCREENSHOT_FAILED",step,registry.version(),state.getGraphId(),null,JevRegistry.fields("reason",reason));
999    }
1000    private void captureFirstStateScreenshot(State state) {
1001        if(ape==null || state==null || stateScreenshotPaths.containsKey(state.getGraphId()) || !screenshotPending.add(state.getGraphId()))return;
1002        if(!imageSlots.tryAcquire()){screenshotPending.remove(state.getGraphId());screenshotFailed(state,"queue_full");return;}
1003        long start=System.nanoTime();Bitmap bitmap=null;
1004        try {
1005            bitmap=ape.captureBitmap();timing.record("image_capture",System.nanoTime()-start);
1006            if(bitmap==null){imageSlots.release();screenshotPending.remove(state.getGraphId());screenshotFailed(state,"capture_null");return;}
1007            final Bitmap captured=bitmap;final File file=new File(screenshotDirectory,state.getGraphId()+"-"+step+".png");
1008            boolean accepted=output.submit("image_compress_io",new JevOutput.Write(){public void run()throws Exception{
1009                try {
1010                    if(!screenshotDirectory.isDirectory() && !screenshotDirectory.mkdirs())throw new IOException("Screenshot directory unavailable");
1011                    FileOutputStream stream=new FileOutputStream(file);
1012                    try{if(!captured.compress(Bitmap.CompressFormat.PNG,100,stream))throw new IOException("PNG compression failed");}finally{stream.close();}
1013                    stateScreenshotPaths.put(state.getGraphId(),"ape-state-screenshots/"+file.getName());
1014                    timing.count("state_screenshot_saved",1);
1015                } catch(Exception error){screenshotFailed(state,"write_failed");throw error;}
1016                finally {captured.recycle();imageSlots.release();screenshotPending.remove(state.getGraphId());}
1017            }});
1018            if(!accepted){captured.recycle();imageSlots.release();screenshotPending.remove(state.getGraphId());screenshotFailed(state,"output_rejected");}
1019        } catch(Exception e){if(bitmap!=null)bitmap.recycle();imageSlots.release();screenshotPending.remove(state.getGraphId());screenshotFailed(state,"capture_exception");output.fail(e);}
1020    }
1021
1022    private JSONObject stateTelemetry(State state,String reason) {
1023        StringBuilder unvisited=new StringBuilder();
1024        int modelActions=0,enabledValid=0;
1025        int unsaturated=state.getUnsaturatedActions().size();
1026        int nativeUnsaturated=0;
1027        int unvisitedCount=0,enabledUnvisited=0;
1028        int effectiveUnvisited=0,navigation=0,local=0;
1029        for(ModelAction action:state.getActions()) {
1030            modelActions++;
1031            if(ActionFilter.ENABLED_VALID.include(action))enabledValid++;
1032            if(ActionFilter.ENABLED_VALID.include(action) && !action.isSaturated())nativeUnsaturated++;
1033            if(action.isUnvisited()) {
1034                if(unvisited.length()>0)unvisited.append(',');
1035                unvisited.append(action.getGraphId());
1036                unvisitedCount++;
1037            }
1038            if(ActionFilter.ENABLED_VALID.include(action) && action.isUnvisited())enabledUnvisited++;
1039            if(action.isUnvisited())effectiveUnvisited++;
1040            if(ActionFilter.ENABLED_VALID_UNSATURATED.include(action))navigation++;
1041        }
1042        float saturation=modelActions==0?1.0F:0.0F;
1043        for(ModelAction action:state.getActions())saturation+=action.getResolvedSaturation();
1044        if(modelActions>0)saturation/=modelActions;
1045        return JevRegistry.fields("snapshot_reason",reason,"state_visits",state.getVisitedCount(),
1046            "state_first_seen",state.getFirstVisitedTimestamp(),"state_last_seen",state.getLastVisitedTimestamp(),
1047            "state_saturated",nativeStateSaturated(state),"state_saturation",saturation,
1048            "native_state_saturated",nativeStateSaturated(state),"native_unsaturated_model_action_count",nativeUnsaturated,
1049            "model_action_count",modelActions,"enabled_valid_action_count",enabledValid,
1050            "unsaturated_model_action_count",unsaturated,"unvisited_model_action_count",unvisitedCount,
1051            "effective_unvisited_model_action_count",effectiveUnvisited,"effective_state_saturated",state.isSaturated(),
1052            "local_probe_action_count",local,"navigation_action_count",navigation,
1053            "enabled_valid_unvisited_action_count",enabledUnvisited,"unvisited_model_action_ids",unvisited.toString());
1054    }
1055
1056    private boolean nativeStateSaturated(State state) {
1057        for(ModelAction action:state.getActions())
1058            if(ActionFilter.ENABLED_VALID.include(action) && !action.isSaturated())return false;
1059        return true;
1060    }
1061}