@@ -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 + / g u , ' ' ) . t r i m ( ) ; r e t u r n t e x t . l e n g t h > l i m i t ? t e x t . s l i c e ( 0 , M a t h . m a x ( 1 , l i m i t - 1 ) ) + ' . . . ' : t e x t ; } ;
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 ( / [ . ] j s o n $ / u , ' ' ) | | n u l l }
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" ` ;