wire dash + the "open BAND" positioning (P1 + P3)#360
Conversation
…ol) + the "open BAND" position Reframes the band.ai "should we clone the dashboard" question: BAND is a direct wire competitor ($17M, closed), and the generic agent dashboard is a saturated OSS category (Mission Control et al.). So: build a wire-aware observability pane none of them have, reuse an OSS UI where it buys leverage, and lean wire into being the open/decentralized BAND. Three sub-projects: P0 shared dash::collect() core, P1 `wire dash` TUI, P2 Mission Control read-only adapter, P3 "open BAND" research-spec brief. Two product calls locked: cross-agent memory = explicit non-goal; cross-framework reach = MCP-as-universal-adapter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…e analysis + roadmap Backed by a 30-page primary-source deep-crawl of band.ai (dossiers/band-ai.md). BAND = $17M-seed closed/centralized SaaS doing wire's thesis. Verified wire wins on identity (self-certifying DID vs account API-key), E2E crypto (BAND has none), consent (operator-gated vs BAND's LLM-auto-accept option), and decentralization (BAND coordination plane is SaaS-only). BAND wins on framework reach (14 native SDKs) + funding. Recommendation: own "no server owns your agent graph"; close reach (MCP-as-universal-adapter, sharpened by BAND relegating MCP to automation-only) + observability (P1/P2); decline the traps (memory, task boards, governance theater) as explicit anti-features. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…om open-BAND brief Ratified Decline list: cross-agent memory store, task boards/kanban, and governance-UI/policy-engine are explicit non-goals — each a deliberate divergence from BAND verified against its shipped API (dossiers/band-ai.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
… box (P0+P1) New `wire dash` [--watch] [--json] [--all] [--probe]: enumerates every wire session on the machine (persona, daemon liveness, pinned peers, relay binding, sync recency), paired sessions first, idle solo daemons collapsed to a count. P0 src/dash.rs: collect() enriches session::list_sessions() by reading each session's trust.json (peers, self-excluded), relay.json (binding), daemon.pid, and last_sync.json mtime — by EXPLICIT path (never the session-scoped config helpers, which would read the current session 270x). Read-only: never spawns/kills a daemon, no per-session network; relay /healthz is one deduped GET, opt-in via --probe. Golden surface: wire-dash-v1 JSON. P1 src/cli/dash.rs: table (ANSI, no ratatui dep) + --watch 2s redraw + --json. In-situ vs the live 273-session store: 269 running / 11 paired / 258 idle, pid-count unchanged before/after (zero spawn/kill). Honest classification: "idle" = running daemon with no real peers (a live daemon heartbeat-syncs regardless of use, so peers not sync-age is the signal). P2 (Mission Control adapter) deferred — needs MC's WebSocket status schema, tracked as a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…exclude, sanitize Round-2 fixes from the 3-reviewer diff gate on 2ef5dd4: - MAJOR: `--watch --json` (and `--watch | pipe` via auto-json) silently one-shotted. Watch is now the outer loop; each tick emits JSON or table. Verified: 2 JSON ticks in 5s (was 1). - MAJOR: table misalignment — `{daemon:<9}` padded an ANSI-wrapped string (zero fill in color mode) and `fp` column was 7 wide for 8-char fingerprints. Pad plain-then-color; fp width 9. - MAJOR: read_peers self-exclusion failed on a did-less self entry → session listed itself as a peer. Now excludes by DID or handle. - Terminal-escape injection: peer handle/tier + name now run through character::sanitize_display_text before render (peer-published text). - Hardening: bounded (256KB) file reads across the ~270-session fan-out; probe_relay rejects non-http(s) + disables redirects; fingerprint_from_did returns None on empty tail. - UX: render cwd (reap-decision hint), pluralize the hidden-count line, fleet-over-SSH footer. - Tests: golden wire-dash-v1 shape anti-drift, StalePid serialization, self-exclude-by-handle, ESC-strip, cwd render. 13 dash tests green. - Doc: design P0 amended to the as-built DaemonState/likely_idle model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
The output path used print!/println!, which PANIC on EPIPE — so `wire dash | head`, `wire dash --json | jq` closing early, or the Mission Control reporter dropping its pipe crashed wire with a backtrace (exit 101). Round-2's watch-loop fix made this the primary failure mode since the tool is now explicitly meant to be piped. Centralize output through emit(), which exits 0 on BrokenPipe. Verified: `wire dash --all | head` and `--watch | head` no longer panic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…ly decommission idle identities The supervisor keeps a daemon alive for every real identity (so it can receive mail), so an idle throwaway's daemon can't just be killed — it respawns within a poll. This adds a retire lifecycle stage: write a `.retired` marker that makes `supervisor_eligible` treat the home as ineligible (the supervisor's existing reconcile loop then kills the child and never respawns), plus a direct graceful-then-force kill for immediacy. Reversible: the marker is the only state change; `wire revive` removes it and the identity comes back intact (relay slots never expire, mail is retained, cursor kept). Surface: `wire retire <handle|fp|key>`, `wire revive <...>`, `wire dash --retire-idle [--older-than D] [--dry-run] [--force] [--json]`, `wire dash --retired`. dash gains a `retired` state (collapsed by default, shown by --retired/--all) + a retired count. Hardened per a 4-persona plan review (5 blockers folded): retired filtered FIRST in supervisor_eligible (above the cwd branch AND the max_idle=None early-return); graceful-then-force kill (bare SIGTERM is a no-op for a headless Windows daemon); current identity matched by HOME PATH and fail-closed (resolve_session_key is None on a bare terminal); pending-inbound + paired + recent excluded from the sweep; --force skips the typed confirm only, never the guards; guards re-checked per target at kill time (TOCTOU); dry-run default; victims named before confirm. CLI-only (no MCP tool — an agent must not retire identities unsupervised). 633 lib tests green (supervisor eligibility proven for retired+cwd/identity/ max_idle=None; retire/revive/marker mechanics; box-wide resolver). NOTE: takes full effect only once the RUNNING supervisor is the new binary — until `wire service install`/upgrade, the old supervisor respawns retired daemons. Bulk-retiring the 258 backlog is the operator running --retire-idle after upgrading. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…not daemon.pid daemon.pid mtime resets to now on every supervisor respawn, so on a box whose supervisor restarted recently it read every daemon as "just started" and the --older-than guard matched almost nothing (1 of 258). private.key is written once at keygen and never rewritten → its mtime is the identity's true age, immune to respawn and to heartbeat sync. Dry-run on the live store now selects 222 at 7d / 257 at 1d, current + paired still correctly excluded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
Deploying wireup-landing with
|
| Latest commit: |
9c3b62a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6f4b34dc.wireup-landing.pages.dev |
| Branch Preview URL: | https://observability-open-band.wireup-landing.pages.dev |
…home fail-closed Gate-2 built-thing review (all 5 plan-blockers verified landed) found one new BLOCKER + one MAJOR: - BLOCKER: cmd_retire_idle joined snapshots to homes via a name-keyed HashMap, but SessionInfo.name is the DID-derived handle (32-bit space) → ~43% birthday-collision at 258 identities → two homes collapse → retire the WRONG identity. Fix: iterate list_sessions() directly, each SessionInfo carries its own home_dir. No join. - MAJOR: current_home() did parent().parent() assuming 2 levels, but a bare terminal's config_dir is 1 level (dirs_config/wire) → returned a bogus ancestor instead of None, defeating the fail-closed current-identity guard. Fix: verify the resolved home has config/wire/private.key, else None. - MINORs: per-target progress print during the sweep; structured JSON result (retired/skipped/errored, not a mixed Vec); consistent is_current in selection; added a render test for retired-row collapse. Re-verified in-situ: 222 candidates @7D / 257 @1d, current excluded, no home collisions. 634 lib tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
Added:
|
…bug)
When a Claude session's wire identity displayed (statusline, live session id)
diverges from what the session operates as (a stale MCP/CLI process frozen to an
old session key across a resume), you get "two names" — dial one identity, the
session answers as another. This was invisible until you noticed by hand.
Adds `session::detect_identity_split()`: compares the session key resolved from
the ENV (what a stale process is frozen to) against the PID-file adapter
(Claude Code's authoritative live session, via the parent-process chain). A
mismatch = the split. `wire dash` now prepends a warning banner naming the fix:
⚠ identity split — this shell operates as <X> but your live Claude session
is <Y>. A stale wire process is bound to the wrong identity. Fix: /mcp
reconnect (or restart this session).
In-process, cross-platform (no ps parsing). Verified in-situ: no banner on a
healthy session, fires on an injected stale WIRE_SESSION_ID. Human view only —
JSON schema unchanged. 636 lib tests green.
Root cause is #351 (wire infers per-session identity with no single stable
source across statusline/MCP/resume); this makes the drift visible + fixable in
one command instead of a mystery. The durable fix (cycle MCP servers on
upgrade/resume) is filed separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
Origin
Started from "look at band.ai and compare — should we build an open-source clone of that dashboard?" Research reframed it twice: band.ai (BAND) is not a dashboard, it's a direct wire competitor ($17M seed, closed/centralized SaaS doing wire's exact thesis), and the generic "watch my agents" dashboard is a saturated OSS category. So instead of cloning: ship a wire-aware observability pane, and lean wire into being the open, decentralized BAND.
What's in this PR
P1 —
wire dash(src/dash.rs+src/cli/dash.rs): one read-only pane for every wire identity on the box — persona, daemon liveness, pinned peers, relay binding, sync recency. Paired sessions float to the top; idle solo-daemon throwaways collapse to a count (--allexpands).--watch(2s),--json(goldenwire-dash-v1surface),--probe(opt-in relay/healthz).session::list_sessions()rather than re-deriving; reuses the cheap pid-liveness check (not the full per-session scan).P3 — "open BAND" strategy (docs only):
docs/design/2026-07-04-open-band-positioning-brief.md(backed by a 30-page primary-source deep-crawl of band.ai, dossier lives in the private dotfiles store) + the architecture doc + anti-features 21–23 (cross-agent memory, task boards, governance-UI theater — each a deliberate divergence verified against BAND's shipped API).Deferred (honest)
P2 — Mission Control adapter: not in this PR. Its live-status path needs Mission Control's WebSocket schema (not yet nailed); a register-only reporter would be thin shelf-ware. Tracked as a follow-up that starts by fetching MC's
openapi.json. A2A-outside interop adapter = separate follow-up spec.Review + test
Built via the build-loop (MEDIUM tier): 3-reviewer diff gate → 4 MAJORs folded (watch/json one-shot, ANSI column misalignment, self-exclusion on a
did-less entry, terminal-escape injection via peer handle) → verifier pass → 1 new MAJOR (EPIPE panic onwire dash | head) folded. Hardening: bounded file reads across the ~270-session fan-out, probe rejects non-http(s) + no redirects.cargo test --lib628 pass (13 new dash tests incl. golden-shape anti-drift, ESC-strip, self-exclude-by-handle)cargo fmt --check+cargo clippy -D warningsclean--watch --jsonstreams (2 ticks/5s), pipe-to-headexits clean🤖 Generated with Claude Code