commit 855653cb77cd83d00fe5b431027146ac4a995f6b Author: Codex AuthorDate: Mon Nov 2 18:28:10 2026 +0800 Commit: Codex CommitDate: Mon Nov 2 18:28:10 2026 +0800 Implement bounded probe review and promote reuse diff --git a/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java b/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java index 38e96b2..4a4d015 100644 --- a/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java +++ b/native/src/com/android/commands/monkey/ape/jev/JevSemanticReview.java @@ -8,6 +8,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.IdentityHashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -35,6 +36,7 @@ public final class JevSemanticReview implements AutoCloseable { public final String key; public final String candidateId; String evidenceScope; + String reviewContract; Candidate(ModelAction a,GUITreeNode n,String k,String id){action=a;node=n;key=k;candidateId=id;} } public static final class Selection { @@ -75,6 +77,9 @@ public final class JevSemanticReview implements AutoCloseable { private long modelVersion=-1; private final Set inFlight=new HashSet(); private final Set served=new HashSet(); + /* Run-local cursor for candidates that remain eligible after a bounded + review. The ledger remains the only durable decision store. */ + private final Map incrementalOffsets=new HashMap(); private final String contract; private final Map fingerprintAttempts=new HashMap(); private final ReceiptSink receiptSink; @@ -124,10 +129,10 @@ public final class JevSemanticReview implements AutoCloseable { public void markExecuted(State state,ModelAction action,GUITreeNode node) {reviewLock.lock();try { if(state==null||action==null||node==null)return; String key=key(action,node); - registry.reviewLedger().markExecuted(action.getState()==null?"":action.getState().getActivity(), - ConcreteActionIdentity.of(action.getType().name(),node)); + ConcreteActionIdentity executedIdentity=ConcreteActionIdentity.of(action.getType().name(),node); + registry.reviewLedger().markExecutedMatching(contract,action.getState()==null?"":action.getState().getActivity(),executedIdentity); if(preparedPage!=null)for(Candidate c:preparedPage.candidates.values())if(c.key.equals(key)) - registry.reviewLedger().markExecuted(scope(c),identity(c)); + registry.reviewLedger().markExecutedMatching(contract,scope(c.action),identity(c)); for(Cache cache:pages.values()) { cache.candidates.remove(key);cache.decisions.remove(key); } inFlight.remove(key); served.remove(key); @@ -165,25 +170,55 @@ public final class JevSemanticReview implements AutoCloseable { if(state==null||tree==null)return; JevDeadline.check(); if(!synchronous)poll(); + Map lightweight=collectCandidates(state,tree); + if(lightweight.isEmpty())return; + Set roundKeys=selectRoundKeys(state,tree,lightweight); + if(roundKeys.isEmpty())return; + // Reusable PROMOTEs are executable work, not a reason to rebuild the + // rich evidence plan. Keep only the lightweight candidates needed by + // Native selection; the final node/action validator remains in + // JevRuntime.nextSemanticProbe(). + if(hasReusablePromote(lightweight, state, tree, roundKeys)) { + Map reusable=new LinkedHashMap(); + for(Map.Entry entry:lightweight.entrySet()) { + if(!roundKeys.contains(entry.getKey()))continue; + Candidate candidate=entry.getValue(); + candidate.reviewContract=contract; + candidate.evidenceScope=scope(candidate.action)+"|reusable"; + reusable.put(entry.getKey(),candidate); + } + preparedPage=new Page(state.getGraphId()+"|reusable",new JSONObject(),reusable); + preparedState=state;preparedTree=tree;preparedVersion=modelVersion; + preparedExecutionRevision=registry.executionRevision(state);preparedConfirmationCount=registry.confirmationCount(); + return; + } Page page;long evidenceStart=System.nanoTime(); - try {page=page(state,tree);}finally{localEvidenceNanos+=System.nanoTime()-evidenceStart;} + try {page=page(state,tree,roundKeys);}finally{localEvidenceNanos+=System.nanoTime()-evidenceStart;} preparedPage=page;preparedState=state;preparedTree=tree;preparedVersion=modelVersion; preparedExecutionRevision=registry.executionRevision(state);preparedConfirmationCount=registry.confirmationCount(); if(page==null||page.candidates.isEmpty()||page.payload.optJSONArray("pairs").length()==0)return; + // A staged PROMOTE is an executable piece of work. Do not spend this + // review window constructing/submitting a new model request when the + // current GUI can still execute one of the old probes. + if(hasReusablePromote(page,state,tree))return; int retries=synchronous?resumeRetained(state,tree,page,deadline):0; if(retries<0)return; Map pending=new LinkedHashMap(); - for(Map.Entry entry:page.candidates.entrySet()) - if(registry.reviewLedger().get(contract,scope(entry.getValue()),identity(entry.getValue()))==null&&!inFlight.contains(entry.getKey()))pending.put(entry.getKey(),entry.getValue()); + String currentGui=JevEvidence.guiIdentity(tree);long currentRevision=registry.executionRevision(state); + for(Map.Entry entry:page.candidates.entrySet()) { + Candidate candidate=entry.getValue(); + JevReviewLedger.Decision prior=registry.reviewLedger().boundDecision(contract,scope(candidate.action),identity(candidate),currentGui,modelVersion,currentRevision,registry.confirmationCount()); + if(prior==null && !inFlight.contains(entry.getKey()))pending.put(entry.getKey(),candidate); + } if(pending.isEmpty())return; try { Page requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,pending),pending); if(requestPage.payload.getJSONArray("pairs").length()==0)return; Cache cache=pages.get(requestPage.cacheKey); if(cache!=null)return; - JevHistoryPairwiseProtocol.Request request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes); + JevHistoryPairwiseProtocol.Prepared prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes); Map fingerprintHits= - registry.reviewLedger().byFingerprint(contract,scopeForPage(pending),request.inputFingerprint); + registry.reviewLedger().byFingerprint(contract,scopeForPage(pending),prepared.inputFingerprint); Map fingerprintMisses=new LinkedHashMap(); for(Map.Entry entry:pending.entrySet()) if(!fingerprintHits.containsKey(identity(entry.getValue())))fingerprintMisses.put(entry.getKey(),entry.getValue()); @@ -191,8 +226,9 @@ public final class JevSemanticReview implements AutoCloseable { if(fingerprintMisses.size()!=pending.size()) { requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,fingerprintMisses),fingerprintMisses); if(requestPage.payload.getJSONArray("pairs").length()==0)return; - request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes); + prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes); } + JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),requestPage.payload); while(true) { long remaining=deadline-System.nanoTime(); if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) { @@ -224,7 +260,7 @@ public final class JevSemanticReview implements AutoCloseable { pages.put(requestPage.cacheKey,submitted); if(synchronous)archiveOnArrival(submitted,currentStep); inFlight.addAll(requestPage.candidates.keySet()); - for(Candidate candidate:pending.values())registry.reviewLedger().bind(contract,scope(candidate),identity(candidate),sourceGui); + for(Candidate candidate:pending.values())registry.reviewLedger().bind(contract,scope(candidate),identity(candidate),sourceGui,modelVersion,historyRevision,registry.confirmationCount()); if(!synchronous)return; registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields( "request_id",request.requestId,"candidate_count",requestPage.candidates.size(),"attempt",attempt)); @@ -238,6 +274,17 @@ public final class JevSemanticReview implements AutoCloseable { } } catch(JSONException ignored) { if("SEMANTIC_REQUEST_BUDGET_EXCEEDED".equals(ignored.getMessage())) { + if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) { + try { + if(submitProjected(state,tree,page,pending,deadline,retries))return; + } catch(JSONException projectedError) { + healthy=false;localFailure=true;selectionBlocked=true; + registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields( + "reason","REQUEST_BUILD_FAILED","error",projectedError.getMessage(), + "evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION)); + return; + } + } selectionBlocked=true; JSONObject fields=JevRegistry.fields("reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId); if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) { @@ -252,6 +299,152 @@ public final class JevSemanticReview implements AutoCloseable { } } } + private boolean hasReusablePromote(Page page,State state,GUITree tree) { + String gui=JevEvidence.guiIdentity(tree); + for(Candidate candidate:page.candidates.values()) + if(reusableDecision(candidate,gui,state)!=null)return true; + return false; + } + private boolean hasReusablePromote(Map candidates,State state,GUITree tree,Set keys) { + String gui=JevEvidence.guiIdentity(tree); + for(Map.Entry entry:candidates.entrySet()) { + if(!keys.contains(entry.getKey()))continue; + if(reusableDecision(entry.getValue(),gui,state)!=null)return true; + } + return false; + } + private Map collectCandidates(State state,GUITree tree) { + Map candidates=new LinkedHashMap();Set ambiguous=new HashSet();int number=0; + for(ModelAction action:state.getActions()) { + JevDeadline.check(); + if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue; + GUITreeNode[] nodes=action.getResolvedNodes(); + if(nodes==null||nodes.length selectRoundKeys(State state,GUITree tree,Map all) { + String gui=JevEvidence.guiIdentity(tree);long revision=registry.executionRevision(state);List reusable=new ArrayList(); + List fresh=new ArrayList(); + for(Candidate c:all.values()) { + JevReviewLedger.Decision prior=registry.reviewLedger().boundDecision(contract,scope(c.action),identity(c),gui,modelVersion,revision,registry.confirmationCount()); + if(prior!=null && "PROBE".equals(prior.outcome))reusable.add(c); + else if(prior==null)fresh.add(c); + } + if(!reusable.isEmpty()) {Set out=new LinkedHashSet();for(Candidate c:reusable)out.add(c.key);return out;} + if(fresh.size()<=3){Set out=new LinkedHashSet();for(Candidate c:fresh)out.add(c.key);return out;} + List values=fresh;String cursorKey=state.getGraphId()+"|"+scope(values.get(0).action); + int offset=incrementalOffsets.containsKey(cursorKey)?incrementalOffsets.get(cursorKey):0;offset=((offset%values.size())+values.size())%values.size(); + int count=(values.size()+1)/2;Set out=new LinkedHashSet(); + for(int i=0;i pending,long deadline,int retries)throws JSONException { + JevPairwiseEvidenceView.Plan projection=new JevPairwiseEvidenceView.Plan(page.payload); + List ordered=new ArrayList(); + for(Candidate candidate:pending.values()) + if(page.payload.getJSONObject("coverage").getJSONObject(candidate.candidateId).getInt("selected_history")>0)ordered.add(candidate); + int offset=0; + while(offset batch=new LinkedHashMap(); + JevHistoryPairwiseProtocol.Prepared prepared=null;JSONObject projected=null; + while(offset selectedIds=new ArrayList(); + for(Candidate c:batch.values())selectedIds.add(c.candidateId); + JSONObject trial=projection.project(selectedIds); + try { + JevHistoryPairwiseProtocol.Prepared candidatePrepared=JevHistoryPairwiseProtocol.prepare(model,trial,maxBytes); + prepared=candidatePrepared;projected=trial;offset++; + } catch(JevHistoryPairwiseProtocol.BudgetExceeded tooLarge) { + batch.remove(next.key); + if(!batch.isEmpty())break; + // An indivisible candidate + its complete original Top-3 + // cannot fit. Skip that closure; do not discard evidence. + budgetFailure(state,tooLarge,next.candidateId,JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION); + offset++; + } + } + if(batch.isEmpty())continue; + if(prepared==null || projected==null)throw new JSONException("PROJECTED_BATCH_EMPTY"); + for(Candidate candidate:batch.values()) { + candidate.reviewContract=projectedContract(); + candidate.evidenceScope=scope(candidate.action)+"|"+prepared.inputSignature; + } + Map hits=registry.reviewLedger().byFingerprint( + projectedContract(),scopeForPage(batch),prepared.inputFingerprint); + boolean allCached=true; + for(Candidate candidate:batch.values())if(!hits.containsKey(identity(candidate))){allCached=false;break;} + if(allCached)continue; // no Request.plan deep copy on a view-local cache hit + String cacheKey=page.cacheKey+"|"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION+"|"+prepared.inputFingerprint; + if(pages.containsKey(cacheKey))continue; + JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),projected); + while(true) { + long remaining=deadline-System.nanoTime(); + if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {failOpen(state,"DEADLINE");return true;} + JevDeadline.check();int attempt=nextAttempt(request.inputFingerprint); + String sourceGui=JevEvidence.guiIdentity(tree);long historyRevision=registry.executionRevision(state); + receipt("semantic-request-"+request.requestId,requestReceipt(request,batch,attempt)); + registry.semanticEvent("JEV_REQUEST_SENT",currentStep,state.getGraphId(),null,JevRegistry.fields( + "request_id",request.requestId,"review_id",reviewId,"candidate_count",batch.size(),"pair_count",request.questionCount, + "request_bytes",request.requestBytes,"wait_mode",synchronous?"synchronous":"asynchronous","attempt",attempt, + "input_fingerprint",request.inputFingerprint,"evidence_view_version",request.evidenceViewVersion)); + JevDeadline.check(); + JevTransport.Call call=synchronous + ?transport.submitRetained(request.requestId,request.bodyJson,requestLifetimeMs,Math.max(16384,maxBytes)) + :transport.submit(request.requestId,request.bodyJson,timeoutMs,Math.max(16384,maxBytes)); + if(call==null) { + requestFailure(request,attempt,"QUEUE_FULL",0,null); + if(retries++=MAX_RETRIES){failOpen(state,"RETRIES_EXHAUSTED");return true;} + request=prepared.request("semantic-"+sequence.incrementAndGet(),projected); + } + // One bounded logical request per Probe entry. Remaining eligible + // candidates stay unreviewed and are selected by the next round's + // rotating cursor; this avoids turning a large page into an + // implicit packet fan-out. + return true; + } + return true; + } + private void budgetFailure(State state,JevHistoryPairwiseProtocol.BudgetExceeded size,String candidateId,String version) { + registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields( + "reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId, + "candidate_id",candidateId,"evidence_view_version",version,"request_bytes",size.requestBytes, + "state_bytes",size.stateBytes,"questions_bytes",size.questionsBytes,"candidate_count",size.candidateCount, + "pair_count",size.pairCount,"limit_bytes",size.limitBytes)); + } private JevTransport.Result await(JevTransport.Call call,long deadline) { long began=System.nanoTime(); try{return call.awaitUntil(deadline);}finally{providerWaitNanos+=System.nanoTime()-began;} @@ -279,7 +472,7 @@ public final class JevSemanticReview implements AutoCloseable { try { JSONObject value=new JSONObject().put("schema",1).put("kind","semantic_provider_arrival") .put("request_id",request.requestId).put("endpoint",endpoint).put("model",model) - .put("contract",contract).put("source_step",sourceStep).put("source_version",sourceVersion) + .put("contract",requestContract(request)).put("source_step",sourceStep).put("source_version",sourceVersion) .put("history_revision",historyRevision).put("source_gui",sourceGui) .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint) .put("http_status",result.httpStatus).put("error",result.error==null?JSONObject.NULL:result.error) @@ -287,6 +480,7 @@ public final class JevSemanticReview implements AutoCloseable { .put("submitted_ns",result.submittedNanos).put("sent_ns",result.sentNanos) .put("completed_ns",result.completedNanos).put("wait_ns",result.waitNanos()) .put("validated",false).put("applied",false); + viewMetadata(value,request); if(!receiptSink.save("semantic-arrival-"+request.requestId,value))healthy=false; }catch(JSONException e){healthy=false;}catch(RuntimeException e){healthy=false;} }});}catch(RejectedExecutionException e){healthy=false;} @@ -300,7 +494,7 @@ public final class JevSemanticReview implements AutoCloseable { old.call.cancel("STALE_HISTORY_OR_VERSION");retained.remove(item.getKey());continue; } JevTransport.Result response=old.call.poll(); - if(!old.request.inputSignature.equals(page.payload.optString("input_signature"))) { + if(!old.request.sourceInputSignature.equals(page.payload.optString("input_signature"))) { if(response!=null && response.error!=null)retained.remove(item.getKey()); continue; } @@ -316,6 +510,10 @@ public final class JevSemanticReview implements AutoCloseable { if(current.size()!=old.request.candidateIds.size()) { old.call.cancel("STALE_CANDIDATES");retained.remove(item.getKey());continue; } + if(!old.request.evidenceViewVersion.isEmpty())for(Candidate candidate:current.values()) { + candidate.evidenceScope=scope(candidate.action)+"|"+old.request.inputSignature; + candidate.reviewContract=effectiveContract(old); + } Cache rebound=new Cache(current,old.request,old.call,old.attempt,JevEvidence.guiIdentity(tree)); rebound.sourceVersion=old.sourceVersion;rebound.historyRevision=old.historyRevision; rebound.confirmationCount=old.confirmationCount; @@ -324,7 +522,10 @@ public final class JevSemanticReview implements AutoCloseable { if(response==null)response=await(old.call,deadline); JevDeadline.check(); if(response==null)throw new JevDeadline.Expired(); - if(consumeCache(item.getKey(),rebound,response))return -1; + if(consumeCache(item.getKey(),rebound,response)) { + if(old.request.evidenceViewVersion.isEmpty())return -1; + continue; + } // Protocol / HTTP failures remain failures, rather than reusable semantic answers. return 1; } @@ -334,12 +535,13 @@ public final class JevSemanticReview implements AutoCloseable { final String cacheKey;final JSONObject payload;final Map candidates; Page(String k,JSONObject p,Map c){cacheKey=k;payload=p;candidates=c;} } - private Page page(final State state,final GUITree tree) { + private Page page(final State state,final GUITree tree) { return page(state,tree,null); } + private Page page(final State state,final GUITree tree,final Set include) { return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier() { - public Page get(){return buildPage(state,tree);} + public Page get(){return buildPage(state,tree,include);} }); } - private Page buildPage(State state,GUITree tree) { + private Page buildPage(State state,GUITree tree,Set include) { long began=System.nanoTime(); try { JevRegistry.ConfirmedHistory history=registry.confirmedHistory(state); @@ -353,6 +555,7 @@ public final class JevSemanticReview implements AutoCloseable { for(GUITreeNode node:nodes) { JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue; String id="c"+(++number),identity=key(action,node); + if(include!=null&&!include.contains(identity))continue; if(ambiguous.contains(identity))continue; if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);} else candidates.put(identity,new Candidate(action,node,identity,id)); @@ -364,39 +567,55 @@ public final class JevSemanticReview implements AutoCloseable { JSONObject recoverable=new JSONObject();Set usedHistory=new java.util.LinkedHashSet(),usedPages=new HashSet(); IdentityHashMap pageIds=new IdentityHashMap(); IdentityHashMap historyIds=new IdentityHashMap(); + IdentityHashMap historyDescriptors=new IdentityHashMap(); + Map historyById=new LinkedHashMap(); + int recoverableHistoryCount=0; for(Candidate c:candidates.values()) { JevDeadline.check();ids.put(c.candidateId); String pageId=sourcePage(tree,pages,pageIds); JSONObject control=JevEvidence.pairAction(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action),pageId); if(control!=null)current.put(c.candidateId,control); List members=history.members(c.action); - Map pool=new LinkedHashMap(); + Map> featurePool=new LinkedHashMap>(); for(com.android.commands.monkey.ape.tree.GUITreeAction h:members) { JevDeadline.check();String hid=historyIds.get(h); if(hid==null) { hid="h"+(historyIds.size()+1);historyIds.put(h,hid); - JSONObject witness=JevEvidence.pairAction(h,sourcePage(h.getGUITree(),pages,pageIds)); - if(witness!=null)recoverable.put(hid,witness); + historyById.put(hid,h); + sourcePage(h.getGUITree(),pages,pageIds); // preserve old Page ID allocation order + JevEvidence.PairDescriptor descriptor=JevEvidence.pairDescriptor(h); + historyDescriptors.put(h,descriptor); + if(descriptor.recoverable)recoverableHistoryCount++; provenance.put(hid,new JSONObject().put("source_tree",h.getGUITree().getId()) .put("source_gui_timestamp",h.getGUITree().getTimestamp()).put("current_state",state.getGraphId()) .put("current_model_action",h.getModelAction().getGraphId()).put("current_version",modelVersion) .put("input_confirmed",true).put("confirmation_basis","live confirmedInputs before review; not function completion") - .put("attributes_recoverable",witness!=null)); + .put("attributes_recoverable",descriptor.recoverable)); } - if(recoverable.has(hid))pool.put(hid,recoverable.getJSONObject(hid)); + JevEvidence.PairDescriptor descriptor=historyDescriptors.get(h); + if(descriptor!=null && descriptor.recoverable)featurePool.put(hid,descriptor.selectionFeatures); } - List selected=control==null?Collections.emptyList():JevHistoryPairwiseProtocol.selectHistory(control,pool); + List selected=control==null?Collections.emptyList():JevHistoryPairwiseProtocol.selectHistory(control,featurePool.keySet(),featurePool); JSONArray selectedIds=new JSONArray(); for(String hid:selected){ selectedIds.put(hid);usedHistory.add(hid); pairs.put(new JSONObject().put("id","p"+(pairs.length()+1)).put("candidate",c.candidateId).put("history",hid)); } - coverage.put(c.candidateId,new JSONObject().put("legal_history_count",members.size()).put("complete",history.complete && pool.size()==members.size()) - .put("history_collection_complete",history.complete).put("recoverable_history_count",pool.size()).put("selected_history",selected.size()).put("selected_history_ids",selectedIds) + coverage.put(c.candidateId,new JSONObject().put("legal_history_count",members.size()).put("complete",history.complete && featurePool.size()==members.size()) + .put("history_collection_complete",history.complete).put("recoverable_history_count",featurePool.size()).put("selected_history",selected.size()).put("selected_history_ids",selectedIds) .put("unknown_mapping_count",history.unknown) .put("mapping_basis","live confirmedInputs; current State/ModelAction object links and node Name") .put("context_recoverable",control!=null)); } + // Only the selected history witnesses receive the expensive rich JSON form. + // Descriptor extraction above has already frozen the exact ranking inputs. + for(String hid:usedHistory) { + com.android.commands.monkey.ape.tree.GUITreeAction h=historyById.get(hid); + if(h==null)throw new IllegalStateException("missing selected history descriptor: "+hid); + JSONObject witness=JevEvidence.pairAction(h,pageIds.get(h.getGUITree())); + if(witness==null)throw new IllegalStateException("selected history became unrecoverable: "+hid); + recoverable.put(hid,witness); + } // Send source context only for current candidates and selected witnesses, once per source page. for(Iterator it=current.keys();it.hasNext();)usedPages.add(current.getJSONObject((String)it.next()).getString("source_page_id")); for(String hid:usedHistory){JSONObject witness=recoverable.getJSONObject(hid);executed.put(hid,witness);usedPages.add(witness.getString("source_page_id"));} @@ -410,11 +629,11 @@ public final class JevSemanticReview implements AutoCloseable { .put("confirmation_count",history.total).put("unknown_mapping_count",history.unknown) .put("execution_revision",registry.executionRevision(state)); String signature=JevEvidence.digest(payload.toString());payload.put("input_signature",signature); - for(Candidate c:candidates.values())c.evidenceScope=scope(c.action)+"|"+signature; + for(Candidate c:candidates.values()){c.evidenceScope=scope(c.action)+"|"+signature;c.reviewContract=contract;} registry.semanticEvent("HISTORY_PAIRWISE_COST",currentStep,state.getGraphId(),null,JevRegistry.fields( "history_enumeration_ns",history.enumerationNanos,"evidence_build_ns",System.nanoTime()-evidenceStart, "prepare_local_ns",System.nanoTime()-began,"confirmed_input_objects",history.total,"unknown_mapping_count",history.unknown, - "history_complete",history.complete,"recoverable_history_count",recoverable.length(),"selected_history",executed.length(),"candidate_count",candidates.size(),"pair_count",pairs.length(), + "history_complete",history.complete,"recoverable_history_count",recoverableHistoryCount,"selected_history",executed.length(),"candidate_count",candidates.size(),"pair_count",pairs.length(), "evidence_bytes",stateEvidence.toString().getBytes(JevHistoryPairwiseProtocol.UTF8).length)); return new Page(state.getGraphId()+"|"+signature,payload,candidates); }catch(JSONException error){throw new IllegalStateException("pair evidence construction failed",error);} @@ -426,7 +645,7 @@ public final class JevSemanticReview implements AutoCloseable { return id; } private static JSONObject pendingPayload(JSONObject original,Map pending)throws JSONException { - JSONObject payload=new JSONObject(original.toString());JSONArray ids=new JSONArray(),pairs=new JSONArray();Set selected=new HashSet(); + JSONObject payload=JevPairwiseEvidenceView.shallowCopy(original);JSONArray ids=new JSONArray(),pairs=new JSONArray();Set selected=new HashSet(); for(Candidate c:pending.values()){ids.put(c.candidateId);selected.add(c.candidateId);} JSONArray originalPairs=payload.getJSONArray("pairs"); for(int i=0;i0.5) { - updates.add(new JevReviewLedger.Update(contract,scope(candidate),identity(candidate),outcome,decision.confidence,decision.representativeModelActionId, + updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),outcome,decision.confidence,decision.representativeModelActionId, cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));ledgerStatus="STAGED"; auditEvents.add(new Object[]{decision.promote()?"DECISION_PROBE":"DECISION_REPRESENTED",currentStep, candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(), @@ -486,7 +705,7 @@ public final class JevSemanticReview implements AutoCloseable { "decision_request_id",cache.request.requestId))}); if(!registry.hasExactExecution(candidate.action.getState(),candidate.action,candidate.action.getResolvedGUITree(),candidate.node))cache.decisions.put(candidate.key,decision); } else { - updates.add(new JevReviewLedger.Update(contract,scope(candidate),identity(candidate),"UNRESOLVED",decision.confidence,decision.representativeModelActionId, + updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),"UNRESOLVED",decision.confidence,decision.representativeModelActionId, cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId)); auditEvents.add(new Object[]{"DECISION_REVIEWED",currentStep,candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(), decision.audit(JevRegistry.fields("entry_id",entryId,"request_id",cache.request.requestId,"candidate_id",candidate.candidateId,"confidence",decision.confidence, @@ -535,8 +754,9 @@ public final class JevSemanticReview implements AutoCloseable { try { JSONObject receipt=response==null?new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response") .put("request_id",request.requestId).put("attempt",attempt).put("retry_count",attempt-1).put("endpoint",endpoint).put("model",model) - .put("contract",contract).put("step",currentStep).put("input_fingerprint",request.inputFingerprint).put("http_status",status) + .put("contract",requestContract(request)).put("step",currentStep).put("input_fingerprint",request.inputFingerprint).put("http_status",status) .put("error",error).put("raw_response",JSONObject.NULL):responseReceipt(new Cache(Collections.emptyMap(),request,null,attempt,""),response,null,error); + viewMetadata(receipt,request); receipt("semantic-response-"+request.requestId,receipt); } catch(JSONException ignored) { } registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",request.requestId,"reason",error, @@ -549,7 +769,7 @@ public final class JevSemanticReview implements AutoCloseable { if(page==null || state!=preparedState || tree!=preparedTree || modelVersion!=preparedVersion || preparedConfirmationCount!=registry.confirmationCount() || preparedExecutionRevision!=registry.executionRevision(state))return null; for(Candidate candidate:page.candidates.values()) { - JevReviewLedger.Decision shared=registry.reviewLedger().get(contract,scope(candidate),identity(candidate)); + JevReviewLedger.Decision shared=reusableDecision(candidate,JevEvidence.guiIdentity(tree),state); Cache cached=pages.get(page.cacheKey); JevHistoryPairwiseProtocol.Decision decision=shared==null?null:new JevHistoryPairwiseProtocol.Decision(candidate.candidateId, shared.representativeModelActionId==null?("PROBE".equals(shared.outcome)?JevHistoryPairwiseProtocol.PROMOTE:"REPRESENTED"):shared.representativeModelActionId, @@ -560,7 +780,7 @@ public final class JevSemanticReview implements AutoCloseable { if(shared!=null && !"PROBE".equals(shared.outcome))continue; if(decision==null||!decision.promote()||decision.confidence<=0.5||served.contains(candidate.key))continue; if(!navigationMask.allow(state,tree,candidate))continue; - registry.reviewLedger().bind(contract,scope(candidate),identity(candidate),JevEvidence.guiIdentity(tree)); + registry.reviewLedger().bind(candidate.reviewContract==null?contract:candidate.reviewContract,scope(candidate),identity(candidate),JevEvidence.guiIdentity(tree),modelVersion,registry.executionRevision(state),registry.confirmationCount()); String requestId=shared!=null?shared.requestId:(cached==null?null:cached.request.requestId); String fingerprint=shared!=null?shared.inputFingerprint:(cached==null?null:cached.request.inputFingerprint); served.add(candidate.key);return new Selection(candidate,entryId(requestId,candidate.candidateId,fingerprint),requestId,decision.confidence,decision.representativeModelActionId); @@ -581,6 +801,14 @@ public final class JevSemanticReview implements AutoCloseable { private static String scope(Candidate candidate) { return candidate==null||candidate.evidenceScope==null?"":candidate.evidenceScope; } + private String projectedContract() { return contract+":"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION; } + private JevReviewLedger.Decision ledgerDecision(Candidate candidate) { + return registry.reviewLedger().get(candidate.reviewContract==null?contract:candidate.reviewContract,scope(candidate),identity(candidate)); + } + private String requestContract(JevHistoryPairwiseProtocol.Request request) { + return request.evidenceViewVersion.isEmpty()?contract:projectedContract(); + } + private String effectiveContract(Cache cache) { return requestContract(cache.request); } private static String scope(ModelAction action) { return action==null||action.getState()==null?"":action.getState().getActivity(); } @@ -616,28 +844,33 @@ public final class JevSemanticReview implements AutoCloseable { } private JSONObject requestReceipt(JevHistoryPairwiseProtocol.Request request,Map candidates,int attempt) { try {JSONArray ids=new JSONArray();for(Candidate c:candidates.values())ids.put(c.candidateId); - return new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_request":"async_semantic_request").put("request_id",request.requestId) - .put("attempt",attempt).put("endpoint",endpoint).put("model",model).put("contract",contract).put("step",currentStep) + JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_request":"async_semantic_request").put("request_id",request.requestId) + .put("attempt",attempt).put("endpoint",endpoint).put("model",model).put("contract",requestContract(request)).put("step",currentStep) .put("retry_count",attempt-1) .put("request_lifetime_ms",synchronous?requestLifetimeMs:timeoutMs).put("late_response_reuse",synchronous) .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint).put("candidate_ids",ids) .put("pair_count",request.questionCount).put("history_coverage",request.plan.getJSONObject("coverage")) .put("history_provenance",request.plan.getJSONObject("history_provenance")) .put("request_bytes",request.requestBytes).put("timeout_ms",timeoutMs).put("body",new JSONObject(request.bodyJson)); + viewMetadata(value,request);return value; } catch(JSONException error) { return new JSONObject(); } } private JSONObject responseReceipt(Cache cache,JevTransport.Result response,JSONArray decisions,String error) { try {JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response") .put("request_id",cache.request.requestId).put("attempt",cache.attempt).put("endpoint",endpoint).put("model",model) .put("retry_count",cache.attempt-1) - .put("contract",contract).put("step",currentStep).put("input_signature",cache.request.inputSignature) + .put("contract",effectiveContract(cache)).put("step",currentStep).put("input_signature",cache.request.inputSignature) .put("input_fingerprint",cache.request.inputFingerprint).put("http_status",response.httpStatus) .put("error",error==null?(response.error==null?JSONObject.NULL:response.error):error) .put("wait_ns",response.waitNanos()).put("latency_ns",response.latencyNanos()) .put("raw_response",response.raw==null?JSONObject.NULL:response.raw); - if(decisions!=null)value.put("decisions",decisions);return value; + if(decisions!=null)value.put("decisions",decisions);viewMetadata(value,cache.request);return value; } catch(JSONException ignored) { return new JSONObject(); } } + private static void viewMetadata(JSONObject receipt,JevHistoryPairwiseProtocol.Request request)throws JSONException { + if(!request.evidenceViewVersion.isEmpty())receipt.put("evidence_view_version",request.evidenceViewVersion) + .put("source_input_signature",request.sourceInputSignature); + } private static String scopeForPage(Map candidates) { for(Candidate candidate:candidates.values())return scope(candidate); return ""; @@ -665,6 +898,10 @@ public final class JevSemanticReview implements AutoCloseable { receiptWorker.shutdownNow();transport.close(); if(preparedPage!=null)contextInvalidated(preparedPage,"run_closed"); List snapshot=registry.reviewLedger().snapshot(contract); + List projected=registry.reviewLedger().snapshot(projectedContract()); + for(JSONObject row:projected)try {row.put("evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);} + catch(JSONException error){throw new IllegalStateException(error);} + snapshot.addAll(projected); for(JSONObject row:snapshot)registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT",currentStep,null,null,row); registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT_END",currentStep,null,null,JevRegistry.fields("decision_count",snapshot.size())); pages.clear();retained.clear();inFlight.clear();served.clear();