Skip to content

fix(broadcast): close cost-guard gate gap (#24 follow-up — broadcast surface)#58

Merged
suzuke merged 1 commit into
mainfrom
fix/broadcast-cost-guard-gap
Apr 27, 2026
Merged

fix(broadcast): close cost-guard gate gap (#24 follow-up — broadcast surface)#58
suzuke merged 1 commit into
mainfrom
fix/broadcast-cost-guard-gap

Conversation

@suzuke

@suzuke suzuke commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Summary

PR #57 (the #24 fix) gated sendToInstance and — transitively via wrapAsSenddelegate_task and request_information. But broadcast runs its own per-target send loop without funnelling through sendToInstance, so a limited instance could still be flooded with broadcast task / query / update messages. ts-reviewer flagged this on PR #57 as Out-of-scope Observation 1. This PR closes the gap so the #24 issue intent ("notify sender on cost-guard limit") covers all outbound-dispatch surfaces an agent can use.

Changes

  • src/outbound-handlers.ts.broadcast (~10 LOC): inside the per-target loop, when ctx.costGuard?.isLimited(target) is true, append the target to a new costLimited array with the same warning string format used in feat(#24): cost-guard pre-check at outbound dispatch #57 (cost-guard: instance '...' has reached its daily cost limit ($X.XX). ...) and continue instead of dispatching. Other targets keep flowing normally — broadcast is never short-circuited as a whole.
  • broadcast result gains cost_limited: [{ target, warning }] alongside sent_to, failed, count. Callers can see exactly which targets were skipped and why.

Out of scope (per ts-lead)

  • Session-routing edge case (Out-of-scope Observation 2 from PR feat(#24): cost-guard pre-check at outbound dispatch #57): gate keys by targetName directly, not by the resolved host. Deferred per dispatch.
  • request_kind: "report" bypass: not applicable here. BroadcastRequestKind (outbound-schemas.ts) already restricts to "query" | "task" | "update" — broadcasts never carry reports because they have no correlation_id to terminate.

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run — 63 files / 527 tests pass (+3 over post-feat(#24): cost-guard pre-check at outbound dispatch #57 baseline; no regressions)
  • tests/outbound-cost-guard-precheck.test.ts — three operator-mandated states for broadcast:
    • (a) mixed → limited targets warned + skipped (no IPC send), others delivered. Result cost_limited contains exactly the limited target with the standard warning string.
    • (b) all limited → zero IPC sends across all targets, cost_limited lists all, sent_to empty, count: 0.
    • (c) no targets limited → identical to pre-fix behaviour (cost_limited: [], sent_to matches all targets).

Risks considered

  • Result shape change: adds new cost_limited field. Existing callers reading sent_to / failed / count see no change. New field default is [], never null. No migration needed.
  • Concurrent cost-guard state changes during loop: same race surface as pre-existing pause flow — gate shrinks the window per target but doesn't eliminate it. Not introduced here.
  • Performance: isLimited() is O(1) (Map lookup + arithmetic). Per-target overhead negligible.

Files

  • src/outbound-handlers.ts — broadcast loop gate + result field
  • tests/outbound-cost-guard-precheck.test.ts — 3 broadcast tests appended

🤖 Generated with Claude Code

…surface)

PR #57 (#24 fix) gated `sendToInstance` and (transitively via wrapAsSend)
`delegate_task` and `request_information`, but `broadcast` runs its own
per-target send loop without funnelling through `sendToInstance` — so a
limited instance could still be flooded via broadcast with task/query/
update kinds. ts-reviewer flagged this as Out-of-scope Observation 1 on
PR #57. This PR closes the gap.

Behaviour: per-target skip with structured warning, never short-circuits
the whole broadcast. Other targets still receive normally.

- broadcast loop: when `ctx.costGuard?.isLimited(target)` is true, push
  the target onto a new `costLimited` array with the same warning string
  format used by sendToInstance and continue the loop.
- broadcast result gains a `cost_limited: [{ target, warning }]` field
  alongside `sent_to` and `failed`. Callers see exactly which targets
  were skipped and why.
- BroadcastRequestKind already excludes `"report"` (broadcasts don't
  carry correlation_id), so no kind-bypass is needed here.
- Session-routing edge case (Out-of-scope Observation 2) explicitly
  deferred per ts-lead — gate keys by `targetName` exactly like the
  rest of the broadcast loop.

Tests cover the operator-mandated three states for broadcast:
- (a) mixed → limited targets warned + skipped, others delivered
- (b) all limited → every target warned, zero IPC sends
- (c) no targets limited → identical to pre-fix, cost_limited empty

`npx tsc --noEmit` clean. `npx vitest run` 63 files / 527 tests pass
(+3 over post-#57 baseline; no regressions).

Closes #24 issue intent (now covers all outbound-dispatch surfaces).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suzuke suzuke merged commit 0ec1244 into main Apr 27, 2026
2 checks passed
@suzuke suzuke deleted the fix/broadcast-cost-guard-gap branch April 27, 2026 03:28
suzuke added a commit that referenced this pull request Apr 27, 2026
Sprint 0 outcome: 0 open issues, 0 open PRs. Three PRs shipped
(#56 #55 fix; #57 #24 fix; #58 #24 follow-up broadcast surface),
four issues closed-with-migration to agend-terminal (#52 #53 #54 #8).

Adds:
- docs/ts-team-sprint-0-2026-04-27-wrap.md — sprint outcome, fix
  details, process notes, next-sprint pointer
- docs/ts-team-backlog.md — non-blocking gaps surfaced during
  deprecated-mode maintenance. B-001 = session-routing edge case
  in cost-guard gate (deferred from PR #57 review)

No production code changes.
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