diff --git a/.changeset/fix-sync-filter-lifecycle.md b/.changeset/fix-sync-filter-lifecycle.md new file mode 100644 index 0000000000..47964a30b1 --- /dev/null +++ b/.changeset/fix-sync-filter-lifecycle.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent duplicate-event tracking from suppressing delayed sync events after reconnects or high event bursts. diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts index 1b0e8679d3..a40ffb60d8 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -96,7 +96,6 @@ export class KiloConnectionService { private remoteService: import("../RemoteStatusService").RemoteStatusService | null = null private readonly eventListeners: Set = new Set() - private readonly duplicateEvent = createDuplicateEventFilter() private readonly stateListeners: Set = new Set() private readonly notificationDismissListeners: Set = new Set() private readonly languageChangeListeners: Set = new Set() @@ -821,6 +820,7 @@ export class KiloConnectionService { }, }) const sse = new SdkSSEAdapter(client) + const duplicateEvent = createDuplicateEventFilter() this.client = client this.sseClient = sse @@ -839,7 +839,7 @@ export class KiloConnectionService { sse.onEvent((event, directory) => { if (this.sseClient !== sse) return // EventV2Bridge also emits these durable compatibility envelopes after their normal live events. - if (this.duplicateEvent(event)) return + if (duplicateEvent(event)) return this.handlePermissionEvent(event, directory) this.handleQuestionEvent(event, directory) for (const listener of this.eventListeners) { diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts b/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts index 271eec4114..97ea3a568f 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts @@ -24,8 +24,7 @@ export function createDuplicateEventFilter() { } if (duplicateLiveEvents.has(event.type)) { - seen.add(event.id) - if (seen.size > DUPLICATE_EVENT_LIMIT) seen.delete(seen.values().next().value!) + if (seen.size < DUPLICATE_EVENT_LIMIT) seen.add(event.id) } return false } diff --git a/packages/kilo-vscode/tests/unit/connection-utils.test.ts b/packages/kilo-vscode/tests/unit/connection-utils.test.ts index c1865616e7..cde5796e8b 100644 --- a/packages/kilo-vscode/tests/unit/connection-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/connection-utils.test.ts @@ -173,7 +173,7 @@ describe("resolveEventSessionId", () => { }) }) -describe("isDuplicateSyncEvent", () => { +describe("createDuplicateEventFilter", () => { it("drops a compatibility envelope only after its live event", () => { const filter = createDuplicateEventFilter() const live = { @@ -233,4 +233,137 @@ describe("isDuplicateSyncEvent", () => { ), ).toBe(false) }) + + it("does not evict pending live events when the cap is reached", () => { + const filter = createDuplicateEventFilter() + for (let index = 0; index < 1024; index++) { + expect( + filter({ + id: `live-${index}`, + type: "message.part.updated", + properties: { sessionID: "s6", part, delta: "x" }, + }), + ).toBe(false) + } + + expect( + filter( + sync({ + type: "sync", + name: "message.part.updated.1", + id: "live-0", + seq: 8, + aggregateID: "s6", + data: { sessionID: "s6", part, time: 0 }, + }), + ), + ).toBe(true) + expect( + filter({ + id: "live-1024", + type: "message.part.updated", + properties: { sessionID: "s6", part, delta: "x" }, + }), + ).toBe(false) + expect( + filter( + sync({ + type: "sync", + name: "message.part.updated.1", + id: "live-1024", + seq: 9, + aggregateID: "s6", + data: { sessionID: "s6", part, time: 0 }, + }), + ), + ).toBe(true) + }) + + it("passes overflow events through without evicting pending IDs", () => { + const filter = createDuplicateEventFilter() + for (let index = 0; index < 1024; index++) { + expect( + filter({ + id: `pending-${index}`, + type: "message.part.updated", + properties: { sessionID: "s6", part, delta: "x" }, + }), + ).toBe(false) + } + + expect( + filter({ + id: "overflow", + type: "message.part.updated", + properties: { sessionID: "s6", part, delta: "x" }, + }), + ).toBe(false) + expect( + filter( + sync({ + type: "sync", + name: "message.part.updated.1", + id: "overflow", + seq: 8, + aggregateID: "s6", + data: { sessionID: "s6", part, time: 0 }, + }), + ), + ).toBe(false) + expect( + filter( + sync({ + type: "sync", + name: "message.part.updated.1", + id: "pending-0", + seq: 9, + aggregateID: "s6", + data: { sessionID: "s6", part, time: 0 }, + }), + ), + ).toBe(true) + expect( + filter({ + id: "after-free", + type: "message.part.updated", + properties: { sessionID: "s6", part, delta: "x" }, + }), + ).toBe(false) + expect( + filter( + sync({ + type: "sync", + name: "message.part.updated.1", + id: "after-free", + seq: 10, + aggregateID: "s6", + data: { sessionID: "s6", part, time: 0 }, + }), + ), + ).toBe(true) + }) + + it("does not carry duplicate IDs between connections", () => { + const first = createDuplicateEventFilter() + const second = createDuplicateEventFilter() + const live = { + id: "connection-event", + type: "message.part.updated", + properties: { sessionID: "s6", part, delta: "x" }, + } satisfies Payload + + expect(first(live)).toBe(false) + expect( + second( + sync({ + type: "sync", + name: "message.part.updated.1", + id: "connection-event", + seq: 11, + aggregateID: "s6", + data: { sessionID: "s6", part, time: 0 }, + }), + ), + ).toBe(false) + }) })