P1: library reply retry usage under ${id}:retry is refused by the broker; retry tokens go unrecorded #1853

Open
opened 2026-11-06 21:40:36 +00:00 by jason.woltje · 0 comments
Owner

Summary

The runner's library reply retry records its usage under the turn id ${m.id}:retry. The broker refuses that id, so the tokens the retry turn uses are never recorded. The campaign budget supervisor and the limit checks count usage_records, so a cap undercounts by whatever retry turns spend.

Found while ruling on the #1852 fix row. It is not part of that row.

Code (read at 3598839e6)

  • packages/harness/src/runner.mjs:708 calls client.call("usage.begin", { turn: \${m.id}:retry` }), and :712callsusage.end` with the same id. This happens on the library path, after a reply is refused with a guard code or request-too-large, before the one retry turn.

  • packages/bus/src/broker.mjs:6012 (#requestUsage) accepts a turn id only if:

    • it is a message delivered to this run (messages JOIN deliveries ... d.op='delivered' AND d.holder_run=?), or
    • for a run with no thread, it is a landing decision the run holds (begin), or it already has a begin row (end).

    ${m.id}:retry is neither, so both calls fail with not-recipient (:6025). The id passes the format check (broker.mjs:283 allows :).

  • The runner catches both refusals and only logs runner: usage baseline unavailable / usage outcome unavailable (runner.mjs:709, :713). No usage_records row is written for the retry. The main turn's begin/end pair on m.id closed at :663, before the retry ran, so its delta (packages/bus/src/usage.mjs delta()) doesn't include the retry's transcript entries either.

  • #limitCheck runs after a successful usage.end, so no limit check runs after the retry turn.

Live check

In the ui797 store on .53 (read-only, 2026-11-06T21:4xZ), there are 0 refused usage events with a :retry turn and 0 usage_records with a :retry turn. So no retry has run on ui797 so far. This is a code-path defect, not one seen in the incident data.

Effect

Every library reply retry is a full model turn whose tokens are missing from usage_records. Campaign caps, token limits and usage views undercount by that amount, and the next limit check comes later than it should. The undercount is silent: the only sign is a runner log line.

Fix direction

The smallest change follows the #1852 approach: keep the retry inside the m.id usage bracket. Move the main turn's usage.end (m.id) to after the retry, and drop the ${m.id}:retry calls. The single end row's transcript delta then covers both turns, with no broker contract change. Tests: one begin and one end for m.id around both turns, and the end row's delta includes the retry's entries.

Before a Round B go

Per the Lead: this is on record before any Round B rerun on @campaign5, because unrecorded retry tokens make the cap undercount.

## Summary The runner's library reply retry records its usage under the turn id `${m.id}:retry`. The broker refuses that id, so the tokens the retry turn uses are never recorded. The campaign budget supervisor and the limit checks count usage_records, so a cap undercounts by whatever retry turns spend. Found while ruling on the #1852 fix row. It is not part of that row. ## Code (read at 3598839e6) - `packages/harness/src/runner.mjs:708` calls `client.call("usage.begin", { turn: \`${m.id}:retry\` })`, and `:712` calls `usage.end` with the same id. This happens on the library path, after a reply is refused with a guard code or request-too-large, before the one retry turn. - `packages/bus/src/broker.mjs:6012` (`#requestUsage`) accepts a turn id only if: - it is a message delivered to this run (`messages JOIN deliveries ... d.op='delivered' AND d.holder_run=?`), or - for a run with no thread, it is a landing decision the run holds (begin), or it already has a begin row (end). `${m.id}:retry` is neither, so both calls fail with `not-recipient` (`:6025`). The id passes the format check (`broker.mjs:283` allows `:`). - The runner catches both refusals and only logs `runner: usage baseline unavailable` / `usage outcome unavailable` (`runner.mjs:709`, `:713`). No usage_records row is written for the retry. The main turn's begin/end pair on `m.id` closed at `:663`, before the retry ran, so its delta (`packages/bus/src/usage.mjs` `delta()`) doesn't include the retry's transcript entries either. - `#limitCheck` runs after a successful `usage.end`, so no limit check runs after the retry turn. ## Live check In the ui797 store on .53 (read-only, 2026-11-06T21:4xZ), there are 0 refused usage events with a `:retry` turn and 0 usage_records with a `:retry` turn. So no retry has run on ui797 so far. This is a code-path defect, not one seen in the incident data. ## Effect Every library reply retry is a full model turn whose tokens are missing from usage_records. Campaign caps, token limits and usage views undercount by that amount, and the next limit check comes later than it should. The undercount is silent: the only sign is a runner log line. ## Fix direction The smallest change follows the #1852 approach: keep the retry inside the `m.id` usage bracket. Move the main turn's `usage.end` (`m.id`) to after the retry, and drop the `${m.id}:retry` calls. The single end row's transcript delta then covers both turns, with no broker contract change. Tests: one begin and one end for `m.id` around both turns, and the end row's delta includes the retry's entries. ## Before a Round B go Per the Lead: this is on record before any Round B rerun on @campaign5, because unrecorded retry tokens make the cap undercount.
jason.woltje added the bug label 2026-11-06 21:40:36 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1853