(handlers: KimiEventHandlers)
| 1242 | // ------------------------------------------------------------------------- |
| 1243 | |
| 1244 | connectEvents(handlers: KimiEventHandlers): KimiEventConnection { |
| 1245 | const wsUrl = buildWsUrl(this.config.serverHttpUrl, this.config.clientId); |
| 1246 | |
| 1247 | // Per-session projector for raw agent-core events. |
| 1248 | // Keyed by session_id; reset when a session is re-subscribed or resynced. |
| 1249 | const projector = createAgentProjector(); |
| 1250 | |
| 1251 | const socket = new DaemonEventSocket(wsUrl, this.config.clientId, { |
| 1252 | // ----------------------------------------------------------------------- |
| 1253 | // Projected "event.*" frames — existing path (kept working for stub / spec) |
| 1254 | // ----------------------------------------------------------------------- |
| 1255 | onWireEvent: (wireEvent: WireEvent) => { |
| 1256 | const sessionId = wireEventSessionId(wireEvent); |
| 1257 | const seq = wireEventSeq(wireEvent); |
| 1258 | const appEvent = toAppEvent(wireEvent); |
| 1259 | |
| 1260 | // Route history_compacted to onResync so the client reloads messages — |
| 1261 | // EXCEPT for compaction itself: the transcript keeps the scrollback and |
| 1262 | // the reducer appends a divider marker instead (reloading would replace |
| 1263 | // the visible conversation with the compacted model context). |
| 1264 | if (appEvent.type === 'historyCompacted' && !isCompactionReason(appEvent.reason)) { |
| 1265 | handlers.onResync(appEvent.sessionId, appEvent.beforeSeq); |
| 1266 | // Still dispatch the event to onEvent so the reducer can update lastSeqBySession |
| 1267 | } |
| 1268 | |
| 1269 | // Deliver the AppEvent together with wire-level seq/session so the |
| 1270 | // reducer can advance lastSeqBySession[sessionId] = seq. |
| 1271 | handlers.onEvent(appEvent, { sessionId, seq }); |
| 1272 | }, |
| 1273 | |
| 1274 | // ----------------------------------------------------------------------- |
| 1275 | // Raw agent-core frames — client-side projection path (real daemon) |
| 1276 | // ----------------------------------------------------------------------- |
| 1277 | onRawAgentEvent: (frame) => { |
| 1278 | const { type, seq, session_id: sessionId, payload, offset } = frame; |
| 1279 | const appEvents = projector.project(type, payload, sessionId, { offset }); |
| 1280 | for (const appEvent of appEvents) { |
| 1281 | // historyCompacted from the projector is either a compaction signal |
| 1282 | // (reason auto_compact — no reload, the divider marker handles it) or |
| 1283 | // a delta-gap recovery (reason delta_gap — a real resync, routed to |
| 1284 | // onResync with the real frame.seq, mirroring the protocol path). |
| 1285 | if (appEvent.type === 'historyCompacted' && !isCompactionReason(appEvent.reason)) { |
| 1286 | handlers.onResync(sessionId, seq); |
| 1287 | } |
| 1288 | handlers.onEvent(appEvent, { sessionId, seq }); |
| 1289 | } |
| 1290 | }, |
| 1291 | |
| 1292 | onResync: (sessionId: string, currentSeq: number, epoch?: string) => { |
| 1293 | // Reset per-session projector state on resync |
| 1294 | projector.reset(sessionId); |
| 1295 | handlers.onResync(sessionId, currentSeq, epoch); |
| 1296 | }, |
| 1297 | |
| 1298 | onConnectionState: (connected: boolean) => { |
| 1299 | handlers.onConnectionChange(connected); |
| 1300 | }, |
| 1301 |
nothing calls this directly
no test coverage detected