feat(daemon): multi-mesh — attach to all joined meshes simultaneously
The 1.26.0 step that finally delivers ambient mode for multi-mesh users. Daemon holds Map<slug, DaemonBrokerClient>; one process, one PID per user, all your meshes online concurrently. run.ts: claudemesh daemon up with no --mesh attaches to every joined mesh from config. --mesh <slug> still scopes to one (legacy mode). The daemon_started log line reports meshes: [...] instead of mesh. drain.ts: dispatches each outbox row to the broker keyed by row.mesh (column added in 1.25.0). Legacy rows with mesh=NULL fall back to the only broker if there's exactly one, otherwise mark dead with a clear error. ipc/server.ts: - GET /v1/peers aggregates across all attached meshes; each peer record gains a mesh field. ?mesh=<slug> narrows server-side. - GET /v1/skills aggregates similarly; /v1/skills/:name walks meshes and returns first match. - POST /v1/send requires mesh field on multi-mesh daemons; auto-picks on single-mesh; returns 400 with attached list if ambiguous. - POST /v1/profile accepts optional mesh; without it, fans out to all attached meshes (consistent presence). CLI: trySendViaDaemon now forwards expectedMesh as the body's mesh field (was informational, now authoritative). claudemesh send --mesh A and --mesh B from the same shell both route to the right broker via the same daemon process. Verified: aggregated peer list across 3 attached meshes; cross-mesh sends from CLI reach status=done with correct broker_message_ids. Released as 1.26.0 on npm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,9 +36,10 @@ interface PendingRow {
|
||||
|
||||
export interface DrainOptions {
|
||||
db: SqliteDb;
|
||||
broker: DaemonBrokerClient;
|
||||
/** Stable peer-target the daemon impersonates for now. Sprint 4 routes
|
||||
* this from the per-row destination_kind/destination_ref. */
|
||||
/** v1.26.0: per-mesh broker map. Drain dispatches each row to the
|
||||
* broker keyed by its `mesh` column. Single-mesh daemons pass a
|
||||
* Map of size 1; multi-mesh daemons pass one entry per joined mesh. */
|
||||
brokers: Map<string, DaemonBrokerClient>;
|
||||
log?: (level: "info" | "warn" | "error", msg: string, meta?: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
@@ -100,6 +101,21 @@ async function drainOnce(opts: DrainOptions, log: NonNullable<DrainOptions["log"
|
||||
if (markInflight(opts.db, row.id, now) === 0) continue; // raced with another drainer
|
||||
const fpHex = bufferToHex(row.request_fingerprint);
|
||||
|
||||
// v1.26.0: pick the broker keyed by the row's mesh. Legacy rows
|
||||
// (mesh=NULL) fall back to the only broker if there's exactly one;
|
||||
// otherwise mark dead because we don't know where to send them.
|
||||
let broker: DaemonBrokerClient | undefined;
|
||||
if (row.mesh) {
|
||||
broker = opts.brokers.get(row.mesh);
|
||||
} else if (opts.brokers.size === 1) {
|
||||
broker = opts.brokers.values().next().value;
|
||||
}
|
||||
if (!broker) {
|
||||
log("warn", "drain_no_broker_for_mesh", { id: row.id, mesh: row.mesh ?? "(null)" });
|
||||
markDead(opts.db, row.id, `no_broker_for_mesh:${row.mesh ?? "null"}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sprint 4: use the row's resolved target/ciphertext if present.
|
||||
// Legacy v0.9.0 rows (NULL on these columns) fall back to the
|
||||
// broadcast smoke-test shape so existing in-flight rows still drain.
|
||||
@@ -121,7 +137,7 @@ async function drainOnce(opts: DrainOptions, log: NonNullable<DrainOptions["log"
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await opts.broker.send({
|
||||
res = await broker.send({
|
||||
targetSpec,
|
||||
priority,
|
||||
nonce,
|
||||
|
||||
Reference in New Issue
Block a user