Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-collect.ts
T
2026-06-25 05:03:11 +08:00

291 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPEC: PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer.
// Responsibility: Offline CLI view renderers for HWLAB web-probe observe artifacts.
import { shellQuote } from "./ssh";
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame";
export function parseNodeWebProbeObserveCollectView(value: string): NodeWebProbeObserveCollectView {
if (value === "files" || value === "turn-summary" || value === "trace-frame") return value;
throw new Error(`web-probe observe collect --view must be files, turn-summary, or trace-frame; got ${value}`);
}
export function nodeWebObserveCollectViewNodeScript(input: {
maxFiles: number;
view: NodeWebProbeObserveCollectView;
traceId: string | null;
sampleSeq: number | null;
timestamp: string | null;
turn: number | null;
}): string {
const requestedSampleSeq = input.sampleSeq === null ? "null" : String(input.sampleSeq);
const requestedTurn = input.turn === null ? "null" : String(input.turn);
return `node -e ${shellQuote(`
const fs=require('fs'),path=require('path'),crypto=require('crypto');
const dir=process.argv[1]; const maxFiles=${input.maxFiles}; const view=${JSON.stringify(input.view)}; const requestedTraceId=${JSON.stringify(input.traceId)}; const requestedSampleSeq=${requestedSampleSeq}; const requestedTimestamp=${JSON.stringify(input.timestamp)}; const requestedTurn=${requestedTurn};
const sha=(value)=>'sha256:'+crypto.createHash('sha256').update(String(value||'')).digest('hex');
const short=(value,limit=96)=>{const text=String(value||'').replace(/\\s+/gu,' ').trim(); return text.length>limit?text.slice(0,Math.max(1,limit-1))+'...':text;};
const textOf=(value)=>String(value?.text||value?.textPreview||value?.preview||'');
const readJson=(rel)=>{try{return JSON.parse(fs.readFileSync(path.join(dir,rel),'utf8'))}catch{return null}};
const readJsonl=(rel)=>{try{return fs.readFileSync(path.join(dir,rel),'utf8').split(/\\r?\\n/u).filter(Boolean).map((line)=>{try{return JSON.parse(line)}catch{return {parseError:true,rawHash:sha(line)}}})}catch{return []}};
const out=[]; const walk=(p)=>{for(const ent of fs.readdirSync(p,{withFileTypes:true})){const full=path.join(p,ent.name); if(ent.isDirectory()) walk(full); else out.push(full); if(out.length>=maxFiles) return;}};
walk(dir);
const files=out.slice(0,maxFiles).map(file=>{const st=fs.statSync(file); const hash=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); return {path:file,relative:path.relative(dir,file),byteCount:st.size,sha256:'sha256:'+hash}});
const totalBytes=files.reduce((sum,item)=>sum+item.byteCount,0);
if(view==='files'){
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,fileCount:files.length,totalBytes,files,valuesRedacted:true},null,2));
process.exit(0);
}
const samples=readJsonl('samples.jsonl');
const control=readJsonl('control.jsonl');
const manifest=readJson('manifest.json')||{};
const report=readJson('analysis/report.json')||{};
function unique(values){return Array.from(new Set(values.filter(Boolean)));}
function numOrNull(value){const n=Number(value); return Number.isFinite(n)?n:null}
function promptIndexOf(value){const n=Number(value); return Number.isInteger(n)&&n>0?n:null}
function tsMs(value){const ms=Date.parse(String(value||'')); return Number.isFinite(ms)?ms:null}
function promptCommands(){
const map=new Map();
for(const item of control){
if(item.type!=='sendPrompt'||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue;
const id=item.commandId||item.seq||String(map.size+1);
const existing=map.get(id)||{};
map.set(id,{...existing,...item,input:{...(existing.input||{}),...(item.input||{})},firstTs:existing.firstTs||item.ts,lastTs:item.ts});
}
return Array.from(map.values()).filter((item)=>tsMs(item.firstTs)!==null).sort((a,b)=>tsMs(a.firstTs)-tsMs(b.firstTs));
}
function segmentFor(index,prompts){
const start=tsMs(prompts[index]?.firstTs); const end=index+1<prompts.length?tsMs(prompts[index+1].firstTs):Infinity;
return samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms>=start&&ms<end});
}
function controlsFor(index,prompts){
const start=tsMs(prompts[index]?.firstTs); const end=index+1<prompts.length?tsMs(prompts[index+1].firstTs):Infinity;
return control.filter((item)=>{const ms=tsMs(item.ts); return ms!==null&&ms>=start&&ms<end});
}
function firstTraceId(text){const match=String(text||'').match(/\\btrc_[A-Za-z0-9_-]+\\b/u); return match?match[0]:null}
function itemTraceId(item){return item?.traceId||firstTraceId(textOf(item))||null}
function entryGroups(sample){
return [
...(Array.isArray(sample.turns)?sample.turns.map((item)=>({group:'turn',item})):[]),
...(Array.isArray(sample.traceRows)?sample.traceRows.map((item)=>({group:'traceRow',item})):[]),
...(Array.isArray(sample.messages)?sample.messages.map((item)=>({group:'message',item})):[]),
];
}
function traceIdStats(items){
const map=new Map();
for(const sample of items){
const seq=numOrNull(sample.seq);
for(const entry of entryGroups(sample)){
const id=itemTraceId(entry.item); if(!id) continue;
const current=map.get(id)||{traceId:id,count:0,firstSeq:null,lastSeq:null,terminalCount:0};
current.count+=1;
current.firstSeq=current.firstSeq===null?seq:seq===null?current.firstSeq:Math.min(current.firstSeq,seq);
current.lastSeq=current.lastSeq===null?seq:seq===null?current.lastSeq:Math.max(current.lastSeq,seq);
if(terminalText(textOf(entry.item))||String(entry.item?.status||'').match(/complete|fail|cancel|terminal/iu)) current.terminalCount+=1;
map.set(id,current);
}
}
return Array.from(map.values()).sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||String(a.traceId).localeCompare(String(b.traceId)));
}
function traceIdsFromSamples(items){return traceIdStats(items).map((item)=>item.traceId)}
function collectReportTurnColumns(value,out=[]){
if(!value||typeof value!=='object') return out;
if(Array.isArray(value)){for(const item of value) collectReportTurnColumns(item,out); return out;}
if(Array.isArray(value.turnColumns)){
for(const item of value.turnColumns){
if(!item||typeof item!=='object') continue;
const traceId=String(item.traceId||'').trim();
if(!traceId) continue;
out.push({...item,traceId,promptIndex:promptIndexOf(item.promptIndex),lastPromptIndex:promptIndexOf(item.lastPromptIndex),firstSeq:numOrNull(item.firstSeq),lastSeq:numOrNull(item.lastSeq)});
}
}
for(const key of ['sampleMetrics','analysis','analysisWindow','recentWindow','windows','recent','archive','summary']){
if(value[key]&&value[key]!==value) collectReportTurnColumns(value[key],out);
}
return out;
}
const reportTurnColumns=(()=>{const seen=new Set(); const rows=[]; for(const item of collectReportTurnColumns(report)){const key=[item.traceId,item.promptIndex,item.lastPromptIndex,item.firstSeq,item.lastSeq,item.source,item.pageRole,item.messageId].join('|'); if(seen.has(key)) continue; seen.add(key); rows.push(item);} return rows.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.promptIndex??9999)-(b.promptIndex??9999));})();
function columnOverlapsSegment(column,segment){
if(segment.length===0) return true;
const first=numOrNull(segment[0]?.seq); const last=numOrNull(segment[segment.length-1]?.seq);
if(first===null||last===null||column.firstSeq===null||column.lastSeq===null) return true;
return column.lastSeq>=first&&column.firstSeq<=last;
}
function reportColumnsForPrompt(index,segment){
const promptIndex=index+1;
const segmentIds=new Set(traceIdsFromSamples(segment));
return reportTurnColumns
.filter((column)=>(column.promptIndex===promptIndex||column.lastPromptIndex===promptIndex)&&columnOverlapsSegment(column,segment))
.filter((column)=>segmentIds.size===0||segmentIds.has(column.traceId)||column.source==='turn')
.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.lastSeq??0)-(b.lastSeq??0)||String(a.label||'').localeCompare(String(b.label||'')));
}
function choosePrimaryTraceId(index,segment){
const columns=reportColumnsForPrompt(index,segment);
const promptIndex=index+1;
const promptColumns=columns.filter((column)=>column.promptIndex===promptIndex);
const preferred=promptColumns.length>0?promptColumns:columns;
if(preferred.length>0) return {traceId:preferred[0].traceId,columns};
const stats=traceIdStats(segment).sort((a,b)=>(b.lastSeq??0)-(a.lastSeq??0)||(b.firstSeq??0)-(a.firstSeq??0));
return {traceId:stats[0]?.traceId||null,columns:[]};
}
function samplesForTrace(items,traceId,maxSeq=null){
if(!traceId) return items;
return items.filter((sample)=>{
const seq=numOrNull(sample.seq);
return (maxSeq===null||seq===null||seq<=maxSeq)&&traceIdsFromSamples([sample]).includes(traceId);
});
}
function latestSampleForTrace(items,traceId,maxSeq=null){
return samplesForTrace(items,traceId,maxSeq).slice(-1)[0]||null;
}
function traceEntries(items,traceId){
const entries=[];
for(const sample of items){
for(const entry of entryGroups(sample)){
const text=textOf(entry.item);
const id=itemTraceId(entry.item);
if(!traceId||id===traceId||text.includes(traceId)) entries.push({...entry,sample,text});
}
}
return entries;
}
function textsFor(items,traceId=null){
const entries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})));
return entries.length>0?entries.map((entry)=>entry.text):[];
}
function parseElapsed(text){
const values=[]; const raw=String(text||'');
for(const m of raw.matchAll(/(?:耗时|总耗时|total\\s*=?)\\s*(?:(\\d+)\\s*(?:小时|h)\\s*)?(?:(\\d+)\\s*(?:分|分钟|m)\\s*)?(?:(\\d+)\\s*(?:秒|s))?/giu)){
const total=Number(m[1]||0)*3600+Number(m[2]||0)*60+Number(m[3]||0); if(total>0) values.push(total);
}
for(const m of raw.matchAll(/\\b(?:total|elapsed)\\s*=\\s*(?:(\\d{1,2}):)?(\\d{1,2}):(\\d{2})\\b/giu)){
values.push(Number(m[1]||0)*3600+Number(m[2]||0)*60+Number(m[3]||0));
}
return values.length?Math.max(...values):null;
}
function parseRecent(text){const m=String(text||'').match(/最近\\s*(\\d+)\\s*(秒|分钟|分|小时)前/u); if(!m)return null; const n=Number(m[1]); return m[2]==='小时'?n*3600:m[2]==='秒'?n:n*60}
function fmtDuration(seconds){if(seconds===null||seconds===undefined||!Number.isFinite(Number(seconds)))return '-'; const value=Math.max(0,Math.round(Number(seconds))); const h=Math.floor(value/3600), m=Math.floor((value%3600)/60), s=value%60; return (h>0?String(h).padStart(2,'0')+':':'')+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')}
function terminalText(text){return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|completed|failed|canceled|cancelled|terminal/iu.test(String(text||''))}
function statusFor(items,traceId=null){
const texts=textsFor(items,traceId);
const joined=texts.join('\\n');
const matchedEntries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})));
const lastTurn=matchedEntries.filter((entry)=>entry.group==='turn').slice(-1)[0]?.item||null;
const raw=String(lastTurn?.status||'').toLowerCase();
if(/cancel/iu.test(joined)||raw.includes('cancel'))return 'canceled';
if(/failed|error|agentrun:error/iu.test(joined)||raw.includes('fail'))return 'failed';
if(texts.some(terminalText)||raw.includes('complete'))return 'completed';
if(/running|运行中|Code Agent\\s*耗时|最近\\s*\\d+/iu.test(joined)||raw.includes('running'))return 'running';
return 'unknown';
}
function userMessageFor(items,prompt){
const hash=prompt?.input?.textHash||null;
for(const sample of items){
for(const message of Array.isArray(sample.messages)?sample.messages:[]){
if(hash&&message.textHash===hash)return {preview:short(textOf(message),90),textHash:message.textHash||hash,textBytes:message.textBytes??prompt?.input?.textBytes??null};
}
}
return {preview:short(prompt?.input?.textPreview||'',90)||'(hash only)',textHash:hash,textBytes:prompt?.input?.textBytes??null};
}
function cleanFinalResponseText(text){
const raw=String(text||'').trim();
if(!raw) return '';
if(/Code Agent\\s*耗时[\\s\\S]*运行记录/iu.test(raw)) return '';
if(/^sent\\s+当前 AgentRun 请求已取消/u.test(raw)) return '';
if(/^(admitted|run|ok|error)\\s+/iu.test(raw)) return '';
if(/^(AgentRun|Code Agent turn was durably admitted|commandExecution|runner-job|command -v|node --version|npm --version|cd |mkdir -p|npm install|npx )/iu.test(raw)) return '';
if(/(?:^|\\s)轮次(完成|失败|取消)?总耗时/iu.test(raw)) return '';
if(/^(轮次完成|轮次失败|轮次取消|已记录)$/u.test(raw.replace(/\\s+/gu,' ').trim())) return '';
return raw;
}
function emptyFinalResponse(){return {preview:'(空内容)',textHash:null,textBytes:0,empty:true}}
function terminalFinalStatus(value){return /^(completed|failed|canceled|cancelled|terminal)$/u.test(String(value||'').trim().toLowerCase())}
function entryRole(entry){return String(entry.item?.role||entry.item?.dataRole||entry.item?.authorRole||entry.item?.messageRole||entry.item?.speaker||'').toLowerCase()}
function finalCandidateText(entry){
const explicit=textOf(entry.item?.finalResponse);
if(explicit) return explicit;
if(entry.group==='message') return entry.text;
return '';
}
function sameAsUserPrompt(entry,text,user){
if(!user) return false;
if(user.textHash&&entry.item?.textHash===user.textHash) return true;
const userPrefix=short(user.preview||'',48).replace(/\\s+/gu,' ').trim().slice(0,32);
const textPrefix=String(text||'').replace(/\\s+/gu,' ').trim().slice(0,32);
return userPrefix.length>=18&&textPrefix===userPrefix;
}
function finalResponseFor(items,traceId,user=null){
if(!terminalFinalStatus(statusFor(items,traceId))) return emptyFinalResponse();
const candidates=(traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})))).slice().reverse();
for(const entry of candidates){
const role=entryRole(entry);
const entryStatus=String(entry.item?.status||'');
if(entry.group==='message'&&!/assistant|agent|system/u.test(role)) continue;
if(/用户|user/iu.test(role)||/用户|user/iu.test(entryStatus)) continue;
const status=String(entry.item?.status||'').toLowerCase(); const text=cleanFinalResponseText(finalCandidateText(entry));
if(!text) continue;
if(/^第\\d+轮\\/\\d+[:]?\\s*请/u.test(text)||/^请(执行|帮|回答|修复|调查|用|不要|直接)/u.test(text)) continue;
if(sameAsUserPrompt(entry,text,user)) continue;
if(status.includes('running')&&!terminalText(entry.text)&&entry.group==='turn')continue;
const preview=short(text,180);
if(preview)return {preview,textHash:entry.item?.textHash||sha(text),textBytes:entry.item?.textBytes??Buffer.byteLength(text),empty:false};
}
return emptyFinalResponse();
}
function turnSummaryRows(){
const prompts=promptCommands();
if(prompts.length===0){
const allTraceIds=traceIdsFromSamples(samples);
return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples,allTraceIds[0]||null),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}];
}
return prompts.map((prompt,index)=>{
const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const selected=choosePrimaryTraceId(index,segment); const traceId=selected.traceId; const user=userMessageFor(segment,prompt);
const primaryColumn=selected.columns.find((column)=>column.traceId===traceId)||null;
const traceSample=latestSampleForTrace(segment,traceId,primaryColumn?.lastSeq??null);
const texts=textsFor(segment,traceId);
const elapsedValues=texts.map(parseElapsed).filter((value)=>value!==null); const recentValues=texts.map(parseRecent).filter((value)=>value!==null);
const marks=unique(segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)).join(',')||'-';
return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,traceIds:unique([traceId,...selected.columns.map((column)=>column.traceId),...traceIdsFromSamples(segment)]).slice(0,8),status:statusFor(segment,traceId),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??primaryColumn?.firstSeq??null,lastSeq:traceSample?.seq??primaryColumn?.lastSeq??segment[segment.length-1]?.seq??null,lastTs:traceSample?.ts??segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId,user),valuesRedacted:true};
});
}
function pad(value,width){const text=short(value,width); return text+Array(Math.max(0,width-text.length)+1).join(' ')}
function renderTurnSummary(rows){
const title='Workbench Session '+(samples[samples.length-1]?.routeSessionId||samples[samples.length-1]?.activeSessionId||'-')+' observer '+(manifest.jobId||'-');
const header='轮次 用户消息 Trace 状态 耗时/最近 标记 Final Response';
const body=rows.map((row)=>pad(row.round,5)+' '+pad((row.userHash?row.userHash.slice(0,18)+' ':'')+row.userPreview,32)+' '+pad(row.traceId||'-',12)+' '+pad(row.status,10)+' '+pad(fmtDuration(row.elapsedSeconds)+'/'+(row.recentUpdateSeconds===null?'-':String(row.recentUpdateSeconds)+'s'),13)+' '+pad(row.marks,11)+' '+short(row.finalResponse?.preview||'(空内容)',80)).join('\\n');
return [title,'=======================================================',header,body||'(空内容)','', 'NEXT', ' detail: bun scripts/cli.ts hwlab nodes web-probe observe collect '+(manifest.jobId||'<observer>')+' --view trace-frame --trace-id <traceId> --sample-seq <seq>'].join('\\n');
}
function selectSample(rows){
if(requestedSampleSeq!==null){const exact=samples.find((sample)=>Number(sample.seq)===requestedSampleSeq); if(exact)return exact;}
if(requestedTimestamp){const target=tsMs(requestedTimestamp); if(target!==null){const before=samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms<=target}).slice(-1)[0]; if(before)return before;}}
if(requestedTurn!==null&&rows[requestedTurn-1]?.traceId){const row=rows[requestedTurn-1]; const byTrace=latestSampleForTrace(samples,row.traceId,numOrNull(row.lastSeq)); if(byTrace)return byTrace;}
if(requestedTurn!==null&&rows[requestedTurn-1]?.lastSeq!==null){const byTurn=samples.find((sample)=>Number(sample.seq)===Number(rows[requestedTurn-1].lastSeq)); if(byTurn)return byTurn;}
if(requestedTraceId){const byTrace=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId)).slice(-1)[0]; if(byTrace)return byTrace;}
return samples[samples.length-1]||null;
}
function renderTraceFrame(sample,rows){
if(!sample)return {ok:false,renderedText:'TRACE_FRAME_BLOCKER\\nno sample matched --sample-seq/--timestamp/--trace-id/--turn',blocker:'sample-not-found'};
const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||(requestedTurn!==null?rows[requestedTurn-1]?.traceId:null)||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||sampleTraceIds[0]||null;
const traceRows=(Array.isArray(sample.traceRows)?sample.traceRows:[]).filter((row)=>!traceId||row.traceId===traceId||!row.traceId||textOf(row).includes(traceId));
const turns=(Array.isArray(sample.turns)?sample.turns:[]).filter((turn)=>!traceId||turn.traceId===traceId||textOf(turn).includes(traceId));
const texts=[...turns.map(textOf),...traceRows.map(textOf)];
const elapsed=Math.max(-1,...texts.map(parseElapsed).filter((value)=>value!==null)); const recent=Math.max(-1,...texts.map(parseRecent).filter((value)=>value!==null));
const status=statusFor([sample],traceId); const finalResponse=finalResponseFor([sample],traceId);
const visibleTraceRows=traceRows.slice(-24);
const rowLines=visibleTraceRows.map((row,index)=>{const text=textOf(row); return short((row.status?row.status+' ':'')+text,180)||('row#'+index+' '+(row.textHash||'-'));});
if(traceRows.length>visibleTraceRows.length) rowLines.unshift('(已省略 '+(traceRows.length-visibleTraceRows.length)+' 条较早 trace rows;需要原始数据请看 samples.jsonl)');
const visibleTurns=turns.slice(-6);
const assistantLines=visibleTurns.map((turn)=>'助手消息 '+(turn.status||'-')+' '+short(textOf(turn),180));
if(turns.length>visibleTurns.length) assistantLines.unshift('(已省略 '+(turns.length-visibleTurns.length)+' 条较早 assistant turn summaries)');
const rendered=['Code Agent 耗时 '+(elapsed>=0?fmtDuration(elapsed):'-')+' 最近 '+(recent>=0?String(recent)+' 秒前':'-')+' '+status+'','=======================================================','sample seq='+(sample.seq??'-')+' ts='+(sample.ts||'-')+' traceId='+(traceId||'-')+' routeSession='+(sample.routeSessionId||'-')+' activeSession='+(sample.activeSessionId||'-'),...(rowLines.length?rowLines:['(无 trace rows;这是 blocker,不能当业务通过证据)']),...(assistantLines.length?assistantLines:[]),'==========================','Final Response',finalResponse.preview||'(空内容)'].join('\\n');
return {ok:rowLines.length>0,renderedText:rendered,blocker:rowLines.length>0?null:'trace-rows-missing',sampleSeq:sample.seq??null,traceId,finalResponse,valuesRedacted:true};
}
const rows=turnSummaryRows();
if(view==='turn-summary'){
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,turnCount:rows.length,rows:rows.slice(0,80),renderedText:renderTurnSummary(rows),sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true},null,2));
process.exit(0);
}
const frame=renderTraceFrame(selectSample(rows),rows);
console.log(JSON.stringify({ok:frame.ok!==false,command:'web-probe-observe collect',view,stateDir:dir,...frame,sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true},null,2));
`)} "$state_dir"`;
}