fix: add bounded web-probe observe timeline drilldown (#937)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-26 10:20:28 +08:00
committed by GitHub
parent effd3656d4
commit 8abf188ee2
7 changed files with 247 additions and 5 deletions
+1
View File
@@ -690,6 +690,7 @@ function webProbeHelpSummary(): unknown {
"bun scripts/cli.ts web-probe run --node D601 --lane v03 --wait-messages-ms 1000",
"bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000",
"bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary",
"bun scripts/cli.ts web-probe observe collect webobs-xxxx --view timeline --turn 1",
"bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run",
],
description: "Run target node/lane HWLAB Cloud Web probes, long observe/analyze sessions, project-management MDTODO commands, and Web sentinel YAML-first control through a single top-level implementation.",
+1
View File
@@ -49,6 +49,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
"bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task R1.1",
"bun scripts/cli.ts web-probe observe status webobs-xxxx",
"bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary",
"bun scripts/cli.ts web-probe observe collect webobs-xxxx --view timeline --command-id cmd-xxxx",
"bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary",
"bun scripts/cli.ts web-probe observe analyze webobs-xxxx",
"bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run",
+187 -4
View File
@@ -2,11 +2,11 @@
// Responsibility: Offline CLI view renderers for HWLAB web-probe observe artifacts.
import { shellQuote } from "./ssh";
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "project-summary" | "project-mdtodo-summary";
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "timeline" | "project-summary" | "project-mdtodo-summary";
export function parseNodeWebProbeObserveCollectView(value: string): NodeWebProbeObserveCollectView {
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "project-summary" || value === "project-mdtodo-summary") return value;
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, project-summary, or project-mdtodo-summary; got ${value}`);
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "timeline" || value === "project-summary" || value === "project-mdtodo-summary") return value;
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, timeline, project-summary, or project-mdtodo-summary; got ${value}`);
}
export function nodeWebObserveCollectViewNodeScript(input: {
@@ -16,12 +16,15 @@ export function nodeWebObserveCollectViewNodeScript(input: {
sampleSeq: number | null;
timestamp: string | null;
turn: number | null;
commandId: string | null;
windowMs: number | null;
}): string {
const requestedSampleSeq = input.sampleSeq === null ? "null" : String(input.sampleSeq);
const requestedTurn = input.turn === null ? "null" : String(input.turn);
const requestedWindowMs = input.windowMs === null ? "null" : String(input.windowMs);
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 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 requestedCommandId=${JSON.stringify(input.commandId)}; const requestedWindowMs=${requestedWindowMs};
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||'');
@@ -349,6 +352,181 @@ function renderTraceFrame(sample,rows){
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||'-'),...bodyRows,'==========================','Final Response',finalResponse.preview||'(空内容)'].join('\\n');
return {ok:!missingRows,renderedText:rendered,blocker:missingRows?'trace-rows-missing':null,sampleSeq:sample.seq??null,traceId,finalResponse,traceDiagnostic:missingRows?{pageRole:sample.pageRole||null,pageId:sample.pageId||null,traceRows:Array.isArray(sample.traceRows)?sample.traceRows.length:0,turns:Array.isArray(sample.turns)?sample.turns.length:0,messages:Array.isArray(sample.messages)?sample.messages.length:0,sampleTraceIds:traceIdsFromSamples([sample]).slice(0,12)}:null,valuesRedacted:true};
}
function firstPresent(values){
for(const value of values){
if(value!==null&&value!==undefined&&String(value)!=='') return value;
}
return null;
}
function commandIdOf(item){return item?.commandId||item?.id||item?.detail?.commandId||item?.input?.commandId||null}
function controlSessionId(item){
const detail=item?.detail&&typeof item.detail==='object'?item.detail:{};
return commandSessionId(item)||detail.sessionId||detail.result?.sessionId||detail.observer?.sessionId||null;
}
function controlTraceId(item){
const detail=item?.detail&&typeof item.detail==='object'?item.detail:{};
return commandTraceId(item)||detail.traceId||detail.chatSubmit?.traceId||detail.result?.traceId||detail.result?.chatSubmit?.traceId||null;
}
function safeListJsonFiles(rel){
const root=path.join(dir,rel); let entries=[];
try{entries=fs.readdirSync(root,{withFileTypes:true});}catch{return [];}
return entries.filter((entry)=>entry.isFile()&&entry.name.endsWith('.json')).map((entry)=>rel+'/'+entry.name);
}
function readCommandFiles(){
const buckets=['pending','processing','done','failed','abandoned'];
const rows=[];
for(const bucket of buckets){
for(const rel of safeListJsonFiles('commands/'+bucket)){
const data=readJson(rel)||{}; let stat=null;
try{stat=fs.statSync(path.join(dir,rel));}catch{}
const name=path.basename(rel);
rows.push({bucket,relative:rel,name,data,mtime:stat?stat.mtime.toISOString():null,valuesRedacted:true});
}
}
return rows.sort((a,b)=>(tsMs(commandFileTs(a))??0)-(tsMs(commandFileTs(b))??0)||a.relative.localeCompare(b.relative));
}
function commandFileId(row){return row?.data?.id||row?.data?.commandId||String(row?.name||'').replace(/[.]json$/u,'')||null}
function commandFileType(row){return row?.data?.type||row?.data?.commandType||row?.data?.input?.type||row?.data?.result?.type||null}
function commandFileTs(row){return firstPresent([row?.data?.createdAt,row?.data?.completedAt,row?.data?.failedAt,row?.data?.abandonedAt,row?.data?.ts,row?.mtime])}
function commandFileSessionId(row){
const data=row?.data||{}; const result=data.result&&typeof data.result==='object'?data.result:{}; const command=data.command&&typeof data.command==='object'?data.command:{};
return data.sessionId||data.input?.sessionId||command.sessionId||result.sessionId||result.observer?.sessionId||sessionIdFromUrl(result.afterUrl)||null;
}
function commandFileTraceId(row){
const data=row?.data||{}; const result=data.result&&typeof data.result==='object'?data.result:{}; const error=data.error&&typeof data.error==='object'?data.error:{};
return data.traceId||data.input?.traceId||result.traceId||result.chatSubmit?.traceId||error.traceId||null;
}
function commandFileSummary(row){
const data=row?.data||{}; const result=data.result&&typeof data.result==='object'?data.result:{}; const error=data.error&&typeof data.error==='object'?data.error:{};
const parts=['file='+short(row.relative,48),'ok='+String(data.ok===true),'bucket='+row.bucket];
const duration=firstPresent([result.durationMs,result.observer?.durationMs,error.durationMs]);
if(duration!==null) parts.push('durationMs='+String(duration));
const status=firstPresent([result.status,result.phase,error.name,error.failureKind]);
if(status!==null) parts.push('status='+short(status,32));
const message=firstPresent([error.message,result.message,result.error?.message]);
if(message!==null) parts.push('message='+short(message,120));
return parts.join(' ');
}
function controlSummary(item){
const detail=item?.detail&&typeof item.detail==='object'?item.detail:{};
const input=item?.input&&typeof item.input==='object'?item.input:{};
const error=detail.error&&typeof detail.error==='object'?detail.error:null;
const parts=[];
if(input.textHash||input.hash) parts.push('textHash='+short(input.textHash||input.hash,24));
if(input.textBytes!==undefined) parts.push('textBytes='+String(input.textBytes));
if(detail.durationMs!==undefined) parts.push('durationMs='+String(detail.durationMs));
if(detail.retry||detail.retryLabel) parts.push('retry='+short(detail.retry||detail.retryLabel,24));
if(error?.message) parts.push('error='+short(error.message,120));
const afterPath=pathOnly(item?.afterUrl||detail.afterUrl);
if(afterPath) parts.push('afterPath='+short(afterPath,64));
return parts.join(' ')||'-';
}
function sampleSummary(sample){
const traceIds=traceIdsFromSamples([sample]).slice(0,4);
const counts='turns='+(Array.isArray(sample.turns)?sample.turns.length:0)+' traceRows='+(Array.isArray(sample.traceRows)?sample.traceRows.length:0)+' messages='+(Array.isArray(sample.messages)?sample.messages.length:0);
const pathText=sample.path||pathOnly(sample.url)||pathOnly(sample.currentUrl)||'-';
return 'path='+short(pathText,64)+' pageRole='+short(sample.pageRole||'-',18)+' '+counts+' traceIds='+(traceIds.join(',')||'-');
}
function isoOrNull(ms){return ms===null||ms===Infinity||ms===-Infinity?null:new Date(ms).toISOString()}
function timelineAnchor(rows,commandFiles){
if(requestedCommandId){
const controls=control.filter((item)=>String(commandIdOf(item)||'')===requestedCommandId);
const files=commandFiles.filter((item)=>String(commandFileId(item)||'')===requestedCommandId);
const times=[...controls.map((item)=>tsMs(item.ts)),...files.map((item)=>tsMs(commandFileTs(item)))].filter((value)=>value!==null);
const start=times.length?Math.min(...times):null; const end=times.length?Math.max(...times):null;
return {mode:'command-id',value:requestedCommandId,startMs:start,endMs:end,anchorMs:end??start,matchedControlCount:controls.length,matchedCommandFileCount:files.length,valuesRedacted:true};
}
if(requestedTurn!==null&&rows[requestedTurn-1]){
const row=rows[requestedTurn-1]; const segment=samples.filter((sample)=>{const seq=numOrNull(sample.seq); return seq!==null&&row.firstSeq!==null&&row.lastSeq!==null&&seq>=Number(row.firstSeq)&&seq<=Number(row.lastSeq);});
const times=[tsMs(row.lastTs),...segment.map((sample)=>tsMs(sample.ts))].filter((value)=>value!==null);
const start=times.length?Math.min(...times):null; const end=times.length?Math.max(...times):null;
return {mode:'turn',value:String(requestedTurn),commandId:row.commandId||null,traceId:row.traceId||null,startMs:start,endMs:end,anchorMs:end??start,valuesRedacted:true};
}
if(requestedTraceId){
const matched=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId));
const times=matched.map((sample)=>tsMs(sample.ts)).filter((value)=>value!==null);
const start=times.length?Math.min(...times):null; const end=times.length?Math.max(...times):null;
return {mode:'trace-id',value:requestedTraceId,startMs:start,endMs:end,anchorMs:end??start,matchedSampleCount:matched.length,valuesRedacted:true};
}
if(requestedSampleSeq!==null){
const sample=samples.find((item)=>Number(item.seq)===requestedSampleSeq)||null;
const anchorMs=tsMs(sample?.ts);
return {mode:'sample-seq',value:String(requestedSampleSeq),startMs:anchorMs,endMs:anchorMs,anchorMs,valuesRedacted:true};
}
if(requestedTimestamp){
const anchorMs=tsMs(requestedTimestamp);
return {mode:'timestamp',value:requestedTimestamp,startMs:anchorMs,endMs:anchorMs,anchorMs,valuesRedacted:true};
}
const latestTimes=[...samples.map((sample)=>tsMs(sample.ts)),...control.map((item)=>tsMs(item.ts)),...commandFiles.map((item)=>tsMs(commandFileTs(item)))].filter((value)=>value!==null);
const anchorMs=latestTimes.length?Math.max(...latestTimes):null;
return {mode:'latest',value:null,startMs:anchorMs,endMs:anchorMs,anchorMs,valuesRedacted:true};
}
function timelineWindow(anchor){
const windowMs=requestedWindowMs??120000;
if(anchor.startMs!==null&&anchor.endMs!==null&&anchor.startMs!==anchor.endMs){
return {fromMs:Math.max(0,anchor.startMs-1000),toMs:anchor.endMs+1000,windowMs,anchorMode:anchor.mode,valuesRedacted:true};
}
if(anchor.anchorMs!==null){
return {fromMs:Math.max(0,anchor.anchorMs-windowMs),toMs:anchor.anchorMs+windowMs,windowMs,anchorMode:anchor.mode,valuesRedacted:true};
}
return {fromMs:null,toMs:null,windowMs,anchorMode:anchor.mode,valuesRedacted:true};
}
function inTimelineWindow(ts,window){
const ms=tsMs(ts);
if(ms===null) return window.fromMs===null&&window.toMs===null;
if(window.fromMs!==null&&ms<window.fromMs) return false;
if(window.toMs!==null&&ms>window.toMs) return false;
return true;
}
function timelineEventRows(anchor,window,commandFiles){
const commandId=anchor.mode==='command-id'?anchor.value:null;
const matchedControl=control.filter((item)=>(commandId&&String(commandIdOf(item)||'')===commandId)||inTimelineWindow(item.ts,window));
const matchedSamples=samples.filter((sample)=>inTimelineWindow(sample.ts,window));
const matchedCommands=commandFiles.filter((item)=>(commandId&&String(commandFileId(item)||'')===commandId)||inTimelineWindow(commandFileTs(item),window));
const rows=[
...matchedControl.map((item)=>({ts:item.ts||null,seq:item.seq??null,kind:'control',phase:item.phase||'-',type:item.type||'-',commandId:commandIdOf(item),sessionId:controlSessionId(item),traceId:controlTraceId(item),summary:controlSummary(item),valuesRedacted:true})),
...matchedSamples.map((sample)=>({ts:sample.ts||null,seq:sample.seq??null,kind:'sample',phase:'sample',type:sample.reason||sample.pageRole||'-',commandId:sample.commandId||null,sessionId:sample.routeSessionId||sample.activeSessionId||null,traceId:traceIdsFromSamples([sample])[0]||null,summary:sampleSummary(sample),valuesRedacted:true})),
...matchedCommands.map((item)=>({ts:commandFileTs(item),seq:null,kind:'command-file',phase:item.bucket,type:commandFileType(item)||'-',commandId:commandFileId(item),sessionId:commandFileSessionId(item),traceId:commandFileTraceId(item),summary:commandFileSummary(item),valuesRedacted:true})),
].filter((item)=>item.ts!==null||item.commandId!==null)
.sort((a,b)=>(tsMs(a.ts)??0)-(tsMs(b.ts)??0)||String(a.kind).localeCompare(String(b.kind))||String(a.commandId||'').localeCompare(String(b.commandId||'')));
const cap=12;
let visible=rows;
if(rows.length>cap){
const evidence=rows.filter((row)=>row.kind!=='sample');
const sampleRows=rows.filter((row)=>row.kind==='sample');
visible=evidence.length>=cap?evidence.slice(evidence.length-cap):[...evidence,...sampleRows.slice(sampleRows.length-(cap-evidence.length))]
.sort((a,b)=>(tsMs(a.ts)??0)-(tsMs(b.ts)??0)||String(a.kind).localeCompare(String(b.kind))||String(a.commandId||'').localeCompare(String(b.commandId||'')));
}
return {rows:visible,omittedRowCount:Math.max(0,rows.length-visible.length),matchedControlCount:matchedControl.length,matchedSampleCount:matchedSamples.length,matchedCommandFileCount:matchedCommands.length,valuesRedacted:true};
}
function renderTimeline(anchor,window,eventRows){
const lines=['Observe artifact timeline '+(manifest.jobId||'-'),'======================================================='];
lines.push('stateDir='+dir);
lines.push('anchor='+anchor.mode+' value='+(anchor.value||'-')+' from='+(isoOrNull(window.fromMs)||'-')+' to='+(isoOrNull(window.toMs)||'-')+' windowMs='+String(window.windowMs));
lines.push('counts control='+String(eventRows.matchedControlCount)+' samples='+String(eventRows.matchedSampleCount)+' commandFiles='+String(eventRows.matchedCommandFileCount)+' shown='+String(eventRows.rows.length)+' omitted='+String(eventRows.omittedRowCount));
lines.push('', 'TIMELINE');
lines.push(pad('TS',24)+' '+pad('SEQ',5)+' '+pad('KIND',12)+' '+pad('PHASE',12)+' '+pad('TYPE',24)+' '+pad('COMMAND',28)+' '+pad('SESSION',18)+' '+pad('TRACE',18)+' SUMMARY');
if(eventRows.rows.length===0) lines.push('(no artifact rows matched; widen --window-ms or choose another --command-id/--timestamp/--turn)');
for(const row of eventRows.rows){
lines.push(pad(row.ts||'-',24)+' '+pad(row.seq??'-',5)+' '+pad(row.kind,12)+' '+pad(row.phase,12)+' '+pad(row.type,24)+' '+pad(row.commandId||'-',28)+' '+pad(row.sessionId||'-',18)+' '+pad(row.traceId||'-',18)+' '+short(row.summary,100));
}
const id=manifest.jobId||'<observer>';
lines.push('', 'NEXT');
lines.push(' turn summary: bun scripts/cli.ts web-probe observe collect '+id+' --view turn-summary');
lines.push(' trace frame: bun scripts/cli.ts web-probe observe collect '+id+' --view trace-frame --trace-id <traceId> --sample-seq <seq>');
lines.push(' command window: bun scripts/cli.ts web-probe observe collect '+id+' --view timeline --command-id <commandId>');
lines.push(' raw control grep: bun scripts/cli.ts web-probe observe collect '+id+' --view files --file control.jsonl --grep <commandId>');
lines.push('', 'DISCLOSURE valuesRedacted=true valuesPrinted=false source=existing observe artifacts');
return lines.join('\\n');
}
function timelineView(rows){
const commandFiles=readCommandFiles();
const anchor=timelineAnchor(rows,commandFiles);
const window=timelineWindow(anchor);
const eventRows=timelineEventRows(anchor,window,commandFiles);
const timelineRows=eventRows.rows.map((row)=>({ts:row.ts?short(row.ts,32):null,seq:row.seq??null,kind:row.kind,phase:short(row.phase,24),type:short(row.type,40),commandId:row.commandId?short(row.commandId,64):null,sessionId:row.sessionId?short(row.sessionId,64):null,traceId:row.traceId?short(row.traceId,64):null,summary:short(row.summary,140),valuesRedacted:true}));
return {anchor:{...anchor,from:isoOrNull(anchor.startMs),to:isoOrNull(anchor.endMs),startMs:undefined,endMs:undefined,anchorMs:undefined},window:{from:isoOrNull(window.fromMs),to:isoOrNull(window.toMs),windowMs:window.windowMs,anchorMode:window.anchorMode,valuesRedacted:true},counts:{control:control.length,samples:samples.length,commandFiles:commandFiles.length,matchedControl:eventRows.matchedControlCount,matchedSamples:eventRows.matchedSampleCount,matchedCommandFiles:eventRows.matchedCommandFileCount,shown:timelineRows.length,omitted:eventRows.omittedRowCount,valuesRedacted:true},timelineRows,renderedText:renderTimeline(anchor,window,eventRows),valuesRedacted:true};
}
function pathOnly(value){try{return value?new URL(String(value),'http://x').pathname:null}catch{return null}}
function projectSummaryFromSamples(){
const projectSamples=samples.filter((sample)=>sample?.projectManagement&&typeof sample.projectManagement==='object');
@@ -419,6 +597,11 @@ 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);
}
if(view==='timeline'){
const timeline=timelineView(rows);
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,...timeline,sourceFiles:['samples.jsonl','control.jsonl','commands/pending/*.json','commands/processing/*.json','commands/done/*.json','commands/failed/*.json','commands/abandoned/*.json'],valuesRedacted:true}));
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"`;
@@ -19,6 +19,10 @@ export interface WebObserveWrapperInput {
readonly collectFile: string | null;
readonly collectTraceId: string | null;
readonly collectSampleSeq: number | null;
readonly collectTimestamp: string | null;
readonly collectTurn: number | null;
readonly collectCommandId: string | null;
readonly collectWindowMs: number | null;
readonly analyzeArchivePrefix: string | null;
}
@@ -36,6 +40,10 @@ export interface WebObserveWrapperOptionsLike {
readonly collectFile: string | null;
readonly collectTraceId: string | null;
readonly collectSampleSeq: number | null;
readonly collectTimestamp: string | null;
readonly collectTurn: number | null;
readonly collectCommandId: string | null;
readonly collectWindowMs: number | null;
readonly analyzeArchivePrefix: string | null;
}
@@ -106,6 +114,10 @@ export function buildWebObserveWrapperContract(input: WebObserveWrapperInput): R
collectFile: input.collectFile,
collectTraceId: input.collectTraceId,
collectSampleSeq: input.collectSampleSeq,
collectTimestamp: input.collectTimestamp,
collectTurn: input.collectTurn,
collectCommandId: input.collectCommandId,
collectWindowMs: input.collectWindowMs,
analyzeArchivePrefix: input.analyzeArchivePrefix,
valuesRedacted: true,
},
@@ -168,6 +180,10 @@ export function buildWebObserveWrapperForObserveOptions(
collectFile: options.collectFile,
collectTraceId: options.collectTraceId,
collectSampleSeq: options.collectSampleSeq,
collectTimestamp: options.collectTimestamp,
collectTurn: options.collectTurn,
collectCommandId: options.collectCommandId,
collectWindowMs: options.collectWindowMs,
analyzeArchivePrefix: options.analyzeArchivePrefix,
});
}
@@ -204,6 +220,10 @@ function webObserveWrapperCommandShape(input: WebObserveWrapperInput): string {
if (input.collectFile !== null) parts.push("--file", input.collectFile);
if (input.collectTraceId !== null) parts.push("--trace-id", input.collectTraceId);
if (input.collectSampleSeq !== null) parts.push("--sample-seq", String(input.collectSampleSeq));
if (input.collectTimestamp !== null) parts.push("--timestamp", input.collectTimestamp);
if (input.collectTurn !== null) parts.push("--turn", String(input.collectTurn));
if (input.collectCommandId !== null) parts.push("--command-id", input.collectCommandId);
if (input.collectWindowMs !== null) parts.push("--window-ms", String(input.collectWindowMs));
}
if (input.action === "analyze" && input.analyzeArchivePrefix !== null) {
parts.push("--archive-prefix", input.analyzeArchivePrefix);
+2
View File
@@ -152,6 +152,8 @@ export interface NodeWebProbeObserveOptions {
collectSampleSeq: number | null;
collectTimestamp: string | null;
collectTurn: number | null;
collectCommandId: string | null;
collectWindowMs: number | null;
analyzeArchivePrefix: string | null;
analyzeTailSamples: number | null;
full: boolean;
+33 -1
View File
@@ -189,6 +189,8 @@ export function parseNodeWebProbeObserveOptions(
"--sample-seq",
"--timestamp",
"--turn",
"--command-id",
"--window-ms",
"--compact-raw",
"--archive-prefix",
"--tail-samples",
@@ -237,6 +239,11 @@ export function parseNodeWebProbeObserveOptions(
const collectTurnRaw = optionValue(args, "--turn") ?? null;
const collectTurn = collectTurnRaw === null ? null : Number(collectTurnRaw);
if (collectTurn !== null && (!Number.isInteger(collectTurn) || collectTurn < 1)) throw new Error("unsafe web-probe observe --turn: expected positive integer");
const collectCommandId = optionValue(args, "--command-id") ?? null;
if (collectCommandId !== null && (!/^[A-Za-z0-9_.:-]+$/u.test(collectCommandId) || collectCommandId.length > 120)) throw new Error("unsafe web-probe observe --command-id: expected 1-120 safe command id chars");
const collectWindowMsRaw = optionValue(args, "--window-ms") ?? null;
const collectWindowMs = collectWindowMsRaw === null ? null : Number(collectWindowMsRaw);
if (collectWindowMs !== null && (!Number.isInteger(collectWindowMs) || collectWindowMs < 1000 || collectWindowMs > 86_400_000)) throw new Error("unsafe web-probe observe --window-ms: expected integer 1000-86400000");
const analyzeArchivePrefix = optionValue(args, "--archive-prefix") ?? null;
if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`);
const analyzeTailSamplesRaw = optionValue(args, "--tail-samples") ?? null;
@@ -308,6 +315,8 @@ export function parseNodeWebProbeObserveOptions(
collectSampleSeq,
collectTimestamp,
collectTurn,
collectCommandId,
collectWindowMs,
analyzeArchivePrefix,
analyzeTailSamples,
full: args.includes("--full"),
@@ -1159,6 +1168,8 @@ export function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOption
sampleSeq: options.collectSampleSeq,
timestamp: options.collectTimestamp,
turn: options.collectTurn,
commandId: options.collectCommandId,
windowMs: options.collectWindowMs,
});
const script = [
"set -eu",
@@ -1179,6 +1190,8 @@ export function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOption
view: options.collectView,
requestedFile: options.collectFile,
requestedGrep: options.collectGrep,
requestedCommandId: options.collectCommandId,
requestedWindowMs: options.collectWindowMs,
degradedReason: collect === null ? "collect-json-parse-failed" : null,
collect: compactRaw ? compactObserveCollectForRaw(collect) : collect,
wrapper: compactRaw
@@ -1217,14 +1230,33 @@ function compactObserveCollectForRaw(collect: Record<string, unknown> | null): R
valuesRedacted: true,
};
}) : undefined;
const timelineRows = Array.isArray(collect.timelineRows) ? collect.timelineRows.slice(0, 12).map((item) => {
const row = observeRecord(item);
return {
ts: row.ts ?? null,
seq: row.seq ?? null,
kind: row.kind ?? null,
phase: row.phase ?? null,
type: row.type ?? null,
commandId: row.commandId ?? null,
sessionId: row.sessionId ?? null,
traceId: row.traceId ?? null,
summary: row.summary ?? null,
valuesRedacted: true,
};
}) : undefined;
return {
ok: collect.ok !== false,
command: collect.command,
view: collect.view,
stateDir: collect.stateDir,
turnCount: collect.turnCount,
anchor: observeRecord(collect.anchor),
window: observeRecord(collect.window),
counts: observeRecord(collect.counts),
...(rows === undefined ? {} : { rows }),
renderedText: typeof collect.renderedText === "string" ? collect.renderedText : undefined,
...(timelineRows === undefined ? {} : { timelineRows }),
renderedText: collect.view === "timeline" ? undefined : typeof collect.renderedText === "string" ? collect.renderedText : undefined,
sourceFiles: Array.isArray(collect.sourceFiles) ? collect.sourceFiles : undefined,
blocker: collect.blocker,
sampleSeq: collect.sampleSeq,