Skip to content

fix(desktop): stop dropping 19 of the harness's log targets - #16

Merged
yjc801 merged 2 commits into
mainfrom
claude/widen-agent-log-filter
Aug 10, 2026
Merged

yjc801 merged 2 commits into
mainfrom
claude/widen-agent-log-filter

Conversation

@yjc801

@yjc801 yjc801 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

child_rust_log_filter handed the spawned harness buzz_acp=info. That looks
right and matches almost nothing.

EnvFilter matches on an event's target, which defaults to the module path —
but buzz-acp sets an explicit target: on its diagnostic lines, and an explicit
target replaces the module path rather than extending it. None of those
targets begin with buzz_acp, so none of them ever matched.

That silenced 19 targets under five roots:

Root Targets
pool:: prompt, session, model, permission, metrics
acp:: wire, update, usage, permission, tool, cancel, thought, stream, session, plan, init
canvas:: fetch
engram:: core
observer

Two of the casualties are lines whose absence cost real debugging time: the only
record that a session was created (created session … for channel …), which made
session rotation unobservable, and pool::model's model-override miss, which is
what would say whether a [1m] model ref actually reaches the API. Both questions
went unanswered for days against a log that was structurally incapable of
answering them.

The fix names each root explicitly. With no RUST_LOG set, the harness gets:

buzz_acp=info,pool=info,acp=info,acp::stream=off,engram=info,canvas=info,observer=info

buzz_acp= stays in the list — lines that don't override target: still fall
back to the module path, which does start with buzz_acp.

Why info and not debug — measured, not assumed. Across these targets the
call sites are roughly 11 debug, 7 info, 6 warn, 2 error. At info the
debug lines stay off, including acp::wire's 11 frame dumps and acp::thought,
so this surfaces the lines worth reading without inflating every agent's log.

Why acp::stream=off — it is the one info-level site that fires per chunk,
logging the text of every agent_message_chunk
(crates/buzz-acp/src/acp.rs), so acp=info
alone would copy every agent response into the runtime log verbatim. Nothing
would bound that: maybe_rotate_log runs only inside open_log_file
(managed_agents/storage.rs), which is called once at spawn, so the 10 MB
ceiling is never re-checked for the life of the process. It stays available on
request — RUST_LOG=acp::stream=trace or acp=debug both turn it on.

Defaults are merged per target, not appended. Appending is not neutral:
EnvFilter keeps one directive per (target, span, fields), and DirectiveSet::add
does Ok(i) => self.directives[i] = directive on a binary-search hit while
Ord for Directive compares target/span/fields and never the level. So a trailing
pool=info wins over an operator's RUST_LOG=pool=off, and clips acp=debug
back to info. Instead, a default is dropped when the operator names its target
or an ancestor of it:

RUST_LOG result
unset all seven defaults
pool=off pool stays off; other roots still covered
acp=debug drops both acp=info and acp::stream=off — debug means debug
acp::stream=trace drops only acp::stream=off; acp=info still covers the siblings
warn a bare level names no target, so it suppresses nothing
anything naming buzz_acp passed through untouched (see Scope)

Directive parsing strips the [span{field=value}] section before splitting on
=, since that section can itself contain one.

Scope

Local agents only. A provider-backed agent's harness is launched by its
backend from a separate env and never through this path, and
get_managed_agent_log refuses remote agents outright. This does nothing for
diagnosing a sprite.

The operator escape hatch is preserved: a RUST_LOG that names buzz_acp
explicitly is passed through untouched, so someone narrowing the filter to one
target doesn't get it widened back out.

Related issue

Upstream block/buzz#3309 — same bug, other side of the seam. Currently open, not merged.

block#3309 diagnoses the identical mechanism (explicit target: replaces the module
path, so buzz_acp=* matches none of it) and fixes it in buzz-acp by
prefixing every custom target with buzz_acp::, so the crate filter reaches
them. That is the better fix and it strictly dominates this one on coverage:

  • it fixes every consumer, including provider-backed sprites, which this PR
    explicitly does not;
  • it makes RUST_LOG=buzz_acp=debug behave as TESTING.md already documents,
    instead of leaving the docs wrong;
  • it needs no per-root list to be kept in sync.

