fail only the disconnected sidecar's in-flight requests

unregisterSidecar rejected every entry in the pending map, not just the ones
belonging to the sidecar that went away. Restarting any single sidecar failed
in-flight work on every other: `pm2 restart officer-music` could kill a running
agent turn with `Sidecar "music" disconnected` — a message pointing at a process
that had nothing to do with it. Pending entries now carry their owning sidecar
id and the rejection loop skips the rest.

Also deletes src/servers/api/anthropic-proxy.ts. It had no importers; PM2's
officer-anthropic-proxy runs src/servers/sidecar/claude/index.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 01:33:29 +00:00
co-authored by Claude Opus 5
parent 69961a52cd
commit 4eeaa3c93d
3 changed files with 118 additions and 96 deletions
+9 -4
View File
@@ -23,6 +23,8 @@ type RegisteredSidecar = {
};
type PendingRequest = {
/** Which sidecar this request was sent to, so a disconnect only fails that sidecar's own work. */
sidecarId: string;
resolve: (value: any) => void;
reject: (error: Error) => void;
timer: Timer;
@@ -73,8 +75,12 @@ export function unregisterSidecar(id: string): void {
sidecars.delete(id);
console.log(`[registry] unregistered sidecar "${sc.name}" (id=${id})`);
// Reject all pending requests for this sidecar
// Reject the pending requests belonging to THIS sidecar, and only those. Until the pending entries
// carried a sidecarId this loop rejected the whole map, so restarting any one sidecar failed in-flight
// work on every other — `pm2 restart officer-music` could kill a running agent turn with the message
// `Sidecar "music" disconnected`, which is the sort of failure nobody traces back to its cause.
for (const [reqId, req] of pending) {
if (req.sidecarId !== id) continue;
clearTimeout(req.timer);
req.reject(new Error(`Sidecar "${sc.name}" disconnected`));
pending.delete(reqId);
@@ -152,7 +158,7 @@ function sendCommand(cap: string, cmd: SidecarCommand, timeoutMs = DEFAULT_TIMEO
reject(new Error(`Sidecar command ${cmd.type} timed out`));
}, timeoutMs);
pending.set((cmd as any).id, { resolve, reject, timer });
pending.set((cmd as any).id, { sidecarId: sc.id, resolve, reject, timer });
sc.ws.send(JSON.stringify(cmd));
});
}
@@ -175,7 +181,7 @@ function sendCommandToSidecar(
reject(new Error(`Sidecar command ${cmd.type} timed out`));
}, timeoutMs);
pending.set((cmd as any).id, { resolve, reject, timer });
pending.set((cmd as any).id, { sidecarId: sc.id, resolve, reject, timer });
sc.ws.send(JSON.stringify(cmd));
});
}
@@ -320,7 +326,6 @@ export function onOpenCodeSession(handler: (sessionKey: string, sessionId: strin
// Nothing here any more. The pty sidecar serves its own listener; `/api/terminal/*` is a proxy and
// `/api/terminal/ws` a byte relay, both keyed off the `pty:server` port like every other HTTP sidecar.
// ── VNC ──
export async function startVnc(params: VncStartParams): Promise<{ port: number; display: number }> {