feat(plugin): SessionStart hook discovers and announces active peers

session-announce now calls /v1/list with the session's X-Mailbox header,
which both registers the session with the daemon and returns all known
mailboxes in one round-trip. The output appends an "Active peers" block
listing mailboxes seen within the last hour (configurable via
--peer-window-minutes), capped at 10 entries by default. Self is
filtered out; the list is sorted most-recent-first.

So when the user says "I started a second session, coordinate with it",
Claude already has the peer's mailbox name in context — no manual
list_mailboxes call needed.

The peer-formatting logic is extracted into formatActivePeerList for
unit testing; CLI tests now pin --url to an unreachable port to keep
assertions stable on machines that have a real daemon running.
This commit is contained in:
Mika Kuns
2026-05-19 11:59:31 +02:00
parent 9fd321043f
commit 48b6ba6452
5 changed files with 166 additions and 10 deletions

View File

@@ -41,6 +41,41 @@ export function deriveSessionName(sessionId: string, base?: string | null): stri
return `claude-${short}`;
}
export interface PeerEntry {
name: string;
lastSeenAt: string;
}
export function formatActivePeerList(
peers: PeerEntry[],
selfName: string,
options: { windowMinutes: number; maxPeers: number; now?: number },
): string[] {
const others = peers.filter((p) => p.name !== selfName);
const cutoff = (options.now ?? Date.now()) - options.windowMinutes * 60_000;
const active = others
.filter((p) => {
const t = new Date(p.lastSeenAt).getTime();
return Number.isFinite(t) && t >= cutoff;
})
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
.slice(0, options.maxPeers);
if (active.length === 0) {
return [
`No other mailboxes seen within the last ${options.windowMinutes} minutes (${others.length} total registered).`,
];
}
const lines = [
`Active peers (seen within last ${options.windowMinutes} min, ${active.length} of ${others.length} total):`,
];
for (const p of active) {
lines.push(` - ${p.name} (last seen ${p.lastSeenAt})`);
}
return lines;
}
export interface HookMessage {
id: number;
from: string;