This PR is a fork-local workaround that works today without waiting on an
upstream merge, and is worth having on those terms — but a reviewer should know
it is the narrower of the two fixes.

They are compatible. If block#3309 lands and this fork merges upstream, the
re-rooted targets match the buzz_acp=info clause that this filter still
carries; the extra roots become inert rather than conflicting. The redundant
roots should be dropped at that point, and HARNESS_TARGET_ROOTS in
log_filter.rs is the thing that will flag it — it asserts against the roots
buzz-acp actually uses, so a re-rooting upstream fails that test loudly instead
of leaving dead config behind. acp::stream=off would need re-expressing as
buzz_acp::acp::stream=off at the same time.

Layout

The helper and its tests moved out of runtime.rs / runtime/tests.rs into
managed_agents/runtime/log_filter.rs (281 lines). Keeping them in place
pushed runtime.rs to 1022 against a 1000-line limit and runtime/tests.rs
to 1333 against its merge-base ratchet of 1275, so just ci could not pass.
Both files are back at their base contents.

Testing

cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib log_filter

test result: ok. 11 passed; 0 failed. Full gate: just ci green.

The tests assert resolved behaviour, not substrings. The defect this module
exists to fix was a filter that read correctly and matched nothing, so
filter.contains("pool=info") is not evidence of anything. Each case parses the
produced filter and asks whether a target is enabled at a level, through
tracing_subscriber::filter::Targets — a newtype over the same
DirectiveSet<StaticDirective> that backs EnvFilter's static directives,
parsed and resolved most-specific-first the same way. Added as a
dev-dependency; it was already in desktop/src-tauri/Cargo.lock, so the
lockfile change is one line.

