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}