@wrongstack/desktop 0.305.0 → 0.305.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/agent-bridge.js +23 -5
- package/dist/main/agent-bridge.js.map +2 -2
- package/dist/main/main.js +23 -5
- package/dist/main/main.js.map +2 -2
- package/package.json +4 -4
|
@@ -90,6 +90,13 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
90
90
|
return new Promise((resolve, reject) => {
|
|
91
91
|
const ws = new WebSocket(wsUrl);
|
|
92
92
|
conversation.ws = ws;
|
|
93
|
+
const myGeneration = ++conversation.socketGeneration;
|
|
94
|
+
const onMessage = (data) => {
|
|
95
|
+
if (conversation.socketGeneration !== myGeneration) return;
|
|
96
|
+
this.handleServerMessage(conversation, String(data));
|
|
97
|
+
};
|
|
98
|
+
conversation.onMessage = onMessage;
|
|
99
|
+
ws.on("message", onMessage);
|
|
93
100
|
const timeout = setTimeout(() => {
|
|
94
101
|
if (conversation.ws === ws) {
|
|
95
102
|
ws.close();
|
|
@@ -107,9 +114,6 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
107
114
|
this.emitReconnectEvent(conversation, "connected");
|
|
108
115
|
resolve();
|
|
109
116
|
});
|
|
110
|
-
ws.on("message", (data) => {
|
|
111
|
-
this.handleServerMessage(conversation, data.toString());
|
|
112
|
-
});
|
|
113
117
|
ws.once("error", (err) => {
|
|
114
118
|
clearTimeout(timeout);
|
|
115
119
|
conversation.status = "error";
|
|
@@ -125,7 +129,14 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
125
129
|
});
|
|
126
130
|
ws.once("close", () => {
|
|
127
131
|
clearTimeout(timeout);
|
|
128
|
-
if (conversation.ws === ws)
|
|
132
|
+
if (conversation.ws === ws) {
|
|
133
|
+
conversation.socketGeneration++;
|
|
134
|
+
conversation.ws = null;
|
|
135
|
+
}
|
|
136
|
+
ws.off("message", onMessage);
|
|
137
|
+
if (conversation.onMessage === onMessage) {
|
|
138
|
+
conversation.onMessage = null;
|
|
139
|
+
}
|
|
129
140
|
conversation.connectPromise = null;
|
|
130
141
|
if (conversation.status !== "error") {
|
|
131
142
|
conversation.status = "disconnected";
|
|
@@ -233,6 +244,11 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
233
244
|
conversation.reconnectAttempt = 0;
|
|
234
245
|
conversation.reconnectUrl = null;
|
|
235
246
|
conversation.connectionState = stopConnection(conversation.connectionState);
|
|
247
|
+
if (conversation.onMessage) {
|
|
248
|
+
conversation.ws?.off("message", conversation.onMessage);
|
|
249
|
+
conversation.onMessage = null;
|
|
250
|
+
}
|
|
251
|
+
conversation.socketGeneration++;
|
|
236
252
|
conversation.ws?.close();
|
|
237
253
|
conversation.ws = null;
|
|
238
254
|
conversation.connectPromise = null;
|
|
@@ -355,7 +371,9 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
355
371
|
reconnectAttempt: 0,
|
|
356
372
|
reconnectTimer: null,
|
|
357
373
|
reconnectUrl: null,
|
|
358
|
-
connectionState: createSurfaceConnectionState()
|
|
374
|
+
connectionState: createSurfaceConnectionState(),
|
|
375
|
+
onMessage: null,
|
|
376
|
+
socketGeneration: 0
|
|
359
377
|
};
|
|
360
378
|
this.conversations.set(runtimeId, conversation);
|
|
361
379
|
return conversation;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/main/agent-bridge.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Desktop Agent Bridge - WebSocket communication with runtimes.\n * Handles conversation management and WebSocket connections with automatic reconnection.\n */\nimport { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport {\n createSurfaceConnectionState,\n DEFAULT_SURFACE_CONNECTION_CONFIG,\n decodeProtocolFrame,\n markConnectionActivity,\n markConnectionConnecting,\n markConnectionOpen,\n planConnectionReconnect,\n resetConnection,\n type SurfaceConnectionState,\n stopConnection,\n} from '@wrongstack/webui-server/protocol';\nimport WebSocket from 'ws';\nimport type {\n DesktopConversationMessage,\n DesktopConversationSnapshot,\n DesktopConversationStatus,\n} from '../shared/types.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface ConversationInternal {\n runtimeId: string;\n status: DesktopConversationStatus;\n sessionId?: string | undefined;\n error?: string | undefined;\n messages: DesktopConversationMessage[];\n ws: WebSocket | null;\n connectPromise: Promise<void> | null;\n activeAssistantMessageId: string | null;\n /** Reconnection state */\n reconnectAttempt: number;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectUrl: string | null;\n connectionState: SurfaceConnectionState;\n}\n\ninterface ServerMessage {\n type: string;\n payload?: Record<string, unknown> | undefined;\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst MAX_MESSAGES = 300;\n\n/** Reconnection configuration */\nconst RECONNECT_CONFIG = {\n /** Maximum number of reconnection attempts (0 = disabled) */\n maxAttempts: 5,\n /** Initial delay in ms before first reconnection */\n initialDelayMs: 1000,\n /** Maximum delay in ms between reconnection attempts */\n maxDelayMs: 30000,\n /** Multiplier for exponential backoff */\n backoffMultiplier: 2,\n /** Jitter factor (0-1) to add randomness to delays */\n jitterFactor: 0.1,\n};\n\nconst DESKTOP_CONNECTION_CONFIG = {\n ...DEFAULT_SURFACE_CONNECTION_CONFIG,\n maxReconnectAttempts: RECONNECT_CONFIG.maxAttempts,\n initialBackoffMs: RECONNECT_CONFIG.initialDelayMs,\n maxBackoffMs: RECONNECT_CONFIG.maxDelayMs,\n backoffMultiplier: RECONNECT_CONFIG.backoffMultiplier,\n jitterRatio: RECONNECT_CONFIG.jitterFactor,\n};\n\n// ============================================================================\n// Bridge Implementation\n// ============================================================================\n\nexport class DesktopAgentBridge extends EventEmitter {\n private readonly conversations = new Map<string, ConversationInternal>();\n\n snapshot(runtimeId: string): DesktopConversationSnapshot {\n return publicConversation(this.getOrCreate(runtimeId));\n }\n\n /**\n * Get reconnection status for a runtime.\n */\n getReconnectStatus(runtimeId: string): { attempt: number; maxAttempts: number } | null {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return null;\n return {\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n };\n }\n\n /**\n * Force reconnection for a runtime (resets reconnection state).\n */\n forceReconnect(runtimeId: string, wsUrl: string): void {\n const conversation = this.getOrCreate(runtimeId);\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n void this.ensureConnected(runtimeId, wsUrl);\n }\n\n async ensureConnected(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n const conversation = this.getOrCreate(runtimeId);\n\n // If already connected, return immediately\n if (conversation.ws?.readyState === WebSocket.OPEN) return publicConversation(conversation);\n\n // If currently connecting, wait for it\n if (conversation.connectPromise) {\n await conversation.connectPromise;\n return publicConversation(conversation);\n }\n\n // If reconnecting in background, return current state\n if (conversation.reconnectTimer) {\n return publicConversation(conversation);\n }\n\n // Start fresh connection\n conversation.reconnectUrl = wsUrl;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n await this.connect(runtimeId, wsUrl);\n return publicConversation(conversation);\n }\n\n /**\n * Internal connect method that actually establishes the WebSocket.\n */\n private async connect(runtimeId: string, wsUrl: string): Promise<void> {\n const conversation = this.getOrCreate(runtimeId);\n\n // Cancel any pending reconnect timer\n this.cancelReconnect(conversation);\n\n conversation.status = 'connecting';\n conversation.connectionState = markConnectionConnecting(conversation.connectionState);\n conversation.error = undefined;\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connecting');\n\n return new Promise((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n conversation.ws = ws;\n\n // Set connection timeout\n const timeout = setTimeout(() => {\n if (conversation.ws === ws) {\n ws.close();\n reject(new Error('Connection timeout'));\n }\n }, 10000);\n\n ws.once('open', () => {\n clearTimeout(timeout);\n conversation.status = 'connected';\n conversation.error = undefined;\n conversation.connectPromise = null;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = markConnectionOpen(conversation.connectionState);\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connected');\n resolve();\n });\n\n ws.on('message', (data) => {\n this.handleServerMessage(conversation, data.toString());\n });\n\n ws.once('error', (err) => {\n clearTimeout(timeout);\n conversation.status = 'error';\n conversation.error = err instanceof Error ? err.message : String(err);\n conversation.connectPromise = null;\n this.appendMessage(conversation, {\n role: 'system',\n text: `Connection error: ${conversation.error}`,\n });\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'error');\n reject(err);\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n if (conversation.ws === ws) conversation.ws = null;\n conversation.connectPromise = null;\n\n if (conversation.status !== 'error') {\n conversation.status = 'disconnected';\n }\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Schedule reconnection if applicable\n if (conversation.reconnectUrl && conversation.status === 'disconnected') {\n this.scheduleReconnect(conversation);\n }\n });\n });\n }\n\n /**\n * Schedule a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(conversation: ConversationInternal): void {\n const reconnect = planConnectionReconnect(\n conversation.connectionState,\n DESKTOP_CONNECTION_CONFIG,\n );\n conversation.connectionState = reconnect.state;\n conversation.reconnectAttempt = reconnect.state.reconnectAttempt;\n if (!reconnect.plan) {\n this.emitReconnectEvent(conversation, 'exhausted');\n return;\n }\n this.emitReconnectEvent(conversation, 'scheduled', {\n delay: reconnect.plan.delayMs,\n attempt: reconnect.plan.attempt,\n });\n\n conversation.reconnectTimer = setTimeout(() => {\n conversation.reconnectTimer = null;\n if (!conversation.reconnectUrl) return;\n\n // Check if still disconnected\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n void this.connect(conversation.runtimeId, conversation.reconnectUrl);\n }\n }, reconnect.plan.delayMs);\n }\n\n /**\n * Cancel pending reconnection.\n */\n private cancelReconnect(conversation: ConversationInternal): void {\n if (conversation.reconnectTimer) {\n clearTimeout(conversation.reconnectTimer);\n conversation.reconnectTimer = null;\n }\n }\n\n /**\n * Emit reconnection event for UI feedback.\n */\n private emitReconnectEvent(\n conversation: ConversationInternal,\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted',\n data?: { delay?: number; attempt?: number },\n ): void {\n this.emit('reconnect', {\n runtimeId: conversation.runtimeId,\n status,\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n ...data,\n });\n }\n\n async sendMessage(\n runtimeId: string,\n wsUrl: string,\n content: string,\n ): Promise<DesktopConversationSnapshot> {\n const trimmed = content.trim();\n if (!trimmed) return this.snapshot(runtimeId);\n\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n\n // Refresh conversation after connect\n const conv = this.getOrCreate(runtimeId);\n this.appendMessage(conv, {\n id: `user_${randomUUID()}`,\n role: 'user',\n text: trimmed,\n });\n conv.status = 'running';\n conv.activeAssistantMessageId = null;\n this.emitChanged(conv);\n this.send(conv, {\n type: 'user_message',\n payload: {\n id: `msg_${Date.now()}_${randomUUID().slice(0, 8)}`,\n content: trimmed,\n timestamp: Date.now(),\n ...(conv.sessionId ? { sessionId: conv.sessionId } : {}),\n },\n });\n return publicConversation(conv);\n }\n\n async abort(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n const conv = this.getOrCreate(runtimeId);\n this.send(conv, {\n type: 'abort',\n payload: conv.sessionId ? { sessionId: conv.sessionId } : {},\n });\n conv.status = 'connected';\n this.appendMessage(conv, { role: 'system', text: 'Abort requested.' });\n return publicConversation(conv);\n }\n\n close(runtimeId: string): void {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return;\n\n // Cancel reconnection and clear state\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.reconnectUrl = null;\n conversation.connectionState = stopConnection(conversation.connectionState);\n\n conversation.ws?.close();\n conversation.ws = null;\n conversation.connectPromise = null;\n conversation.status = 'disconnected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Remove from the map so closed conversations don't accumulate in memory.\n this.conversations.delete(runtimeId);\n }\n\n closeAll(): void {\n // Snapshot keys first \u2014 close() deletes from the map during iteration.\n for (const runtimeId of [...this.conversations.keys()]) {\n this.close(runtimeId);\n }\n }\n\n private handleServerMessage(conversation: ConversationInternal, raw: string): void {\n const decoded = decodeProtocolFrame(raw, 'server');\n if (!decoded.ok) return;\n conversation.connectionState = markConnectionActivity(conversation.connectionState);\n const message = decoded.message as ServerMessage;\n const payload = message.payload ?? {};\n switch (message.type) {\n case 'session.start': {\n const sessionId = stringValue(payload['sessionId']);\n if (sessionId) conversation.sessionId = sessionId;\n conversation.status = conversation.status === 'running' ? 'running' : 'connected';\n this.emitChanged(conversation);\n break;\n }\n case 'provider.text_delta': {\n conversation.status = 'running';\n this.appendAssistantDelta(conversation, stringValue(payload['text']) ?? '');\n break;\n }\n case 'tool.started': {\n conversation.status = 'running';\n const name = stringValue(payload['name']) ?? 'tool';\n this.appendMessage(conversation, { role: 'tool', text: `Started ${name}` });\n break;\n }\n case 'tool.executed': {\n const name = stringValue(payload['name']) ?? 'tool';\n const ok = payload['ok'] === true;\n this.appendMessage(conversation, {\n role: 'tool',\n text: `${name} ${ok ? 'completed' : 'failed'}`,\n });\n break;\n }\n case 'provider.error':\n case 'provider.stream_error':\n case 'error': {\n const text =\n stringValue(payload['message']) ??\n stringValue(payload['description']) ??\n `${message.type} received`;\n conversation.status = 'error';\n conversation.error = text;\n this.appendMessage(conversation, { role: 'system', text });\n break;\n }\n case 'run.result': {\n const finalText = stringValue(payload['finalText']);\n if (finalText && !this.lastAssistantHasText(conversation)) {\n this.appendMessage(conversation, { role: 'assistant', text: finalText });\n }\n conversation.status = payload['status'] === 'failed' ? 'error' : 'connected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n break;\n }\n }\n }\n\n private send(conversation: ConversationInternal, message: Record<string, unknown>): void {\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n throw new Error('Runtime socket is not connected');\n }\n conversation.ws.send(JSON.stringify(message));\n }\n\n private appendAssistantDelta(conversation: ConversationInternal, text: string): void {\n if (!text) return;\n let message = conversation.messages.find((m) => m.id === conversation.activeAssistantMessageId);\n if (!message) {\n message = {\n id: `assistant_${randomUUID()}`,\n role: 'assistant',\n text: '',\n timestamp: Date.now(),\n };\n conversation.activeAssistantMessageId = message.id;\n conversation.messages.push(message);\n }\n message.text += text;\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private appendMessage(\n conversation: ConversationInternal,\n input: Partial<DesktopConversationMessage> & Pick<DesktopConversationMessage, 'role' | 'text'>,\n ): void {\n conversation.messages.push({\n id: input.id ?? `${input.role}_${randomUUID()}`,\n role: input.role,\n text: input.text,\n timestamp: input.timestamp ?? Date.now(),\n });\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private lastAssistantHasText(conversation: ConversationInternal): boolean {\n const lastAssistant = [...conversation.messages].reverse().find((m) => m.role === 'assistant');\n return Boolean(lastAssistant?.text.trim());\n }\n\n private trimMessages(conversation: ConversationInternal): void {\n if (conversation.messages.length <= MAX_MESSAGES) return;\n conversation.messages.splice(0, conversation.messages.length - MAX_MESSAGES);\n }\n\n private getOrCreate(runtimeId: string): ConversationInternal {\n let conversation = this.conversations.get(runtimeId);\n if (conversation) return conversation;\n conversation = {\n runtimeId,\n status: 'disconnected',\n messages: [],\n ws: null,\n connectPromise: null,\n activeAssistantMessageId: null,\n reconnectAttempt: 0,\n reconnectTimer: null,\n reconnectUrl: null,\n connectionState: createSurfaceConnectionState(),\n };\n this.conversations.set(runtimeId, conversation);\n return conversation;\n }\n\n private emitChanged(conversation: ConversationInternal): void {\n this.emit('changed', publicConversation(conversation));\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction publicConversation(conversation: ConversationInternal): DesktopConversationSnapshot {\n return {\n runtimeId: conversation.runtimeId,\n status: conversation.status,\n sessionId: conversation.sessionId,\n error: conversation.error,\n messages: conversation.messages.map((message) => ({ ...message })),\n };\n}\n\nfunction stringValue(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\n// ============================================================================\n// Reconnect Event Type (for external consumers)\n// ============================================================================\n\nexport interface DesktopBridgeReconnectEvent {\n runtimeId: string;\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted';\n attempt: number;\n maxAttempts: number;\n delay?: number;\n}\n"],
|
|
5
|
-
"mappings": ";AAIA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,OAAO,eAAe;
|
|
4
|
+
"sourcesContent": ["/**\n * Desktop Agent Bridge - WebSocket communication with runtimes.\n * Handles conversation management and WebSocket connections with automatic reconnection.\n */\nimport { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport {\n createSurfaceConnectionState,\n DEFAULT_SURFACE_CONNECTION_CONFIG,\n decodeProtocolFrame,\n markConnectionActivity,\n markConnectionConnecting,\n markConnectionOpen,\n planConnectionReconnect,\n resetConnection,\n type SurfaceConnectionState,\n stopConnection,\n} from '@wrongstack/webui-server/protocol';\nimport WebSocket from 'ws';\nimport type {\n DesktopConversationMessage,\n DesktopConversationSnapshot,\n DesktopConversationStatus,\n} from '../shared/types.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface ConversationInternal {\n runtimeId: string;\n status: DesktopConversationStatus;\n sessionId?: string | undefined;\n error?: string | undefined;\n messages: DesktopConversationMessage[];\n ws: WebSocket | null;\n connectPromise: Promise<void> | null;\n activeAssistantMessageId: string | null;\n /** Reconnection state */\n reconnectAttempt: number;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectUrl: string | null;\n connectionState: SurfaceConnectionState;\n // Per-reconnect message-handler bookkeeping. The anonymous\n // `ws.on('message', \u2026)` pattern pins the socket + its closure (which\n // captures `conversation`) for the socket's full lifetime \u2014 fine for\n // open sockets, fatal across reconnects, where each new connect() leaks\n // a stale message listener + closure onto the previous (now-orphaned)\n // WebSocket. RAM-leak audit 2026-08-11, HIGH.\n onMessage: ((data: WebSocket.RawData) => void) | null;\n socketGeneration: number;\n}\n\ninterface ServerMessage {\n type: string;\n payload?: Record<string, unknown> | undefined;\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst MAX_MESSAGES = 300;\n\n/** Reconnection configuration */\nconst RECONNECT_CONFIG = {\n /** Maximum number of reconnection attempts (0 = disabled) */\n maxAttempts: 5,\n /** Initial delay in ms before first reconnection */\n initialDelayMs: 1000,\n /** Maximum delay in ms between reconnection attempts */\n maxDelayMs: 30000,\n /** Multiplier for exponential backoff */\n backoffMultiplier: 2,\n /** Jitter factor (0-1) to add randomness to delays */\n jitterFactor: 0.1,\n};\n\nconst DESKTOP_CONNECTION_CONFIG = {\n ...DEFAULT_SURFACE_CONNECTION_CONFIG,\n maxReconnectAttempts: RECONNECT_CONFIG.maxAttempts,\n initialBackoffMs: RECONNECT_CONFIG.initialDelayMs,\n maxBackoffMs: RECONNECT_CONFIG.maxDelayMs,\n backoffMultiplier: RECONNECT_CONFIG.backoffMultiplier,\n jitterRatio: RECONNECT_CONFIG.jitterFactor,\n};\n\n// ============================================================================\n// Bridge Implementation\n// ============================================================================\n\nexport class DesktopAgentBridge extends EventEmitter {\n private readonly conversations = new Map<string, ConversationInternal>();\n\n snapshot(runtimeId: string): DesktopConversationSnapshot {\n return publicConversation(this.getOrCreate(runtimeId));\n }\n\n /**\n * Get reconnection status for a runtime.\n */\n getReconnectStatus(runtimeId: string): { attempt: number; maxAttempts: number } | null {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return null;\n return {\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n };\n }\n\n /**\n * Force reconnection for a runtime (resets reconnection state).\n */\n forceReconnect(runtimeId: string, wsUrl: string): void {\n const conversation = this.getOrCreate(runtimeId);\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n void this.ensureConnected(runtimeId, wsUrl);\n }\n\n async ensureConnected(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n const conversation = this.getOrCreate(runtimeId);\n\n // If already connected, return immediately\n if (conversation.ws?.readyState === WebSocket.OPEN) return publicConversation(conversation);\n\n // If currently connecting, wait for it\n if (conversation.connectPromise) {\n await conversation.connectPromise;\n return publicConversation(conversation);\n }\n\n // If reconnecting in background, return current state\n if (conversation.reconnectTimer) {\n return publicConversation(conversation);\n }\n\n // Start fresh connection\n conversation.reconnectUrl = wsUrl;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n await this.connect(runtimeId, wsUrl);\n return publicConversation(conversation);\n }\n\n /**\n * Internal connect method that actually establishes the WebSocket.\n */\n private async connect(runtimeId: string, wsUrl: string): Promise<void> {\n const conversation = this.getOrCreate(runtimeId);\n\n // Cancel any pending reconnect timer\n this.cancelReconnect(conversation);\n\n conversation.status = 'connecting';\n conversation.connectionState = markConnectionConnecting(conversation.connectionState);\n conversation.error = undefined;\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connecting');\n\n return new Promise((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n conversation.ws = ws;\n\n // Bump the generation so a message handler from this socket knows it\n // is the current one. Each connect() call gets a unique generation,\n // so a stale socket that fires a late 'message' after its successor\n // has taken over sees a mismatch and bails before touching state.\n // RAM-leak audit 2026-08-11, HIGH.\n const myGeneration = ++conversation.socketGeneration;\n\n // Named (not anonymous) so we can detach it in close() and in the\n // 'close' handler. An anonymous ws.on('message', \u2026) was never removed,\n // so the closure pinned ConversationInternal (and its messages[],\n // ws, sessionId, \u2026) until the WebSocket was GC'd \u2014 across a reconnect\n // storm this leaked every ConversationInternal that ever connected.\n // RAM-leak audit 2026-08-11, HIGH.\n const onMessage = (data: unknown): void => {\n if (conversation.socketGeneration !== myGeneration) return;\n this.handleServerMessage(conversation, String(data));\n };\n conversation.onMessage = onMessage;\n ws.on('message', onMessage);\n\n // Set connection timeout\n const timeout = setTimeout(() => {\n if (conversation.ws === ws) {\n ws.close();\n reject(new Error('Connection timeout'));\n }\n }, 10000);\n\n ws.once('open', () => {\n clearTimeout(timeout);\n conversation.status = 'connected';\n conversation.error = undefined;\n conversation.connectPromise = null;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = markConnectionOpen(conversation.connectionState);\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connected');\n resolve();\n });\n\n ws.once('error', (err) => {\n clearTimeout(timeout);\n conversation.status = 'error';\n conversation.error = err instanceof Error ? err.message : String(err);\n conversation.connectPromise = null;\n this.appendMessage(conversation, {\n role: 'system',\n text: `Connection error: ${conversation.error}`,\n });\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'error');\n reject(err);\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n // Only the socket that currently owns the conversation invalidates\n // the generation \u2014 a superseded socket closing must not clobber its\n // successor's generation, or late messages from the successor would\n // be wrongly dropped. RAM-leak audit 2026-08-11, HIGH (chimera review).\n if (conversation.ws === ws) {\n conversation.socketGeneration++;\n conversation.ws = null;\n }\n // Detach our named message handler so the closure (which retains\n // conversation) is releasable even if the ws is kept alive by the\n // runtime or pending GC. RAM-leak audit 2026-08-11, HIGH.\n ws.off('message', onMessage);\n // Only clear the tracked handler if it's still ours \u2014 a newer\n // connect() may have already installed its own. Clearing\n // unconditionally would orphan the new handler's reference,\n // making it impossible for close(runtimeId) to detach it later.\n // RAM-leak audit 2026-08-11, HIGH (chimera review Medium #1).\n if (conversation.onMessage === onMessage) {\n conversation.onMessage = null;\n }\n conversation.connectPromise = null;\n\n if (conversation.status !== 'error') {\n conversation.status = 'disconnected';\n }\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Schedule reconnection if applicable\n if (conversation.reconnectUrl && conversation.status === 'disconnected') {\n this.scheduleReconnect(conversation);\n }\n });\n });\n }\n\n /**\n * Schedule a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(conversation: ConversationInternal): void {\n const reconnect = planConnectionReconnect(\n conversation.connectionState,\n DESKTOP_CONNECTION_CONFIG,\n );\n conversation.connectionState = reconnect.state;\n conversation.reconnectAttempt = reconnect.state.reconnectAttempt;\n if (!reconnect.plan) {\n this.emitReconnectEvent(conversation, 'exhausted');\n return;\n }\n this.emitReconnectEvent(conversation, 'scheduled', {\n delay: reconnect.plan.delayMs,\n attempt: reconnect.plan.attempt,\n });\n\n conversation.reconnectTimer = setTimeout(() => {\n conversation.reconnectTimer = null;\n if (!conversation.reconnectUrl) return;\n\n // Check if still disconnected\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n void this.connect(conversation.runtimeId, conversation.reconnectUrl);\n }\n }, reconnect.plan.delayMs);\n }\n\n /**\n * Cancel pending reconnection.\n */\n private cancelReconnect(conversation: ConversationInternal): void {\n if (conversation.reconnectTimer) {\n clearTimeout(conversation.reconnectTimer);\n conversation.reconnectTimer = null;\n }\n }\n\n /**\n * Emit reconnection event for UI feedback.\n */\n private emitReconnectEvent(\n conversation: ConversationInternal,\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted',\n data?: { delay?: number; attempt?: number },\n ): void {\n this.emit('reconnect', {\n runtimeId: conversation.runtimeId,\n status,\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n ...data,\n });\n }\n\n async sendMessage(\n runtimeId: string,\n wsUrl: string,\n content: string,\n ): Promise<DesktopConversationSnapshot> {\n const trimmed = content.trim();\n if (!trimmed) return this.snapshot(runtimeId);\n\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n\n // Refresh conversation after connect\n const conv = this.getOrCreate(runtimeId);\n this.appendMessage(conv, {\n id: `user_${randomUUID()}`,\n role: 'user',\n text: trimmed,\n });\n conv.status = 'running';\n conv.activeAssistantMessageId = null;\n this.emitChanged(conv);\n this.send(conv, {\n type: 'user_message',\n payload: {\n id: `msg_${Date.now()}_${randomUUID().slice(0, 8)}`,\n content: trimmed,\n timestamp: Date.now(),\n ...(conv.sessionId ? { sessionId: conv.sessionId } : {}),\n },\n });\n return publicConversation(conv);\n }\n\n async abort(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n const conv = this.getOrCreate(runtimeId);\n this.send(conv, {\n type: 'abort',\n payload: conv.sessionId ? { sessionId: conv.sessionId } : {},\n });\n conv.status = 'connected';\n this.appendMessage(conv, { role: 'system', text: 'Abort requested.' });\n return publicConversation(conv);\n }\n\n close(runtimeId: string): void {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return;\n\n // Cancel reconnection and clear state\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.reconnectUrl = null;\n conversation.connectionState = stopConnection(conversation.connectionState);\n\n // Detach the per-reconnect message listener BEFORE nulling the ws ref.\n // Without this, the ws.on('message', \u2026) closure pins ConversationInternal\n // (and its messages[]) until the socket is GC'd. RAM-leak audit 2026-08-11, HIGH.\n if (conversation.onMessage) {\n conversation.ws?.off('message', conversation.onMessage);\n conversation.onMessage = null;\n }\n // Invalidate this conversation's generation so any in-flight socket whose\n // 'close' event fires after deletion cannot resurrect the conversation ref.\n conversation.socketGeneration++;\n conversation.ws?.close();\n conversation.ws = null;\n conversation.connectPromise = null;\n conversation.status = 'disconnected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Remove from the map so closed conversations don't accumulate in memory.\n this.conversations.delete(runtimeId);\n }\n\n closeAll(): void {\n // Snapshot keys first \u2014 close() deletes from the map during iteration.\n for (const runtimeId of [...this.conversations.keys()]) {\n this.close(runtimeId);\n }\n }\n\n private handleServerMessage(conversation: ConversationInternal, raw: string): void {\n const decoded = decodeProtocolFrame(raw, 'server');\n if (!decoded.ok) return;\n conversation.connectionState = markConnectionActivity(conversation.connectionState);\n const message = decoded.message as ServerMessage;\n const payload = message.payload ?? {};\n switch (message.type) {\n case 'session.start': {\n const sessionId = stringValue(payload['sessionId']);\n if (sessionId) conversation.sessionId = sessionId;\n conversation.status = conversation.status === 'running' ? 'running' : 'connected';\n this.emitChanged(conversation);\n break;\n }\n case 'provider.text_delta': {\n conversation.status = 'running';\n this.appendAssistantDelta(conversation, stringValue(payload['text']) ?? '');\n break;\n }\n case 'tool.started': {\n conversation.status = 'running';\n const name = stringValue(payload['name']) ?? 'tool';\n this.appendMessage(conversation, { role: 'tool', text: `Started ${name}` });\n break;\n }\n case 'tool.executed': {\n const name = stringValue(payload['name']) ?? 'tool';\n const ok = payload['ok'] === true;\n this.appendMessage(conversation, {\n role: 'tool',\n text: `${name} ${ok ? 'completed' : 'failed'}`,\n });\n break;\n }\n case 'provider.error':\n case 'provider.stream_error':\n case 'error': {\n const text =\n stringValue(payload['message']) ??\n stringValue(payload['description']) ??\n `${message.type} received`;\n conversation.status = 'error';\n conversation.error = text;\n this.appendMessage(conversation, { role: 'system', text });\n break;\n }\n case 'run.result': {\n const finalText = stringValue(payload['finalText']);\n if (finalText && !this.lastAssistantHasText(conversation)) {\n this.appendMessage(conversation, { role: 'assistant', text: finalText });\n }\n conversation.status = payload['status'] === 'failed' ? 'error' : 'connected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n break;\n }\n }\n }\n\n private send(conversation: ConversationInternal, message: Record<string, unknown>): void {\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n throw new Error('Runtime socket is not connected');\n }\n conversation.ws.send(JSON.stringify(message));\n }\n\n private appendAssistantDelta(conversation: ConversationInternal, text: string): void {\n if (!text) return;\n let message = conversation.messages.find((m) => m.id === conversation.activeAssistantMessageId);\n if (!message) {\n message = {\n id: `assistant_${randomUUID()}`,\n role: 'assistant',\n text: '',\n timestamp: Date.now(),\n };\n conversation.activeAssistantMessageId = message.id;\n conversation.messages.push(message);\n }\n message.text += text;\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private appendMessage(\n conversation: ConversationInternal,\n input: Partial<DesktopConversationMessage> & Pick<DesktopConversationMessage, 'role' | 'text'>,\n ): void {\n conversation.messages.push({\n id: input.id ?? `${input.role}_${randomUUID()}`,\n role: input.role,\n text: input.text,\n timestamp: input.timestamp ?? Date.now(),\n });\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private lastAssistantHasText(conversation: ConversationInternal): boolean {\n const lastAssistant = [...conversation.messages].reverse().find((m) => m.role === 'assistant');\n return Boolean(lastAssistant?.text.trim());\n }\n\n private trimMessages(conversation: ConversationInternal): void {\n if (conversation.messages.length <= MAX_MESSAGES) return;\n conversation.messages.splice(0, conversation.messages.length - MAX_MESSAGES);\n }\n\n private getOrCreate(runtimeId: string): ConversationInternal {\n let conversation = this.conversations.get(runtimeId);\n if (conversation) return conversation;\n conversation = {\n runtimeId,\n status: 'disconnected',\n messages: [],\n ws: null,\n connectPromise: null,\n activeAssistantMessageId: null,\n reconnectAttempt: 0,\n reconnectTimer: null,\n reconnectUrl: null,\n connectionState: createSurfaceConnectionState(),\n onMessage: null,\n socketGeneration: 0,\n };\n this.conversations.set(runtimeId, conversation);\n return conversation;\n }\n\n private emitChanged(conversation: ConversationInternal): void {\n this.emit('changed', publicConversation(conversation));\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction publicConversation(conversation: ConversationInternal): DesktopConversationSnapshot {\n return {\n runtimeId: conversation.runtimeId,\n status: conversation.status,\n sessionId: conversation.sessionId,\n error: conversation.error,\n messages: conversation.messages.map((message) => ({ ...message })),\n };\n}\n\nfunction stringValue(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\n// ============================================================================\n// Reconnect Event Type (for external consumers)\n// ============================================================================\n\nexport interface DesktopBridgeReconnectEvent {\n runtimeId: string;\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted';\n attempt: number;\n maxAttempts: number;\n delay?: number;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/main/main.js
CHANGED
|
@@ -148,6 +148,13 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
148
148
|
return new Promise((resolve3, reject) => {
|
|
149
149
|
const ws = new WebSocket(wsUrl);
|
|
150
150
|
conversation.ws = ws;
|
|
151
|
+
const myGeneration = ++conversation.socketGeneration;
|
|
152
|
+
const onMessage = (data) => {
|
|
153
|
+
if (conversation.socketGeneration !== myGeneration) return;
|
|
154
|
+
this.handleServerMessage(conversation, String(data));
|
|
155
|
+
};
|
|
156
|
+
conversation.onMessage = onMessage;
|
|
157
|
+
ws.on("message", onMessage);
|
|
151
158
|
const timeout = setTimeout(() => {
|
|
152
159
|
if (conversation.ws === ws) {
|
|
153
160
|
ws.close();
|
|
@@ -165,9 +172,6 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
165
172
|
this.emitReconnectEvent(conversation, "connected");
|
|
166
173
|
resolve3();
|
|
167
174
|
});
|
|
168
|
-
ws.on("message", (data) => {
|
|
169
|
-
this.handleServerMessage(conversation, data.toString());
|
|
170
|
-
});
|
|
171
175
|
ws.once("error", (err) => {
|
|
172
176
|
clearTimeout(timeout);
|
|
173
177
|
conversation.status = "error";
|
|
@@ -183,7 +187,14 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
183
187
|
});
|
|
184
188
|
ws.once("close", () => {
|
|
185
189
|
clearTimeout(timeout);
|
|
186
|
-
if (conversation.ws === ws)
|
|
190
|
+
if (conversation.ws === ws) {
|
|
191
|
+
conversation.socketGeneration++;
|
|
192
|
+
conversation.ws = null;
|
|
193
|
+
}
|
|
194
|
+
ws.off("message", onMessage);
|
|
195
|
+
if (conversation.onMessage === onMessage) {
|
|
196
|
+
conversation.onMessage = null;
|
|
197
|
+
}
|
|
187
198
|
conversation.connectPromise = null;
|
|
188
199
|
if (conversation.status !== "error") {
|
|
189
200
|
conversation.status = "disconnected";
|
|
@@ -291,6 +302,11 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
291
302
|
conversation.reconnectAttempt = 0;
|
|
292
303
|
conversation.reconnectUrl = null;
|
|
293
304
|
conversation.connectionState = stopConnection(conversation.connectionState);
|
|
305
|
+
if (conversation.onMessage) {
|
|
306
|
+
conversation.ws?.off("message", conversation.onMessage);
|
|
307
|
+
conversation.onMessage = null;
|
|
308
|
+
}
|
|
309
|
+
conversation.socketGeneration++;
|
|
294
310
|
conversation.ws?.close();
|
|
295
311
|
conversation.ws = null;
|
|
296
312
|
conversation.connectPromise = null;
|
|
@@ -413,7 +429,9 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
413
429
|
reconnectAttempt: 0,
|
|
414
430
|
reconnectTimer: null,
|
|
415
431
|
reconnectUrl: null,
|
|
416
|
-
connectionState: createSurfaceConnectionState()
|
|
432
|
+
connectionState: createSurfaceConnectionState(),
|
|
433
|
+
onMessage: null,
|
|
434
|
+
socketGeneration: 0
|
|
417
435
|
};
|
|
418
436
|
this.conversations.set(runtimeId, conversation);
|
|
419
437
|
return conversation;
|