Test Asserts
default_filter_enables_every_harness_target_root every root in HARNESS_TARGET_ROOTS resolves to enabled at info; a sixth root added upstream fails here rather than silently going dark
harness_targets_are_enabled_at_info_not_debug acp::wire/pool::prompt stay off at debug — guards against "fixing" quiet logs by turning on the frame dumps
response_chunks_are_off_by_default acp::stream off at every level, siblings still on
an_empty_or_blank_rust_log_is_treated_as_unset "" and whitespace
an_unrelated_rust_log_is_extended_not_replaced hyper=warn survives, defaults appended
an_explicit_buzz_acp_filter_is_passed_through_untouched operator override
an_operator_silenced_root_stays_silenced pool=off is not re-enabled — the regression test for the append bug
an_operator_raised_root_is_not_downgraded acp=debug reaches debug, including acp::stream
asking_for_the_stream_gets_the_stream_and_keeps_the_rest a leaf directive suppresses only the leaf default
a_bare_level_names_no_target_and_suppresses_nothing RUST_LOG=warn
a_span_field_directive_does_not_confuse_target_parsing pool[work{id=7}]=off parses as target pool, not pool[work{id

Two traps worth naming, because both produce a green result that means nothing:

  1. The desktop crate is excluded from the root workspace, so a bare cargo test
    at the repo root does not cover this. The --manifest-path is required.
  2. cargo test exits 0 when a name filter matches nothing. Filtering on a
    function name that no test is called leaves you with a passing build that
    verified nothing — check the reported test count, not the exit code.

No UI change, so no screenshots.

🤖 Generated with Claude Code

yjc801 added 2 commits August 9, 2026 22:55
`child_rust_log_filter` handed the spawned harness `buzz_acp=info`, which
looks right and matches almost nothing. EnvFilter matches on a span's TARGET,
which defaults to the module path — but buzz-acp sets an explicit `target:` on
its diagnostic lines, and an explicit target REPLACES the module path rather
than extending it. None of those targets start with `buzz_acp`.

Silenced: 19 targets under five roots — pool:: (prompt, session, model,
permission, metrics), acp:: (wire, update, usage, permission, tool, cancel,
thought, stream, session, plan, init), canvas::fetch, engram::core, observer.

Two of the casualties are lines whose absence cost real debugging time: the
only record that a session was created ("created session … for channel …"),
so session rotation was unobservable, and pool::model's model-override miss,
which is what would say whether a `[1m]` model ref actually reaches the API.
Both questions went unanswered for days against a log that was structurally
incapable of answering them.

info, not debug, and that is measured rather than assumed: across these
targets the call sites are roughly 11 debug, 7 info, 6 warn, 2 error. At info
the debug lines stay off — including acp::wire's frame dumps — so this
surfaces the ~15 lines worth reading without inflating every agent's log.

Split the env read from the rule so the rule is testable without mutating
process state that parallel tests share. The regression test asserts coverage
against a list of the roots buzz-acp actually uses, so adding a sixth root
upstream fails here rather than silently going dark.

Scope worth stating plainly: LOCAL agents only. A provider-backed agent's
harness is launched by its backend from a separate env and never through this
path, and `get_managed_agent_log` refuses remote agents outright. This does
nothing for diagnosing a sprite.

Signed-off-by: Junchao Yan <yjc801@gmail.com>
Round 2 on #16. Three P2s from review, all reproduced against source
before fixing.

1. acp::stream is a per-chunk firehose, not a diagnostic line.

   `acp=info` also enables `acp::stream`, whose agent_message_chunk arm
   is `tracing::info!` on the chunk text (buzz-acp/src/acp.rs:1734) —
   every agent response copied verbatim into the runtime log. Nothing
   bounds that: `maybe_rotate_log` runs only inside `open_log_file`
   (managed_agents/storage.rs), which is called once at spawn, so the
   10 MB ceiling is never re-checked for the life of the process.

   Default is now `acp::stream=off`. It is the only info-level per-chunk
   site in the set — acp::thought and all 11 acp::wire sites are already
   debug, so nothing else needed naming.

2. Appending defaults silently overrode operator directives.

   `RUST_LOG=pool=off` produced `pool=off,…,pool=info`. That is not a
   no-op: EnvFilter keeps one directive per (target, span, fields), and
   `DirectiveSet::add` does `Ok(i) => self.directives[i] = directive` on
   a binary-search hit while `Ord for Directive` compares target/span/
   fields and never the level. The later duplicate wins, so the default
   re-enabled logs the operator had switched off, and `acp=debug` was
   clipped back to info.

   Defaults are now merged per target rather than concatenated: a
   default is dropped when the operator names its target or an ancestor
   of it. `acp=debug` therefore suppresses both `acp=info` and
   `acp::stream=off`; `acp::stream=trace` suppresses only the latter,
   leaving `acp=info` to cover the sibling targets the operator said
   nothing about. Directive parsing strips the `[span{field=value}]`
   section before splitting on `=`, since that section can contain one.

3. The additions broke the desktop file-size ratchet.

   runtime.rs went 985 -> 1022 against a 1000-line limit, and
   runtime/tests.rs 1275 -> 1333 while already capped at its merge-base
   1275, so `just ci` could not pass. Both files are back at their base
   contents; the helper and its tests live in a new
   managed_agents/runtime/log_filter.rs (281 lines).

Tests now assert resolved behaviour instead of substrings. The defect
this module exists to fix was a filter that read correctly and matched
nothing, so `filter.contains("pool=info")` is not evidence. Each case
parses the produced filter and asks whether a target is enabled at a
level, via `filter::Targets` — a newtype over the same
`DirectiveSet<StaticDirective>` that backs EnvFilter's static
directives, added as a dev-dependency (already in the desktop lockfile;
one line added).

Signed-off-by: Junchao Yan <yjc801@gmail.com>
@yjc801
yjc801 merged commit 81ffbcd into main Aug 10, 2026
41 of 48 checks passed
@yjc801
yjc801 deleted the claude/widen-agent-log-filter branch August 10, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant