Skip to content

Commit 3561bd7

Browse files
committed
feat: add stale agent cleanup via PID probing (coordinator)
The coordinator probes registered agent PIDs every 5 seconds using signal 0 (existence check). Dead agents are marked offline and the status is broadcast to all peers. Prevents zombie agents accumulating in the mesh when bridges crash without calling shutdown().
1 parent 8d44fc6 commit 3561bd7

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

src/core/mesh-store.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export class MeshStore implements CommsStore {
146146
{ socket: net.Socket; buffer: MessageBuffer }
147147
>();
148148
private peerInfo = new Map<string, PeerInfo>();
149+
private staleCheckTimer: ReturnType<typeof setInterval> | undefined;
149150

150151
onDelivery:
151152
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
@@ -250,6 +251,7 @@ export class MeshStore implements CommsStore {
250251

251252
this.coordinatorServer.listen(COORDINATOR_PORT, COORDINATOR_HOST, () => {
252253
this.isCoordinator = true;
254+
this.startStaleCheck();
253255
this.peerInfo.set(this.peerId, {
254256
id: this.peerId,
255257
port: this.dataPort,
@@ -880,6 +882,54 @@ export class MeshStore implements CommsStore {
880882
return events;
881883
}
882884

885+
// -----------------------------------------------------------------------
886+
// Stale agent cleanup (coordinator only)
887+
// -----------------------------------------------------------------------
888+
889+
private startStaleCheck(): void {
890+
if (this.staleCheckTimer) return;
891+
this.staleCheckTimer = setInterval(() => {
892+
void this.probeStaleAgents();
893+
}, 5000);
894+
}
895+
896+
private stopStaleCheck(): void {
897+
if (this.staleCheckTimer) {
898+
clearInterval(this.staleCheckTimer);
899+
this.staleCheckTimer = undefined;
900+
}
901+
}
902+
903+
private async probeStaleAgents(): Promise<void> {
904+
const deadIds: string[] = [];
905+
906+
for (const [id, agent] of this.agents) {
907+
if (agent.status !== "active") continue;
908+
if (!this.isProcessAlive(agent.pid)) {
909+
deadIds.push(id);
910+
}
911+
}
912+
913+
for (const id of deadIds) {
914+
const agent = this.agents.get(id);
915+
if (agent) {
916+
agent.status = "offline";
917+
this.agents.set(id, agent);
918+
}
919+
await this.broadcastPatch({ type: "agent_offline", agentId: id });
920+
}
921+
}
922+
923+
private isProcessAlive(pid: number): boolean {
924+
try {
925+
// Sending signal 0 doesn't kill the process — it just checks existence
926+
process.kill(pid, 0);
927+
return true;
928+
} catch {
929+
return false;
930+
}
931+
}
932+
883933
// -----------------------------------------------------------------------
884934
// Coordinator handover
885935
// -----------------------------------------------------------------------
@@ -908,6 +958,7 @@ export class MeshStore implements CommsStore {
908958
await writeAsync(peer.socket, encode(msg));
909959
}
910960

961+
this.stopStaleCheck();
911962
this.coordinatorServer?.close();
912963
this.coordinatorServer = undefined;
913964
this.isCoordinator = false;
@@ -934,6 +985,7 @@ export class MeshStore implements CommsStore {
934985
}
935986
this.peerConnections.clear();
936987

988+
this.stopStaleCheck();
937989
this.dataServer?.close();
938990
this.dataServer = undefined;
939991
this.coordinatorServer?.close();

0 commit comments

Comments
 (0)