Row-budget crush for updates/list_refhosts/openqa_overview - #610
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #610 +/- ##
==========================================
+ Coverage 96.76% 96.83% +0.07%
==========================================
Files 213 214 +1
Lines 61858 62943 +1085
==========================================
+ Hits 59854 60951 +1097
+ Misses 2004 1992 -12 ☔ View full report in Codecov by Harness. |
mimi1vx
left a comment
There was a problem hiding this comment.
High priority blocker
- [crates/mtui-core/src/commands/updates.rs:369]
--jsonemits invalid JSON when truncated. When output exceedsROW_CAP(100 rows), appending the plain-text notice line…[truncated N of M rows; narrow with …]after the JSON array breaks standard JSON parsers (jq,serde_json, Pythonjson.loads()). Any pipeline pipingmtui updates --json | jq .or an MCP client parsing JSON tool results will fail with syntax/decode errors. Route the truncation notice to stderr/eprintlnso stdout remains strictly valid JSON, or omit it when--jsonis active. - [crates/mtui-core/src/commands/list_refhosts.rs:449]
--jsonemits invalid JSON when truncated. Plain-text notice appended to stdout after the JSON array corrupts JSON parsing onlist_refhosts --json. Route to stderr/eprintlnor omit on--json.
Blocker
- [crates/mtui-core/src/commands/openqa_overview.rs:261] Redundant heap allocations during crush.
single_incidents.clone()andgroup.versions.clone()duplicate largeVec<VersionResult>structures solely to extract deduplication keys. Pass&[VersionResult]or take ownership to avoid multiple bulk vector clones. - [crates/mtui-core/src/commands/row_budget.rs:35] Optimize dedup key generation in
updates.key_ofcurrently runsserde_json::to_string(v)for everyValuein the queue to insert into aHashSet<String>. For large queues, keying by a lightweight tuple (such as(id, status, priority)) avoids hundreds of JSON serialization allocations during dedup.
83076fb to
34cf8c8
Compare
mimi1vx
left a comment
There was a problem hiding this comment.
Blockers
- [P0] The row-crush truncation notice for
--jsonmode goes to realeprintln!/stderr, whichmtui-mcpnever surfaces on a successful call.commands/updates.rsandcommands/list_refhosts.rswrite the notice to stderr, butMcpSession::run_commandonly returns the captured display sink on success — stderr is not merged in. An MCP client requestingupdates --jsonorlist_refhosts --jsonon a large result silently gets an incomplete listing with no per-call signal that rows were dropped. This is inconsistent with the precedent already set bymtui-mcp/src/slim.rs's byte-cap (cap_output/truncation_notice), which accepts corrupting stdout's JSON validity specifically so the client is informed. Route the row-crush notice the same way (in-band, even if it costs JSON validity) before merging — this is the exact failure mode ("silent truncation") the row-budget feature exists to prevent. - [P1]
commands/updates.rs::update_keymishandles an explicit"id": nullrow.field("id")returnsSome("null")for an explicit null (notNone, since the key is present), so such a row skips therest-based exact-dedup fallback and is keyed only by(Some("null"), status, priority)— two genuinely different null-id rows sharing status/priority silently collapse into one. Check.is_null()alongside "key absent." - [P2]
commands/row_budget.rs:94has no compile-time or runtime guard onROW_HEAD + ROW_TAIL <= ROW_CAP.anomaly_idx.truncate(ROW_CAP - ROW_HEAD - ROW_TAIL)will underflow (panic in debug, wrap to a huge length in release, silently defeating the cap) if a future edit changes any of the three constants without preserving the relation. Addconst _: () = assert!(ROW_HEAD + ROW_TAIL <= ROW_CAP);. - [P2] No
instasnapshot coverage for the new notice text or the crushed table/JSON shape — only ad-hoccontains/starts_withassertions. Per this project's testing conventions, display-output text contracts belong underinstaso incidental wording drift is caught.
mimi1vx
left a comment
There was a problem hiding this comment.
BLOCKING: crates/mtui-core/src/commands/updates.rs:930-938 and list_refhosts.rs:148-155 — --json truncation is routed to eprintln!/stderr to keep stdout valid JSON, but MCP never captures real stderr (confirmed by this PR's own test). An MCP --json call over the row cap returns a silently truncated array with zero in-band signal, contradicting this project's own "never a silent cut" convention and regressing the guarantee the existing byte-cap mechanism (slim.rs::cap_output/truncation_notice) already provides.
BLOCKING: crates/mtui-core/src/commands/row_budget.rs:696-699 — anomaly retention is capped at 50 and kept in arbitrary positional order, not by severity. Real failures (failed jobs, locked hosts) can be dropped silently alongside routine rows with no distinguishing count in the notice.
BLOCKING: crates/mtui-core/src/commands/list_refhosts.rs:113-126 — probe_locks under --free still runs against every matched host before --offset/--limit windowing is applied, so paging doesn't reduce the SSH probe cost this feature exists to address.
FLAG: same root failure mode as #620/#611 — this PR silently drops information instead of signaling it in-band, mirroring #620's secret-redaction gap. Please resolve all three PRs under one shared pattern rather than as three separate patches to the same lesson.
get: restore 'full' disambiguating full remote size vs returned bytes. testreport_patch/write relpath: example back at property level (e.g. install_logs/<host>.log), mirroring read. updates --json: describe final openSUSE#610 in-band trailing-notice + strip protocol, pointing at --limit only (--offset absent here; one-token divergence vs openSUSE#610). Land after openSUSE#610: help describes not-yet-merged behavior until then. cli.md via xtask gen-docs; 3 snaps regen, no other churn.
f15319e claimed over-cap --json is a kept-rows array plus trailing in-band notice line, but this tree has no row budget (plain --limit slice + to_string_pretty; over-cap goes through the byte-cap path). Truthful minimal wording: --limit-capped pretty array, unlike -F, plus the --limit narrowing hint; nothing about truncation. Land after openSUSE#610: openSUSE#609's help hunk yields to openSUSE#610's exact text at land time; openSUSE#610's merge brings the real truncation-protocol wording. cli.md via xtask gen-docs; 1 snap regen, no other churn.
|
Review-sweep responses (2026-09-12) — all findings addressed, each rework adversarially re-reviewed (GO) with red-proven tests, full workspace gates green. Cross-PR summary (per-PR diffs carry the detail):
One systemic thread across #610/#611/#620: new I/O or truncation reuses the project primitives (spawn_blocking for blocking I/O, in-band truncation notices) — applied in each. |
mimi1vx
left a comment
There was a problem hiding this comment.
Blocker (P0)
crates/mtui-core/src/commands/list_refhosts.rs:473-486:--freewindowing emits truncation notice on user-limited queries, breaking--jsonand fabricating anomaly totals. Inlist_refhosts,noticeis triggered whenever(free && window_dropped > 0). When a caller explicitly requests--free --limit 10 --json,window_droppedismatched_total - 10 > 0, which forces a plain-text notice line…[truncated 140 of 150 rows (K/K anomalies kept); narrow with --limit/...]to be appended to stdout after the JSON array. This breaks JSON parsers for callers who explicitly narrowed their query. Furthermore,row_noticeasserts thatK/K anomalies keptwere retained across all 150 hosts, even though the 140 unprobed hosts were never surveyed for locks. Only emitrow_noticewhen the budget crush itself drops rows (crushed.truncated > 0), and decouple partial-inventory probe notes from row truncation.crates/mtui-core/src/commands/updates.rs:38-44:update_keydoes not handle JSONnullIDs, falsely deduping distinct id-less rows. Inupdate_key,let id = field("id");producesSome("null")when"id": nullbecauseToStringformatsValue::Nullas"null". Consequently,id.is_none()evaluates tofalse, leavingrestasNone. Any two distinct update rows that have"id": nulland matchingstatusandpriorityreceive the exact same key(Some("null"), status, priority, None), causing the second row to be silently dropped as a duplicate. Filter out nulls:let id = v.get("id").filter(|v| !v.is_null()).map(ToString::to_string);.
Blocker (P1)
crates/mtui-core/src/commands/row_budget.rs:59-67:row_noticereports middle-slice anomaly counts as whole-dataset counts. Whentotal > ROW_CAP,scoredis gathered strictly from(ROW_HEAD..tail_start).anomaly_totalandanomaly_keptcompletely ignore anomalies in the head (first 40 rows) and tail (last 10 rows). If 5 failed jobs or locked hosts are located in the head and none in the middle slice, the notice reports…[truncated 50 of 150 rows (0/0 anomalies kept); narrow with …]despite 5 anomalies being clearly visible, misleading callers into believing no anomalies exist. Calculateanomaly_totalandanomaly_keptacross the complete dataset (0..total).crates/mtui-core/src/commands/updates.rs:397-404,list_refhosts.rs:482-486: Trailing non-JSON in-band notice corrupts stdout for standard JSON parsers and CLI pipelines. Appending…[truncated N of M rows …]directly to stdout breaks standard CLI JSON tooling (mtui updates --json | jq .) whenever output exceeds the row budget. Emit structured metadata / JSON envelope when--jsonis requested, rather than concatenating plain text to a serialized JSON array.
Blocker (P2)
crates/mtui-core/src/commands/row_budget.rs:158-168: Omit(0/0 anomalies kept)whenanomaly_total == 0. Formatting(0/0 anomalies kept)when no anomalies exist looks like an unhandled edge case or template defect. Omit the parenthetical whenanomaly_total == 0.
get: restore 'full' disambiguating full remote size vs returned bytes. testreport_patch/write relpath: example back at property level (e.g. install_logs/<host>.log), mirroring read. updates --json: describe final #610 in-band trailing-notice + strip protocol, pointing at --limit only (--offset absent here; one-token divergence vs #610). Land after #610: help describes not-yet-merged behavior until then. cli.md via xtask gen-docs; 3 snaps regen, no other churn.
f15319e claimed over-cap --json is a kept-rows array plus trailing in-band notice line, but this tree has no row budget (plain --limit slice + to_string_pretty; over-cap goes through the byte-cap path). Truthful minimal wording: --limit-capped pretty array, unlike -F, plus the --limit narrowing hint; nothing about truncation. Land after #610: #609's help hunk yields to #610's exact text at land time; #610's merge brings the real truncation-protocol wording. cli.md via xtask gen-docs; 1 snap regen, no other churn.
8937634 to
ff85d94
Compare
mimi1vx
left a comment
There was a problem hiding this comment.
All four prior blockers (--json validity, stderr-routed truncation notice, --free window fabricating anomalies) are resolved and verified against PR head — I reverted each fix independently and confirmed the regression tests go red. Tests/clippy/fmt/doc-gen all clean. Approving.
|
This PR is approved and was @plusky could you rebase |
|
and with rebase also squash commits ? |
updates, list_refhosts and openqa_overview keep first-40 + last-10 + all anomaly rows (exact-deduped, hard cap 100) with a narrowing-hint notice; --json stays a valid array with the notice trailing. openqa --export still writes the full overview.
--json help states the over-cap protocol (valid array + trailing …[truncated line, strip prefix). updates gains --offset, list_refhosts gains --limit/--offset (pre-crush paging, fits --limit plumbing). Unknown/missing/null status is anomaly-keep; row-cap != byte-cap noted (~600 openqa total); export still full.
--json truncation notice goes to stderr so stdout stays a valid JSON array; updates dedups on (id, status, priority); openqa crush borrows slices instead of cloning bulk vecs.
…test Both --json helps now state MCP results carry no truncation signal and MUST page with --limit/--offset; stderr clause kept CLI-only. CHANGELOG, cli.md, slimmed snapshot regen. Vestigial strip test now parses stdout as-is with fake row preserved.
Over-cap --json stdout is a JSON array plus a trailing …[truncated notice line, like the byte-cap convention, so MCP captures the signal too; naive parse fails loudly, strip …[truncated lines. Keeps crush/offset/anomaly, tuple keys, slice path.
Anomaly overflow keeps severe rows first (updates failed/blocked, refhosts locked/claimed, openQA failed) with kept/total counts in the notice; list_refhosts --free windows before probing so paging cuts SSH cost, notice/footer stay pre-window.
…y counts R4-P0a: list_refhosts emits truncation only on real row-budget cut; --free windowing gets decoupled probe note/envelope, no fabricated K/K. R4-P0b: update_key filters null ids to full-row fallback. R4-P1: anomaly counts cover 0..total incl. head/tail. R4-P2a: --json over-cap is rows+truncation(/probe) envelope, stdout stays valid JSON. R4-P2b: row_notice omits (K/L) when L==0. R2-P2s: insta snapshots for notice/probe/envelope. R3-D: shared truncation-notice convention in row_budget docs.
eb41af3 to
bc1cdb2
Compare
Summary
Caps the three unbounded MCP listings (
updates,list_refhosts,openqa_overview) at a row budget (head 40 + tail 10 + first 50 middle anomalies, cap 100) so large queues/inventories no longer blow past[mcp] max_output_bytes. Adds--offsettoupdatesand--limit/--offsettolist_refhostsso any dropped middle slice stays recoverable.Changes
crates/mtui-core/src/commands/row_budget.rs: deterministic crush (exact-dedup, head+tail+anomaly keep, order-preserving) plusrow_noticenaming the narrowing flags.updates: anomaly = status other thantesting(unknown/missing kept, safe direction); new--offset, applied pre-crush (skipthentruncate).list_refhosts: anomaly = non-free lock/pool claim; new--limit/--offset.openqa_overview: per-section crush, anomalies = non-passed/non-empty matches;--exportstill writes the full uncrushed overview.--jsonstays a valid array; over-cap output appends one trailing…[truncated N of M rows; …]line — strip lines starting with that prefix before parsing (documented in both--jsonhelps, which flow into the MCP descriptions).Checklist
cargo fmt --all --checkis clean.cargo clippy --workspace --all-targets --all-features -- -D warningsis clean.cargo test --workspacepasses.--no-default-features,--all-features, compile-only).CHANGELOG.md.docs/src/cli.mdregenerated viacargo xtask gen-docs;--jsonhelps document the wire protocol).Related issues
Part 1 of 4 in the MCP token-reduction stack (row-budget crush for the 99k-token listing spikes). Merge order: log-fold → json-crush → reread-dedup → token-nudges. Expected trivial conflicts with siblings on
CHANGELOG.mdandcrates/mtui-mcp/tests/it.rs; resolved at merge time in that order.