@mclawnet/swarm
v0.1.18
Published
Swarm coordination, message routing, and role management. Phase 2 adds an **Inbox layer** (`InboxStore` + `InboxRelay`) on top of the existing `SwarmCoordinator`, giving the host a push-based path for delivering messages to agent sessions.
Readme
@mclawnet/swarm
Swarm coordination, message routing, and role management. Phase 2 adds an
Inbox layer (InboxStore + InboxRelay) on top of the existing
SwarmCoordinator, giving the host a push-based path for delivering
messages to agent sessions.
BREAKING CHANGE — persistence path migrated
Swarm state has moved under the per-project directory shared with
@mclawnet/task:
OLD: ~/.clawnet/swarms/<swarmId>/...
NEW: ~/.clawnet/projects/<encoded-cwd>/swarms/<swarmId>/...There is no backward-compatibility shim. Snapshots, message logs, and inbox files written by 0.1.4 and earlier will not be picked up by 0.1.5+. Any in-flight swarms should be drained before upgrading.
<encoded-cwd> is computed by encodeCwd() from @mclawnet/task (absolute
path with / replaced by -).
Modules
Coordinator (existing)
SwarmCoordinator, action parsing, role/template
loading, persistence, and recovery — see exports in src/index.ts.
Inter-role messaging is delivered via InboxStore + InboxRelay (see below).
Inbox (new)
interface InboxMessage {
id: string;
from: string;
type: string;
data: string;
taskId?: string;
timestamp: number;
delivered: boolean;
deliveredAt?: number;
}
class InboxStore {
constructor(workDir: string, swarmId: string);
append(instanceId: string, msg: InboxMessage): Promise<void>;
readAll(instanceId: string): Promise<InboxMessage[]>;
readUndelivered(instanceId: string): Promise<InboxMessage[]>;
markDelivered(instanceId: string, ids: string[]): Promise<void>;
}
class InboxRelay {
constructor(
sessionAdapter: SessionAdapter,
getSwarm: (swarmId: string) => SwarmInstance | undefined,
);
deliver(swarmId: string, instanceId: string): Promise<void>;
onAgentTurnSettled(swarmId: string, instanceId: string): Promise<void>;
}InboxStore writes one JSON file per recipient under
~/.clawnet/projects/<encoded-cwd>/swarms/<swarmId>/inboxes/<instanceId>.json,
guarded by proper-lockfile for concurrent appenders.
InboxRelay is the push side. It wraps undelivered messages in a single
<info_for_agent> envelope and feeds them to the session through
SessionAdapter.sendInput. Four gates guard the push:
- single-flight — at most one in-flight
deliver()per(swarmId, instanceId). - liveness — swarm and role must exist with a
workDir. - lifecycle — agents in
spawningorstoppedare skipped. - pending-echo — messages already pushed but not yet ack'd by a turn completion are not re-pushed.
onAgentTurnSettled() is called when the host sees a result event for a
turn; it marks pending messages delivered=true and triggers another
deliver pass to flush messages that arrived mid-turn.
Wiring points
The host connects the relay to the coordinator at three places:
- spawn — after
SwarmCoordinatorcreates a role and the session is ready, callrelay.deliver(swarmId, instanceId)to flush any inbox messages buffered before the agent existed. - resume — on snapshot recovery, call
deliver()for each restored role so agents receive what queued up while offline. - turn-complete — on the session adapter's turn-settled signal, call
relay.onAgentTurnSettled(swarmId, instanceId).
Producers append to the inbox via InboxStore.append() (e.g. router
fan-out, task notifications); the relay handles delivery semantics.
Example
import { InboxStore, InboxRelay } from "@mclawnet/swarm";
const store = new InboxStore(process.cwd(), swarmId);
await store.append("worker-1", {
id: crypto.randomUUID(),
from: "host",
type: "task-assigned",
data: "please pick up TASK-42",
taskId: "TASK-42",
timestamp: Date.now(),
delivered: false,
});
const relay = new InboxRelay(sessionAdapter, (id) => coordinator.get(id));
await relay.deliver(swarmId, "worker-1");
// later, when the agent's turn completes:
await relay.onAgentTurnSettled(swarmId, "worker-1");