mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-10-23 03:47:00 +09:00
* feat(federation): ADR-097 Phase 2.a — peer state machine entity layer FederationNode entity gains a state field (ACTIVE/SUSPENDED/EVICTED) plus suspend()/evict()/reactivate() methods. All transitions flow through a canTransition() table in the new federation-node-state.ts value object — self-loops are rejected (so a duplicate suspend can't reset the auto-eviction grace timer), EVICTED is terminal under breaker flow, operator reactivate is the only escape. Also ships pure helpers isCooldownElapsed + shouldAutoEvict so the Phase 2.b breaker service (separate file, deferred) can drive transitions deterministically without owning the clock. 27 new tests pin: legal/illegal edges, no-op invariant on duplicate suspend, EVICTED terminality, props round-trip preservation, cooldown boundaries, auto-evict grace > cooldown. Phase 2.b (breaker service wiring cost-tracker → suspend; outbound short-circuit on !isActive) and Phase 4 (federation_breaker_status MCP + doctor surface) remain deferred per ADR-097 status table. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(federation): ADR-097 Phase 2.b — breaker service + outbound short-circuit Two-layer breaker shipping the active-defense half of the circuit breaker: 1. **evaluatePolicy** (pure) — given rolling samples + node + policy + now, returns SUSPEND/EVICT/REACTIVATE_ELIGIBLE/NONE. Cost-takes-priority over failure-ratio when both trip (cost is the harder-to-fake signal). Auto-evict supersedes reactivate-eligible from SUSPENDED. 2. **FederationBreakerService** — owns per-peer rolling buffer (bounded, default 100k samples), wraps evaluatePolicy with entity mutation, exposes recordOutcome/evaluate/tick/snapshot/forget. Crucially does NOT auto-reactivate — the breaker only signals eligibility; the integrator's health probe owns the reactivate() call. This avoids the "false-positive recovery" failure mode where a peer flaps SUSPENDED↔ ACTIVE on transient telemetry. Coordinator wiring: sendMessage now short-circuits on !peer.isActive BEFORE session lookup, with PEER_SUSPENDED / PEER_EVICTED constant-string errors (no remaining-budget echo per the Phase 1 anti-oracle posture). Security invariants pinned by 25 new tests: - Negative tokens/usd clamped to 0 (no credit-inflation attack) - Failure ratio gated on minSamples=10 (anti-noise) - Strict > on threshold comparisons (boundary correctness) - EVICTED peers always return NONE (terminal under breaker flow) - apply=false dry-run preserves entity state Phase 4 (federation_breaker_status MCP + ruflo doctor surface) is now unblocked: snapshot() + node.stateRecord give it everything it needs. Phase 3 upstream (federation_spend bus emission) remains separate work — when it lands, the bus subscriber calls recordOutcome() and the in-memory buffer becomes a transparent cache. No breaker code changes. Witness manifest: 87 fixes (was 85), Phase2b + Phase2b-coordinator markers signed. Co-Authored-By: RuFlo <ruv@ruv.net> * chore(release): @claude-flow/plugin-agent-federation@1.0.0-alpha.6 Ships ADR-097 Phase 2.a (peer state machine) + Phase 2.b (breaker service + outbound short-circuit). Published to npm dist-tag alpha; latest is held at alpha.5 until the breaker bakes for one /loop cycle. Verified: tarball contains dist/application/federation-breaker-service.js, dist/domain/value-objects/federation-node-state.js, and PEER_EVICTED short-circuit in dist/application/federation-coordinator.js. All three witness markers present in the published artifact. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(federation): ADR-097 Phase 4 — operator surface (3 MCP tools + doctor) Closes the loop on the breaker so operators can see and operate it: **3 new MCP tools** (mcp-tools.ts): - federation_breaker_status — per-peer state snapshot + bucket counts; optional nodeId filter - federation_evict — operator-initiated MANUAL_EVICT with audit log + session-termination side effect; idempotent (already-evicted returns false but still emits audit for operator visibility) - federation_reactivate — escape hatch from SUSPENDED (after probe) or EVICTED (operator override) **Coordinator additions** (federation-coordinator.ts): - getPeerStates() — projects stateRecord over discovery.listPeers() - getPeerStateCounts() — O(peers) bucket count for status lines - evictPeer() / reactivatePeer() — async wrappers that audit-log every outcome (applied:true/false in metadata) **Doctor health-check** (cli/doctor.ts): - checkFederationBreaker — probes plugin loadability + asserts FederationNodeState export presence (catches version-skew where users have a pre-Phase-2 plugin installed). Optional plugin → "not installed" is a pass, not a fail. Selectable as `--component federation`. **Plugin index exports** — FederationNodeState + breaker primitives so external integrators (and the doctor probe) can detect Phase 2/4 support. 11 new coordinator tests pin: bucket-count math, audit emission on both success/no-op evict paths, EVICTED → ACTIVE operator override, unknown-peer no-op (no audit silence). 60 plugin-level tests updated to assert the 12-tool count. Witness markers added: ADR-097-Phase4-mcp + ADR-097-Phase4-doctor. 89 signed fixes in the manifest (was 87). Published @claude-flow/plugin-agent-federation@1.0.0-alpha.7 to npm alpha tag. Tarball validated: all 7 markers (Phase 2.a + 2.b + 4) present in dist/. ADR-097 status: Phases 1, 2.a, 2.b, 3-consumer, and 4 all Implemented. Only Phase 3 upstream (federation_spend bus emission) remains deferred — when it lands, the breaker's in-memory rolling buffer becomes a transparent cache with zero breaker code changes. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(federation): ADR-097 Phase 3 upstream — closes ADR-097 100% The last remaining "Deferred" entry from ADR-097's status table. With this, the breaker has end-to-end telemetry: integrators call coordinator.reportSpend() after each downstream agent completes, and the event fans out to TWO subscribers in parallel: 1. SpendReporter — pushes to whatever durable backend the integrator wires (cost-tracker memory namespace, Datadog, accounting DB). The event shape matches the cost-tracker consumer in plugins/ruflo-cost-tracker/scripts/federation.mjs verbatim: { peerId, taskId, tokensUsed, usdSpent, ts, success } 2. FederationBreakerService.recordOutcome — feeds the breaker's in-memory rolling buffer so cost/failure-ratio thresholds fire correctly. Without this fan-out, integrators would have to wire two parallel pipelines for the same telemetry. Both integrations are constructor-optional. A coordinator with neither still accepts reportSpend() calls (silent no-op) so callers don't branch on configuration. **New files:** - application/spend-reporter.ts — SpendReporter interface, FederationSpendEvent type, InMemorySpendReporter reference impl - __tests__/unit/coordinator-spend-reporting.test.ts — 10 tests pinning fan-out, contract shape, ts auto-fill, throw-bubble **New MCP tool:** federation_report_spend (13 tools total) **Coordinator constructor:** added optional 9th arg `integrations: { spendReporter?, breakerService? }` — backward-compat since omission preserves the existing 8-arg shape. **Anti-coupling design:** federation doesn't own model pricing; the caller owns it. reportSpend() is an explicit POST-completion call, not auto-emitted from federation_send. Future enhancement could auto-emit a "predicted cost" placeholder, but deferred until a concrete request justifies the additional state. **Witness:** 90 signed fixes (was 89). Phase 3 upstream marker added. **Published:** @claude-flow/plugin-agent-federation@1.0.0-alpha.8 to npm alpha tag. Tarball validated — all 8 markers (Phase 2.a + 2.b + 3-upstream + 4) present. ADR-097 status: ALL FIVE PHASES IMPLEMENTED (1, 2.a, 2.b, 3-consumer + 3-upstream, 4). Future work is integrator-side (production SpendReporter adapter that persists to ruflo memory; auto-emit predicted-cost placeholder). Co-Authored-By: RuFlo <ruv@ruv.net> * chore(release): umbrella alpha.20 — ships ADR-097 Phase 4 doctor surface Bumps + publishes the 3-package umbrella chain: - @claude-flow/cli@3.7.0-alpha.20 - claude-flow@3.7.0-alpha.20 - ruflo@3.7.0-alpha.20 All three on alpha + latest + v3alpha tags per CLAUDE.md publishing rules. Static check confirms `checkFederationBreaker` ships in the published @claude-flow/cli@alpha dist (3 occurrences in package/dist/src/commands/doctor.js). Now visible to users: - `npx ruflo@latest doctor --component federation` reports plugin loadability + version-skew detection (catches users running an older pre-Phase-2 federation plugin against the new MCP tools) Federation plugin already at @claude-flow/plugin-agent-federation @1.0.0-alpha.8 (published earlier this session) — installs via `npm install @claude-flow/plugin-agent-federation@alpha`. Witness manifest re-signed with new release versions; 90 fixes still verified. No new fix entries since this is a release-only bump (the underlying code already shipped in the prior 3 commits today). Co-Authored-By: RuFlo <ruv@ruv.net> * docs(adr): ADR-104 — federation wire transport (agentic-flow loader, WS today, QUIC roadmap) Documents the transport selection for ADR-097 federation. Smoke test mac↔ruvultra over tailscale revealed agentic-flow's published QUIC is API-only — loadWasmModule returns {}, encode/decode are placeholders, crates/agentic-flow-quic/src/wasm.rs explicitly notes the WASM build is a stub since browsers can't do raw UDP. Decision: adopt the loadQuicTransport() loader pattern with WebSocket fallback. Same AgentTransport interface today (WS) and tomorrow (native QUIC when it lands — set AGENTIC_FLOW_QUIC_NATIVE=1). Federation code stays transport-agnostic. Validated: real mac→ruvultra:9101 over tailscale, 125ms send latency (real I/O, not the prior 0ms stub). Upstream contribution: - ruvnet/agentic-flow#153 — adds quic-loader.ts to the published inner package, with WebSocketServer-backed real fallback - Patched build published as agentic-flow@2.0.12-fix.1 (`fix` dist-tag) so federation can consume the working transport without waiting for upstream merge 12h verification routine updated with Check 8 (ADR-104 transport smoke): - Pulls agentic-flow@fix - Stands up real WebSocketServer + client send round-trip on localhost - Asserts real send latency (>0, <5000ms) — catches if a future regression re-introduces the no-op stub behavior - SEVERITY:HIGH if stub regression detected Re-open this ADR when: - ruvnet/agentic-flow ships native QUIC binding - Federation traffic exceeds ~100 RPS to any single peer - Federation peers cross outside tailnet boundary - We have measured WS-vs-QUIC switch data Co-Authored-By: RuFlo <ruv@ruv.net> * feat(federation): wire ADR-104 transport — real mac↔ruvultra peering ADR-104 documented the design (agentic-flow loader pattern, WebSocket fallback today, native QUIC roadmap). This commit wires it into the plugin so federation_send actually moves bytes on the wire. Changes to src/plugin.ts: - New dep: agentic-flow@2.0.12-fix.2 (PR ruvnet/agentic-flow#153 patched build that fixes the published QUIC stub) - initialize() loads transport via loadQuicTransport(), registers it as federation:transport service - If config.port is set, transport.listen(port, host='0.0.0.0') binds an inbound listener (peers that only initiate outbound leave port unset) - sendToNode callback now resolves peer.endpoint via discovery.getPeer, strips the protocol prefix (ws://, tailscale://), and calls transport.send(address, AgentMessage{id, type, payload, metadata}) - shutdown() closes the transport (terminates outbound + inbound clients, awaits server close cb) - Graceful degrade: if transport load fails, plugin logs a warn and continues with the legacy in-process noop (preserves backward compat for tests/sandboxes without ws available) Validated end-to-end on real hardware: ruvultra (linux/x64): plugin booted, listening on 0.0.0.0:9101 mac (darwin/arm64): plugin booted, sent envelope in 150ms over tailscale Witness manifest: 91 fixes (was 90), ADR-104-transport marker added. Service-count assertion bumped 7 → 8 (added federation:transport). All 450 federation tests still pass (transport load is async + safe in the mocked test PluginContext). Published: @claude-flow/plugin-agent-federation@1.0.0-alpha.9 to npm alpha tag. Tarball validated: dist/plugin.js contains 'loadQuicTransport' + 'agentic-flow/transport/loader' import + 'federation:transport' service registration + dep agentic-flow@2.0.12-fix.2. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(deps): regenerate pnpm-lock — was out of sync since alpha.9 (agentic-flow dep adds) * chore: bump agentic-flow dep to 2.0.12-fix.8 to match lockfile (CI fix) --------- Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
173 lines
15 KiB
Markdown
173 lines
15 KiB
Markdown
# ADR-097: Federation-wide Budget Circuit Breaker and Token Quotas
|
||
|
||
**Status**: Accepted — Partially Implemented (Phase 1 + Phase 3 consumer-side landed; Phases 2 and 4 deferred)
|
||
**Date**: 2026-05-04 · **Updated**: 2026-05-09
|
||
**Version**: Phase 1 shipped in `@claude-flow/plugin-agent-federation` (v3.6.x); Phase 3 consumer in `ruflo-cost-tracker@0.14.0`
|
||
**Supersedes**: nothing
|
||
**Related**: ADR-086 (Agent Federation), ADR-095 (Architectural gaps — G2 includes federation transport), issues #1723 and #1724 (#1724 closed as duplicate of #1723), commit `6f495369` (G2 Ed25519 signing in federation)
|
||
|
||
## Context
|
||
|
||
Agent Federation (ADR-086) lets agents on one Ruflo node delegate tasks to peer nodes across trust boundaries. The implementation in `@claude-flow/plugin-agent-federation` already covers identity (Ed25519 keypairs), trust scoring, and `federation_send` for cross-node delegation. What it does **not** cover today:
|
||
|
||
- **Recursive delegation loops**. Node A → Node B → Node A → … If a peer wraps a received task and delegates it back, there is no hop counter to break the cycle. A pathological multi-node ring can run until process memory or the network gives out.
|
||
- **Cost cascades**. A 200-token task delegated to a peer can spawn a sub-swarm of five worker agents on the remote, each potentially calling expensive frontier models. The originator never sees the bill until the cost-tracker reconciles after the fact.
|
||
- **No backpressure on hostile / faulty peers**. A misbehaving peer that returns expensive responses cannot be muted automatically — the local trust score drifts but there's no mechanism that says "this peer is now too expensive, stop sending it work."
|
||
- **No unified cost surface**. The `ruflo-cost-tracker` plugin tracks local agent token usage but federation traffic is invisible to it.
|
||
|
||
The original issue (#1723, #1724 dup) frames this as a stability + enterprise-readiness gap. The fix is a **federated circuit breaker** layered on top of the existing federation protocol — no breaking changes to the wire format, just additive metadata.
|
||
|
||
### Existing surface to build on
|
||
|
||
| Component | Path | Today |
|
||
|---|---|---|
|
||
| Federation MCP tool | `v3/@claude-flow/plugin-agent-federation/src/mcp-tools.ts` (`federation_send`) | Sends a task to a peer; no cost / hop awareness |
|
||
| Federation node entity | `v3/@claude-flow/plugin-agent-federation/src/domain/entities/federation-node.ts` | `trustScore`, `trustLevel`, `lastSeen`; no `state: SUSPENDED` |
|
||
| Cost tracker plugin | `plugins/ruflo-cost-tracker/` | Tracks local model spend per agent / per session |
|
||
| Behavioral trust | ADR-086 §"Trust scoring" | Adjusts on protocol misbehavior; no cost-based decay yet |
|
||
|
||
## Decision
|
||
|
||
Ship four cohesive but independently-shippable parts. Each part is one iteration; the parts compose into the full circuit breaker.
|
||
|
||
### Part 1 — Budget header on `federation_send`
|
||
|
||
Extend the `federation_send` MCP tool input schema with three optional metadata fields:
|
||
|
||
```ts
|
||
{
|
||
// existing
|
||
peerId: string,
|
||
taskId: string,
|
||
payload: unknown,
|
||
// new (all optional, defaults preserve current behavior)
|
||
budget?: {
|
||
maxTokens?: number, // hard cap on Σ tokens across the whole hop chain
|
||
maxUsd?: number, // hard cap on Σ USD spend; enforced via cost-tracker
|
||
},
|
||
maxHops?: number, // default 8; 0 = no remote delegation allowed
|
||
}
|
||
```
|
||
|
||
These travel inside the federation envelope as a `budget` block alongside the payload. The receiving peer:
|
||
|
||
1. **Decrements `maxHops`** on receive. If it drops below 0, returns an error response (`HOP_LIMIT_EXCEEDED`) without invoking any agent. The originator gets the failure synchronously.
|
||
2. **Subtracts from the budget** as it spends. Each completed sub-task reports `tokensUsed` and `usdSpent`; the running total is checked against the cap before each subsequent action. Overshoot returns `BUDGET_EXCEEDED` and refuses further work.
|
||
3. **Propagates the remaining budget** if the peer itself federates onward. The downstream call inherits whatever budget remains, never more.
|
||
|
||
Budget defaults: when omitted, treat as `Infinity` for `maxTokens` / `maxUsd` and `8` for `maxHops`. The default hop limit alone closes the recursion-loop class without any caller change.
|
||
|
||
### Part 2 — Peer state machine: ACTIVE / SUSPENDED / EVICTED
|
||
|
||
Extend `federation-node.ts` with a `state` field driven by the breaker:
|
||
|
||
| State | When transitioned | What it means |
|
||
|---|---|---|
|
||
| `ACTIVE` | default; healthy | `federation_send` accepts deliveries to this peer |
|
||
| `SUSPENDED` | breaker tripped (cost threshold or repeated failures) | sends to this peer return `PEER_SUSPENDED` immediately; receives still accepted but ignored for trust accumulation |
|
||
| `EVICTED` | manual or post-grace-period escalation from SUSPENDED | peer removed from registry; new delivery errors are emitted as `PEER_EVICTED` |
|
||
|
||
Transition triggers:
|
||
|
||
- `ACTIVE → SUSPENDED` when **either**:
|
||
- Trailing 24h cost from this peer > configured threshold (`peer.costSuspensionUsd`, default `$5.00`)
|
||
- Trailing 1h failure ratio > 50% across ≥10 sends
|
||
- `SUSPENDED → ACTIVE` after a configurable cooldown (default 30 min) AND a successful health probe
|
||
- `SUSPENDED → EVICTED` after 24h continuous suspension OR explicit `federation_evict` MCP call
|
||
|
||
Cooldown + auto-recovery prevents the breaker from being a one-way door — same shape as a typical hystrix-style breaker.
|
||
|
||
### Part 3 — Cost-tracker integration
|
||
|
||
Wire the federation layer into `ruflo-cost-tracker` so federated spend appears in the same dashboards as local spend:
|
||
|
||
1. New event type `federation_spend` published to the cost-tracker bus on every `federation_send` completion: `{ peerId, taskId, tokensUsed, usdSpent, ts }`.
|
||
2. Cost-tracker aggregates per-peer rolling windows (1h / 24h / 7d) and exposes them via the existing `cost-report` skill.
|
||
3. The breaker queries cost-tracker for the per-peer 24h sum to evaluate the suspension threshold.
|
||
|
||
This is one direction (federation → cost-tracker). Cost-tracker doesn't need to mutate federation state directly; the breaker pulls.
|
||
|
||
### Part 4 — Doctor + observability
|
||
|
||
- `ruflo doctor` reports the current state of every known peer (`ACTIVE`/`SUSPENDED`/`EVICTED`) and the trailing-24h cost. A peer pinned in `SUSPENDED` for >1h shows up as a yellow warning.
|
||
- New MCP tool `federation_breaker_status` returns the same info programmatically for swarm coordinators that want to route around suspended peers.
|
||
- Structured log line on every state transition with `{prevState, newState, reason, peerId}` so post-incident triage doesn't need a debugger.
|
||
|
||
## Implementation outline
|
||
|
||
Per the user directive, the implementation team owns this section. Each phase is one iteration to keep blast radius bounded:
|
||
|
||
| Phase | Scope | Lands in |
|
||
|---|---|---|
|
||
| P1 | Budget envelope + hop counter, no peer state changes | federation plugin + new tests |
|
||
| P2 | Peer state machine (`ACTIVE`/`SUSPENDED`/`EVICTED`) + transition rules | federation-node.ts + tests |
|
||
| P3 | Cost-tracker bus event + per-peer rolling aggregation | cost-tracker plugin |
|
||
| P4 | Doctor + `federation_breaker_status` MCP tool | doctor command + mcp-tools |
|
||
|
||
Test surface:
|
||
|
||
- **Unit**: budget arithmetic, hop counter underflow, state-transition table (ACTIVE→SUSPENDED→ACTIVE→EVICTED).
|
||
- **Integration**: a synthetic peer that intentionally exceeds budget should produce `BUDGET_EXCEEDED` on the originator AND transition to `SUSPENDED` after the threshold.
|
||
- **Property**: random hop chains must always terminate at `maxHops` ≤ 8.
|
||
|
||
## Trade-offs
|
||
|
||
| Decision | Alternative | Why this |
|
||
|---|---|---|
|
||
| Optional budget fields, default `Infinity` | Required budget on every `federation_send` | Backward compatible: existing code paths keep working. Callers opt in to limits. |
|
||
| Hop counter default `8` | Default unlimited | `8` is generous for legitimate multi-hop flows (typical ≤3) and cheap insurance against the recursion-loop class. |
|
||
| Peer state in federation plugin (not cost-tracker) | Centralized in cost-tracker | Federation owns peer identity + trust; the breaker is a federation concern. Cost-tracker provides numbers; federation decides actions. |
|
||
| Trailing-24h $5 default suspension threshold | Per-peer config only | Sane default avoids "gun, foot" for new installs. Override via plugin config. |
|
||
| `EVICTED` separate from `SUSPENDED` | Single `INACTIVE` state | Eviction is operationally permanent (admin removes the peer); suspension is a soft, auto-recovering state. They have different audit/log/recovery semantics. |
|
||
| Pull from cost-tracker for breaker decisions | Push spend events into federation | Federation already speaks events to cost-tracker; reverse direction would couple in both ways. Pull-on-decision is simpler and just-in-time. |
|
||
|
||
## Risks
|
||
|
||
1. **Race between budget check and remote spend**. The receiving peer checks budget *before* invoking the agent, but the agent's actual spend is reported *after*. A budget can be modestly exceeded by the last-call's actual cost > predicted cost. Mitigation: enforce on the *predicted* cost (max_tokens × per-token-rate), then refund unused on the running total. Worst case overshoot is one model call's worth.
|
||
2. **Clock skew across peers**. The 24h rolling window is computed locally per-peer, so divergent clocks don't change the threshold semantics. Trust-score timestamps (`lastSeen`) already rely on local clocks; same threat model.
|
||
3. **Suspended peer recovery storm**. If many peers transition `SUSPENDED → ACTIVE` simultaneously after cooldown, they could all retry pending sends at once. Mitigation: jitter the cooldown by ±10% per peer.
|
||
4. **Cost-tracker plugin not installed**. If the user runs federation without cost-tracker, USD-based breaker rules degrade to "always permitted." Token-count breaker still works (federation tracks tokens itself). Document this — cost-tracker is the recommended pair, not strictly required.
|
||
|
||
## Implementation status (2026-05-09)
|
||
|
||
**ADR-097 is functionally complete end-to-end.** All five phases are landed: Phase 1 (budget envelope), Phase 2.a (state machine entity), Phase 2.b (breaker service + outbound short-circuit), Phase 3 (consumer-side cost aggregation + upstream `reportSpend` emission with `SpendReporter` interface and breaker fan-out), Phase 4 (operator surface — `federation_breaker_status` / `federation_evict` / `federation_reactivate` MCP tools + `ruflo doctor --component federation`). Integrators wire a `SpendReporter` implementation to push to their cost-tracker / Datadog / accounting backend; the included `InMemorySpendReporter` covers tests + reference impl.
|
||
|
||
| Phase / Component | Status | Files | Commit(s) |
|
||
|---|---|---|---|
|
||
| **Phase 1** — Budget envelope + hop counter on `federation_send` | Implemented | `v3/@claude-flow/plugin-agent-federation/src/domain/value-objects/federation-budget.ts` (new), `mcp-tools.ts` updated | `7e1cc06df feat(federation): ADR-097 Phase 1 — budget envelope + hop counter (#1723)` |
|
||
| **Phase 2.a** — Peer state machine value object + entity transitions | Implemented | `domain/value-objects/federation-node-state.ts` (new), `domain/entities/federation-node.ts` (state field + suspend/evict/reactivate), `__tests__/unit/federation-node-state.test.ts` (27 tests) | `feat/adr-100-promote-097-phase2` |
|
||
| **Phase 2.b** — Breaker service + outbound short-circuit | Implemented | `application/federation-breaker-service.ts` (new — pure `evaluatePolicy` + stateful `FederationBreakerService` with bounded per-peer buffer), `application/federation-coordinator.ts` (sendMessage gates on `!peer.isActive` with `PEER_SUSPENDED`/`PEER_EVICTED` constant errors), `__tests__/unit/federation-breaker-service.test.ts` (25 tests) | `feat/adr-100-promote-097-phase2` |
|
||
| **Phase 3 consumer** — Cost-tracker bus event + per-peer rolling aggregation | Implemented | `plugins/ruflo-cost-tracker/scripts/federation.mjs`, `skills/cost-federation/SKILL.md` | `1c0804315 feat(cost-tracker): P6 — ADR-097 Phase 3 federation_spend consumer (v0.14.0)` |
|
||
| **Phase 3 upstream** — Federation `reportSpend()` + `SpendReporter` interface + breaker fan-out + `federation_report_spend` MCP tool | Implemented | `application/spend-reporter.ts` (new — interface + `InMemorySpendReporter` reference impl), `application/federation-coordinator.ts` (new optional `spendReporter` + `breakerService` constructor integrations + `reportSpend` method), `mcp-tools.ts` (federation_report_spend), `__tests__/unit/coordinator-spend-reporting.test.ts` (10 tests) | `feat/adr-100-promote-097-phase2` |
|
||
| **Phase 3 plugin wiring** — federation plugin adopts budget integration + ADR-097 doc | Implemented | `plugins/ruflo-federation/` (v0.2.0), `docs/adrs/0001-federation-contract.md` | `b0168e4a5 feat(ruflo-federation): adopt plugin contract — 3-gate alignment + ADR-097 budget integration + smoke` |
|
||
| **Phase 4** — Operator surface: 3 MCP tools + doctor health-check | Implemented | `mcp-tools.ts` (federation_breaker_status / federation_evict / federation_reactivate), `application/federation-coordinator.ts` (getPeerStates / getPeerStateCounts / evictPeer / reactivatePeer), `v3/@claude-flow/cli/src/commands/doctor.ts` (checkFederationBreaker), `__tests__/unit/federation-coordinator-breaker.test.ts` (11 tests) | `feat/adr-100-promote-097-phase2` |
|
||
|
||
### Open questions resolved during implementation
|
||
|
||
| Original question | Resolution |
|
||
|---|---|
|
||
| Is upstream `federation_spend` event emission required for Phase 3 consumer? | No — the consumer (`scripts/federation.mjs`) activates the moment the upstream emitter lands; it reads from the `federation-spend` namespace. Until Phase 2/3 emit events, the consumer runs against an empty namespace and reports zero spend. |
|
||
| Does Phase 1's default `maxHops=8` close the recursion-loop class for all callers? | Yes — hop enforcement fires even when no explicit budget is passed (`validateBudget(undefined)` returns a default-bounded budget). |
|
||
|
||
### Deferred
|
||
|
||
_All ADR-097 phases are landed. Future work is integrator-side wiring:_
|
||
|
||
- **Production `SpendReporter` adapter** that persists to ruflo memory (`namespace=federation-spend`, `key=fed-spend-<peerId>-<ts>`) per the cost-tracker consumer convention. Reference implementation: `InMemorySpendReporter` (in-memory buffer, fine for tests, NOT for production). A real adapter is a thin shell around `memory_store` — left to the integrator since they own the credentials/quota policy.
|
||
- **Auto-emission from `federation_send`** — currently `reportSpend()` is an explicit caller-side call (correct since the federation layer doesn't own model pricing). A future enhancement could auto-emit a "send-completed" event with the budget enforcement's predicted cost as a placeholder, but the integrator overrides with actual cost on completion. Decision deferred until a concrete integration request justifies the additional state.
|
||
|
||
---
|
||
|
||
## Acceptance criteria
|
||
|
||
The full implementation is done when:
|
||
|
||
- `federation_send` accepts and propagates `budget` + `maxHops` without breaking any existing test
|
||
- A synthetic recursive delegation chain (peer A → B → A → …) terminates at `maxHops` with `HOP_LIMIT_EXCEEDED` on the originator
|
||
- A peer that exceeds the cost threshold over 24h transitions to `SUSPENDED`, refuses sends for the cooldown window, and auto-recovers on a successful probe
|
||
- `ruflo doctor` shows peer states + trailing-24h cost
|
||
- `federation_breaker_status` MCP tool returns structured state per peer
|
||
- Cost-tracker `cost-report` shows federated spend grouped by peer
|
||
- New tests cover budget arithmetic, hop counter, state transitions, and cost-tracker bus events
|
||
- Full vitest suite stays green; no regressions in the 1917-test baseline
|