story-kit: merge 138_bug_no_heartbeat_to_detect_stale_websocket_connections

This commit is contained in:
Dave
2026-02-24 13:05:30 +00:00
parent 71e07041cf
commit 5226438b16
3 changed files with 163 additions and 4 deletions

View File

@@ -11,7 +11,8 @@ export type WsRequest =
type: "permission_response";
request_id: string;
approved: boolean;
};
}
| { type: "ping" };
export interface AgentAssignment {
agent_name: string;
@@ -60,7 +61,9 @@ export type WsResponse =
}
/** `.story_kit/project.toml` was modified; re-fetch the agent roster. */
| { type: "agent_config_changed" }
| { type: "tool_activity"; tool_name: string };
| { type: "tool_activity"; tool_name: string }
/** Heartbeat response confirming the connection is alive. */
| { type: "pong" };
export interface ProviderConfig {
provider: string;
@@ -283,6 +286,30 @@ export class ChatWebSocket {
private reconnectTimer?: number;
private reconnectDelay = 1000;
private shouldReconnect = false;
private heartbeatInterval?: number;
private heartbeatTimeout?: number;
private static readonly HEARTBEAT_INTERVAL = 30_000;
private static readonly HEARTBEAT_TIMEOUT = 5_000;
private _startHeartbeat(): void {
this._stopHeartbeat();
this.heartbeatInterval = window.setInterval(() => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
const ping: WsRequest = { type: "ping" };
this.socket.send(JSON.stringify(ping));
this.heartbeatTimeout = window.setTimeout(() => {
// No pong received within timeout; close socket to trigger reconnect.
this.socket?.close();
}, ChatWebSocket.HEARTBEAT_TIMEOUT);
}, ChatWebSocket.HEARTBEAT_INTERVAL);
}
private _stopHeartbeat(): void {
window.clearInterval(this.heartbeatInterval);
window.clearTimeout(this.heartbeatTimeout);
this.heartbeatInterval = undefined;
this.heartbeatTimeout = undefined;
}
private _buildWsUrl(): string {
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
@@ -298,6 +325,7 @@ export class ChatWebSocket {
if (!this.socket) return;
this.socket.onopen = () => {
this.reconnectDelay = 1000;
this._startHeartbeat();
};
this.socket.onmessage = (event) => {
try {
@@ -327,6 +355,10 @@ export class ChatWebSocket {
data.message,
);
if (data.type === "agent_config_changed") this.onAgentConfigChanged?.();
if (data.type === "pong") {
window.clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = undefined;
}
} catch (err) {
this.onError?.(String(err));
}
@@ -420,6 +452,7 @@ export class ChatWebSocket {
close() {
this.shouldReconnect = false;
this._stopHeartbeat();
window.clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;