import { SessionPeer, type AckResult } from "./peer.js"; import type { Envelope } from "./envelope.js"; import type { Broker } from "./broker.js"; import { RelayClient } from "../transport/relay_client.js"; import type { Ed25519Keypair } from "./../pairing/crypto.js"; /** * MeshNode — the single composition point for "join the agent mesh". * * Wraps the two layers every mesh participant needs, and nothing else * (pairing is app↔Pi and stays OUT of here): * * 1. **Local UDS mesh** — always. A `SessionPeer` joins (or leads) the * broker at `sockPath`: `send` / `sendWithAck` / `request` / * `onMessage`, leader election + failover for free. * * 2. **Cross-PC relay bridge** — optional, only when the node leads (the * leader hosts the Broker, so it owns the `BrokerRemote`). Two ways to * supply the relay: * - **Self-managed** (MCP): pass `bridge: { relayUrl, cwd }` and the * node creates + owns the RelayClient on connect-if-leader, with * its own machine Pi-key. * - **Injected** (Pi extension): call `attachBridge({ relay, … })` * with the RelayClient the host already owns (and also uses for * app↔Pi pairing). MeshNode never closes an injected relay. * * Both the Pi extension and the MCP mesh server build on this so the mesh * wiring lives in one place. A follower never brings the bridge up — * cross-PC routing works transitively through whoever is leader (a Pi, the * daemon, or another MeshNode). On UDS failover that promotes this node to * leader, the bridge re-attaches automatically against the fresh broker. */ /** Self-managed-relay bridge config (MCP path). */ export interface MeshSelfRelayBridge { /** Relay URL in http(s):// form (converted to ws(s):// internally). */ relayUrl: string; /** cwd — derives the relay room id and the room_meta. */ cwd: string; /** Display name for room_meta. Defaults to the assigned mesh name. */ sessionName?: string; } export interface MeshNodeOptions { /** UDS broker socket path (e.g. ~/.pi/remote/sessions/local/broker.sock). */ sockPath: string; /** Requested mesh name (broker may add a #N collision suffix). */ name: string; /** Working directory, forwarded to the broker in `register` so peers are * keyed by (cwd, name) and a same-folder same-name reincarnation takes over * instead of colliding into `#N`. Optional (legacy peers omit it). */ cwd?: string; /** Optional audit log path passed through to SessionPeer. */ auditPath?: string; /** Self-managed relay bridge — brought up if this node leads. */ bridge?: MeshSelfRelayBridge; /** Diagnostic logger. Defaults to a no-op (avoids leaking into TUIs). */ log?: (msg: string) => void; } export type { AckResult } from "./peer.js"; interface SiblingInfo { pcLabel: string; pcPubkey: string; } export declare class MeshNode { private readonly peer_; private readonly log; private relay; private relayOwned; private brokerRemote; private piForward; private keypair; private bridgeParams; private reconnectWired; /** Self-managed relay reconnect (MCP path). The injected-relay path (Pi) * owns its own reconnect upstream, so these stay idle there. */ private relayReconnectTimer; private relayBackoffIdx; private static readonly RELAY_RECONNECT_BACKOFFS_MS; constructor(opts: MeshNodeOptions); /** Join (or lead) the mesh. Resolves with the assigned name. */ connect(): Promise; /** * Attach a cross-PC bridge on top of an EXTERNALLY-owned relay (Pi path). * Idempotent; only attaches when this node is the leader. Remembers the * params so the bridge re-attaches after a UDS failover. Call again with a * fresh relay after a relay reconnect. */ attachBridge(opts: { relay: RelayClient; relayUrl: string; keypair?: Ed25519Keypair; }): Promise; /** * Tear down the bridge AND forget its params (no auto re-attach until the * next `attachBridge`/`connect`). Closes the relay only if MeshNode created * it — an injected relay belongs to the host. Use on stop / relay drop. */ detachBridge(): void; private _wireReconnect; private _onReconnect; private _maybeBridge; private _detachBridgeKeepingParams; /** Self-managed relay dropped. Tear down the dead bridge (the WS is already * closed — don't double-close it) and schedule a reconnect with backoff. * Ignores stale closes from a relay we've already replaced. */ private _onSelfRelayClosed; private _scheduleRelayReconnect; private _attemptRelayReconnect; /** Keep the cross-PC sibling set in sync (Pi SelfRevoke onMembersChanged). */ setSiblings(siblings: SiblingInfo[]): void; /** Announce the local peer set to siblings (Pi broker peer_joined/left). */ onLocalPeersChanged(local: string[]): void; /** True when the cross-PC relay bridge is active (this node is leader). */ hasBridge(): boolean; /** The underlying SessionPeer — for consumers that need it directly (tools). */ peer(): SessionPeer; /** Fire-and-forget send. `to` may be a name, `:`, or "broadcast". */ send(to: string | string[], body: unknown, re?: string | null): Promise; /** Unicast send + await broker ACK (received/busy/denied/timeout). */ sendWithAck(to: string, body: unknown, re?: string | null, timeoutMs?: number): Promise; /** Send + await the first reply whose `re` matches the outbound id. */ request(to: string, body: unknown, timeoutMs?: number): Promise; /** Subscribe to inbound envelopes. Returns an unsubscribe fn. */ onMessage(handler: (env: Envelope) => void): () => void; /** Subscribe to post-failover reconnects. Returns an unsubscribe fn. */ onReconnect(handler: () => void): () => void; /** Assigned clean mesh name (after any #N collision suffix). */ name(): string; /** Canonical mesh address (`[:]@`) — echo, never compose. */ address(): string; /** * Rename this peer on the broker via a soft leave+rejoin (re-registers under * `newName`; the broker may append a `#N` on collision — returns the assigned * name). Keeps the process + onMessage handlers alive. Does NOT touch the * cross-PC bridge or the relay room — the caller must cycle the relay so the * App↔Pi room (keyed by `(cwd, name)`, plan/41) follows the new name. */ rename(newName: string): Promise; /** "leader" | "follower". */ currentRole(): "leader" | "follower"; /** The locally-hosted Broker when leader, else null. */ localBroker(): Broker | null; /** * Aggregated mesh roster (local UDS peers + cross-PC `:`), * excluding self. Asks the broker, which merges its remote router cache. */ listPeers(timeoutMs?: number): Promise; /** Tear down the bridge (if any) and leave the mesh. */ close(): Promise; }