fa_hub_client 0.2.8
fa_hub_client: ^0.2.8 copied to clipboard
DAP/1 hub client for flutter_agent_harness: MessagingRepository over a signed WebSocket, FahPlugin wiring delivery via Agent.externalSteeringSource.
Changelog #
0.2.8 #
- Renamed and published as
fa_hub_client(thefah_hub_clientpackage name is owned by a different pub.dev account; same code, MIT license preserved, original copyright retained). - Critical CPU fix:
HubClient.defaultBackoffclamped the attempt BEFORE shifting —1 << (attempt - 1)overflows 64-bit int semantics (attempt 64 → negative, attempt 65+ → 0), so the 30 s cap never applied past the 64th failed reconnect and the loop degraded into a tight spin reconnecting to a dead hub at full speed forever. Observed live:_reconnectAttempt = 69,730,601with every idlefaprocess burning ~60–90% CPU (pluskernel_task/xprotectdamplification from the socket churn). Every reconnect now waits ≥1 s, capped at 30 s, no matter how long the hub stays down.
0.2.4 #
- Additive liveness surface for the messaging fabric.
AgentInfo.lastSeen(DateTime?, null on legacy hubs — never treat null as stale) parses the hub roster's activity-accuratelastSeen(dap v0.3.1+; bumped per authenticated inbound frame instead of frozen at connect time).MailboxEntry.isLive(bool?) +MailboxEntry.lastActivity(DateTime?) carry SOURCE-DEFINED semantics — a hub-backed repository fills them from the roster (online flag + hub lastSeen), a file-backed repository from mailbox mtimes; NEVER compare across sources; null = unknown = never hidden.HubMessagingRepository.directory()populates both so downstream fabrics (flutter_agent_harness agent_directory) consume one authoritative type instead of patching a local copy.
0.2.3 #
- 401 self-recovery for a stale persisted client secret (live incident
2026-09-01: a hub restart wiped the server-side secrets, so every
previously enrolled client 401-looped — the cached
clientSecretwon precedence overDAP_MASTER_SECRETand the rejection was fatal).resolveDapClientSecretnow also reports WHERE the dial credential came from (DapSecretSource: env / config / master / none) and echoes the raw master secret. On a pre-upgrade 401,HubClientescalates EXACTLY ONCE when — and only when — the rejected secret came from~/.dap/config.jsonAND a master secret is available: it drops the provably dead cache entry from the persisted config, dials again in enroll-mode (the re-enroll binds to the same hello name — key files and agent identity untouched), and persists the newly issued secret through the same path as a first enroll. A second 401 is fatal with the frozen cross-adapter hint; env-sourced secrets (DAP_CLIENT_SECRET= explicit user intent) and master-less setups keep today's hard fail. No retry loops, no new env vars.
0.2.2 #
- Closed 0.2.1's residual first-query steal window:
HubClientnow fires a welcome-time warm-uppresence_queryon EVERY fresh connection — a throwaway whose only job is to make the hub echoreplyToand arm the echo-seen latch BEFORE any consumer query, so the startup join self-echo (a replyTo-less broadcast) can no longer drain the firstpeers()with a one-agent roster. Up to 2 retries while the latch is still unarmed; legacy hubs (answers withoutreplyTo) leave the latch unarmed and keep the one-completes-all path; a reconnect re-runs the warm-up (a no-op once armed — the latch stays client-lifetime). The warm-up result is discarded and send failures clean up their waiter (the whois pattern — a listener-less pending completer could surface as an unhandled async error at teardown). Value contract for concurrent user queries unchanged.
0.2.1 #
- Fixed the owner-reported presence race (bug 2026-08-31, FA "BUG 5"):
an UNSOLICITED
presencebroadcast completing a pendingpresence_querywaiter madepeers()return ONLY the broadcast's agent instead of the answer's roster.presenceQuery()now sends a frame id; per the hubreplyTocontract (additive, docs/protocol.md §presence) an ANSWER echoes that id inreplyToand completes only the matching waiters — concurrent queries keep their own ids. On a hub known to echo, replyTo-lesspresenceframes are broadcasts and never complete waiters. Legacy hubs (no echo) keep the 0.1.4 one-completes-all answer path — old deployments keep working; the residual window (a broadcast racing the FIRST query to a new hub, before any echo is seen) is documented onHubClient._onPresence.
0.2.0 #
- BREAKING (owner decision 2026-08-31):
peers()takes NO parameters — theincludeOfflineflag is gone. It now ALWAYS returns online-only agents and marks our own entry withAgentInfo.self == true(self stays in the list, not excluded).AgentInfogains theselffield (default false); rawpresenceQuery()/whoisentries are unchanged.HubPlugin.peersandHubMessagingRepository.peersaligned to the same no-flag shape.
0.1.4 #
- Request doors (
whois/flush/presenceQuery) now fan out to waiter LISTS: concurrent callers all complete with the answer. Previously a second caller clobbered the single completer and ORPHANED the first — the live hang was the 15s PendingInvites presence poller racing a tool'speers(), and an inbound-DM sender whois racing asendDmto the same peer. 0.1.3's request timeouts and error-frame completion are unchanged (each caller keeps its own 10s cap; genuine hub silence still fails loudly). - Ported the FA SilentHub acceptance suite (
test/silent_hub_test.dart): 5 scenarios — silentpresence_query, silentwhois, error-answeredpresence_query, and the two clobber races (poller-vs-toolpresenceQuery, whois-vs-whois on one target) — asserting every request COMPLETES within 3s; with a healthy hub answering once, both concurrent callers must receive the answer.
0.1.3 #
- Request timeouts on
whois/flush/presenceQuery(HubClient.requestTimeout, default 10 s): a hub that never answers no longer hangs the caller forever — the request fails loudly ("hub did not answer - Hub
errorframes now also complete pending flush/presence requests (mirroring disconnect handling); previously they only surfaced on theerrorsstream and left the caller waiting — the CLI no longer hangs when the hub answers with an error.
0.1.2 #
- Ported FA review fixes (db65522):
HubIdentity.loadwarns loudly and returns false when the key file cannot be locked to mode 0600 (injectablesetPrivateMode/warn); HKDF honors the RFC 5869 255-block cap;HubPlugin.isDefaultUrllets hosts keep zero-config default-hub connect failures quiet. - Whole-tree
dart format; CI gained adart formatgate.
0.1.1 #
- Enrollment auth: first connect enrolls via
DAP_MASTER_SECRET; the hub-issued client secret is persisted in~/.dap/config.json. HubPlugin.inviteTo/connectTo: dap_invite and dap_connect, including pending invites armed for offline names and auto-delivered on presence.- WebSocket keepalive so long-lived hub connections survive idle periods.
- Zero-config startup: settings resolve from env (
DAP_HUB_URL,DAP_AGENT_NAME,DAP_KEY_PATH,DAP_CHANNELS_FILE) >~/.dap/config.json> defaults; identity auto-created at~/.dap/keys/fah/<name>.key.
0.2.5 #
- feat(plugin): public sendToChannel — channel-send parity with omp's dap_send
0.2.6 #
- chore: apply dart format
- fix: plugin inert without DAP_MASTER_SECRET
0.2.7 #
- fix(ci): publish to pub.dev from tag refs via workflow_dispatch