design-855653c.diff
原始文件
1commit 855653cb77cd83d00fe5b431027146ac4a995f6b
2Author: Codex <codex@local>
3AuthorDate: Mon Nov 2 18:28:10 2026 +0800
4Commit: Codex <codex@local>
5CommitDate: Mon Nov 2 18:28:10 2026 +0800
6
7 Implement bounded probe review and promote reuse
8
9diff --git a/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java b/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java
10index 38e96b2..4a4d015 100644
11--- a/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java
12+++ b/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java
13@@ -8,6 +8,7 @@ import java.util.HashSet;
14 import java.util.Iterator;
15 import java.util.IdentityHashMap;
16 import java.util.LinkedHashMap;
17+import java.util.LinkedHashSet;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21@@ -35,6 +36,7 @@ public final class JevSemanticReview implements AutoCloseable {
22 public final String key;
23 public final String candidateId;
24 String evidenceScope;
25+ String reviewContract;
26 Candidate(ModelAction a,GUITreeNode n,String k,String id){action=a;node=n;key=k;candidateId=id;}
27 }
28 public static final class Selection {
29@@ -75,6 +77,9 @@ public final class JevSemanticReview implements AutoCloseable {
30 private long modelVersion=-1;
31 private final Set<String> inFlight=new HashSet<String>();
32 private final Set<String> served=new HashSet<String>();
33+ /* Run-local cursor for candidates that remain eligible after a bounded
34+ review. The ledger remains the only durable decision store. */
35+ private final Map<String,Integer> incrementalOffsets=new HashMap<String,Integer>();
36 private final String contract;
37 private final Map<String,Integer> fingerprintAttempts=new HashMap<String,Integer>();
38 private final ReceiptSink receiptSink;
39@@ -124,10 +129,10 @@ public final class JevSemanticReview implements AutoCloseable {
40 public void markExecuted(State state,ModelAction action,GUITreeNode node) {reviewLock.lock();try {
41 if(state==null||action==null||node==null)return;
42 String key=key(action,node);
43- registry.reviewLedger().markExecuted(action.getState()==null?"":action.getState().getActivity(),
44- ConcreteActionIdentity.of(action.getType().name(),node));
45+ ConcreteActionIdentity executedIdentity=ConcreteActionIdentity.of(action.getType().name(),node);
46+ registry.reviewLedger().markExecutedMatching(contract,action.getState()==null?"":action.getState().getActivity(),executedIdentity);
47 if(preparedPage!=null)for(Candidate c:preparedPage.candidates.values())if(c.key.equals(key))
48- registry.reviewLedger().markExecuted(scope(c),identity(c));
49+ registry.reviewLedger().markExecutedMatching(contract,scope(c.action),identity(c));
50 for(Cache cache:pages.values()) { cache.candidates.remove(key);cache.decisions.remove(key); }
51 inFlight.remove(key);
52 served.remove(key);
53@@ -165,25 +170,55 @@ public final class JevSemanticReview implements AutoCloseable {
54 if(state==null||tree==null)return;
55 JevDeadline.check();
56 if(!synchronous)poll();
57+ Map<String,Candidate> lightweight=collectCandidates(state,tree);
58+ if(lightweight.isEmpty())return;
59+ Set<String> roundKeys=selectRoundKeys(state,tree,lightweight);
60+ if(roundKeys.isEmpty())return;
61+ // Reusable PROMOTEs are executable work, not a reason to rebuild the
62+ // rich evidence plan. Keep only the lightweight candidates needed by
63+ // Native selection; the final node/action validator remains in
64+ // JevRuntime.nextSemanticProbe().
65+ if(hasReusablePromote(lightweight, state, tree, roundKeys)) {
66+ Map<String,Candidate> reusable=new LinkedHashMap<String,Candidate>();
67+ for(Map.Entry<String,Candidate> entry:lightweight.entrySet()) {
68+ if(!roundKeys.contains(entry.getKey()))continue;
69+ Candidate candidate=entry.getValue();
70+ candidate.reviewContract=contract;
71+ candidate.evidenceScope=scope(candidate.action)+"|reusable";
72+ reusable.put(entry.getKey(),candidate);
73+ }
74+ preparedPage=new Page(state.getGraphId()+"|reusable",new JSONObject(),reusable);
75+ preparedState=state;preparedTree=tree;preparedVersion=modelVersion;
76+ preparedExecutionRevision=registry.executionRevision(state);preparedConfirmationCount=registry.confirmationCount();
77+ return;
78+ }
79 Page page;long evidenceStart=System.nanoTime();
80- try {page=page(state,tree);}finally{localEvidenceNanos+=System.nanoTime()-evidenceStart;}
81+ try {page=page(state,tree,roundKeys);}finally{localEvidenceNanos+=System.nanoTime()-evidenceStart;}
82 preparedPage=page;preparedState=state;preparedTree=tree;preparedVersion=modelVersion;
83 preparedExecutionRevision=registry.executionRevision(state);preparedConfirmationCount=registry.confirmationCount();
84 if(page==null||page.candidates.isEmpty()||page.payload.optJSONArray("pairs").length()==0)return;
85+ // A staged PROMOTE is an executable piece of work. Do not spend this
86+ // review window constructing/submitting a new model request when the
87+ // current GUI can still execute one of the old probes.
88+ if(hasReusablePromote(page,state,tree))return;
89 int retries=synchronous?resumeRetained(state,tree,page,deadline):0;
90 if(retries<0)return;
91 Map<String,Candidate> pending=new LinkedHashMap<String,Candidate>();
92- for(Map.Entry<String,Candidate> entry:page.candidates.entrySet())
93- if(registry.reviewLedger().get(contract,scope(entry.getValue()),identity(entry.getValue()))==null&&!inFlight.contains(entry.getKey()))pending.put(entry.getKey(),entry.getValue());
94+ String currentGui=JevEvidence.guiIdentity(tree);long currentRevision=registry.executionRevision(state);
95+ for(Map.Entry<String,Candidate> entry:page.candidates.entrySet()) {
96+ Candidate candidate=entry.getValue();
97+ JevReviewLedger.Decision prior=registry.reviewLedger().boundDecision(contract,scope(candidate.action),identity(candidate),currentGui,modelVersion,currentRevision,registry.confirmationCount());
98+ if(prior==null && !inFlight.contains(entry.getKey()))pending.put(entry.getKey(),candidate);
99+ }
100 if(pending.isEmpty())return;
101 try {
102 Page requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,pending),pending);
103 if(requestPage.payload.getJSONArray("pairs").length()==0)return;
104 Cache cache=pages.get(requestPage.cacheKey);
105 if(cache!=null)return;
106- JevHistoryPairwiseProtocol.Request request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes);
107+ JevHistoryPairwiseProtocol.Prepared prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes);
108 Map<ConcreteActionIdentity,JevReviewLedger.Decision> fingerprintHits=
109- registry.reviewLedger().byFingerprint(contract,scopeForPage(pending),request.inputFingerprint);
110+ registry.reviewLedger().byFingerprint(contract,scopeForPage(pending),prepared.inputFingerprint);
111 Map<String,Candidate> fingerprintMisses=new LinkedHashMap<String,Candidate>();
112 for(Map.Entry<String,Candidate> entry:pending.entrySet())
113 if(!fingerprintHits.containsKey(identity(entry.getValue())))fingerprintMisses.put(entry.getKey(),entry.getValue());
114@@ -191,8 +226,9 @@ public final class JevSemanticReview implements AutoCloseable {
115 if(fingerprintMisses.size()!=pending.size()) {
116 requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,fingerprintMisses),fingerprintMisses);
117 if(requestPage.payload.getJSONArray("pairs").length()==0)return;
118- request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes);
119+ prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes);
120 }
121+ JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),requestPage.payload);
122 while(true) {
123 long remaining=deadline-System.nanoTime();
124 if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {
125@@ -224,7 +260,7 @@ public final class JevSemanticReview implements AutoCloseable {
126 pages.put(requestPage.cacheKey,submitted);
127 if(synchronous)archiveOnArrival(submitted,currentStep);
128 inFlight.addAll(requestPage.candidates.keySet());
129- for(Candidate candidate:pending.values())registry.reviewLedger().bind(contract,scope(candidate),identity(candidate),sourceGui);
130+ for(Candidate candidate:pending.values())registry.reviewLedger().bind(contract,scope(candidate),identity(candidate),sourceGui,modelVersion,historyRevision,registry.confirmationCount());
131 if(!synchronous)return;
132 registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields(
133 "request_id",request.requestId,"candidate_count",requestPage.candidates.size(),"attempt",attempt));
134@@ -238,6 +274,17 @@ public final class JevSemanticReview implements AutoCloseable {
135 }
136 } catch(JSONException ignored) {
137 if("SEMANTIC_REQUEST_BUDGET_EXCEEDED".equals(ignored.getMessage())) {
138+ if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) {
139+ try {
140+ if(submitProjected(state,tree,page,pending,deadline,retries))return;
141+ } catch(JSONException projectedError) {
142+ healthy=false;localFailure=true;selectionBlocked=true;
143+ registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields(
144+ "reason","REQUEST_BUILD_FAILED","error",projectedError.getMessage(),
145+ "evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION));
146+ return;
147+ }
148+ }
149 selectionBlocked=true;
150 JSONObject fields=JevRegistry.fields("reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId);
151 if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) {
152@@ -252,6 +299,152 @@ public final class JevSemanticReview implements AutoCloseable {
153 }
154 }
155 }
156+ private boolean hasReusablePromote(Page page,State state,GUITree tree) {
157+ String gui=JevEvidence.guiIdentity(tree);
158+ for(Candidate candidate:page.candidates.values())
159+ if(reusableDecision(candidate,gui,state)!=null)return true;
160+ return false;
161+ }
162+ private boolean hasReusablePromote(Map<String,Candidate> candidates,State state,GUITree tree,Set<String> keys) {
163+ String gui=JevEvidence.guiIdentity(tree);
164+ for(Map.Entry<String,Candidate> entry:candidates.entrySet()) {
165+ if(!keys.contains(entry.getKey()))continue;
166+ if(reusableDecision(entry.getValue(),gui,state)!=null)return true;
167+ }
168+ return false;
169+ }
170+ private Map<String,Candidate> collectCandidates(State state,GUITree tree) {
171+ Map<String,Candidate> candidates=new LinkedHashMap<String,Candidate>();Set<String> ambiguous=new HashSet<String>();int number=0;
172+ for(ModelAction action:state.getActions()) {
173+ JevDeadline.check();
174+ if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
175+ GUITreeNode[] nodes=action.getResolvedNodes();
176+ if(nodes==null||nodes.length<MIN_CONCRETE_ACTIONS||action.getResolvedGUITree()!=tree)continue;
177+ for(GUITreeNode node:nodes) {
178+ JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue;
179+ String id="c"+(++number),identity=key(action,node);
180+ if(ambiguous.contains(identity))continue;
181+ if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);}
182+ else candidates.put(identity,new Candidate(action,node,identity,id));
183+ }
184+ }
185+ return candidates;
186+ }
187+ private Set<String> selectRoundKeys(State state,GUITree tree,Map<String,Candidate> all) {
188+ String gui=JevEvidence.guiIdentity(tree);long revision=registry.executionRevision(state);List<Candidate> reusable=new ArrayList<Candidate>();
189+ List<Candidate> fresh=new ArrayList<Candidate>();
190+ for(Candidate c:all.values()) {
191+ JevReviewLedger.Decision prior=registry.reviewLedger().boundDecision(contract,scope(c.action),identity(c),gui,modelVersion,revision,registry.confirmationCount());
192+ if(prior!=null && "PROBE".equals(prior.outcome))reusable.add(c);
193+ else if(prior==null)fresh.add(c);
194+ }
195+ if(!reusable.isEmpty()) {Set<String> out=new LinkedHashSet<String>();for(Candidate c:reusable)out.add(c.key);return out;}
196+ if(fresh.size()<=3){Set<String> out=new LinkedHashSet<String>();for(Candidate c:fresh)out.add(c.key);return out;}
197+ List<Candidate> values=fresh;String cursorKey=state.getGraphId()+"|"+scope(values.get(0).action);
198+ int offset=incrementalOffsets.containsKey(cursorKey)?incrementalOffsets.get(cursorKey):0;offset=((offset%values.size())+values.size())%values.size();
199+ int count=(values.size()+1)/2;Set<String> out=new LinkedHashSet<String>();
200+ for(int i=0;i<count;i++)out.add(values.get((offset+i)%values.size()).key);
201+ incrementalOffsets.put(cursorKey,offset+count);return out;
202+ }
203+ private JevReviewLedger.Decision reusableDecision(Candidate candidate,String gui,State state) {
204+ // Ledger entries may be scoped by the complete evidence signature;
205+ // lookup starts at the stable activity scope and accepts that suffix.
206+ return registry.reviewLedger().reusablePromote(contract,scope(candidate.action),identity(candidate),gui,modelVersion,registry.executionRevision(state),registry.confirmationCount());
207+ }
208+ /**
209+ * Build and submit candidate-closure views only after the legacy full view
210+ * has been proven over budget. Top-3 and all coverage/provenance are taken
211+ * from the already-frozen full plan; this method never ranks a projection.
212+ */
213+ private boolean submitProjected(State state,GUITree tree,Page page,Map<String,Candidate> pending,long deadline,int retries)throws JSONException {
214+ JevPairwiseEvidenceView.Plan projection=new JevPairwiseEvidenceView.Plan(page.payload);
215+ List<Candidate> ordered=new ArrayList<Candidate>();
216+ for(Candidate candidate:pending.values())
217+ if(page.payload.getJSONObject("coverage").getJSONObject(candidate.candidateId).getInt("selected_history")>0)ordered.add(candidate);
218+ int offset=0;
219+ while(offset<ordered.size()) {
220+ LinkedHashMap<String,Candidate> batch=new LinkedHashMap<String,Candidate>();
221+ JevHistoryPairwiseProtocol.Prepared prepared=null;JSONObject projected=null;
222+ while(offset<ordered.size()) {
223+ JevDeadline.check();Candidate next=ordered.get(offset);batch.put(next.key,next);
224+ List<String> selectedIds=new ArrayList<String>();
225+ for(Candidate c:batch.values())selectedIds.add(c.candidateId);
226+ JSONObject trial=projection.project(selectedIds);
227+ try {
228+ JevHistoryPairwiseProtocol.Prepared candidatePrepared=JevHistoryPairwiseProtocol.prepare(model,trial,maxBytes);
229+ prepared=candidatePrepared;projected=trial;offset++;
230+ } catch(JevHistoryPairwiseProtocol.BudgetExceeded tooLarge) {
231+ batch.remove(next.key);
232+ if(!batch.isEmpty())break;
233+ // An indivisible candidate + its complete original Top-3
234+ // cannot fit. Skip that closure; do not discard evidence.
235+ budgetFailure(state,tooLarge,next.candidateId,JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);
236+ offset++;
237+ }
238+ }
239+ if(batch.isEmpty())continue;
240+ if(prepared==null || projected==null)throw new JSONException("PROJECTED_BATCH_EMPTY");
241+ for(Candidate candidate:batch.values()) {
242+ candidate.reviewContract=projectedContract();
243+ candidate.evidenceScope=scope(candidate.action)+"|"+prepared.inputSignature;
244+ }
245+ Map<ConcreteActionIdentity,JevReviewLedger.Decision> hits=registry.reviewLedger().byFingerprint(
246+ projectedContract(),scopeForPage(batch),prepared.inputFingerprint);
247+ boolean allCached=true;
248+ for(Candidate candidate:batch.values())if(!hits.containsKey(identity(candidate))){allCached=false;break;}
249+ if(allCached)continue; // no Request.plan deep copy on a view-local cache hit
250+ String cacheKey=page.cacheKey+"|"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION+"|"+prepared.inputFingerprint;
251+ if(pages.containsKey(cacheKey))continue;
252+ JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);
253+ while(true) {
254+ long remaining=deadline-System.nanoTime();
255+ if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {failOpen(state,"DEADLINE");return true;}
256+ JevDeadline.check();int attempt=nextAttempt(request.inputFingerprint);
257+ String sourceGui=JevEvidence.guiIdentity(tree);long historyRevision=registry.executionRevision(state);
258+ receipt("semantic-request-"+request.requestId,requestReceipt(request,batch,attempt));
259+ registry.semanticEvent("JEV_REQUEST_SENT",currentStep,state.getGraphId(),null,JevRegistry.fields(
260+ "request_id",request.requestId,"review_id",reviewId,"candidate_count",batch.size(),"pair_count",request.questionCount,
261+ "request_bytes",request.requestBytes,"wait_mode",synchronous?"synchronous":"asynchronous","attempt",attempt,
262+ "input_fingerprint",request.inputFingerprint,"evidence_view_version",request.evidenceViewVersion));
263+ JevDeadline.check();
264+ JevTransport.Call call=synchronous
265+ ?transport.submitRetained(request.requestId,request.bodyJson,requestLifetimeMs,Math.max(16384,maxBytes))
266+ :transport.submit(request.requestId,request.bodyJson,timeoutMs,Math.max(16384,maxBytes));
267+ if(call==null) {
268+ requestFailure(request,attempt,"QUEUE_FULL",0,null);
269+ if(retries++<MAX_RETRIES){request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);continue;}
270+ failOpen(state,"QUEUE_FULL");return true;
271+ }
272+ Cache submitted=new Cache(batch,request,call,attempt,sourceGui);
273+ submitted.sourceVersion=modelVersion;submitted.historyRevision=historyRevision;submitted.confirmationCount=registry.confirmationCount();
274+ pages.put(cacheKey,submitted);if(synchronous)archiveOnArrival(submitted,currentStep);
275+ inFlight.addAll(batch.keySet());
276+ for(Candidate candidate:batch.values())registry.reviewLedger().bind(projectedContract(),scope(candidate),identity(candidate),sourceGui,modelVersion,historyRevision,registry.confirmationCount());
277+ if(!synchronous)break;
278+ registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields(
279+ "request_id",request.requestId,"candidate_count",batch.size(),"attempt",attempt,
280+ "evidence_view_version",request.evidenceViewVersion));
281+ JevTransport.Result response=await(call,deadline);JevDeadline.check();
282+ if(response==null)throw new JevDeadline.Expired();
283+ if(consumeCache(cacheKey,submitted,response))break;
284+ if(retries++>=MAX_RETRIES){failOpen(state,"RETRIES_EXHAUSTED");return true;}
285+ request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);
286+ }
287+ // One bounded logical request per Probe entry. Remaining eligible
288+ // candidates stay unreviewed and are selected by the next round's
289+ // rotating cursor; this avoids turning a large page into an
290+ // implicit packet fan-out.
291+ return true;
292+ }
293+ return true;
294+ }
295+ private void budgetFailure(State state,JevHistoryPairwiseProtocol.BudgetExceeded size,String candidateId,String version) {
296+ registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields(
297+ "reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId,
298+ "candidate_id",candidateId,"evidence_view_version",version,"request_bytes",size.requestBytes,
299+ "state_bytes",size.stateBytes,"questions_bytes",size.questionsBytes,"candidate_count",size.candidateCount,
300+ "pair_count",size.pairCount,"limit_bytes",size.limitBytes));
301+ }
302 private JevTransport.Result await(JevTransport.Call call,long deadline) {
303 long began=System.nanoTime();
304 try{return call.awaitUntil(deadline);}finally{providerWaitNanos+=System.nanoTime()-began;}
305@@ -279,7 +472,7 @@ public final class JevSemanticReview implements AutoCloseable {
306 try {
307 JSONObject value=new JSONObject().put("schema",1).put("kind","semantic_provider_arrival")
308 .put("request_id",request.requestId).put("endpoint",endpoint).put("model",model)
309- .put("contract",contract).put("source_step",sourceStep).put("source_version",sourceVersion)
310+ .put("contract",requestContract(request)).put("source_step",sourceStep).put("source_version",sourceVersion)
311 .put("history_revision",historyRevision).put("source_gui",sourceGui)
312 .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint)
313 .put("http_status",result.httpStatus).put("error",result.error==null?JSONObject.NULL:result.error)
314@@ -287,6 +480,7 @@ public final class JevSemanticReview implements AutoCloseable {
315 .put("submitted_ns",result.submittedNanos).put("sent_ns",result.sentNanos)
316 .put("completed_ns",result.completedNanos).put("wait_ns",result.waitNanos())
317 .put("validated",false).put("applied",false);
318+ viewMetadata(value,request);
319 if(!receiptSink.save("semantic-arrival-"+request.requestId,value))healthy=false;
320 }catch(JSONException e){healthy=false;}catch(RuntimeException e){healthy=false;}
321 }});}catch(RejectedExecutionException e){healthy=false;}
322@@ -300,7 +494,7 @@ public final class JevSemanticReview implements AutoCloseable {
323 old.call.cancel("STALE_HISTORY_OR_VERSION");retained.remove(item.getKey());continue;
324 }
325 JevTransport.Result response=old.call.poll();
326- if(!old.request.inputSignature.equals(page.payload.optString("input_signature"))) {
327+ if(!old.request.sourceInputSignature.equals(page.payload.optString("input_signature"))) {
328 if(response!=null && response.error!=null)retained.remove(item.getKey());
329 continue;
330 }
331@@ -316,6 +510,10 @@ public final class JevSemanticReview implements AutoCloseable {
332 if(current.size()!=old.request.candidateIds.size()) {
333 old.call.cancel("STALE_CANDIDATES");retained.remove(item.getKey());continue;
334 }
335+ if(!old.request.evidenceViewVersion.isEmpty())for(Candidate candidate:current.values()) {
336+ candidate.evidenceScope=scope(candidate.action)+"|"+old.request.inputSignature;
337+ candidate.reviewContract=effectiveContract(old);
338+ }
339 Cache rebound=new Cache(current,old.request,old.call,old.attempt,JevEvidence.guiIdentity(tree));
340 rebound.sourceVersion=old.sourceVersion;rebound.historyRevision=old.historyRevision;
341 rebound.confirmationCount=old.confirmationCount;
342@@ -324,7 +522,10 @@ public final class JevSemanticReview implements AutoCloseable {
343 if(response==null)response=await(old.call,deadline);
344 JevDeadline.check();
345 if(response==null)throw new JevDeadline.Expired();
346- if(consumeCache(item.getKey(),rebound,response))return -1;
347+ if(consumeCache(item.getKey(),rebound,response)) {
348+ if(old.request.evidenceViewVersion.isEmpty())return -1;
349+ continue;
350+ }
351 // Protocol / HTTP failures remain failures, rather than reusable semantic answers.
352 return 1;
353 }
354@@ -334,12 +535,13 @@ public final class JevSemanticReview implements AutoCloseable {
355 final String cacheKey;final JSONObject payload;final Map<String,Candidate> candidates;
356 Page(String k,JSONObject p,Map<String,Candidate> c){cacheKey=k;payload=p;candidates=c;}
357 }
358- private Page page(final State state,final GUITree tree) {
359+ private Page page(final State state,final GUITree tree) { return page(state,tree,null); }
360+ private Page page(final State state,final GUITree tree,final Set<String> include) {
361 return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<Page>() {
362- public Page get(){return buildPage(state,tree);}
363+ public Page get(){return buildPage(state,tree,include);}
364 });
365 }
366- private Page buildPage(State state,GUITree tree) {
367+ private Page buildPage(State state,GUITree tree,Set<String> include) {
368 long began=System.nanoTime();
369 try {
370 JevRegistry.ConfirmedHistory history=registry.confirmedHistory(state);
371@@ -353,6 +555,7 @@ public final class JevSemanticReview implements AutoCloseable {
372 for(GUITreeNode node:nodes) {
373 JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue;
374 String id="c"+(++number),identity=key(action,node);
375+ if(include!=null&&!include.contains(identity))continue;
376 if(ambiguous.contains(identity))continue;
377 if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);}
378 else candidates.put(identity,new Candidate(action,node,identity,id));
379@@ -364,39 +567,55 @@ public final class JevSemanticReview implements AutoCloseable {
380 JSONObject recoverable=new JSONObject();Set<String> usedHistory=new java.util.LinkedHashSet<String>(),usedPages=new HashSet<String>();
381 IdentityHashMap<GUITree,String> pageIds=new IdentityHashMap<GUITree,String>();
382 IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,String> historyIds=new IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,String>();
383+ IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,JevEvidence.PairDescriptor> historyDescriptors=new IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,JevEvidence.PairDescriptor>();
384+ Map<String,com.android.commands.monkey.ape.tree.GUITreeAction> historyById=new LinkedHashMap<String,com.android.commands.monkey.ape.tree.GUITreeAction>();
385+ int recoverableHistoryCount=0;
386 for(Candidate c:candidates.values()) {
387 JevDeadline.check();ids.put(c.candidateId);
388 String pageId=sourcePage(tree,pages,pageIds);
389 JSONObject control=JevEvidence.pairAction(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action),pageId);
390 if(control!=null)current.put(c.candidateId,control);
391 List<com.android.commands.monkey.ape.tree.GUITreeAction> members=history.members(c.action);
392- Map<String,JSONObject> pool=new LinkedHashMap<String,JSONObject>();
393+ Map<String,Set<String>> featurePool=new LinkedHashMap<String,Set<String>>();
394 for(com.android.commands.monkey.ape.tree.GUITreeAction h:members) {
395 JevDeadline.check();String hid=historyIds.get(h);
396 if(hid==null) {
397 hid="h"+(historyIds.size()+1);historyIds.put(h,hid);
398- JSONObject witness=JevEvidence.pairAction(h,sourcePage(h.getGUITree(),pages,pageIds));
399- if(witness!=null)recoverable.put(hid,witness);
400+ historyById.put(hid,h);
401+ sourcePage(h.getGUITree(),pages,pageIds); // preserve old Page ID allocation order
402+ JevEvidence.PairDescriptor descriptor=JevEvidence.pairDescriptor(h);
403+ historyDescriptors.put(h,descriptor);
404+ if(descriptor.recoverable)recoverableHistoryCount++;
405 provenance.put(hid,new JSONObject().put("source_tree",h.getGUITree().getId())
406 .put("source_gui_timestamp",h.getGUITree().getTimestamp()).put("current_state",state.getGraphId())
407 .put("current_model_action",h.getModelAction().getGraphId()).put("current_version",modelVersion)
408 .put("input_confirmed",true).put("confirmation_basis","live confirmedInputs before review; not function completion")
409- .put("attributes_recoverable",witness!=null));
410+ .put("attributes_recoverable",descriptor.recoverable));
411 }
412- if(recoverable.has(hid))pool.put(hid,recoverable.getJSONObject(hid));
413+ JevEvidence.PairDescriptor descriptor=historyDescriptors.get(h);
414+ if(descriptor!=null && descriptor.recoverable)featurePool.put(hid,descriptor.selectionFeatures);
415 }
416- List<String> selected=control==null?Collections.<String>emptyList():JevHistoryPairwiseProtocol.selectHistory(control,pool);
417+ List<String> selected=control==null?Collections.<String>emptyList():JevHistoryPairwiseProtocol.selectHistory(control,featurePool.keySet(),featurePool);
418 JSONArray selectedIds=new JSONArray();
419 for(String hid:selected){
420 selectedIds.put(hid);usedHistory.add(hid);
421 pairs.put(new JSONObject().put("id","p"+(pairs.length()+1)).put("candidate",c.candidateId).put("history",hid));
422 }
423- coverage.put(c.candidateId,new JSONObject().put("legal_history_count",members.size()).put("complete",history.complete && pool.size()==members.size())
424- .put("history_collection_complete",history.complete).put("recoverable_history_count",pool.size()).put("selected_history",selected.size()).put("selected_history_ids",selectedIds)
425+ coverage.put(c.candidateId,new JSONObject().put("legal_history_count",members.size()).put("complete",history.complete && featurePool.size()==members.size())
426+ .put("history_collection_complete",history.complete).put("recoverable_history_count",featurePool.size()).put("selected_history",selected.size()).put("selected_history_ids",selectedIds)
427 .put("unknown_mapping_count",history.unknown)
428 .put("mapping_basis","live confirmedInputs; current State/ModelAction object links and node Name")
429 .put("context_recoverable",control!=null));
430 }
431+ // Only the selected history witnesses receive the expensive rich JSON form.
432+ // Descriptor extraction above has already frozen the exact ranking inputs.
433+ for(String hid:usedHistory) {
434+ com.android.commands.monkey.ape.tree.GUITreeAction h=historyById.get(hid);
435+ if(h==null)throw new IllegalStateException("missing selected history descriptor: "+hid);
436+ JSONObject witness=JevEvidence.pairAction(h,pageIds.get(h.getGUITree()));
437+ if(witness==null)throw new IllegalStateException("selected history became unrecoverable: "+hid);
438+ recoverable.put(hid,witness);
439+ }
440 // Send source context only for current candidates and selected witnesses, once per source page.
441 for(Iterator<?> it=current.keys();it.hasNext();)usedPages.add(current.getJSONObject((String)it.next()).getString("source_page_id"));
442 for(String hid:usedHistory){JSONObject witness=recoverable.getJSONObject(hid);executed.put(hid,witness);usedPages.add(witness.getString("source_page_id"));}
443@@ -410,11 +629,11 @@ public final class JevSemanticReview implements AutoCloseable {
444 .put("confirmation_count",history.total).put("unknown_mapping_count",history.unknown)
445 .put("execution_revision",registry.executionRevision(state));
446 String signature=JevEvidence.digest(payload.toString());payload.put("input_signature",signature);
447- for(Candidate c:candidates.values())c.evidenceScope=scope(c.action)+"|"+signature;
448+ for(Candidate c:candidates.values()){c.evidenceScope=scope(c.action)+"|"+signature;c.reviewContract=contract;}
449 registry.semanticEvent("HISTORY_PAIRWISE_COST",currentStep,state.getGraphId(),null,JevRegistry.fields(
450 "history_enumeration_ns",history.enumerationNanos,"evidence_build_ns",System.nanoTime()-evidenceStart,
451 "prepare_local_ns",System.nanoTime()-began,"confirmed_input_objects",history.total,"unknown_mapping_count",history.unknown,
452- "history_complete",history.complete,"recoverable_history_count",recoverable.length(),"selected_history",executed.length(),"candidate_count",candidates.size(),"pair_count",pairs.length(),
453+ "history_complete",history.complete,"recoverable_history_count",recoverableHistoryCount,"selected_history",executed.length(),"candidate_count",candidates.size(),"pair_count",pairs.length(),
454 "evidence_bytes",stateEvidence.toString().getBytes(JevHistoryPairwiseProtocol.UTF8).length));
455 return new Page(state.getGraphId()+"|"+signature,payload,candidates);
456 }catch(JSONException error){throw new IllegalStateException("pair evidence construction failed",error);}
457@@ -426,7 +645,7 @@ public final class JevSemanticReview implements AutoCloseable {
458 return id;
459 }
460 private static JSONObject pendingPayload(JSONObject original,Map<String,Candidate> pending)throws JSONException {
461- JSONObject payload=new JSONObject(original.toString());JSONArray ids=new JSONArray(),pairs=new JSONArray();Set<String> selected=new HashSet<String>();
462+ JSONObject payload=JevPairwiseEvidenceView.shallowCopy(original);JSONArray ids=new JSONArray(),pairs=new JSONArray();Set<String> selected=new HashSet<String>();
463 for(Candidate c:pending.values()){ids.put(c.candidateId);selected.add(c.candidateId);}
464 JSONArray originalPairs=payload.getJSONArray("pairs");
465 for(int i=0;i<originalPairs.length();i++)if(selected.contains(originalPairs.getJSONObject(i).getString("candidate")))pairs.put(originalPairs.get(i));
466@@ -474,7 +693,7 @@ public final class JevSemanticReview implements AutoCloseable {
467 inFlight.remove(candidate.key);String outcome=decision.promote()?"PROBE":"REPRESENTED";
468 String ledgerStatus="REVIEWED";String entryId=entryId(cache.request.requestId,candidate.candidateId,cache.request.inputFingerprint);
469 if(decision.resolved && decision.confidence>0.5) {
470- updates.add(new JevReviewLedger.Update(contract,scope(candidate),identity(candidate),outcome,decision.confidence,decision.representativeModelActionId,
471+ updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),outcome,decision.confidence,decision.representativeModelActionId,
472 cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));ledgerStatus="STAGED";
473 auditEvents.add(new Object[]{decision.promote()?"DECISION_PROBE":"DECISION_REPRESENTED",currentStep,
474 candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(),
475@@ -486,7 +705,7 @@ public final class JevSemanticReview implements AutoCloseable {
476 "decision_request_id",cache.request.requestId))});
477 if(!registry.hasExactExecution(candidate.action.getState(),candidate.action,candidate.action.getResolvedGUITree(),candidate.node))cache.decisions.put(candidate.key,decision);
478 } else {
479- updates.add(new JevReviewLedger.Update(contract,scope(candidate),identity(candidate),"UNRESOLVED",decision.confidence,decision.representativeModelActionId,
480+ updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),"UNRESOLVED",decision.confidence,decision.representativeModelActionId,
481 cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));
482 auditEvents.add(new Object[]{"DECISION_REVIEWED",currentStep,candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(),
483 decision.audit(JevRegistry.fields("entry_id",entryId,"request_id",cache.request.requestId,"candidate_id",candidate.candidateId,"confidence",decision.confidence,
484@@ -535,8 +754,9 @@ public final class JevSemanticReview implements AutoCloseable {
485 try {
486 JSONObject receipt=response==null?new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response")
487 .put("request_id",request.requestId).put("attempt",attempt).put("retry_count",attempt-1).put("endpoint",endpoint).put("model",model)
488- .put("contract",contract).put("step",currentStep).put("input_fingerprint",request.inputFingerprint).put("http_status",status)
489+ .put("contract",requestContract(request)).put("step",currentStep).put("input_fingerprint",request.inputFingerprint).put("http_status",status)
490 .put("error",error).put("raw_response",JSONObject.NULL):responseReceipt(new Cache(Collections.<String,Candidate>emptyMap(),request,null,attempt,""),response,null,error);
491+ viewMetadata(receipt,request);
492 receipt("semantic-response-"+request.requestId,receipt);
493 } catch(JSONException ignored) { }
494 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",request.requestId,"reason",error,
495@@ -549,7 +769,7 @@ public final class JevSemanticReview implements AutoCloseable {
496 if(page==null || state!=preparedState || tree!=preparedTree || modelVersion!=preparedVersion
497 || preparedConfirmationCount!=registry.confirmationCount() || preparedExecutionRevision!=registry.executionRevision(state))return null;
498 for(Candidate candidate:page.candidates.values()) {
499- JevReviewLedger.Decision shared=registry.reviewLedger().get(contract,scope(candidate),identity(candidate));
500+ JevReviewLedger.Decision shared=reusableDecision(candidate,JevEvidence.guiIdentity(tree),state);
501 Cache cached=pages.get(page.cacheKey);
502 JevHistoryPairwiseProtocol.Decision decision=shared==null?null:new JevHistoryPairwiseProtocol.Decision(candidate.candidateId,
503 shared.representativeModelActionId==null?("PROBE".equals(shared.outcome)?JevHistoryPairwiseProtocol.PROMOTE:"REPRESENTED"):shared.representativeModelActionId,
504@@ -560,7 +780,7 @@ public final class JevSemanticReview implements AutoCloseable {
505 if(shared!=null && !"PROBE".equals(shared.outcome))continue;
506 if(decision==null||!decision.promote()||decision.confidence<=0.5||served.contains(candidate.key))continue;
507 if(!navigationMask.allow(state,tree,candidate))continue;
508- registry.reviewLedger().bind(contract,scope(candidate),identity(candidate),JevEvidence.guiIdentity(tree));
509+ registry.reviewLedger().bind(candidate.reviewContract==null?contract:candidate.reviewContract,scope(candidate),identity(candidate),JevEvidence.guiIdentity(tree),modelVersion,registry.executionRevision(state),registry.confirmationCount());
510 String requestId=shared!=null?shared.requestId:(cached==null?null:cached.request.requestId);
511 String fingerprint=shared!=null?shared.inputFingerprint:(cached==null?null:cached.request.inputFingerprint);
512 served.add(candidate.key);return new Selection(candidate,entryId(requestId,candidate.candidateId,fingerprint),requestId,decision.confidence,decision.representativeModelActionId);
513@@ -581,6 +801,14 @@ public final class JevSemanticReview implements AutoCloseable {
514 private static String scope(Candidate candidate) {
515 return candidate==null||candidate.evidenceScope==null?"":candidate.evidenceScope;
516 }
517+ private String projectedContract() { return contract+":"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION; }
518+ private JevReviewLedger.Decision ledgerDecision(Candidate candidate) {
519+ return registry.reviewLedger().get(candidate.reviewContract==null?contract:candidate.reviewContract,scope(candidate),identity(candidate));
520+ }
521+ private String requestContract(JevHistoryPairwiseProtocol.Request request) {
522+ return request.evidenceViewVersion.isEmpty()?contract:projectedContract();
523+ }
524+ private String effectiveContract(Cache cache) { return requestContract(cache.request); }
525 private static String scope(ModelAction action) {
526 return action==null||action.getState()==null?"":action.getState().getActivity();
527 }
528@@ -616,28 +844,33 @@ public final class JevSemanticReview implements AutoCloseable {
529 }
530 private JSONObject requestReceipt(JevHistoryPairwiseProtocol.Request request,Map<String,Candidate> candidates,int attempt) {
531 try {JSONArray ids=new JSONArray();for(Candidate c:candidates.values())ids.put(c.candidateId);
532- return new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_request":"async_semantic_request").put("request_id",request.requestId)
533- .put("attempt",attempt).put("endpoint",endpoint).put("model",model).put("contract",contract).put("step",currentStep)
534+ JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_request":"async_semantic_request").put("request_id",request.requestId)
535+ .put("attempt",attempt).put("endpoint",endpoint).put("model",model).put("contract",requestContract(request)).put("step",currentStep)
536 .put("retry_count",attempt-1)
537 .put("request_lifetime_ms",synchronous?requestLifetimeMs:timeoutMs).put("late_response_reuse",synchronous)
538 .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint).put("candidate_ids",ids)
539 .put("pair_count",request.questionCount).put("history_coverage",request.plan.getJSONObject("coverage"))
540 .put("history_provenance",request.plan.getJSONObject("history_provenance"))
541 .put("request_bytes",request.requestBytes).put("timeout_ms",timeoutMs).put("body",new JSONObject(request.bodyJson));
542+ viewMetadata(value,request);return value;
543 } catch(JSONException error) { return new JSONObject(); }
544 }
545 private JSONObject responseReceipt(Cache cache,JevTransport.Result response,JSONArray decisions,String error) {
546 try {JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response")
547 .put("request_id",cache.request.requestId).put("attempt",cache.attempt).put("endpoint",endpoint).put("model",model)
548 .put("retry_count",cache.attempt-1)
549- .put("contract",contract).put("step",currentStep).put("input_signature",cache.request.inputSignature)
550+ .put("contract",effectiveContract(cache)).put("step",currentStep).put("input_signature",cache.request.inputSignature)
551 .put("input_fingerprint",cache.request.inputFingerprint).put("http_status",response.httpStatus)
552 .put("error",error==null?(response.error==null?JSONObject.NULL:response.error):error)
553 .put("wait_ns",response.waitNanos()).put("latency_ns",response.latencyNanos())
554 .put("raw_response",response.raw==null?JSONObject.NULL:response.raw);
555- if(decisions!=null)value.put("decisions",decisions);return value;
556+ if(decisions!=null)value.put("decisions",decisions);viewMetadata(value,cache.request);return value;
557 } catch(JSONException ignored) { return new JSONObject(); }
558 }
559+ private static void viewMetadata(JSONObject receipt,JevHistoryPairwiseProtocol.Request request)throws JSONException {
560+ if(!request.evidenceViewVersion.isEmpty())receipt.put("evidence_view_version",request.evidenceViewVersion)
561+ .put("source_input_signature",request.sourceInputSignature);
562+ }
563 private static String scopeForPage(Map<String,Candidate> candidates) {
564 for(Candidate candidate:candidates.values())return scope(candidate);
565 return "";
566@@ -665,6 +898,10 @@ public final class JevSemanticReview implements AutoCloseable {
567 receiptWorker.shutdownNow();transport.close();
568 if(preparedPage!=null)contextInvalidated(preparedPage,"run_closed");
569 List<JSONObject> snapshot=registry.reviewLedger().snapshot(contract);
570+ List<JSONObject> projected=registry.reviewLedger().snapshot(projectedContract());
571+ for(JSONObject row:projected)try {row.put("evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);}
572+ catch(JSONException error){throw new IllegalStateException(error);}
573+ snapshot.addAll(projected);
574 for(JSONObject row:snapshot)registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT",currentStep,null,null,row);
575 registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT_END",currentStep,null,null,JevRegistry.fields("decision_count",snapshot.size()));
576 pages.clear();retained.clear();inFlight.clear();served.clear();