(rawFrame: WireServerFrame)
| 261 | // --------------------------------------------------------------------------- |
| 262 | |
| 263 | private handleFrame(rawFrame: WireServerFrame): void { |
| 264 | // WireServerFrame union contains WireAck (payload: unknown) which prevents |
| 265 | // TypeScript from narrowing .payload in each case arm. Cast once here. |
| 266 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 267 | const frame = rawFrame as any; |
| 268 | switch ((rawFrame as { type: string }).type) { |
| 269 | case 'server_hello': |
| 270 | this.onServerHello(); |
| 271 | break; |
| 272 | |
| 273 | case 'ping': |
| 274 | this.send({ type: 'pong', payload: { nonce: frame.payload.nonce } }); |
| 275 | break; |
| 276 | |
| 277 | case 'resync_required': { |
| 278 | const sid = frame.payload.session_id as string; |
| 279 | const epoch = frame.payload.epoch as string | undefined; |
| 280 | // Adopt the announced cursor so the next reconnect handshake doesn't |
| 281 | // re-trigger the same resync before the snapshot reload lands. |
| 282 | this.subscriptions.set(sid, { seq: frame.payload.current_seq, epoch }); |
| 283 | this.handlers.onResync(sid, frame.payload.current_seq, epoch); |
| 284 | break; |
| 285 | } |
| 286 | |
| 287 | case 'error': { |
| 288 | // A session-scoped error (has top-level session_id) is a real agent-core |
| 289 | // 'error' event — e.g. a 403 from the model provider — whose message |
| 290 | // must surface in the conversation. A connection-level control error |
| 291 | // (no session_id) goes to onError. |
| 292 | const sid = (frame as { session_id?: unknown }).session_id; |
| 293 | if (typeof sid === 'string' && this.handlers.onRawAgentEvent) { |
| 294 | this.handlers.onRawAgentEvent({ |
| 295 | type: 'error', |
| 296 | seq: frame.seq, |
| 297 | session_id: sid, |
| 298 | timestamp: frame.timestamp, |
| 299 | payload: frame.payload, |
| 300 | }); |
| 301 | } else { |
| 302 | this.handlers.onError(frame.payload.code, frame.payload.msg, frame.payload.fatal); |
| 303 | } |
| 304 | break; |
| 305 | } |
| 306 | |
| 307 | case 'ack': |
| 308 | // ack frames are fire-and-forget for now (no request tracking) |
| 309 | break; |
| 310 | |
| 311 | case 'terminal_output': { |
| 312 | const sessionId = frame.session_id as string; |
| 313 | const terminalId = frame.terminal_id as string; |
| 314 | const seq = frame.seq as number; |
| 315 | const key = terminalKey(sessionId, terminalId); |
| 316 | const existing = this.terminalAttachments.get(key); |
| 317 | if (existing) { |
| 318 | this.terminalAttachments.set(key, { |
| 319 | ...existing, |
| 320 | lastSeq: Math.max(existing.lastSeq, seq), |
no test coverage detected