Skip to content

feat(budget): sliding-window cost accounting for agent exchanges - #1

Merged
mfethe1 merged 12 commits into
product/mainfrom
worktree-buzz-budget-accountant
Sep 20, 2026
Merged

mfethe1 merged 12 commits into
product/mainfrom
worktree-buzz-budget-accountant

Conversation

@mfethe1

@mfethe1 mfethe1 commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

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-budget budgets the thing that actually matters: measured cost_usd from the kind:44200 turn metric (NIP-AM).

  • $5 per (channel, agent-pair) per rolling hour. The pair is unordered — A→B and B→A share one budget, because a runaway is a property of the pair, not a direction.
  • Self-healing. The window slides; there is no reset step.
  • Human-triggered turns are never charged. Cutting off someone who is sitting there watching is the failure this policy exists to avoid.

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 a Verdict; 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 new buzz-acp command. 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

  • Attribution fails open. An observation gap yields no trigger, which is not charged — so a dropped subscription disables the budget rather than tripping it.
  • The attribution boundary is end-of-turn. The metric carries no turn start, so a message arriving mid-turn is attributed to the turn it did not trigger, and one owner message can free two turns. Errs toward under-charging. The durable fix is a turn-start or trigger field on NIP-AM (ci: baseline probe of product/main (not for merge) #7 OQ7.5).
  • A harness reporting no cost_usd is invisible to the budget (ci: baseline probe of product/main (not for merge) #7 OQ7.2).

Review

Both axes of /code-review ran against this branch and found two real bugs, since fixed in 72d86f0:

  • Security — turn_end was 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 signed created_at.
  • Correctness — trigger consumption was per channel rather than per agent, so with two agents in one channel the first metric ate the other's trigger. Relay reordering alone disabled the budget.

Verified at runtime, not just tested

cargo run -p buzz-budget --example runaway drives the supervisor with a real tracing subscriber. Two of the policy's claims hold under observation, and one does not:

Claim Observed
D5 — human turns never blocked 40 owner-driven turns @ $2.50: 0 blocked, $0.00 charged ✅
D4 — self-healing, no reset after the window slid, spend reset and the verdict returned to Allow ✅
D4 — runaway leaves a sawtooth in the logs ❌ no sawtooth — spend climbs monotonically $5.40 → $18.00 across 22 consecutive WARN lines

The 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 clippy clean, cargo fmt --check clean. #![deny(unsafe_code)] and #![warn(missing_docs)] per CONTRIBUTING.md.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: ad42d6e7-b776-487f-ac33-7db82ae5fa31

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

mfethe1 pushed a commit that referenced this pull request Aug 14, 2026
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]>
mfethe1 pushed a commit that referenced this pull request Aug 27, 2026
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>
mfethe1 pushed a commit that referenced this pull request Aug 27, 2026
…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)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@tanstack%2freact-virtual/3.14.10?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tanstack%2freact-virtual/3.14.9/3.14.10?slim=true)
|

---

> [!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)@&#8203;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>
mfethe1 pushed a commit that referenced this pull request Aug 27, 2026
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) |
![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.11?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.10/4.1.11?slim=true)
|

---

> [!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>
mfethe1 pushed a commit that referenced this pull request Aug 27, 2026
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>
mfethe1 added a commit that referenced this pull request Sep 2, 2026
…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]>
mfethe1 added a commit that referenced this pull request Sep 3, 2026
…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]>
@mfethe1
mfethe1 changed the base branch from main to product/main September 12, 2026 03:05
@mfethe1
mfethe1 marked this pull request as ready for review September 12, 2026 03:11
mfethe1 added a commit that referenced this pull request Sep 12, 2026
@mfethe1

mfethe1 commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

🧪 Jev PR-Triage (v3)

PR #1: feat(budget): sliding-window cost accounting for agent exchanges — verdict: REVIEW

dimension metric value
Correctness problem resolved P(yes)=0.16
Correctness rework needed 1.12/2
Quality code quality 1.94/2
Quality test coverage P(yes)=0.36
Architecture integration compatibility 1.91/2
Architecture future proof 1.77/2
Governance sensitive path P(yes)=0.93
Governance size concern P(yes)=0.93
Synthesis merge risk 1.01/2
Synthesis route needs_review (0.90)
Diff security exposure P(yes)=0.24
Diff performance impact 0.16/2

🔍 Reviewer focus

  • File: crates/buzz-budget/src/ingest.rs
  • Concern: security

Block reason (if any): none
Pilot: Jev (System One) typed triage, 15 metrics + reviewer focus. Verdict thresholds in code.

Michael Feth and others added 11 commits September 19, 2026 18:00
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
@mfethe1
mfethe1 force-pushed the worktree-buzz-budget-accountant branch from 9dbd59d to 5c224db Compare September 19, 2026 22:02
@mfethe1

mfethe1 commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

🧪 Jev PR-Triage (v3)

PR #1: feat(budget): sliding-window cost accounting for agent exchanges — verdict: REVIEW

dimension metric value
Correctness problem resolved P(yes)=0.17
Correctness rework needed 1.12/2
Quality code quality 1.95/2
Quality test coverage P(yes)=0.41
Architecture integration compatibility 1.94/2
Architecture future proof 1.76/2
Governance sensitive path P(yes)=0.92
Governance size concern P(yes)=0.93
Synthesis merge risk 1.06/2
Synthesis route needs_review (0.91)
Diff security exposure P(yes)=0.23
Diff performance impact 0.19/2

🔍 Reviewer focus

  • File: crates/buzz-budget/src/ingest.rs
  • Concern: security

Block reason (if any): none
Pilot: Jev (System One) typed triage, 15 metrics + reviewer focus. Verdict thresholds in code.

@mfethe1
mfethe1 merged commit 2e2a1c4 into product/main Sep 20, 2026
73 checks passed
@mfethe1
mfethe1 deleted the worktree-buzz-budget-accountant branch September 20, 2026 01:15
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