Repository navigation
feat(budget): sliding-window cost accounting for agent exchanges - #1
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Verified on macOS: this branch failed 14 of main's thread-settle widget tests, all of them variants of "an ordinary open settles on the latest reply". Those tests set no read markers at all, so every reply resolved as unread, the resume targeted reply #1, and the thread head stayed on screen. Resuming is for *returning* to a thread. A reader with no marker over any reply has no place to return to, so that case now settles on the tail the way it always has. The divider still marks the replies new — it just does not drag the reader to the top of a thread they have never seen. The case the resume exists for is unaffected: once the channel marker alone covers the replies, hasThreadReadHistory is true and the resume fires. Also drops the readState.isReady gate on scheduling. It is unnecessary now that a missing marker set falls back to the tail on its own, and it risked stalling the settle behind a marker fetch. readState.isReady stays in the effect's dependencies, so markers landing while the settle is still pending still replace the target through InitialThreadTailSettle's generation guard. mobile: dart format clean, flutter analyze clean, flutter test 1377/1377. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Michael Feth <[email protected]>
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ubuntu](https://hub.docker.com/_/ubuntu) ([source](https://git.launchpad.net/cloud-images/+oci/ubuntu-base)) | container | digest | `561618e` → `33ceb71` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…lock#6666) This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@tanstack/react-virtual](https://tanstack.com/virtual) ([source](https://redirect.github.com/TanStack/virtual/tree/HEAD/packages/react-virtual)) | [`3.14.9` → `3.14.10`](https://renovatebot.com/diffs/npm/@tanstack%2freact-virtual/3.14.9/3.14.10) |  |  | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>TanStack/virtual (@&block#8203;tanstack/react-virtual)</summary> ### [`v3.14.10`](https://redirect.github.com/TanStack/virtual/blob/HEAD/packages/react-virtual/CHANGELOG.md#31410) [Compare Source](https://redirect.github.com/TanStack/virtual/compare/@tanstack/[email protected]...@tanstack/[email protected]) ##### Patch Changes - Updated dependencies \[[`a0a411e`](https://redirect.github.com/TanStack/virtual/commit/a0a411e06f7334a063422de35d59b12b264b3573), [`d2cf98b`](https://redirect.github.com/TanStack/virtual/commit/d2cf98beea1696c7187c06b57c9e724d1957963c)]: - [@&block#8203;tanstack/virtual-core](https://redirect.github.com/tanstack/virtual-core)@​3.17.8 </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [vitest](https://vitest.dev) ([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.10` → `4.1.11`](https://renovatebot.com/diffs/npm/vitest/4.1.10/4.1.11) |  |  | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>vitest-dev/vitest (vitest)</summary> ### [`v4.1.11`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v4.1.11) [Compare Source](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11) ##### 🐞 Bug Fixes - Revive global concurrency limit for test lifecycle \[backport to v4] - by [@&block#8203;sheremet-va](https://redirect.github.com/sheremet-va) and [@&block#8203;hi-ogawa](https://redirect.github.com/hi-ogawa) in [#&block#8203;10992](https://redirect.github.com/vitest-dev/vitest/issues/10992) [<samp>(5146d)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/5146df80b) - **browser**: - Encode iframeId in tester iframe URL \[backport to v4] - by [@&block#8203;sheremet-va](https://redirect.github.com/sheremet-va), **Pduhard** and **Claude Opus 4.8** in [#&block#8203;10955](https://redirect.github.com/vitest-dev/vitest/issues/10955) [<samp>(10b2c)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/10b2cd201) - Trigger playwright/chromium gc on lower disk availability \[backport to v4] - by [@&block#8203;hi-ogawa](https://redirect.github.com/hi-ogawa), **Hiroshi Ogawa** and **OpenCode** in [#&block#8203;10951](https://redirect.github.com/vitest-dev/vitest/issues/10951) [<samp>(9851d)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/9851dbc41) - **mocker**: - Restrict redirect mocks to the fs allowlist \[backport to v4] - by [@&block#8203;sheremet-va](https://redirect.github.com/sheremet-va) in [#&block#8203;10974](https://redirect.github.com/vitest-dev/vitest/issues/10974) [<samp>(fe5a1)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/fe5a11d3c) ##### [View changes on GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [rui314/setup-mold](https://redirect.github.com/rui314/setup-mold) ([changelog](https://redirect.github.com/rui314/setup-mold/compare/9c9c13bf4c3f1adef0cc596abc155580bcb04444..7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3)) | action | digest | `9c9c13b` → `7e4f20a` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…e transport, version subagent cache - subagent.rs/kind.rs: 20003 documented as RESERVED (observer frames are the transport; frame agent tag = parent identity) — review #1/#2 - observerRelayStore: subagentsVersion bump on every real fold + reset, cache keyed on version not length — review #3 (stale-tree UI bug) - SPEC wire contract section updated to as-built Signed-off-by: Michael Feth <[email protected]>
…e transport, version subagent cache - subagent.rs/kind.rs: 20003 documented as RESERVED (observer frames are the transport; frame agent tag = parent identity) — review #1/#2 - observerRelayStore: subagentsVersion bump on every real fold + reset, cache keyed on version not length — review #3 (stale-tree UI bug) - SPEC wire contract section updated to as-built Signed-off-by: Michael Feth <[email protected]>
🧪 Jev PR-Triage (v3)PR #1: feat(budget): sliding-window cost accounting for agent exchanges — verdict: REVIEW
🔍 Reviewer focus
Block reason (if any): |
Implements the accounting half of the loop-protection policy decided in wayfinder ticket #7 (mfethe1/agent-mesh#7). Buzz already has throttles — thread depth capped at 100, tiered rate limits, in-flight deadlines, session rotation — but nothing that says an exchange is finished. Two agents that open fresh threads at each other never accumulate depth, so they run at a rate-limited 120 messages a minute indefinitely, each one a paid LLM call. This crate budgets the thing that actually matters: measured cost_usd from the kind 44200 turn metric, $5 per (channel, agent-pair) per rolling hour. The window slides, so it self-heals with no human reset. Human-triggered turns are never charged — cutting off someone who is sitting there watching is the failure the policy exists to avoid. Deliberately does not enforce. record() returns a Verdict and the caller decides. Buzz's owner commands (!shutdown, !cancel, !rotate) cannot mute a single peer, so the enforcement mechanism is still open; keeping the accounting pure lets that decision land without touching this code. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
Decision D5 of wayfinder #7 says only agent-triggered turns consume budget. Implementing it surfaced a gap: the kind 44200 turn metric has no field naming the author that caused the turn. Its payload is harness, model, channel_id, session_id, turn_id, turn_seq, timestamp, turn, cumulative, delta_reliable and stop_reason — nothing about the trigger. TriggerLog recovers it without a wire-format change, using a property buzz-acp already guarantees: turns are serialised per channel, and all pending events for a channel drain into one batch. So the messages seen in a channel since its last turn are that turn's trigger. A batch mixing human and agent messages counts as human. D5 exists to protect the case where a person is present and watching; charging that turn risks muting an agent mid-conversation with its owner. Erring the other way costs a runaway one extra turn before it trips. Attribution is best-effort and fails open: an observation gap yields None, which is not charged, so a dropped subscription disables the budget rather than tripping it. The durable fix is a trigger field on NIP-AM, which the spec's forward-compatibility rule already permits. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
Ledger charges a pair; TriggerLog attributes a turn. Held separately, a caller has to remember to attribute before charging and to prune both on the same schedule. Supervisor owns that sequencing so it cannot be got wrong, and gives the crate one entry point instead of two loose halves. Adds the integration coverage the pieces lacked. The end-to-end runaway now has a test: two agents alternating at $0.30 a turn trip the $5 budget on the seventeenth charged turn. So does the failure this policy must never cause — two hundred consecutive owner-driven turns at $1.50 each, none blocked, nothing charged. Also covers a human joining a runaway mid-flight, which makes that turn free because the mixed batch attributes to the human; separate pairs in one channel not pooling budget; and an unobserved turn failing open. Writing the runaway test caught a real arithmetic error in its own expectation, not in the code: $5 at $0.30 a turn first exceeds budget on the seventeenth charge, not the eighteenth. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
The crate could account and attribute, but nothing connected it to real data. charge_from_metric decrypts a kind:44200 event with the owner's keys and extracts channel, agent, cost and end-of-turn timestamp. Kept in its own module because it is the only part of the crate that knows about Nostr. Ledger and TriggerLog stay testable on plain values with no keys and no events, which is why they have the coverage they do. Tests build real signed events with buzz-core's own encrypt helper and round-trip them, so this is checked against Buzz's actual wire format rather than a hand-rolled fixture. A stranger's key fails closed, an unrelated kind is rejected before any decrypt attempt, and missing or malformed channel ids and timestamps are errors rather than silent defaults. Notable while writing it: decrypt_agent_turn_metric already rejects negative and non-finite costUsd per NIP-AM, so the ledger's own guard against a refund is defence in depth rather than the only barrier. delta_reliable is surfaced on TurnCharge rather than swallowed — #7 OQ7.1 asks how often it is false, and that cannot be answered if the ingest layer discards it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
Two real bugs, one security and one correctness, plus the documented standards this crate was skipping. Security: charge_from_metric took turn_end from the agent's own encrypted payload and never checked it. AgentTurnMetricPayload::validate covers only the numerics, so a metric dated in the future moved the ledger's eviction cutoff forward and wiped the pair's whole window — the budgeted agent could zero its own budget at will. The payload timestamp is now tied to the signed created_at within a 300s tolerance. Correctness: TriggerLog consumed the batch per channel rather than per agent, so with two agents in one channel whichever metric was processed first ate the other's trigger and the second turn went uncharged. Relay reordering alone disabled the budget. Consumption is now tracked per (channel, agent) with independent cursors. The spec axis found a third defect that per-agent cursors do NOT fix: the metric's timestamp is end-of-turn and the payload carries no start, so a message arriving mid-turn is attributed to the turn it did not trigger, and one owner message can free two turns. It errs toward under-charging like every other approximation here, and the durable fix is a turn-start or trigger field on NIP-AM. Documented rather than papered over — it is a second argument for #7 OQ7.5. Both reviewers independently flagged Verdict::Allow{spent_usd: 0.0} on human and unattributed turns as a lie: the pair may hold $4.90, and a caller logging spend would see a sawtooth already collapsed to zero. Replaced with an explicit Unbudgeted variant. Standards: adds deny(unsafe_code) and warn(missing_docs) per CONTRIBUTING.md, documents the public API those lints then surfaced, and registers the crate in the AGENTS.md crate map. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
The spec-review agent cited agent_turn_metric.rs:107 for the end-of-turn timestamp and I carried that into origin.rs without opening the file. Line 107 is turn_seq documentation; the RFC 3339 end-of-turn line is 111. Every other cross-crate citation in the crate was re-checked by opening the cited line: buzz-acp/src/lib.rs:192 (is_owner_or_sibling), buzz-acp/src/queue.rs:1-7 (per-channel serialisation), agent_turn_metric.rs:86 (forward-compatibility rule) and :1-5 (NIP-44 addressing). All four hold. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
Two gaps the spec review found, both now closed. D4 justifies a self-healing window by the sawtooth it leaves in the logs — "a diagnosable signature rather than a silent drain" — but the crate had no tracing dependency and emitted nothing, so the signature the decision rests on did not exist. Exhaustion now logs at warn with the pair, spend and budget; ordinary charges log at debug. delta_reliable was populated by the ingest layer and read by nobody, so D2 charged unreliable deltas at face value with no seam for the fallback OQ7.1 asks about. on_turn_charge takes a wire-decoded TurnCharge, warns when the publisher lost its cumulative baseline, and still charges — because OQ7.1 is genuinely undecided and its suggested fallback needs cumulative, which TurnCharge does not yet carry. Explicit and logged beats silent, and it lets the question be answered from real data. Both behaviours are tested rather than left accidental. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
…sing The verification gate is right that tests are not runtime evidence. This example launches the supervisor with a real tracing subscriber and shows what an operator would actually see. Two of D4's claims hold under observation. Forty consecutive owner-driven turns at $2.50 each: zero blocked, zero charged. After the window slides, spend resets and the verdict returns to Allow with no reset step. The third does not. D4 justifies the design by predicting "a runaway stops within minutes and restarts only to stop again, producing a sawtooth in the logs — a diagnosable signature rather than a silent drain." Running it produces no sawtooth: spend climbs monotonically from $5.40 to $18.00 across 22 consecutive WARN lines, because nothing acts on the verdict. The sawtooth is a property of enforcement, not of accounting. Until an enforcer exists the signature is a continuous alarm, which is arguably worse for diagnosis than the silent drain it was meant to replace. That is a flaw in the decision's reasoning rather than in this code, and it is recorded on the ticket. spent_usd is logged raw rather than rounded. The float noise is real data and these are structured events meant for machine aggregation; rounding at the emit site would lose precision for no gain. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
The spec-review agent flagged it as unrequested; spot-checking that claim found exactly one caller, and it was the test asserting on it. A public accessor whose only consumer is its own test is not API, it is speculative generality. The test it existed for still matters — both argument orders must reach one budget — so it now asserts that directly by hashing both keys into a set, which is closer to how PairKey is actually used. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013okf5qYe8U8zja6wPBAL2y
9dbd59d to
5c224db
Compare
🧪 Jev PR-Triage (v3)PR #1: feat(budget): sliding-window cost accounting for agent exchanges — verdict: REVIEW
🔍 Reviewer focus
Block reason (if any): |
Implements the accounting half of the loop-protection policy decided in wayfinder ticket mfethe1/agent-mesh#7.
Draft — this crate has no consumer yet, by design. It stops at a seam; see Deliberately not implemented.
Why
Buzz already has throttles — thread depth capped at 100, tiered rate limits (human 60 / agent 120–600 msg/min), in-flight deadlines, session rotation. It has no termination condition. Two agents that open fresh threads at each other never accumulate
thread_metadata.depth, so they run at a rate-limited 120 messages a minute indefinitely, each one a paid LLM call.What
crates/buzz-budgetbudgets the thing that actually matters: measuredcost_usdfrom thekind:44200turn metric (NIP-AM).Four modules:
ingest(wire → values, the only part that knows Nostr),origin(attributes a turn to its trigger),Ledger(the sliding window),Supervisor(composes them so a caller cannot get the order wrong).Deliberately not implemented
Enforcement.
record()returns aVerdict; acting on it is the caller's job. Buzz's owner commands are!shutdown(kills the whole harness),!cancel(one turn) and!rotate(session) — none can mute a single peer, so enforcing this needs a newbuzz-acpcommand. Keeping the accounting pure means that decision can land without touching this code.Also absent: the relay subscription loop, which needs an enforcement host (ticket #7 OQ7.3).
Known limitations, carried honestly
cost_usdis invisible to the budget (ci: baseline probe of product/main (not for merge) #7 OQ7.2).Review
Both axes of
/code-reviewran against this branch and found two real bugs, since fixed in72d86f0:turn_endwas taken from the agent's own encrypted payload and never checked, so a future-dated metric wiped the pair's window. A runaway could zero its own budget. Now tied to the signedcreated_at.Verified at runtime, not just tested
cargo run -p buzz-budget --example runawaydrives the supervisor with a realtracingsubscriber. Two of the policy's claims hold under observation, and one does not:Allow✅WARNlinesThe sawtooth is a property of enforcement, not accounting. Until something acts on the verdict, the "diagnosable signature" D4 promised is a continuous alarm — arguably worse than the silent drain it was meant to replace. That is a flaw in the decision's reasoning rather than in this code, recorded as #7 OQ7.6.
Checks
41 tests + 1 doctest passing.
cargo clippyclean,cargo fmt --checkclean.#![deny(unsafe_code)]and#![warn(missing_docs)]per CONTRIBUTING.md.