Repository navigation
feat(drain)!: bound the advisory payload by a per-rule cap and a token budget - #438
Merged
wenzowski merged 2 commits intoAug 14, 2026
Conversation
CLOUD-82 Keep advisory drain output pointer-only under a token budget
Why Acceptance
Refinement from the identity decision (CLOUD-123) — this issue owns the cardinality cap:
Refinement — Ready (the drain's emission contract: pointers, caps, and a test-asserted token budget) Refinement gate: Definition of Ready & Done. This body carries only specializations.
|
wenzowski
marked this pull request as ready for review
August 14, 2026 02:56
wenzowski
force-pushed
the
wenzowski/cloud-82-keep-advisory-drain-output-pointer-only-under-a-token-budget
branch
from
August 14, 2026 03:04
31cbf2e to
28d8812
Compare
wenzowski
marked this pull request as draft
August 14, 2026 03:44
wenzowski
force-pushed
the
wenzowski/cloud-82-keep-advisory-drain-output-pointer-only-under-a-token-budget
branch
from
August 14, 2026 03:57
28d8812 to
a41fc38
Compare
…n budget The drain rendered one line per surfaced identity with no ceiling: a rule firing on two hundred identities inside one changed scope spent two hundred lines of the agent's context, and nothing asserted a bound on the payload at all. `cycle` is now three named stages — select, cap, clamp — because the cap is a decision about a SET: a renderer that emitted as it walked could not know an eleventh identity for the same rule was still coming. Two bounds, measuring different things, and the store can tell them apart. The per-rule cardinality cap collapses a rule over `K` distinct identities to one `rule R: K+ findings` line and journals what it withheld as `over-cardinality-cap` — a statement about the rule, and the counter rule-health telemetry reads. The token budget clamps the payload as a whole, measured with `budget::estimate_tokens` rather than a second estimator, and journals what it dropped as `drain-suppressed` — a statement about this boundary, reconsidered on the next one. Between them, lines are ordered salient-first by tier, then rule, then fingerprint. The occurrence count is deliberately not a sort key: that is CLOUD-80's no-escalation law made structural rather than remembered. A group re-raise renders `old->new` against what this session's last drain actually said, which the wake state now remembers per emitted identity. The store carries no count anchor, and the honest anchor for "worth saying again" is what the agent was last told. A count that fell renders plainly — a ratchet is not a re-raise, because re-raising on incremental fixing punishes the fix. Volume and suppression counts go to the operator on the verbose rung, never into the agent payload; routing them into `systemMessage` proper rides CLOUD-44's per-host emitter shims. BREAKING CHANGE: `DrainConfig` gains `cardinality_cap` and `token_budget` and `WakeState` gains `counts`, so neither is constructible from its fields alone without `..Default::default()`; `drain::cycle` takes the config and the previous drain's counts, and `WakeState::drained` takes the completed cycle rather than a bare digest. `[drain]` in `batten.toml` is unchanged for anyone who declared no new key — both default. Refs: CLOUD-82
…nd means The module map said the cardinality cap and the token budget were CLOUD-82's, which was true while they did not exist. It now says what they are, and — the part a reader cannot recover from the code without tracing both call sites — why the two withheld sets are journalled under different reasons: one is a statement about the rule, the other about this boundary. Refs: CLOUD-82
wenzowski
marked this pull request as ready for review
August 14, 2026 04:23
wenzowski
force-pushed
the
wenzowski/cloud-82-keep-advisory-drain-output-pointer-only-under-a-token-budget
branch
from
August 14, 2026 04:23
a41fc38 to
991973b
Compare
|
Contributor
Author
|
/fast-forward |
wenzowski
deleted the
wenzowski/cloud-82-keep-advisory-drain-output-pointer-only-under-a-token-budget
branch
August 14, 2026 04:34
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.



Closes CLOUD-82.
CLOUD-79 landed the drain's delivery half; this is its emission half. The
renderer had no ceiling — a rule firing on two hundred identities inside one
changed scope spent two hundred lines of the agent's context, and nothing
asserted a bound on the payload at all.
cycleis now three named stages(select → cap → clamp), because the cap is a decision about a set: a renderer
that emitted as it walked could not know an eleventh identity for the same rule
was still coming.
Two bounds, and the store can tell them apart
[drain] cardinality_cap(default 10)over-cardinality-cap, the counter CLOUD-78's sampled review reads[drain] token_budget(default 1024)drain-suppressed, reconsidered next drainA rule over the cap contributes one
rule R: K+ findingsline and no entries. Aclamped payload closes with
budget: N findings withheld. The estimate isbudget::estimate_tokens, the same one[budget]gates instruction files with,rather than a second estimator that could disagree about what a token is.
Both keys sit on the existing
[drain]table and inherit its unlayeredtreatment: they bound the engine's own output, so there is no raise-only reading
for a local file to weaken.
0is the honest bottom of each range, not adisabled feature.
Salience is independent of the count, structurally
Lines are ordered by tier, then rule, then fingerprint. The occurrence count is
deliberately not a sort key — that is CLOUD-80's no-escalation law made
structural rather than remembered, and it is what
a_rising_count_never_moves_a_finding_ahead_of_a_stronger_tierpins.The group re-raise
Renders
old->newagainst what this session's last drain actually said(
WakeState::counts), because the store carries no count anchor and the honestanchor for "worth saying again" is what the agent was last told. Only emitted
identities are remembered, so a capped rule anchors nothing and cannot make the
next drain's re-raise silent. A count that fell renders plainly: a ratchet is not
a re-raise, because re-raising on incremental fixing punishes the fix.
One identity's occurrences are a count by construction, so a 500→501 re-raise is
one line with one in-scope pointer — 501 pointers is unreachable, not merely
avoided.
§7
Renderer unit tests in
src/drain.rs, end-to-end over the compiled binary intests/advisory_drain.rs(nottests/cli.rs, which the Ready block namedbefore that file was under concurrent edit):
same assertion over the unclamped set fails — so the fixture is shown to
actually overflow;
E2E, two caps in
batten.tomlproduce two payloads, so the key is live;500->501and one in-scope pointer;Plus: a zero budget says nothing rather than one thing; the capped and the
clamped sets are disjoint, so no identity is journalled under two reasons.
Notes
!:DrainConfig/WakeStategain fields andcycle/drainedchangearity.
batten.tomlis unchanged for anyone who declared no new key.batten:rung, never into the agent payload. Routing them intosystemMessageproper rides CLOUD-44's per-host emitter shims, which the Ready block states
does not block this.
fuzz/Cargo.lockcatches up to v0.0.70, which the release commit left behind.Generated by Claude Code