feat(traces): add output bounding flags to traces get#64
Open
dark-sorceror wants to merge 8 commits into
Open
Conversation
…sonl to traces get
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #57
traces getwas unbounded:--jsonserialized the entire trace — every span — as one physical line, and the human tree recursively printed every span with no depth or count cap. On a several-thousand-span agent trace that's a multi-megabyte single line, which is a context bomb for the CLI's core consumer: a coding agent pipingtraces get --jsoninto its own context.This adds four opt-in flags to bound the output —
--max-spans,--depth,--errors-only,--output jsonl— all default-off, so default output stays byte-identical to today. Field selection (the backend'sfieldsprojection) is deliberately deferred to #51's--fields.How it works
--max-spans <n>— caps emitted spans. JSON gainsspans_truncated: { shown, total }; the human tree appends a… N more spansline, whereNis the true remainder.--depth <n>— caps tree depth (roots = 1). Spans deeper than the cap are dropped from both the human tree and the JSON/JSONL body; the human tree marks each elided subtree with… N deeper spans hidden.--errors-only— keeps only error-status spans plus their full ancestor chains, dropping unrelated branches. When nothing matches, that's surfaced explicitly rather than as a silent empty list:errors_only_no_matches: truein JSON,(no error spans in this trace)in the human tree.--output jsonl— streams a header line (the trace minus itsspansarray, plusfindingand any truncation markers) followed by one JSON line per span, written incrementally so a downstreamhead/grepcan stop early. Implies machine output whether or not--jsonis also set, and the global EPIPE handler keeps it clean under a closed pipe.The flags compose in a fixed order —
--errors-onlyfirst, then--depth, then--max-spans— applied once inboundSpans()so every emit path (human tree, JSON, JSONL) reads from the same result and can't diverge. The--max-spanstruncation total always reflects the post-filter set (e.g.--errors-only --max-spans 2on 5 error-path spans reportstotal: 5, not the trace's full span count).Truncation selects spans in tree traversal order — the same depth-first, sibling-by-start-time order the human tree prints in (
treeOrder()), built on the same sharedbuildTree()the renderer and depth/errors-only filters use — so the human tree and the JSON/JSONL body always keep the same spans, even when array order and tree order disagree (e.g. a later-starting sibling listed earlier in the backend array) or the span graph is malformed (orphans, self-parents, parent cycles). The kept spans are then emitted in the backend's original array order, so the JSON body stays array-stable under truncation.What's included
src/render/tree.tsbuildTree()— shared roots/children/sibling-order construction, used byrenderTree,treeOrder, and depth computation so they can't drift apart.isErrorStatus()— hoisted out as the single source of truth for error classification (renderer +--errors-onlyfilter).spansWithinDepth()— flat-array depth filter matching the renderer's depth cap, including cycle/orphan recovery.filterErrorsWithAncestors()— keeps error spans plus their ancestor chain.treeOrder()— spans in the renderer's exact traversal order; the shared selection order for--max-spans.renderTree()— newmaxDepth/maxSpansoptions, with… N deeper spans hiddenand… N more spanselision markers.src/commands/traces/get.tsboundSpans()— applies--errors-only→--depth→--max-spansonce, feeding every output path.--max-spans <n>,--depth <n>,--errors-only,--output jsonlflag registration, plus apositiveIntOnce()validator shared in spirit withonceOption.runGet.Tests
tests/render/tree.test.ts— depth cap, max-spans cap,spansWithinDepth,filterErrorsWithAncestors,isErrorStatus,treeOrder, and adversarial cases (cycles, orphans, self-parents) pinning human/JSON agreement.tests/commands/traces-get.test.ts— unit coverage for each flag, flag composition, and tree-order selection consistency (sibling reordering, child-before-parent array order).tests/helpers/traceServer.mjs— throwaway local stub server (own process, since contract tests block the event loop onspawnSync) serving a configurable-size trace.tests/output.contract.test.ts— contract tests against the spawned binary + stub server: jsonl validity over a 2000-span trace, truncation marker totals, and a real| head -1EPIPE check.README.mdTest plan
tests/helpers/traceServer.mjs) over a 2000-span trace, and a realtraces get ... --output jsonl | head -1EPIPE checkapp.traceroot.aion a 123-span trace: default output unchanged — 123 spans, no marker keys--max-spans 10→ 10 spans +spans_truncated: {shown: 10, total: 123}+… 113 more spansin the human tree--depth 1→ root only +… 122 deeper spans hidden--errors-onlyon an error trace → 2 error spans + 1 ancestor; on a clean trace → expliciterrors_only_no_matches/(no error spans in this trace)--output jsonl→ 124 lines, all independently JSON-valid--max-spans 10) / 9,395 bytes (--errors-only)--max-spans 0and--output jsonboth rejected as usage errorsSummary by cubic
Adds opt-in bounds to
traces getto cap span count, depth, or show errors-only, plus a--output jsonlstreaming mode; default output stays unchanged. Also ensures depth-elision markers remain visible when a span cap is applied.New Features
--max-spans <n>: cap span count; JSON addsspans_truncated { shown, total }; human tree shows… N more spans.--depth <n>: cap tree depth (roots = 1); deeper spans are dropped; human tree shows… N deeper spans hidden.--errors-only: keep error spans and their ancestors; explicit no-match indicators in human and JSON (errors_only_no_matches: true).--output jsonl: stream a header (trace withoutspans, plusfindingand markers) then one span per line; safe forhead/grep; implies machine output.errors-only→depth→max-spans. Truncation selects by tree traversal order so human and JSON/JSONL keep the same spans; JSON/JSONL emit kept spans in original backend array order.Refactors
buildTree,treeOrder), depth filter (spansWithinDepth), and error check (isErrorStatus) keep human and JSON/JSONL in sync, including cycles/orphans.--depthand--max-spans.Written for commit c0b42bb. Summary will update on new commits.