refactor(cli): m1 lifecycle + role-aware peer list
Foundational cleanups before agentic-comms architecture work (.artifacts/specs/2026-05-04-agentic-comms-architecture-v2.md). All behavior-preserving. 1. Extract `connectWsWithBackoff` into apps/cli/src/daemon/ws-lifecycle.ts. Both DaemonBrokerClient and SessionBrokerClient now share one lifecycle implementation (connect, hello-handshake, ack-timeout, close + backoff reconnect). Each client provides its own buildHello / isHelloAck / onMessage hooks and keeps its own RPC bookkeeping (pendingAcks, peerListResolvers, onPush). Composition over inheritance per Codex's review; no protocol shape changes. 2. Drop daemon-WS ephemeral session pubkey. DaemonBrokerClient no longer mints + sends a per-reconnect ephemeral keypair in its hello. Session-targeted DMs land on SessionBrokerClient since 1.32.1, not the member-keyed daemon-WS, so the field was vestigial. Send-encrypt path now signs DMs with the stable mesh member secret. handleBrokerPush invocations from daemon-WS only pass the member secret — session decryption is the session-WS's job. 3. Role-aware peer list. `peer list` now hides peers whose broker-emitted `role` is `'control-plane'`. `--all` opts back in. JSON output emits `role` at top level. Older brokers that don't emit role yet default to 'session', so legacy peer rows stay visible without the broker-side change shipped first. Replaces the prior `peerType === 'claudemesh-daemon'` channel-name hack. Typecheck + tests + build all green.
This commit is contained in:
@@ -21,14 +21,28 @@ export interface PeersFlags {
|
||||
mesh?: string;
|
||||
/** `true`/`undefined` = full record; comma-separated string = field projection. */
|
||||
json?: boolean | string;
|
||||
/** When false (default), hide claudemesh-daemon presence rows from the
|
||||
* human renderer — they're infrastructure, not interactive peers, and
|
||||
* confused users into thinking the daemon counted as a "peer". The
|
||||
* JSON output still includes them so scripts that need a full inventory
|
||||
* can opt in via --all (or just consume JSON). */
|
||||
/** When false (default), hide control-plane presence rows from the
|
||||
* human renderer — they're infrastructure (daemon-WS member-keyed
|
||||
* presence), not interactive peers, and confused users into thinking
|
||||
* the daemon counted as a "peer". The JSON output still includes them
|
||||
* so scripts that need a full inventory can opt in via --all (or
|
||||
* just consume JSON).
|
||||
*
|
||||
* Source of truth is the broker-side `role` field
|
||||
* (`'control-plane' | 'session' | 'service'`). Older brokers don't
|
||||
* emit `role` yet — this code falls back to treating missing role as
|
||||
* `'session'` so legacy peer rows stay visible. */
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broker-emitted peer classification, added 2026-05-04. Older brokers
|
||||
* may omit it — treat missing as 'session' so legacy meshes still
|
||||
* render their peers (and don't accidentally hide them all). The CLI
|
||||
* never emits 'control-plane' on its own; that comes from the broker.
|
||||
*/
|
||||
export type PeerRole = "control-plane" | "session" | "service";
|
||||
|
||||
interface PeerRecord {
|
||||
pubkey: string;
|
||||
/** Stable member pubkey (independent of session). When sender shares
|
||||
@@ -43,10 +57,17 @@ interface PeerRecord {
|
||||
status?: string;
|
||||
summary?: string;
|
||||
groups: Array<{ name: string; role?: string }>;
|
||||
/** Top-level convenience alias for `profile.role`. Lifted by the
|
||||
* CLI so JSON consumers see role at the shape's top level instead
|
||||
* of nested under profile. Same value either way. */
|
||||
role?: string;
|
||||
/** Broker-emitted classification: 'control-plane' | 'session' |
|
||||
* 'service'. Source of truth for the --all visibility filter and the
|
||||
* default-hide rule. Older brokers omit this; the CLI fills missing
|
||||
* values with 'session' so legacy peer rows stay visible.
|
||||
*
|
||||
* Note: this replaces the prior CLI-side lift of `profile.role` to
|
||||
* the top-level `role` field — `profile.role` is user-supplied
|
||||
* metadata (e.g. "lead", "reviewer"), distinct from the broker's
|
||||
* presence-class taxonomy. The user-facing string still lives at
|
||||
* `profile.role` and is rendered inline as `role:<value>`. */
|
||||
role?: PeerRole;
|
||||
peerType?: string;
|
||||
channel?: string;
|
||||
model?: string;
|
||||
@@ -139,12 +160,9 @@ async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
|
||||
* surfaced a sender's siblings as separate rows because they're separate
|
||||
* presence rows; the cli just hadn't been making that visible.
|
||||
*
|
||||
* Also lifts `profile.role` to a top-level `role` field. The broker has
|
||||
* always returned role nested under `profile.role`, but downstream JSON
|
||||
* consumers (LLMs in launched sessions, jq pipelines, dashboards) kept
|
||||
* missing it because nothing pointed at the nesting. A dedicated
|
||||
* top-level alias makes the intent unmissable without breaking the
|
||||
* `profile` object's shape for callers that already drill into it.
|
||||
* Also normalizes the broker's `role` classification: missing values
|
||||
* (older brokers) default to 'session' so legacy peer rows stay
|
||||
* visible under the default `--all=false` filter.
|
||||
*/
|
||||
function annotateSelf(
|
||||
peer: PeerRecord,
|
||||
@@ -161,8 +179,8 @@ function annotateSelf(
|
||||
selfSessionPubkey &&
|
||||
peer.pubkey === selfSessionPubkey
|
||||
);
|
||||
const role = peer.profile?.role?.trim() || undefined;
|
||||
return { ...peer, ...(role ? { role } : {}), isSelf, isThisSession };
|
||||
const role: PeerRole = peer.role ?? "session";
|
||||
return { ...peer, role, isSelf, isThisSession };
|
||||
}
|
||||
|
||||
export async function runPeers(flags: PeersFlags): Promise<void> {
|
||||
@@ -208,13 +226,18 @@ export async function runPeers(flags: PeersFlags): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hide claudemesh-daemon rows by default — they're infrastructure
|
||||
// (the daemon's own member-keyed presence), not interactive peers,
|
||||
// and they confused users into thinking the daemon counted as a
|
||||
// Hide control-plane rows by default — they're infrastructure
|
||||
// (daemon-WS member-keyed presence), not interactive peers, and
|
||||
// they confused users into thinking the daemon counted as a
|
||||
// separate peer. --all opts back in for debugging.
|
||||
//
|
||||
// Source of truth: broker-emitted `role` field (added 2026-05-04).
|
||||
// annotateSelf() already filled in 'session' for older brokers
|
||||
// that don't emit role yet, so this filter is backwards-compatible
|
||||
// by construction — legacy rows show up.
|
||||
const visible = flags.all
|
||||
? peers
|
||||
: peers.filter((p) => p.channel !== "claudemesh-daemon");
|
||||
: peers.filter((p) => p.role !== "control-plane");
|
||||
|
||||
// Sort: this-session first, then your-other-sessions, then real
|
||||
// peers. Within each group, idle/working ahead of dnd. Inside the
|
||||
@@ -226,9 +249,9 @@ export async function runPeers(flags: PeersFlags): Promise<void> {
|
||||
return score(a) - score(b);
|
||||
});
|
||||
|
||||
const hiddenDaemons = peers.length - visible.length;
|
||||
const header = hiddenDaemons > 0
|
||||
? `peers on ${slug} (${sorted.length}, ${hiddenDaemons} daemon hidden — use --all)`
|
||||
const hiddenControlPlane = peers.length - visible.length;
|
||||
const header = hiddenControlPlane > 0
|
||||
? `peers on ${slug} (${sorted.length}, ${hiddenControlPlane} control-plane hidden — use --all)`
|
||||
: `peers on ${slug} (${sorted.length})`;
|
||||
render.section(header);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user