3 Commits
Author SHA1 Message Date
rUvandReuven 19a569b1a7 feat(federation): ADR-097 phases 2.a/2.b/3-up/4 + ADR-104 transport (#1876)
* 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>
2026-05-09 21:55:49 -04:00
ReuvenandRuFlo 3f35608a98 docs(adr): ADR-097/098/099/100 status — Proposed → actual implementation state
- ADR-097: Partially Implemented — Phase 1 (budget envelope + hop counter,
  commit 7e1cc06df) and Phase 3 consumer-side (cost-tracker federation.mjs,
  commit 1c0804315) landed; Phase 2 peer state machine and Phase 4 doctor
  tool deferred
- ADR-098: Partially Implemented — Parts 1–4 all landed across v3.6.25–26
  (commits 6a4057474, 6130f4061, 00a9d13b5, cf96a562c for Part 1; four token-
  diet slices for Part 2; 29542ce6d Part 3; 2e5c90c90 Part 4); Part 5 worker
  dispatch deferred per ADR ordering
- ADR-099: Implemented — dossier-investigator agent + dossier-collect skill
  fully shipped in ruflo-goals (commit 1e11ac84e, plugin contract adoption
  714cd534c)
- ADR-100: Partially Implemented — cli-core@3.7.0-alpha.5 published and
  proven 38× faster cold cache (671 ms vs 25.5s, commit 0acf557ba); backend
  abstraction (alpha.1), tool-def/handler split (alpha.2), and cli re-exports
  (alpha.5) all landed; latest-tag promotion and issue #1760 comment deferred

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-05-09 15:08:37 -04:00
Reuven 62a6fc5fb0 docs(adr): ADR-097 — federation-wide budget circuit breaker (#1723)
Closes design half of #1723 (and dup #1724). Builds on ADR-086 (Agent
Federation) and the G2 work that landed real Ed25519 signing in
6f495369. Audit-trail-style ADR following the same shape as
ADR-093/094/095/096.

Four cohesive parts, each shippable as one iteration:

  P1 — Budget envelope on federation_send
    Optional { maxTokens, maxUsd } + maxHops (default 8, generous but
    finite). Budget decremented on the receiving peer, propagated to
    onward delegation as remaining-budget, refused with BUDGET_EXCEEDED
    on overshoot. Hop counter alone closes the recursion-loop class.

  P2 — Peer state machine: ACTIVE / SUSPENDED / EVICTED
    Trust-score-driven breaker. Suspended on (trailing-24h cost >
    $5 default OR trailing-1h failure ratio > 50%). Auto-recovery
    after 30min cooldown + health probe. Eviction permanent until
    admin re-adds.

  P3 — Cost-tracker integration
    federation_spend event published on every send completion.
    cost-tracker aggregates rolling windows; breaker queries on each
    transition decision. One-way coupling — federation publishes,
    cost-tracker exposes via the existing cost-report skill.

  P4 — Doctor + observability
    ruflo doctor reports peer states + cost; new
    federation_breaker_status MCP tool returns the same data
    programmatically; structured logs on every transition.

Trade-offs documented:
  - Optional budget (backward compat) over required-everywhere
  - max_hops=8 over unlimited
  - Peer state owned by federation plugin over cost-tracker (federation
    owns peer identity; cost-tracker provides numbers)
  - Pull-on-decision over push-on-spend (one direction of coupling)

Acceptance criteria pin: synthetic recursion ring terminates at
maxHops; cost-threshold breach auto-suspends + auto-recovers; full
1917-test baseline stays green.

Issue #1724 was an exact duplicate (24s after #1723) — closed with a
pointer comment to this ADR.
2026-05-03 21:08:52 -04:00