diff --git a/src/daemon/run-manager.ts b/src/daemon/run-manager.ts index b67d4f1..5ed87e5 100644 --- a/src/daemon/run-manager.ts +++ b/src/daemon/run-manager.ts @@ -150,6 +150,7 @@ export const cancelRun = (runId: string): boolean => { }; export const sendInput = (runId: string, text: string): boolean => { + if (!text || typeof text !== 'string') return false; const tracked = runs.get(runId); if (!tracked) return false; if (tracked.run.state !== 'input_required') return false; diff --git a/src/daemon/websocket.ts b/src/daemon/websocket.ts index cd71381..f54b3bd 100644 --- a/src/daemon/websocket.ts +++ b/src/daemon/websocket.ts @@ -52,6 +52,19 @@ const bufferMessage = (runId: string, msg: BufferedMessage): void => { const replayBuffer = (ws: WebSocket, runId: string): void => { const buf = runBuffers.get(runId); if (!buf) return; + + // B3: Extend TTL when a client subscribes to prevent race between + // buffer expiry and replay delivery + const existingTimer = bufferTimers.get(runId); + if (existingTimer) { + clearTimeout(existingTimer); + const timer = setTimeout(() => { + runBuffers.delete(runId); + bufferTimers.delete(runId); + }, BUFFER_TTL_MS); + bufferTimers.set(runId, timer); + } + logDebug(`Replaying ${buf.length} buffered messages for run ${runId}`); for (const msg of buf) { sendToClient(ws, msg); diff --git a/src/hub/reconnection.ts b/src/hub/reconnection.ts index 2521837..a1bd7dd 100644 --- a/src/hub/reconnection.ts +++ b/src/hub/reconnection.ts @@ -18,6 +18,7 @@ export const createReconnector = (opts: ReconnectorOptions): Reconnector => { let currentDelay = initialDelay; let timer: ReturnType | null = null; let stopped = false; + let running = false; const attempt = async (): Promise => { if (stopped) return; @@ -25,6 +26,7 @@ export const createReconnector = (opts: ReconnectorOptions): Reconnector => { try { await opts.onReconnect(); currentDelay = initialDelay; + running = false; } catch (err) { opts.onError?.(err); timer = setTimeout(() => { @@ -36,12 +38,15 @@ export const createReconnector = (opts: ReconnectorOptions): Reconnector => { return { start: () => { + if (running) return; stopped = false; + running = true; attempt(); }, stop: () => { stopped = true; + running = false; if (timer) { clearTimeout(timer); timer = null;