package com.android.commands.monkey.ape.jev;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import org.json.*;
import android.graphics.Rect;
import android.graphics.Bitmap;
import com.android.commands.monkey.ape.model.*;
import com.android.commands.monkey.ape.tree.*;
import com.android.commands.monkey.ape.utils.Config;
import com.android.commands.monkey.ape.utils.ApeSnapshot;
import com.android.commands.monkey.ape.naming.NameManager;
import com.android.commands.monkey.MonkeyEvent;
import com.android.commands.monkey.MonkeySourceApe;
import com.android.commands.monkey.ape.Subsequence;
import com.android.commands.monkey.ape.ActionFilter;

/** External advice and receipts. APE alone owns its graph and exploration model. */
public final class JevRuntime {
    public interface Validator { boolean valid(ModelAction action); }
    private final JevTiming timing=new JevTiming();
    private final JevOutput output=new JevOutput(timing);
    private final JevEvents events;
    private final JevRegistry registry;
    private JevTransport probeTransport;
    private JevSemanticReview semanticReview;
    private final JevTransport regionTransport;
    private final String endpoint,token;
    private final int queueCapacity;
    private final File receipts,graphSnapshotFile,screenshotDirectory;
    private final MonkeySourceApe ape;
    private boolean observeOnly,probeEnabled,snapshotResumed;
    private final boolean regionShadowAudit,regionContextAudit;
    private final boolean semanticReviewEnabled;
    private final JevRegion region;
    private final int maxBytes,probeTimeoutMs;
    private final String providerModel;
    private final Map<String,String> stateScreenshotPaths=new ConcurrentHashMap<String,String>();
    private final Set<String> screenshotPending=Collections.newSetFromMap(new ConcurrentHashMap<String,Boolean>());
    private final Map<String,Integer> screenshotFailures=new ConcurrentHashMap<String,Integer>();
    private final Map<String,String> screenshotFailureReasons=new ConcurrentHashMap<String,String>();
    private final IdentityHashMap<GUITreeTransition,String> transitionIds=new IdentityHashMap<GUITreeTransition,String>();
    private final Set<State> knownStates=Collections.newSetFromMap(new IdentityHashMap<State,Boolean>());
    private final Set<ModelAction> knownActions=Collections.newSetFromMap(new IdentityHashMap<ModelAction,Boolean>());
    private int indexed;
    private Model lastModel;
    private long step,transitions,startNanos=ApeSnapshot.nowNanos();
    private final String runId="run-"+Long.toHexString(startNanos);
    private boolean closed,decisionHealthy=true;
    private boolean probeRequested;
    private List<JevRegistry.Entry> roundSuggestions=Collections.emptyList();
    private int suggestionIndex;
    private JevRegistry.Entry selected;
    private boolean semanticSelected;
    private String semanticEntryId,semanticRequestId,semanticCandidateId;
    private double semanticConfidence;
    private String semanticChoice;
    private long selectedVersion;
    private GUITreeAction selectedAction,originalResolvedAction;
    // Freeze at the input boundary, before APE resolves another GUI/action.
    private JSONObject probeSourceScreenshot;
    private final JevEvidenceAudit evidenceAudit;
    private GUITreeNode originalResolvedNode;
    private final IdentityHashMap<MonkeyEvent,InputTrace> inputEvents=new IdentityHashMap<MonkeyEvent,InputTrace>();
    private final Set<GUITreeAction> confirmedInputs=Collections.newSetFromMap(new IdentityHashMap<GUITreeAction,Boolean>());
    private final IdentityHashMap<GUITreeAction,String> confirmedInputBranches=new IdentityHashMap<GUITreeAction,String>();
    private final IdentityHashMap<GUITreeAction,ExecutionContext> executionContexts=new IdentityHashMap<GUITreeAction,ExecutionContext>();
    private long executionContextSequence,historyAuditSequence;
    private long historyAuditSelectionStart,historyAuditSelectionStep;
    // One timing span for the existing review/Native-selection boundary, not a scheduler.
    private long semanticReviewStart,semanticPrepareEnd,semanticReviewStep,semanticReviewSequence;
    private String semanticReviewId;
    private long diagnosticConfirmedSummaries;
    private static final class ExecutionContext {
        final String id;final long step;final JSONObject snapshot;
        ExecutionContext(String id,long step,JSONObject snapshot){this.id=id;this.step=step;this.snapshot=snapshot;}
    }
    private InputTrace selectedInput;
    private static final class InputTrace {
        final JevRegistry.Entry entry;
        final GUITreeAction action;
        final String semanticEntryId,semanticRequestId,semanticCandidateId;
        final String branch=ApeSnapshot.branch();
        int total,success,failed,discarded;
        InputTrace(JevRegistry.Entry e,GUITreeAction a){this(e,a,null,null,null);}
        InputTrace(JevRegistry.Entry e,GUITreeAction a,String entryId,String requestId,String candidateId){entry=e;action=a;semanticEntryId=entryId;semanticRequestId=requestId;semanticCandidateId=candidateId;}
        int remaining(){return total-success-failed-discarded;}
        boolean confirmed(){return total>0 && remaining()==0 && failed==0 && discarded==0;}
        String phase(){return total==0?"no_input_events":confirmed()?"input_injected":"input_incomplete";}
    }

    public static JevRuntime create(File output){return create(output,null,false);}
    public static JevRuntime create(File output,MonkeySourceApe ape){return create(output,ape,false);}
    public static JevRuntime create(File output,MonkeySourceApe ape,boolean observeOnly) {
        JevRegion.validateConfiguration(!observeOnly || Config.getBoolean("ape.jev.region_shadow_audit", false));
        try {return new JevRuntime(output,Config.get("ape.jev.endpoint",JevProtocol.DEFAULT_ENDPOINT),
            JevProvider.token(Config.get("ape.jev.provider","startlux")),Config.getInteger("ape.jev.timeoutMs",60000),
            Config.getInteger("ape.jev.maxRequestBytes",32768),Config.get("ape.jev.model",JevProtocol.DEFAULT_MODEL),ape,observeOnly);}
        catch(Exception e){throw new IllegalStateException("Decision runtime initialization failed",e);}
    }
    public JevRuntime(File output,String endpoint,String token,int timeoutMs,int maxBytes,String model) throws IOException {
        this(output,endpoint,token,timeoutMs,maxBytes,model,null,false);
    }
    private JevRuntime(File directory,String endpoint,String token,int timeoutMs,int maxBytes,String model,MonkeySourceApe ape,boolean observeOnly) throws IOException {
        boolean configuredShadowAudit=Config.getBoolean("ape.jev.region_shadow_audit", false) || Config.getBoolean("ape.jev.region_locator_shadow",false);
        JevRegion.validateConfiguration(!observeOnly || configuredShadowAudit);
        receipts=new File(directory,"jev-receipts");receipts.mkdirs();graphSnapshotFile=new File(directory,"jev-graph.json");
        screenshotDirectory=new File(directory,"ape-state-screenshots");this.ape=ape;
        observeOnly |= Config.getBoolean("ape.jev.region_locator_shadow",false)
            || !Config.getBoolean("ape.jev.probe.enabled",true) && !Config.getBoolean("ape.jev.regionBlocking.enabled",false);
        this.observeOnly=observeOnly;
        this.regionShadowAudit=configuredShadowAudit;
        this.regionContextAudit=regionShadowAudit && Config.getBoolean("ape.jev.region_context_audit",false);
        if(maxBytes<=0)throw new IllegalArgumentException("invalid probe bounds");
        // The provider contract has a single hard body limit for both sync and
        // async review. Older callers may still pass a larger profile value.
        this.maxBytes=Math.min(maxBytes,32768);providerModel=model;
        probeEnabled=!observeOnly && Config.getBoolean("ape.jev.probe.enabled",true);
        semanticReviewEnabled=probeEnabled && Config.isJevSemanticReviewEnabled();
        probeTimeoutMs=Config.getInteger("ape.jev.probeTimeoutMs",5000);
        if(probeTimeoutMs<=0)throw new IllegalArgumentException("invalid probe bounds");
        double confidence=Double.parseDouble(Config.get("ape.jev.localProbeConfidence","0.5"));
        if(confidence!=0.5)throw new IllegalArgumentException("probe confidence threshold must be 0.5");
        events=new JevEvents(new File(directory,"jev-events.jsonl"),this.output);registry=new JevRegistry(events,confidence);
        queueCapacity=Config.getInteger("ape.jev.queueCapacity",4);
        this.endpoint=endpoint;this.token=token;
        evidenceAudit=new JevEvidenceAudit(directory,runId,timing,imageSlots);
        boolean regionEnabled=!observeOnly && Config.getBoolean("ape.jev.regionBlocking.enabled",false);
        // Semantic mode owns its asynchronous page-review transport; do not start
        // the synchronous probe worker that it does not use.
        probeTransport=probeEnabled && !semanticReviewEnabled?new JevTransport(endpoint,token,queueCapacity,"probe"):null;
        if(probeTransport!=null)probeTransport.setSnapshotIdentity(ApeSnapshot.transportIdentity());
        semanticReview=semanticReviewEnabled?new JevSemanticReview(endpoint,token,queueCapacity,probeTimeoutMs,maxBytes,model,registry,new JevSemanticReview.ReceiptSink(){
            public boolean save(String name,JSONObject receipt){try{JevRuntime.this.save(name,receipt);return true;}catch(IOException error){receiptFailed(error);return false;}}
        },Config.isJevSemanticReviewSynchronous()):null;
        if(semanticReview!=null){semanticReview.setSnapshotIdentity(ApeSnapshot.transportIdentity());semanticReview.setEvidenceAudit(evidenceAudit,ape);}
        regionTransport=regionEnabled && !configuredShadowAudit && !Config.getBoolean("ape.jev.region_auto",false)?new JevTransport(endpoint,token,queueCapacity,"region"):null;
        region=(regionEnabled || regionShadowAudit)?
            new JevRegion(this,timing,events,regionTransport,maxBytes,model,timeoutMs,Config.getLong("ape.jev.regionSeed",0),regionShadowAudit):null;
        events.emit("PROBE_CONFIG",0,0,null,null,JevRegistry.fields("probe_enabled",probeEnabled,"confidence_threshold",0.5,
            "timeout_ms",probeTimeoutMs,"probe_receipt_schema",6,"semantic_review_enabled",semanticReviewEnabled,
            "semantic_review_synchronous",semanticReview!=null && semanticReview.isSynchronous(),"semantic_observability_schema",semanticReview!=null?1:0));
    }
    public JevRegistry registry(){return registry;}
    public JevRegion region(){return region;}
    public boolean isDecisionEnabled(){return !observeOnly;}
    public boolean probeEnabled(){return probeEnabled;}
    public boolean semanticReviewEnabled(){return semanticReview!=null;}
    public boolean semanticReviewSynchronous(){return semanticReview!=null && semanticReview.isSynchronous();}
    public boolean regionShadowAudit(){return regionShadowAudit;}
    /** No observation is invented to settle the last injected action: its input facts
     * remain in memory until the next real GUI observation after restoration. */
    public void snapshotDrain() {
        ApeSnapshot.requireRegionOff();
        if(closed || probeEnabled || probeTransport!=null || regionTransport!=null || region!=null
                || selected!=null || selectedInput!=null || !inputEvents.isEmpty() || !roundSuggestions.isEmpty())
            throw new IllegalStateException("Checkpoint requires idle native observation runtime");
        events.flush();output.drain();
        if(!output.isHealthy() || !events.isHealthy())throw new IllegalStateException("Checkpoint evidence incomplete");
    }
    public JSONObject snapshotEvidence() {
        return JevRegistry.fields("confirmed_input_actions",confirmedInputs.size(),"observed_transitions",transitionIds.size(),
            "indexed_history",indexed,"pending_input_events",inputEvents.size(),"pending_probe",selected!=null,
            "active_requests",(probeTransport==null?0:probeTransport.outstanding())+(regionTransport==null?0:regionTransport.outstanding()),"region_enabled",region!=null,
            "state_screenshots_saved",stateScreenshotPaths.size(),"state_screenshot_failures",new JSONObject(screenshotFailures),
            "registry",registry.historyEvidence());
    }
    public void snapshotResume(boolean live,String namespace) throws IOException {
        if(snapshotResumed)throw new IllegalStateException("Restore checkpoint before another branch");
        snapshotDrain();
        registry.requestNamespace(namespace);
        probeEnabled=live;observeOnly=!live;
        // Rebind retained concrete executions against the current APE action objects.
        // Replaying history emits no new transition and consumes no APE RNG.
        if(live && lastModel!=null) {
            registry.rebuildHistory(lastModel.getVersion(),step);
            for(GUITreeTransition transition:lastModel.getGraph().getTreeHistory())index(transition,true);
            indexed=lastModel.getGraph().getTreeHistory().size();
        }
        if(live){probeTransport=new JevTransport(endpoint,token,queueCapacity,"probe");probeTransport.setSnapshotIdentity(ApeSnapshot.transportIdentity());}
        snapshotResumed=true;
        events.emit("PROBE_CONFIG",step,registry.version(),null,null,JevRegistry.fields("probe_enabled",live,
            "confidence_threshold",0.5,"timeout_ms",probeTimeoutMs,"probe_receipt_schema",6));
    }
    boolean regionContextAuditEnabled(){return regionContextAudit;}
    JSONObject contextSnapshot(GUITree tree,State state,ModelAction action,GUITreeNode node) {
        int before=NameManager.registeredNameCount();long start=System.nanoTime();
        try{return JevEvidence.contextSnapshot(tree,state,action,node);}
        finally{timing.record("context_snapshot_build",System.nanoTime()-start);
            timing.count("context_name_registry_growth",NameManager.registeredNameCount()-before);}
    }
    long nowNanos(){return System.nanoTime();}
    private java.util.function.LongSupplier regionClock=new java.util.function.LongSupplier(){public long getAsLong(){return System.nanoTime();}};
    long regionNowNanos(){return regionClock.getAsLong();}
    void save(String name,JSONObject receipt) throws IOException {
        long start=System.nanoTime();
        try{output.save(new File(receipts,name.replace(':','-')+".json"),ApeSnapshot.identify(receipt),true);}
        finally{timing.record("receipt_enqueue",System.nanoTime()-start);}
    }
    String saveDiagnostic(String name,JSONObject receipt) {
        if(!regionContextAudit)return null;
        try {save(name,receipt);return name;}
        catch(IOException error){receiptFailed(error);return null;}
    }
    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()));}
    public void regionRoot(State state){if(region!=null)region.root(state);}
    public void beforeSelection(Model model,State state,GUITree tree,int timestamp){if(region!=null)region.safePoint(model,state,tree,timestamp);}
    public void shadowSelection(ModelAction action,GUITree tree,int timestamp,String strategy){if(region!=null && regionShadowAudit)region.auditSelection(action,tree,timestamp,strategy);}
    JevRegion.Fact explorationFact(State state) {
        GUITree tree=state.getLatestGUITree();boolean known=tree!=null && tree.getCurrentNames()!=null;
        boolean saturated=true;
        for(ModelAction action:state.getActions())if(ActionFilter.ENABLED_VALID.include(action))saturated &= action.isSaturated();
        return new JevRegion.Fact(known,saturated,true);
    }
    public void sync(Model model,long currentStep) {
        step=currentStep;lastModel=model;
        registry.confirmationSource(model,confirmedInputs);
        List<GUITreeTransition> history=model.getGraph().getTreeHistory();
        if(registry.version()!=model.getVersion()) {
            roundSuggestions=Collections.emptyList();suggestionIndex=0;
            registry.reset(model.getVersion(),step);indexed=0;
            if(region!=null)region.reset(model.getVersion());
            for(GUITreeTransition transition:history)index(transition,true);
            indexed=history.size();
            knownStates.addAll(model.getGraph().getStates());
            for(State state:model.getGraph().getStates())knownActions.addAll(state.getActions());
        }else while(indexed<history.size())index(history.get(indexed++),false);
        if(semanticReview!=null)semanticReview.setModelVersion(registry.version());
    }
    private void index(GUITreeTransition transition,boolean rebuilding) {
        boolean fresh=!transitionIds.containsKey(transition);
        String id=transitionIds.get(transition);
        if(fresh){id=transition.getCurrentStateTransition().getGraphId()+":"+(transitionIds.size()+1);transitionIds.put(transition,id);}
        ModelAction action=transition.getAction().getModelAction();
        State source=transition.getSource().getCurrentState(),target=transition.getTarget().getCurrentState();
        boolean probe=selected!=null && selectedAction==transition.getAction();
        boolean semanticProbe=semanticSelected && selectedAction==transition.getAction();
        boolean refined=probe && selectedVersion!=registry.version();
        boolean newState=!rebuilding && !knownStates.contains(target),newAction=false;
        if(!rebuilding)for(ModelAction candidate:target.getActions())if(!knownActions.contains(candidate))newAction=true;
        boolean loop=source==target;
        boolean guiUnchanged=JevEvidence.guiIdentity(transition.getSource()).equals(JevEvidence.guiIdentity(transition.getTarget()));
        boolean inputConfirmed=confirmedInputs.contains(transition.getAction());
        JSONObject probeTargetScreenshot=null;
        if(fresh && !rebuilding && inputConfirmed && (probe || semanticProbe))
            probeTargetScreenshot=captureProbeScreenshot(transition.getTarget(),null,null,"target");
        if(fresh)evidenceAudit.transition(rebuilding?null:ape,transition,id,step,registry.version(),inputConfirmed,probe||semanticProbe,probeTargetScreenshot);
        ExecutionContext executionContext=executionContexts.remove(transition.getAction());
        if(region!=null)region.transition(transition,id,(regionShadowAudit && !Config.getBoolean("ape.jev.region_locator_shadow",false)) || inputConfirmed,
            executionContext==null?null:executionContext.id,executionContext==null?null:executionContext.snapshot);
        GUITreeNode node=transition.getAction().getGUITreeNode();
        if(probeEnabled && inputConfirmed && node!=null)registry.executed(source.getGraphId(),action.getGraphId(),JevEvidence.member(action.getType().name(),node),
            transition.getAction(),transition.getTarget(),source.getActivity(),target.getGraphId(),target.getActivity(),JevEvidence.resultLabels(transition.getTarget().getRootNode()),
            id,step,newState,newAction,refined,false,!fresh,JevEvidence.resultUnsafe(transition.getTarget().getRootNode()),guiUnchanged);
        else if(fresh)events.emit("NATIVE_TRANSITION",step,registry.version(),source.getGraphId(),action.getGraphId(),
            JevRegistry.fields("transition_id",id,"new_state",newState,"new_model_action",newAction,"refinement",refined,
                "self_loop",loop,"target_state",target.getGraphId(),"gui_unchanged",guiUnchanged,"input_confirmed",inputConfirmed,
                "execution_context_id",executionContext==null?JSONObject.NULL:executionContext.id,
                "source_gui_id",transition.getSource().getId(),"target_gui_id",transition.getTarget().getId()));
        if(inputConfirmed && node!=null && semanticReview!=null)semanticReview.markExecuted(source,action,node);
        if(semanticProbe) {
            String entryId=semanticEntryId,requestId=semanticRequestId,candidateId=semanticCandidateId;double confidence=semanticConfidence;String choice=semanticChoice;
            if(inputConfirmed && !rebuilding)events.emit("PROBE_EXECUTED",step,registry.version(),source.getGraphId(),action.getGraphId(),JevRegistry.fields(
                "entry_id",entryId==null?JSONObject.NULL:entryId,"request_id",requestId==null?JSONObject.NULL:requestId,
                "candidate_id",candidateId==null?JSONObject.NULL:candidateId,"confidence",confidence,"choice",choice==null?"PROMOTE":choice,
                "transition_id",id,"target_state",target.getGraphId(),"input_confirmed",true));
            else if(!inputConfirmed)events.emit("PROBE_EXECUTION_FAILED",step,registry.version(),source.getGraphId(),action.getGraphId(),JevRegistry.fields(
                "entry_id",entryId==null?JSONObject.NULL:entryId,"request_id",requestId==null?JSONObject.NULL:requestId,
                "candidate_id",candidateId==null?JSONObject.NULL:candidateId,"reason","native_transition_without_confirmed_input"));
            semanticSelected=false;selectedAction=null;originalResolvedNode=null;originalResolvedAction=null;
            semanticEntryId=null;semanticRequestId=null;semanticCandidateId=null;semanticConfidence=0;semanticChoice=null;
        }
        if(fresh) {
            transitions++;
            JSONObject receipt=JevRegistry.fields("transition_id",id,"native_edge_id",transition.getCurrentStateTransition().getGraphId(),
                "native_history_position",transitionIds.size()-1,"source_gui_timestamp",transition.getSource().getTimestamp(),
                "target_gui_timestamp",transition.getTarget().getTimestamp(),"state_id",source.getGraphId(),"model_action_id",action.getGraphId(),
                "source_gui_id",transition.getSource().getId(),"target_gui_id",transition.getTarget().getId(),
                "abstraction_version",registry.version(),"source_activity",source.getActivity(),
                "execution_branch",confirmedInputBranches.get(transition.getAction()),"input_confirmed",inputConfirmed);
            try {
                // The immutable source snapshot already has an exclusive execution receipt.
                if(executionContext!=null)receipt.put("execution_context_id",executionContext.id);
                if(probe || semanticProbe) {
                    receipt.put("probe_source_screenshot",probeSourceScreenshot==null?JSONObject.NULL:probeSourceScreenshot);
                    receipt.put("probe_target_screenshot",probeTargetScreenshot==null?JSONObject.NULL:probeTargetScreenshot);
                }
                save("transition-"+id,receipt);
            } catch(IOException error){receiptFailed(error);
            } catch(JSONException error){throw new IllegalStateException(error);}
        }
        knownStates.add(source);knownStates.add(target);knownActions.addAll(source.getActions());knownActions.addAll(target.getActions());
        if(probe) {
            if(selectedInput!=null && selectedInput.confirmed()) {
                registry.recordProbeExecution(selected,step);
                entryEvent("PROBE_EXECUTED",selected,JevRegistry.fields("transition_id",id,"new_state",newState,"new_model_action",newAction,
                    "refinement",refined,"novelty_known",!rebuilding,"self_loop",loop,"gui_unchanged",guiUnchanged,
                    "target_state",target.getGraphId(),"input_confirmed",true));

            }else {registry.failed(selected,step,"native_transition_without_confirmed_input");}
            selected=null;selectedAction=null;selectedInput=null;originalResolvedNode=null;originalResolvedAction=null;
        }
        if(probe || semanticProbe)probeSourceScreenshot=null;
    }
    public void safePoint(Model model,State state,GUITree tree,int timestamp) {
        long started=System.nanoTime();sync(model,timestamp);
        if(probeNavigationGlobal)finishProbeNavigation(state,timestamp,"global_target_observed");
        if(selected!=null || semanticSelected)selectionFailed("no_native_transition");
        if(region!=null)region.interrupted("no_native_transition");

        captureFirstStateScreenshot(state);
        knownStates.add(state);knownActions.addAll(state.getActions());
        if(semanticReview!=null) {
            semanticReview.setStep(step);
            semanticReview.poll();
        }
        timing.record("safe_point_total",System.nanoTime()-started);
    }
    public void prepareSemanticReviews(Model model,State current,GUITree tree) {prepareSemanticReviews(model,current,tree,null);}
    public void prepareSemanticReviews(Model model,State current,GUITree tree,Validator validator) {
        if(semanticReview==null)return;
        if(semanticReviewStart!=0) {
            // An interrupted prior span must never add an unbounded lock wait before the next review.
            try(JevDeadline ignored=JevDeadline.enter(System.nanoTime())) {
                finishSemanticTiming(null,null,"review_reentered_without_selection");
            } catch(JevDeadline.Expired lost) {
                semanticReviewStart=0;semanticReviewId=null;
                output.fail(new IOException("Interrupted semantic timing receipt could not be enqueued without waiting"));
            }
        }
        semanticReviewStart=System.nanoTime();semanticPrepareEnd=0;semanticReviewStep=step;
        semanticReviewId=runId+":semantic:"+(++semanticReviewSequence);
        semanticReview.setReviewId(semanticReviewId,semanticReviewStart);
        if(Config.getBoolean("ape.jev.historyAudit",false)) {
            long began=System.nanoTime();
            try {
                JSONObject audit=JevHistoryAudit.capture(model,current,registry,confirmedInputs,transitionIds,indexed,step);
                timing.record("history_audit_enumeration_build",System.nanoTime()-began);
                audit.put("diagnostic_confirmed_summary_events",diagnosticConfirmedSummaries);
                audit.put("pending_tracked_input_events",inputEvents.size());
                long copyStart=System.nanoTime();
                save("history-audit-"+step+"-"+(++historyAuditSequence),audit);
                timing.record("history_audit_copy_enqueue",System.nanoTime()-copyStart);
            }
            catch(IOException error){receiptFailed(error);}
            catch(JSONException error){throw new IllegalStateException(error);}
            finally {timing.record("history_audit_diagnostic",System.nanoTime()-began);}
        }
        long prepareStart=System.nanoTime();
        try {semanticReview.prepare(current,tree,step,validator);}
        finally {semanticPrepareEnd=System.nanoTime();if(Config.getBoolean("ape.jev.historyAudit",false)) {
            timing.record("history_audit_semantic_prepare",System.nanoTime()-prepareStart);
            historyAuditSelectionStart=System.nanoTime();historyAuditSelectionStep=step;
        }}
        if(semanticReview.isSynchronous() && !semanticReview.isHealthy()) {
            decisionHealthy=false;
            // Provider failures are audited as unhealthy, but must not invoke APE bad-state recovery.
            if(semanticReview.hasLocalFailure())
                throw new com.android.commands.monkey.ape.BadStateException("synchronous semantic request construction failed");
        }
    }
    public void beginProbeSelection() {
        probeRequested=false;suggestionIndex=0;roundSuggestions=Collections.emptyList();
        registry.discardAdvice(selected,step);
        if(semanticReview!=null)semanticReview.beginSelection();
    }
    public void endProbeSelection() {
        roundSuggestions=Collections.emptyList();suggestionIndex=0;
        registry.discardAdvice(selected,step);
        if(semanticReview!=null)semanticReview.endSelection();
    }
    private boolean currentWindow(GUITree tree) {
        if(ape==null)return true;
        String expected=tree.getJevObservationIdentity();
        android.view.accessibility.AccessibilityNodeInfo root=ape.getRootInActiveWindow();
        try {return expected!=null && expected.equals(JevEvidence.accessibilityIdentity(root));}
        finally {if(root!=null)root.recycle();}
    }
    public ModelAction nextProbe(State state,GUITree tree,int timestamp,Validator validator) {
        if(!probeEnabled || closed)return null;
        step=timestamp;
        if(semanticReview!=null)semanticReview.setStep(timestamp);
        if(semanticReview!=null)return nextSemanticProbe(state,tree,timestamp,validator);
        if(!probeRequested) {
            probeRequested=true;
            if(!requestProbe(state,tree))return null;
            roundSuggestions=registry.suggestions(state,tree);
        }
        if(!currentWindow(tree))
            throw new com.android.commands.monkey.ape.BadStateException("GUI changed during probe review");
        while(suggestionIndex<roundSuggestions.size()) {
            JevRegistry.Entry entry=roundSuggestions.get(suggestionIndex++);
            if(registry.retireIfExecuted(entry,step))continue;
            ModelAction action=entry.concreteAction.getModelAction();
            GUITreeNode node=JevRegistry.correspondingNode(entry.concreteAction,tree,action);
            if(node==null || !node.isEnabled() || !action.isValid() || !action.isEnabled() || !action.isResolvedAt(timestamp)
                || action.getResolvedGUITree()!=tree)continue;
            GUITreeNode previous=action.getResolvedNode();GUITreeAction previousAction=action.getResolvedGUITreeAction();
            GUITreeNode[] nodes=action.getResolvedNodes();int throttle=action.getThrottle();
            action.resolveAt(timestamp,throttle,tree,node,nodes);
            if(!validator.valid(action) || !registry.claimProbe(entry,step)) {
                action.resolveAt(timestamp,throttle,tree,previous,nodes);action.setResolvedGUITreeAction(previousAction);
                continue;
            }
            originalResolvedNode=previous;originalResolvedAction=previousAction;
            selected=entry;selectedVersion=registry.version();selectedAction=action.getResolvedGUITreeAction();selectedInput=null;
            registry.selected(entry,step);
            entryEvent("PROBE_BOUND",entry,JevRegistry.fields("confidence",entry.confidence,
                "selection_rank",suggestionIndex,"source","local","changed_node",previous!=node));
            return action;
        }
        return null;
    }
    private ModelAction nextSemanticProbe(State state,GUITree tree,int timestamp,Validator validator) {
        if(semanticSelected || !currentWindow(tree))return null;
        while(true) {
            JevSemanticReview.Selection selection=semanticReview.nextSelection(state,tree);
            if(selection==null)return null;
            JevSemanticReview.Candidate candidate=selection.candidate;
            ModelAction action=candidate.action;
            if(!action.isValid() || !action.isEnabled() || !action.isResolvedAt(timestamp) || action.getResolvedGUITree()!=tree) {
                semanticSelectionRejected(selection,timestamp,"native_binding_invalid");continue;
            }
            GUITreeNode[] nodes=action.getResolvedNodes();
            if(nodes==null || !candidate.node.isEnabled()){semanticSelectionRejected(selection,timestamp,"resolved_nodes_unavailable_or_disabled");continue;}
            GUITreeNode previous=action.getResolvedNode();GUITreeAction previousAction=action.getResolvedGUITreeAction();
            int throttle=action.getThrottle();
            action.resolveAt(timestamp,throttle,tree,candidate.node,nodes);
            if(!validator.valid(action)) {
                action.resolveAt(timestamp,throttle,tree,previous,nodes);action.setResolvedGUITreeAction(previousAction);
                semanticSelectionRejected(selection,timestamp,"native_validator_rejected");
                continue;
            }
            originalResolvedNode=previous;originalResolvedAction=previousAction;
            selectedVersion=registry.version();selectedAction=action.getResolvedGUITreeAction();semanticSelected=true;
            semanticEntryId=selection.entryId;semanticRequestId=selection.requestId;semanticCandidateId=selection.candidateId;
            semanticConfidence=selection.confidence;semanticChoice="PROMOTE";
            events.emit("PROBE_BOUND",timestamp,registry.version(),state.getGraphId(),action.getGraphId(),JevRegistry.fields(
                "entry_id",semanticEntryId==null?JSONObject.NULL:semanticEntryId,"request_id",semanticRequestId==null?JSONObject.NULL:semanticRequestId,
                "candidate_id",semanticCandidateId,"confidence",semanticConfidence,"choice","PROMOTE","source",semanticReviewSynchronous()?"semantic_sync":"semantic_async",
                "input_signature",selection.inputSignature,"decision_scope",selection.decisionScope,
                "concrete_identity",JevEvidence.digest(ConcreteActionIdentity.of(action.getType().name(),action.getResolvedNode()).toString())));
            events.emit("PROBE_SELECTED",timestamp,registry.version(),state.getGraphId(),action.getGraphId(),JevRegistry.fields(
                "entry_id",semanticEntryId==null?JSONObject.NULL:semanticEntryId,"request_id",semanticRequestId==null?JSONObject.NULL:semanticRequestId,
                "candidate_id",semanticCandidateId,"confidence",semanticConfidence,"choice","PROMOTE","source",semanticReviewSynchronous()?"semantic_sync":"semantic_async"));
            return action;
        }
    }
    private void semanticSelectionRejected(JevSemanticReview.Selection selection,long timestamp,String reason) {
        ModelAction action=selection.candidate.action;
        evidenceAudit.navigation(ape,action.getResolvedGUITree(),timestamp,registry.version(),JevRegistry.fields("stage","native_binding_rejected",
            "entry_id",selection.entryId,"request_id",selection.requestId,"candidate_id",selection.candidateId,"reason",reason,
            "source_review_path","jev-audit/reviews/"+selection.requestId+".json","concrete",JevEvidenceAudit.concrete(action,selection.candidate.node)),true);
        events.emit("SEMANTIC_SELECTION_REJECTED",timestamp,registry.version(),action.getState().getGraphId(),action.getGraphId(),
            JevRegistry.fields("entry_id",selection.entryId,"request_id",selection.requestId,"candidate_id",selection.candidateId,
                "concrete_identity",selection.concreteIdentity,"decision_scope",selection.decisionScope,
                "input_signature",selection.inputSignature,"reason",reason,"decision_role","candidate_decision"));
    }
    private boolean requestProbe(State state,GUITree tree) {
        List<JevRegistry.Observation> observations=new ArrayList<JevRegistry.Observation>();
        for(ModelAction action:state.getActions()) {
            if(!action.requireTarget() || action.isBack() || !action.isValid() || !action.isEnabled() || action.isUnvisited() || !action.isSaturated()
                || !action.isResolvedAt((int)step) || action.getResolvedGUITree()!=tree || action.getResolvedNodes()==null || action.getResolvedNodes().length==0)continue;
            List<JevRegistry.Member> members=new ArrayList<JevRegistry.Member>();List<GUITreeNode> nodes=new ArrayList<GUITreeNode>();
            for(GUITreeNode node:action.getResolvedNodes()){members.add(JevEvidence.member(action.getType().name(),node));nodes.add(node);}
            observations.add(new JevRegistry.Observation(state.getGraphId(),action.getGraphId(),action.getType().name(),state.getActivity(),tree,
                action.getResolvedGUITreeAction(),nodes,true,true,members));
        }
        JevRegistry.Batch batch=null;List<JevRegistry.Batch> groups=Collections.emptyList();long started=nowNanos();
        JevTransport.Result lastResponse=null;boolean anySent=false;
        try {
            registry.setSemanticReviewModel(providerModel);
            groups=registry.discover(observations,step);
            if(groups.isEmpty()) {
                events.emit("PROBE_NO_CANDIDATES",step,registry.version(),state.getGraphId(),null,
                    JevRegistry.fields("reason","no_reviewable_concrete"));
                return true;
            }
            // Assign the page/entry identity before enforcing the wire budget so a
            // rejected request still has a complete, auditable terminal receipt.
            batch=registry.page(groups,Integer.MAX_VALUE,providerModel);
            save("request-"+batch.id,JevRegistry.merge(registry.receipt(batch),JevRegistry.fields("request",new JSONObject(batch.request.bodyJson),
                "evidence_digest",batch.request.evidenceDigest,"gui_identity",JevEvidence.guiIdentity(tree))));
            if(registry.reuseFingerprint(batch,step)) {
                GUITree currentTree=state.getLatestGUITree();registry.bindCurrent(batch,currentTree==null?tree:currentTree);
                events.emit("PROBE_SYNC_RESULT",step,registry.version(),state.getGraphId(),null,
                    JevRegistry.fields("request_id",batch.id,"reason","input_fingerprint_hit","input_fingerprint",batch.request.inputFingerprint,"wait_ns",0));
                return true;
            }
            events.emit("PROBE_SYNC_REQUEST",step,registry.version(),state.getGraphId(),null,JevRegistry.fields("request_id",batch.id,
                "candidate_count",batch.entries.size(),"request_bytes",batch.request.requestBytes,"timeout_ms",probeTimeoutMs));
            JevProbePackets.Plan plan=JevProbePackets.plan(batch.request,maxBytes);
            save("packet-map-"+batch.id,plan.receipt());
            for(JevProtocol.Request packet:plan.packets)save("physical-request-"+packet.requestId,JevRegistry.fields(
                "logical_request_id",batch.id,"physical_request_id",packet.requestId,"request_bytes",packet.requestBytes,
                "candidate_ids",new JSONArray(packet.candidateIds),"request",new JSONObject(packet.bodyJson)));
            // One deadline for the entire logical selection, including packet retries.
            long networkStart=nowNanos(),deadline=networkStart+TimeUnit.MILLISECONDS.toNanos(probeTimeoutMs),firstSent=0;
            boolean admitted=false,usageKnown=true;long inputTokens=0,outputTokens=0;
            JSONObject answers=new JSONObject();
            for(JevProtocol.Request packet:plan.packets) {
                for(int attempt=1;attempt<=2;attempt++) {
                    if(!currentWindow(tree)) {
                        failRequest(state,batch,"STALE_GUI",started,lastResponse,anySent);
                        throw new com.android.commands.monkey.ape.BadStateException("GUI changed during probe review");
                    }
                    long remaining=TimeUnit.NANOSECONDS.toMillis(deadline-nowNanos());
                    if(remaining<=0){failRequest(state,batch,"TIMEOUT",started,lastResponse,anySent);return false;}
                    JevTransport.Call call=probeTransport.submit(packet.requestId,packet.bodyJson,(int)remaining,Math.max(16384,maxBytes));
                    if(call==null){failRequest(state,batch,"QUEUE_FULL",started,lastResponse,anySent);return false;}
                    if(!admitted){registry.admitted(batch,step);admitted=true;}
                    JevTransport.Result response=call.await();lastResponse=response;
                    if(response.sentNanos>0) {
                        if(!anySent){firstSent=response.sentNanos;registry.requestEvent("JEV_REQUEST_SENT",batch,step,JevRegistry.fields("physical_packet_count",plan.packets.size()));}
                        anySent=true;
                        registry.requestEvent("PROBE_PACKET_SENT",batch,step,JevRegistry.fields("physical_request_id",packet.requestId,"attempt",attempt,"request_bytes",packet.requestBytes));
                    }
                    String error=response.error;JSONObject provider=null;
                    if(error==null)try {
                        JevProtocol.fromProvider(response.raw,packet);provider=StrictJson.object(response.raw);
                    }catch(JSONException invalid){error="INVALID_RESPONSE";}
                    save("physical-response-"+packet.requestId+"-attempt-"+attempt,JevRegistry.fields(
                        "logical_request_id",batch.id,"physical_request_id",packet.requestId,"attempt",attempt,
                        "http_status",response.httpStatus,"sent",response.sentNanos>0,"error",error==null?JSONObject.NULL:error,
                        "wait_ns",response.waitNanos(),"latency_ns",response.latencyNanos(),
                        "raw_response",response.raw==null?JSONObject.NULL:response.raw));
                    registry.requestEvent("PROBE_PACKET_RESULT",batch,step,JevRegistry.fields("physical_request_id",packet.requestId,"attempt",attempt,
                        "reason",error==null?"success":error,"wait_ns",response.waitNanos()));
                    if(error==null) {
                        JSONObject received=provider.getJSONObject("answers");
                        for(String id:packet.candidateIds) {
                            if(answers.has(id))throw new JSONException("duplicate packet result");
                            answers.put(id,received.get(id));
                        }
                        JSONObject usage=provider.optJSONObject("usage");
                        if(usage==null || !usage.has("input_tokens") || !usage.has("output_tokens"))usageKnown=false;
                        else {inputTokens+=usage.getLong("input_tokens");outputTokens+=usage.getLong("output_tokens");}
                        break;
                    }
                    if(attempt<2 && retryablePacket(error) && nowNanos()<deadline)continue;
                    failRequest(state,batch,error,started,response,anySent);return false;
                }
            }
            JSONObject raw=JevRegistry.fields("model",providerModel,"answers",answers);
            if(usageKnown)raw.put("usage",JevRegistry.fields("input_tokens",inputTokens,"output_tokens",outputTokens));
            JevProtocol.Result result=JevProtocol.fromProvider(raw.toString(),batch.request);
            long latency=firstSent==0?0:nowNanos()-firstSent,wait=nowNanos()-networkStart;
            save("response-"+batch.id,JevRegistry.fields("request_id",batch.id,"abstraction_version",batch.version,
                "evidence_digest",batch.request.evidenceDigest,"latency_ns",latency,"wait_ns",wait,
                "physical_packet_count",plan.packets.size(),"raw_response",raw,"response",result.toJson()));
            registry.requestEvent("JEV_RESULT_RECEIVED",batch,step,JevRegistry.fields("latency_ns",latency,"wait_ns",wait));
            if(!currentWindow(tree)) {
                failRequest(state,batch,"STALE_GUI",started,lastResponse,anySent);
                throw new com.android.commands.monkey.ape.BadStateException("GUI changed during probe review");
            }
            registry.apply(batch,raw.toString(),step);
            GUITree currentTree=state.getLatestGUITree();registry.bindCurrent(batch,currentTree==null?tree:currentTree);
            events.emit("PROBE_SYNC_RESULT",step,registry.version(),state.getGraphId(),null,JevRegistry.fields("request_id",batch.id,"reason","success","wait_ns",nowNanos()-started));
            return true;
        }catch(JSONException error){
            events.emit("PROBE_REQUEST_ERROR",step,registry.version(),state.getGraphId(),null,JevRegistry.fields(
                "exception",error.getClass().getSimpleName(),"message",error.getMessage(),
                "request_bytes",error instanceof JevProtocol.BudgetExceeded?((JevProtocol.BudgetExceeded)error).request.requestBytes:JSONObject.NULL));
            failRequest(state,batch,error instanceof JevProtocol.BudgetExceeded?"REQUEST_BUDGET_EXCEEDED":"INVALID_RESPONSE_OR_REQUEST",started,lastResponse,anySent);
        }
        catch(IOException error){receiptFailed(error);failRequest(state,batch,"RECEIPT_FAILED",started,lastResponse,anySent);}
        if(batch==null)for(JevRegistry.Batch group:groups)registry.deliveryFailed(group,step,"REQUEST_CONSTRUCTION_FAILED");
        return false;
    }
    private static boolean retryablePacket(String error) {
        if("TRANSPORT_FAILURE".equals(error) || "TIMEOUT".equals(error) || "HTTP_408".equals(error) || "HTTP_429".equals(error))return true;
        return error!=null && error.matches("HTTP_5[0-9][0-9]");
    }
    private void failRequest(State state,JevRegistry.Batch batch,String error,long started,JevTransport.Result response,boolean anySent) {
        if(batch!=null){registry.deliveryFailed(batch,step,error);registry.requestEvent("JEV_REQUEST_FAILED",batch,step,JevRegistry.fields("reason",error,"latency_ns",nowNanos()-started));}
        events.emit("PROBE_SYNC_RESULT",step,registry.version(),state.getGraphId(),null,
            JevRegistry.fields("request_id",batch==null?JSONObject.NULL:batch.id,"reason",error,"wait_ns",nowNanos()-started));
        try {if(batch!=null)save("failure-"+batch.id,JevRegistry.fields("request_id",batch.id,"error",error,"wait_ns",nowNanos()-started,
            "http_status",response==null?0:response.httpStatus,"sent",anySent));}
        catch(IOException failure){receiptFailed(failure);}
    }
    /** One native navigation's receipt, never a queue of targets. The Ledger
     * alone owns pending decisions and their one-arrival navigation guard. */
    private List<JevReviewLedger.PendingProbe> navigatingProbes=Collections.emptyList();
    private State probeNavigationTarget;
    private boolean probeNavigationGlobal;
    private List<JevReviewLedger.PendingProbe> eligibleNavigationProbes(State state,boolean diagnose) {
        List<JevReviewLedger.PendingProbe> eligible=new ArrayList<JevReviewLedger.PendingProbe>();
        if(!probeEnabled || observeOnly || semanticReview==null || lastModel==null || state==null)return eligible;
        Collection<State> live=lastModel.getGraph().getStates();
        for(JevReviewLedger.PendingProbe probe:semanticReview.pendingNavigationProbes(state)) {
            JevReviewLedger.Decision d=probe.decision;GUITreeAction source=probe.source;
            boolean accepted=JevHistoryPairwiseProtocol.PROMOTE.equals(d.representativeModelActionId)
                && d.requestId!=null && d.candidateId!=null && d.inputFingerprint!=null;
            boolean sourceValid=accepted && JevHistoryAudit.mappingProblem(source,live)==null
                && source.getGUITree().getCurrentState()==state && source.getGUITreeNode()!=null
                && probe.identity.equals(ConcreteActionIdentity.of(source.getActionType().name(),source.getGUITreeNode()))
                && JevRegistry.correspondingNode(source,source.getGUITree(),source.getModelAction())==source.getGUITreeNode();
            boolean exact=sourceValid && registry.hasExactExecution(state,source.getModelAction(),source.getGUITree(),source.getGUITreeNode());
            boolean related=sourceValid && registry.hasRelatedConcreteExecution(probe.identity.actionType(),state.getActivity(),probe.identity.canonicalFullName());
            // PROMOTE has already passed the semantic pair trust gate. Remote
            // navigation keeps only identity and confirmed-execution guards;
            // an independent confidence cutoff here rejected valid probes.
            boolean confidencePass=true;
            String reason=!sourceValid?"source_invalid":exact?"same_state_confirmed":
                related?"related_concrete_confirmed":probe.navigationArrived?"already_navigated_to_source":"eligible";
            if("eligible".equals(reason))eligible.add(probe);
            if(diagnose)events.emit("PROBE_NAVIGATION_ELIGIBILITY",step,registry.version(),state.getGraphId(),
                source==null||source.getModelAction()==null?null:source.getModelAction().getGraphId(),
                JevRegistry.merge(probeNavigationFields(probe),JevRegistry.fields("reason",reason,"navigation", "eligible".equals(reason),
                    "confidence_pass",confidencePass,"source_valid",sourceValid,"same_state_confirmed",exact,
                    "related_execution_suppressed",related,"navigation_arrived",probe.navigationArrived)));
        }
        return eligible;
    }
    private JSONObject probeNavigationFields(JevReviewLedger.PendingProbe probe) {
        JevReviewLedger.Decision d=probe.decision;
        return JevRegistry.fields("entry_id",d.requestId+"-"+d.candidateId,"request_id",d.requestId,"candidate_id",d.candidateId,
                "concrete_identity",JevEvidence.digest(probe.identity.toString()),"confidence",d.confidence!=null&&Double.isFinite(d.confidence)?d.confidence:JSONObject.NULL,
                "path_id",activePath);
    }
    /** Called by the native Remote Greedy endpoint filter, within its one search. */
    public boolean pendingProbeNavigationCandidate(State state) {
        List<JevReviewLedger.PendingProbe> probes=eligibleNavigationProbes(state,true);
        if(probes.isEmpty())return false;
        JSONArray entries=new JSONArray();for(JevReviewLedger.PendingProbe p:probes)entries.put(p.decision.requestId+"-"+p.decision.candidateId);
        events.emit("PROBE_NAVIGATION_CANDIDATE",step,registry.version(),state.getGraphId(),null,
            JevRegistry.fields("probe_entries",entries.toString(),"candidate_count",probes.size()));
        return true;
    }
    public void probeNavigationSelected(State target,long timestamp,boolean global) {
        finishProbeNavigation(null,timestamp,"replaced");
        navigatingProbes=eligibleNavigationProbes(target,false);
        if(navigatingProbes.isEmpty())return;
        probeNavigationTarget=target;probeNavigationGlobal=global;
        for(JevReviewLedger.PendingProbe p:navigatingProbes)
            events.emit("PROBE_NAVIGATION_SELECTED",timestamp,registry.version(),target.getGraphId(),p.source.getModelAction().getGraphId(),
                JevRegistry.merge(probeNavigationFields(p),JevRegistry.fields("source",global?"native_global_action":"native_remote_path")));
    }
    private void finishProbeNavigation(State actual,long timestamp,String reason) {
        boolean arrived=actual!=null && actual==probeNavigationTarget;
        for(JevReviewLedger.PendingProbe p:navigatingProbes) {
            if(arrived)registry.reviewLedger().navigationArrived(p);
            String availability="not_arrived";JSONObject binding=new JSONObject();
            if(arrived) {
                binding=JevRegistry.bindingDiagnostic(p.source,actual.getLatestGUITree(),p.source.getModelAction());
                GUITreeNode node=JevRegistry.correspondingNode(p.source,actual.getLatestGUITree(),p.source.getModelAction());
                availability=node==null?"node_absent_or_ambiguous":!node.isEnabled()?"node_disabled":
                    !ActionFilter.ENABLED_VALID.include(p.source.getModelAction())?"model_action_unavailable":"awaiting_native_binding";
            }
            JSONObject local=evidenceAudit.navigation(ape,arrived?actual.getLatestGUITree():p.source.getGUITree(),timestamp,registry.version(),
                JevRegistry.merge(probeNavigationFields(p),JevRegistry.fields("stage",arrived?"arrival":"navigation_ended","reason",reason,"binding",binding,
                    "original_concrete_identity",p.identity.toString(),"source_review_path","jev-audit/reviews/"+p.decision.requestId+".json")),arrived);
            events.emit(arrived?"PROBE_NAVIGATION_ARRIVED":"PROBE_NAVIGATION_ENDED",timestamp,registry.version(),
                probeNavigationTarget.getGraphId(),p.source.getModelAction()==null?null:p.source.getModelAction().getGraphId(),
                JevRegistry.merge(probeNavigationFields(p),JevRegistry.fields("arrived",arrived,"reason",reason,"last_availability",availability,
                    "binding_reason",binding.optString("reason","not_arrived"),"source_match_count",binding.optInt("source_match_count",-1),
                    "target_match_count",binding.optInt("target_match_count",-1),"audit_path",local.opt("path"))));
        }
        navigatingProbes=Collections.emptyList();probeNavigationTarget=null;probeNavigationGlobal=false;
    }
    private long pathSequence,pathStartNanos,pathStartStep;
    private String activePath,navigationTarget;
    private boolean navigationProbeOnly;
    public void navigationStarted(Subsequence path,long timestamp) {
        navigationEnded(null,timestamp,"replaced");activePath="p"+(++pathSequence);pathStartNanos=ApeSnapshot.nowNanos();pathStartStep=timestamp;
        State target=path.getLastState();navigationTarget=target.getGraphId();int unsaturated=0;
        for(ModelAction action:target.getActions())if(ActionFilter.ENABLED_VALID.include(action) && !action.isSaturated())unsaturated++;
        JSONArray entries=new JSONArray();Set<ModelAction> saturatedProbes=new HashSet<ModelAction>();
        for(JevReviewLedger.PendingProbe p:eligibleNavigationProbes(target,false)) {
            entries.put(p.decision.requestId+"-"+p.decision.candidateId);
            if(p.source.getModelAction().isSaturated())saturatedProbes.add(p.source.getModelAction());
        }
        navigationProbeOnly=unsaturated==0 && !saturatedProbes.isEmpty();
        if(entries.length()>0) {
            JSONArray edges=new JSONArray();for(StateTransition edge:path.getEdges())edges.put(JevRegistry.fields("edge_id",edge.getGraphId(),
                "source_state",edge.getSource().getGraphId(),"target_state",edge.getTarget().getGraphId(),"model_action_id",edge.getAction().getGraphId()));
            evidenceAudit.save("jev-audit/navigation/path-"+activePath+".json",JevRegistry.fields("kind","native_bfs_selected_path","path_id",activePath,
                "step",timestamp,"abstraction_version",registry.version(),"probe_entries",entries,"edges",edges));
        }
        events.emit("NAVIGATION_STARTED",timestamp,registry.version(),target.getGraphId(),null,JevRegistry.fields("path_id",activePath,
            "source_state",path.getFirstAction().getState().getGraphId(),"path_length",path.size(),"native_unsaturated_actions",unsaturated,
            "probe_only_actions",saturatedProbes.size(),"probe_entries",entries.toString()));
    }
    public void navigationEnded(State actual,long timestamp,String reason) {
        finishProbeNavigation(actual,timestamp,reason);
        if(activePath==null)return;
        events.emit("NAVIGATION_ENDED",timestamp,registry.version(),actual==null?null:actual.getGraphId(),null,JevRegistry.fields("path_id",activePath,
            "reason",reason,"elapsed_steps",timestamp-pathStartStep,"elapsed_ns",ApeSnapshot.nowNanos()-pathStartNanos,
            "arrived",actual!=null && actual.getGraphId().equals(navigationTarget),"probe_only",navigationProbeOnly));
        activePath=null;navigationTarget=null;
    }
    private void entryEvent(String name,JevRegistry.Entry entry,JSONObject fields) {
        events.emit(name,step,entry.version,entry.state,entry.action,JevRegistry.merge(fields,JevRegistry.fields("entry_id",entry.id,"request_id",entry.requestId)));
    }
    public void directTextInput(Action action,String text,boolean sent) {
        if(action instanceof ModelAction)evidenceAudit.directText(((ModelAction)action).getResolvedGUITreeAction(),text,sent,step,registry.version());
    }
    public void beforeInput(Action action){
        if(!(action instanceof ModelAction))evidenceAudit.beforeOtherInput(action,step,registry.version());
        boolean auditProbe=action instanceof ModelAction && (selected!=null || semanticSelected)
            && ((ModelAction)action).getResolvedGUITreeAction()==selectedAction;
        if(auditProbe) {
            ModelAction modelAction=(ModelAction)action;
            probeSourceScreenshot=captureProbeScreenshot(modelAction.getResolvedGUITree(),modelAction,modelAction.getResolvedNode(),"source");
        }
        if(action instanceof ModelAction)evidenceAudit.beforeInput(ape,(ModelAction)action,step,registry.version(),auditProbe,
            auditProbe?probeSourceScreenshot:null,auditProbe?(selected!=null?selected.id:semanticEntryId):null,
            auditProbe?(selected!=null?selected.requestId:semanticRequestId):null,auditProbe?semanticCandidateId:null);
        if(regionContextAudit && action instanceof ModelAction) {
            ModelAction modelAction=(ModelAction)action;GUITreeAction concrete=modelAction.getResolvedGUITreeAction();
            GUITree tree=modelAction.getResolvedGUITree();GUITreeNode node=modelAction.getResolvedNode();
            if(concrete!=null && tree!=null) {
                String id="ctx-exec-"+(++executionContextSequence);
                JSONObject snapshot=contextSnapshot(tree,modelAction.getState(),modelAction,node);
                executionContexts.put(concrete,new ExecutionContext(id,step,snapshot));
                JSONObject receipt=new JSONObject();
                try {receipt.put("schema",1).put("kind","execution_source_context").put("context_id",id).put("step",step)
                    .put("snapshot",snapshot);} catch(JSONException error){throw new IllegalStateException(error);}
                saveDiagnostic("region-context-execution-"+executionContextSequence,receipt);
                events.emit("REGION_CONTEXT_CAPTURED",step,registry.version(),modelAction.getState().getGraphId(),modelAction.getGraphId(),
                    JevRegistry.fields("context_snapshot_id",id,"region_context_audit",true));
            }
        }
        if(region!=null)region.beforeInput(action);
    }
    public boolean gate(ModelAction action,GUITree tree,String strategy) {
        if(region==null)return true;
        boolean allow=region.gate(action,tree,strategy,step);
        if(!allow && selected!=null && action.getResolvedGUITreeAction()==selectedAction)selectionFailed("region_gate_skipped");
        return allow;
    }
    public void eventsGenerated(Action action,List<? extends MonkeyEvent> queue,int first) {
        evidenceAudit.generated(action,queue,first,step,registry.version());
        if(!(action instanceof ModelAction))return;
        GUITreeAction concrete=((ModelAction)action).getResolvedGUITreeAction();
        InputTrace trace=concrete==selectedAction
            ?(semanticSelected?new InputTrace(null,concrete,semanticEntryId,semanticRequestId,semanticCandidateId):new InputTrace(selected,concrete))
            :new InputTrace(null,concrete);int index=0;
        for(MonkeyEvent event:queue) {
            if(index++<first)continue;
            int type=event.getEventType();
            if(type==MonkeyEvent.EVENT_TYPE_KEY || type==MonkeyEvent.EVENT_TYPE_TOUCH || type==MonkeyEvent.EVENT_TYPE_TRACKBALL){inputEvents.put(event,trace);trace.total++;}
        }
        if(trace.entry!=null){selectedInput=trace;entryEvent("PROBE_INPUT_GENERATED",trace.entry,JevRegistry.fields("event_count",trace.total));}
        else if(trace.semanticEntryId!=null){selectedInput=trace;events.emit("PROBE_INPUT_GENERATED",step,registry.version(),null,null,JevRegistry.fields(
            "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId,"event_count",trace.total));}
        evidenceAudit.input(trace.action,step,registry.version(),"generated",trace.total,0,0,0);
        if(trace.total==0)inputSummary(trace);
    }
    public void eventInjected(MonkeyEvent event,int code){inputResult(event,code,false);}
    public void eventsDiscarded(List<? extends MonkeyEvent> queue){for(MonkeyEvent event:queue)inputResult(event,0,true);}
    private void inputResult(MonkeyEvent event,int code,boolean discarded) {
        evidenceAudit.injected(event,code,discarded,step,registry.version());
        InputTrace trace=inputEvents.remove(event);if(trace==null)return;
        if(discarded)trace.discarded++;else if(code==MonkeyEvent.INJECT_SUCCESS)trace.success++;else trace.failed++;
        if(trace.entry!=null && !discarded && code!=MonkeyEvent.INJECT_SUCCESS)entryEvent("PROBE_INPUT_ERROR",trace.entry,JevRegistry.fields("inject_code",code,"event_type",event.getEventType()));
        else if(trace.semanticEntryId!=null && !discarded && code!=MonkeyEvent.INJECT_SUCCESS)events.emit("PROBE_INPUT_ERROR",step,registry.version(),null,null,JevRegistry.fields(
            "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId,"inject_code",code,"event_type",event.getEventType()));
        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);
        if(trace.remaining()==0)inputSummary(trace);
    }
    private void inputSummary(InputTrace trace) {
        evidenceAudit.input(trace.action,step,registry.version(),trace.phase(),trace.total,trace.success,trace.failed,trace.discarded);
        boolean added=false;
        if(trace.confirmed()){
            added=confirmedInputs.add(trace.action);confirmedInputBranches.put(trace.action,trace.branch);
            if(semanticReview!=null && trace.action!=null && trace.action.getGUITree()!=null)
                semanticReview.markExecuted(trace.action.getGUITree().getCurrentState(),trace.action.getModelAction(),trace.action.getGUITreeNode());
        }
        if(Config.getBoolean("ape.jev.historyAudit",false)) {
            if(trace.confirmed())diagnosticConfirmedSummaries++;
            GUITree tree=trace.action==null?null:trace.action.getGUITree();
            ModelAction action=trace.action==null?null:trace.action.getModelAction();
            events.emit("HISTORY_INPUT_SUMMARY",step,registry.version(),action==null?null:action.getState().getGraphId(),
                action==null?null:action.getGraphId(),JevRegistry.fields("input_confirmed",trace.confirmed(),
                "new_confirmation_object",added,"confirmation_set_objects",confirmedInputs.size(),
                "confirmed_summary_events",diagnosticConfirmedSummaries,"event_count",trace.total,
                "injected_success",trace.success,"injected_failed",trace.failed,"discarded",trace.discarded,
                "unreported",trace.remaining(),"source_tree",tree==null?JSONObject.NULL:tree.getId(),
                "source_gui_timestamp",tree==null?JSONObject.NULL:tree.getTimestamp()));
        }
        if(trace.entry==null && trace.semanticEntryId==null)return;
        JSONObject fields=JevRegistry.fields("input_phase",trace.phase(),"event_count",trace.total,
            "injected_success",trace.success,"injected_failed",trace.failed,"discarded",trace.discarded,"unreported",trace.remaining());
        if(trace.entry!=null)entryEvent("PROBE_INPUT_COMPLETED",trace.entry,fields);
        else events.emit("PROBE_INPUT_COMPLETED",step,registry.version(),null,null,JevRegistry.merge(fields,JevRegistry.fields(
            "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId)));
    }
    public void selectionFailed(String reason) {
        if(region!=null)region.interrupted(reason);
        if(selected==null && !semanticSelected)return;
        probeSourceScreenshot=null;
        if(selected==null) {
            ModelAction action=selectedAction==null?null:selectedAction.getModelAction();
            if(action!=null && action.getResolvedGUITreeAction()==selectedAction && action.isResolvedAt((int)step)
                    && selectedVersion==registry.version()) {
                action.resolveAt((int)step,action.getThrottle(),selectedAction.getGUITree(),originalResolvedNode,action.getResolvedNodes());
                action.setResolvedGUITreeAction(originalResolvedAction);
            }
            if(semanticEntryId!=null)events.emit("PROBE_EXECUTION_FAILED",step,registry.version(),action==null?null:action.getState().getGraphId(),action==null?null:action.getGraphId(),JevRegistry.fields(
                "entry_id",semanticEntryId,"request_id",semanticRequestId,"candidate_id",semanticCandidateId,"reason",reason));
            semanticSelected=false;selectedAction=null;originalResolvedNode=null;originalResolvedAction=null;
            semanticEntryId=null;semanticRequestId=null;semanticCandidateId=null;semanticConfidence=0;semanticChoice=null;
            return;
        }
        entryEvent("PROBE_INTERRUPTED",selected,JevRegistry.fields("reason",reason,"input_phase",selectedInput==null?"selected":selectedInput.phase()));
        registry.failed(selected,step,reason);
        ModelAction action=selectedAction.getModelAction();
        if(action.getResolvedGUITreeAction()==selectedAction && action.getResolvedGUITree()==selectedAction.getGUITree()
                && action.isResolvedAt((int)step) && selectedVersion==registry.version()) {
            action.resolveAt((int)step,action.getThrottle(),selectedAction.getGUITree(),originalResolvedNode,action.getResolvedNodes());
            action.setResolvedGUITreeAction(originalResolvedAction);
        }
        selected=null;selectedAction=null;selectedInput=null;originalResolvedNode=null;originalResolvedAction=null;
        semanticSelected=false;
    }
    public void recordCrash(String process,int pid,String shortMsg,String longMsg,long timestamp,String stack) {
        evidenceAudit.crash(ape,process,pid,shortMsg,longMsg,timestamp,stack,step,registry.version());
    }
    JSONObject performance(){return timing.snapshot();}
    void resetPerformance(){timing.reset();}
    public void close() {
        if(closed)return;
        if(semanticReview!=null && semanticReview.isSynchronous() && !semanticReview.isHealthy())decisionHealthy=false;
        finishSemanticTiming(null,null,"run_closed_without_selection");
        if(region!=null)region.close();
        navigationEnded(null,step,"run_closed");selectionFailed("run_ended_without_native_transition");endProbeSelection();
        if(probeTransport!=null)probeTransport.close();
        if(semanticReview!=null)semanticReview.close();
        if(regionTransport!=null)regionTransport.close();
        for(InputTrace trace:new HashSet<InputTrace>(inputEvents.values()))inputSummary(trace);inputEvents.clear();
        output.drain();recordFinalStateTelemetry();writeGraphSnapshot();registry.finishAccounting(step);registry.counters(step);
        output.drain();closed=true;timing.count("output_queue_peak",output.peak());
        try{output.save(new File(graphSnapshotFile.getParentFile(),"jev-performance.json"),timing.snapshot(),true);}catch(IOException error){output.fail(error);}
        events.emit("RUN_FINISHED",step,registry.version(),null,null,JevRegistry.fields("run_id",runId,"jev_enabled",!observeOnly,
            "probe_enabled",probeEnabled,"region_enabled",region!=null,"probe_pending",selected==null?0:1,
            "decision_healthy",decisionHealthy && events.isHealthy(),"evidence_healthy",output.isHealthy(),
            "final_state_count",lastModel==null?JSONObject.NULL:lastModel.getGraph().getStates().size(),"elapsed_ns",ApeSnapshot.nowNanos()-startNanos,"native_model_actions",transitions));
        evidenceAudit.close();events.close();output.close();
        File completion=new File(graphSnapshotFile.getParentFile(),"jev-evidence-complete.json");
        try {Writer writer=new OutputStreamWriter(new FileOutputStream(completion),"UTF-8");
            try{writer.write(JevRegistry.fields("healthy",output.isHealthy() && events.isHealthy(),"output_accounting",output.accounting(),"step",step,"abstraction_version",registry.version(),
                "wall_time_ms",System.currentTimeMillis(),"monotonic_ns",nowNanos()).toString());writer.write('\n');}finally{writer.close();}}
        catch(IOException error){output.fail(error);}
    }
    public void recordSelectionDecision(String strategy,State state,Action selected,int bufferAfter) {
        finishSemanticTiming(state,selected,selected==null?"no_native_action":"native_action_selected");
        if(historyAuditSelectionStart!=0) {
            if(historyAuditSelectionStep==step)timing.record("history_audit_post_prepare_to_selection",System.nanoTime()-historyAuditSelectionStart);
            historyAuditSelectionStart=0;
        }
        if(state==null)return;
        JSONObject fields=stateTelemetry(state,"selection");
        JevRegistry.merge(fields,JevRegistry.fields("selection_strategy",strategy==null?"unknown":strategy,
            "selection_context",activePath!=null?"navigation":"exploration",
            "selection_buffer_size_after",bufferAfter));
        if(selected instanceof ModelAction) {
            ModelAction action=(ModelAction)selected;
            JevRegistry.merge(fields,JevRegistry.fields("selected_model_action_type",action.getType().name(),
                "selected_model_action_saturated",action.isSaturated(),"selected_model_action_unvisited",action.isUnvisited(),

                "selected_effective_unvisited",action.isUnvisited(),"selected_navigation_target",!action.isSaturated(),
                "selected_model_action_visits",action.getVisitedCount(),"selected_action_fingerprint",action.getActionFingerprint(),
                "selected_resolved_node_count",action.getResolvedNodes()==null?0:action.getResolvedNodes().length));
            events.emit("SELECTION_DECISION",step,registry.version(),state.getGraphId(),action.getGraphId(),fields);
            return;
        }
        events.emit("SELECTION_DECISION",step,registry.version(),state.getGraphId(),null,fields);
    }

    private void finishSemanticTiming(State state,Action selected,String reason) {
        if(semanticReviewStart==0)return;
        long end=System.nanoTime(),prepared=semanticPrepareEnd==0?end:semanticPrepareEnd;
        long evidence=semanticReview.localEvidenceNanos(),wait=semanticReview.providerWaitNanos();
        long prepare=prepared-semanticReviewStart;
        events.emit("SEMANTIC_REVIEW_TIMING",semanticReviewStep,registry.version(),state==null?null:state.getGraphId(),
            selected instanceof ModelAction?((ModelAction)selected).getGraphId():null,
            JevRegistry.fields("review_id",semanticReviewId,"reason",reason,"elapsed_ns",end-semanticReviewStart,
                "prepare_ns",prepare,"local_evidence_ns",evidence,"provider_wait_ns",wait,
                "other_prepare_ns",prepare-evidence-wait,"post_prepare_selection_ns",end-prepared));
        timing.record("semantic_review_to_native_selection",end-semanticReviewStart);
        semanticReviewStart=0;semanticReviewId=null;
    }

    private void recordFinalStateTelemetry() {
        if(lastModel==null)return;
        int batch=0;
        for(State state:lastModel.getGraph().getStates()) {
            events.emit("STATE_FINAL",step,registry.version(),state.getGraphId(),null,stateTelemetry(state,"close"));
            if(++batch%32==0)output.drain();
            for(ModelAction action:state.getActions()) {
                events.emit("MODEL_ACTION_FINAL",step,registry.version(),state.getGraphId(),action.getGraphId(),
                    JevRegistry.fields("snapshot_reason","close","model_action_type",action.getType().name(),
                        "model_action_visits",action.getVisitedCount(),"model_action_unvisited",action.isUnvisited(),
                        "effective_model_action_unvisited",action.isUnvisited(),"navigation_target",!action.isSaturated(),
                        "model_action_saturated",action.isSaturated(),"native_model_action_saturated",action.isSaturated(),"resolved_node_count",
                        action.getResolvedNodes()==null?0:action.getResolvedNodes().length,
                        "action_fingerprint",action.getActionFingerprint()));
                if(++batch%32==0)output.drain();
            }
        }
        output.drain();
    }

    private void writeGraphSnapshot() {
        if(lastModel==null)return;
        try {
            Graph graph=lastModel.getGraph();JSONObject snapshot=new JSONObject();
            snapshot.put("event","graph_snapshot").put("abstraction_version",registry.version()).put("graph_step",step);
            JSONArray roots=new JSONArray();for(State state:graph.getEntryStates())roots.put(state.getGraphId());snapshot.put("roots",roots);
            JSONArray states=new JSONArray(),edges=new JSONArray();
            Set<StateTransition> allEdges=new HashSet<StateTransition>();
            for(State state:graph.getStates()) {
                JSONObject item=stateTelemetry(state,"close");item.put("id",state.getGraphId());
                item.put("activity",String.valueOf(state.getActivity()));item.put("visits",state.getVisitedCount());
                item.put("first_seen",state.getFirstVisitedTimestamp());item.put("last_seen",state.getLastVisitedTimestamp());
                JSONArray shots=new JSONArray();
                String captured=stateScreenshotPaths.get(state.getGraphId());
                if(captured!=null)shots.put(captured);
                else for(GUITree tree:state.getGUITrees()) {String path=relativeImagePath(tree.getJevImage());if(path!=null){shots.put(path);break;}}
                item.put("screenshot_paths",shots).put("screenshot_status",shots.length()>0?"saved":screenshotFailureReasons.containsKey(state.getGraphId())?
                    screenshotFailureReasons.get(state.getGraphId()):"not_observed_at_safe_point")
                    .put("screenshot_failure_count",screenshotFailures.containsKey(state.getGraphId())?screenshotFailures.get(state.getGraphId()):0);
                item.put("model_actions",graphActionDetails(state));states.put(item);
                allEdges.addAll(graph.getOutStateTransitions(state));
            }
            for(StateTransition edge:allEdges) {
                JSONObject item=new JSONObject();item.put("id",edge.getGraphId()).put("source",edge.getSource().getGraphId())
                    .put("target",edge.getTarget().getGraphId()).put("action",edge.getAction().getGraphId())
                    .put("action_fingerprint",edge.getAction().getActionFingerprint()).put("target_activity",String.valueOf(edge.getTarget().getActivity()))
                    .put("type",String.valueOf(edge.getType())).put("visits",edge.getVisitedCount()).put("is_circle",edge.isCircle())
                    .put("first_seen",edge.getFirstVisitedTimestamp()).put("last_seen",edge.getLastVisitedTimestamp());edges.put(item);
            }
            snapshot.put("states",states).put("edges",edges);
            output.save(graphSnapshotFile,snapshot,false);
        } catch(Exception e) {output.fail(e);}
    }

    private JSONArray graphActionDetails(State state) throws JSONException {
        JSONArray actions=new JSONArray();
        for(ModelAction action:state.getActions()) {
            JSONObject item=new JSONObject().put("id",action.getGraphId()).put("type",action.getType().name())
                .put("visits",action.getVisitedCount()).put("unvisited",action.isUnvisited())
                .put("effective_unvisited",action.isUnvisited()).put("navigation_target",!action.isSaturated())
                .put("saturated",action.isSaturated()).put("native_saturated",action.isSaturated())
                .put("fingerprint",action.getActionFingerprint());
            JSONArray concrete=new JSONArray();
            if(!action.requireTarget())concrete.put(graphConcreteDetails(action,null));
            else if(action.getResolvedNodes()!=null)for(GUITreeNode node:action.getResolvedNodes())concrete.put(graphConcreteDetails(action,node));
            item.put("concrete_actions",concrete);actions.put(item);
        }
        return actions;
    }

    private JSONObject graphConcreteDetails(ModelAction action,GUITreeNode node) throws JSONException {
        JSONObject item=new JSONObject().put("key",action.getGraphId());if(node==null)return item;
        Rect bounds=node.getBoundsInScreen();JSONArray rect=new JSONArray().put(bounds.left).put(bounds.top).put(bounds.right).put(bounds.bottom);
        return item.put("key",action.getGraphId()+"|"+String.valueOf(node.getText())).put("text",String.valueOf(node.getText()))
            .put("resource_id",String.valueOf(node.getResourceID())).put("class",String.valueOf(node.getClassName())).put("bounds",rect);
    }

    private String relativeImagePath(JevImages.Snapshot image) {
        if(image==null || image.path==null)return null;
        String root=graphSnapshotFile.getParentFile().getAbsolutePath(),path=image.path;
        return path.startsWith(root+File.separator)?path.substring(root.length()+1):path;
    }

    private final Semaphore imageSlots=new Semaphore(2);
    /** Take pixels before input or at the first observed native target. Only
     * compression/IO is deferred; a worker never captures a later live screen. */
    private JSONObject captureProbeScreenshot(GUITree tree,ModelAction action,GUITreeNode chosen,String side) {
        JSONObject record=evidenceAudit.capture(ape,tree,step,registry.version(),"source".equals(side)?"before_input_generation":"first_observed_target");
        try {
            record=new JSONObject(record.toString());record.put("side",side);
            if(action!=null) {
                JSONArray members=new JSONArray();
                if(action.getResolvedNodes()!=null)for(GUITreeNode node:action.getResolvedNodes())
                    members.put(JevRegistry.merge(graphConcreteDetails(action,node),JevEvidenceAudit.concrete(action,node)).put("is_probe",node==chosen));
                record.put("concrete_actions",members).put("resolved_node_count",members.length())
                    .put("selected_concrete",JevRegistry.merge(graphConcreteDetails(action,chosen),JevEvidenceAudit.concrete(action,chosen)))
                    .put("execution_fingerprint",action.getActionFingerprint());
            }
        }catch(Exception error){ /* Audit failure must not alter the Native action. */ }
        if(!"capture_queued".equals(record.optString("status"))) {
            timing.count("probe_screenshot_failed",1);
            events.emit("PROBE_SCREENSHOT_FAILED",step,registry.version(),tree==null||tree.getCurrentState()==null?null:tree.getCurrentState().getGraphId(),
                action==null?null:action.getGraphId(),JevRegistry.fields("screenshot_side",side,"reason",record.optString("status")));
        }
        return record;
    }

    private void screenshotFailed(State state,String reason) {
        Integer count=screenshotFailures.get(state.getGraphId());screenshotFailures.put(state.getGraphId(),count==null?1:count+1);
        screenshotFailureReasons.put(state.getGraphId(),reason);
        timing.count("state_screenshot_failed",1);timing.count("state_screenshot_failed_"+reason,1);
        events.emit("STATE_SCREENSHOT_FAILED",step,registry.version(),state.getGraphId(),null,JevRegistry.fields("reason",reason));
    }
    private void captureFirstStateScreenshot(State state) {
        if(ape==null || state==null || stateScreenshotPaths.containsKey(state.getGraphId()) || !screenshotPending.add(state.getGraphId()))return;
        if(!imageSlots.tryAcquire()){screenshotPending.remove(state.getGraphId());screenshotFailed(state,"queue_full");return;}
        long start=System.nanoTime();Bitmap bitmap=null;
        try {
            bitmap=ape.captureBitmap();timing.record("image_capture",System.nanoTime()-start);
            if(bitmap==null){imageSlots.release();screenshotPending.remove(state.getGraphId());screenshotFailed(state,"capture_null");return;}
            final Bitmap captured=bitmap;final File file=new File(screenshotDirectory,state.getGraphId()+"-"+step+".png");
            boolean accepted=output.submit("image_compress_io",new JevOutput.Write(){public void run()throws Exception{
                try {
                    if(!screenshotDirectory.isDirectory() && !screenshotDirectory.mkdirs())throw new IOException("Screenshot directory unavailable");
                    FileOutputStream stream=new FileOutputStream(file);
                    try{if(!captured.compress(Bitmap.CompressFormat.PNG,100,stream))throw new IOException("PNG compression failed");}finally{stream.close();}
                    stateScreenshotPaths.put(state.getGraphId(),"ape-state-screenshots/"+file.getName());
                    timing.count("state_screenshot_saved",1);
                } catch(Exception error){screenshotFailed(state,"write_failed");throw error;}
                finally {captured.recycle();imageSlots.release();screenshotPending.remove(state.getGraphId());}
            }});
            if(!accepted){captured.recycle();imageSlots.release();screenshotPending.remove(state.getGraphId());screenshotFailed(state,"output_rejected");}
        } catch(Exception e){if(bitmap!=null)bitmap.recycle();imageSlots.release();screenshotPending.remove(state.getGraphId());screenshotFailed(state,"capture_exception");output.fail(e);}
    }

    private JSONObject stateTelemetry(State state,String reason) {
        StringBuilder unvisited=new StringBuilder();
        int modelActions=0,enabledValid=0;
        int unsaturated=state.getUnsaturatedActions().size();
        int nativeUnsaturated=0;
        int unvisitedCount=0,enabledUnvisited=0;
        int effectiveUnvisited=0,navigation=0,local=0;
        for(ModelAction action:state.getActions()) {
            modelActions++;
            if(ActionFilter.ENABLED_VALID.include(action))enabledValid++;
            if(ActionFilter.ENABLED_VALID.include(action) && !action.isSaturated())nativeUnsaturated++;
            if(action.isUnvisited()) {
                if(unvisited.length()>0)unvisited.append(',');
                unvisited.append(action.getGraphId());
                unvisitedCount++;
            }
            if(ActionFilter.ENABLED_VALID.include(action) && action.isUnvisited())enabledUnvisited++;
            if(action.isUnvisited())effectiveUnvisited++;
            if(ActionFilter.ENABLED_VALID_UNSATURATED.include(action))navigation++;
        }
        float saturation=modelActions==0?1.0F:0.0F;
        for(ModelAction action:state.getActions())saturation+=action.getResolvedSaturation();
        if(modelActions>0)saturation/=modelActions;
        return JevRegistry.fields("snapshot_reason",reason,"state_visits",state.getVisitedCount(),
            "state_first_seen",state.getFirstVisitedTimestamp(),"state_last_seen",state.getLastVisitedTimestamp(),
            "state_saturated",nativeStateSaturated(state),"state_saturation",saturation,
            "native_state_saturated",nativeStateSaturated(state),"native_unsaturated_model_action_count",nativeUnsaturated,
            "model_action_count",modelActions,"enabled_valid_action_count",enabledValid,
            "unsaturated_model_action_count",unsaturated,"unvisited_model_action_count",unvisitedCount,
            "effective_unvisited_model_action_count",effectiveUnvisited,"effective_state_saturated",state.isSaturated(),
            "local_probe_action_count",local,"navigation_action_count",navigation,
            "enabled_valid_unvisited_action_count",enabledUnvisited,"unvisited_model_action_ids",unvisited.toString());
    }

    private boolean nativeStateSaturated(State state) {
        for(ModelAction action:state.getActions())
            if(ActionFilter.ENABLED_VALID.include(action) && !action.isSaturated())return false;
        return true;
    }
}
