Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-agent-manager-streams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Reduce duplicate event processing across VS Code when multiple sessions run concurrently.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ServerManager } from "./server-manager"
import { createKiloClient, type KiloClient } from "@kilocode/sdk/v2/client"
import { SdkSSEAdapter, type SSEPayload } from "./sdk-sse-adapter"
import type { ServerConfig } from "./types"
import { resolveEventSessionId as resolveEventSessionIdPure } from "./connection-utils"
import { createDuplicateEventFilter, resolveEventSessionId as resolveEventSessionIdPure } from "./connection-utils"
import { SandboxPreference } from "../sandbox-preference"

export type ConnectionState = "connecting" | "connected" | "disconnected" | "error"
Expand Down Expand Up @@ -96,6 +96,7 @@ export class KiloConnectionService {
private remoteService: import("../RemoteStatusService").RemoteStatusService | null = null

private readonly eventListeners: Set<SSEEventListener> = new Set()
private readonly duplicateEvent = createDuplicateEventFilter()
private readonly stateListeners: Set<StateListener> = new Set()
private readonly notificationDismissListeners: Set<NotificationDismissListener> = new Set()
private readonly languageChangeListeners: Set<LanguageChangeListener> = new Set()
Expand Down Expand Up @@ -837,6 +838,8 @@ export class KiloConnectionService {
// Wire SSE events → broadcast to all registered listeners
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
Comment thread
marius-kilocode marked this conversation as resolved.
this.handlePermissionEvent(event, directory)
this.handleQuestionEvent(event, directory)
for (const listener of this.eventListeners) {
Expand Down
27 changes: 27 additions & 0 deletions packages/kilo-vscode/src/services/cli-backend/connection-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ export type { SSEPayload } from "./sdk-sse-adapter"
type SyncPayload = Extract<SSEPayload, { type: "sync" }>
type TransientPayload = Exclude<SSEPayload, SyncPayload>

const duplicateSyncEvents = new Set([
"message.updated.1",
"message.removed.1",
"message.part.updated.1",
"message.part.removed.1",
"session.created.1",
"session.deleted.1",
])

const duplicateLiveEvents = new Set([...duplicateSyncEvents].map((name) => name.slice(0, -2)))
const DUPLICATE_EVENT_LIMIT = 1024

export function createDuplicateEventFilter() {
const seen = new Set<string>()
return (event: SSEPayload): boolean => {
if (event.type === "sync") {
return duplicateSyncEvents.has(event.name) && seen.delete(event.id)
}

if (duplicateLiveEvents.has(event.type)) {
seen.add(event.id)
if (seen.size > DUPLICATE_EVENT_LIMIT) seen.delete(seen.values().next().value!)
Comment thread
marius-kilocode marked this conversation as resolved.
}
return false
}
}

/**
* Pure session ID resolution for SSE events.
* The lookupMessageSessionId callback remains part of the public resolver contract for
Expand Down
64 changes: 63 additions & 1 deletion packages/kilo-vscode/tests/unit/connection-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"
import { resolveEventSessionId } from "../../src/services/cli-backend/connection-utils"
import { createDuplicateEventFilter, resolveEventSessionId } from "../../src/services/cli-backend/connection-utils"
import type { SSEPayload as Payload } from "../../src/services/cli-backend/sdk-sse-adapter"

const noLookup = (_: string) => undefined
Expand Down Expand Up @@ -172,3 +172,65 @@ describe("resolveEventSessionId", () => {
expect(resolveEventSessionId(event, noLookup)).toBeUndefined()
})
})

describe("isDuplicateSyncEvent", () => {
Comment thread
marius-kilocode marked this conversation as resolved.
it("drops a compatibility envelope only after its live event", () => {
const filter = createDuplicateEventFilter()
const live = {
id: "e13",
type: "message.part.updated",
properties: { sessionID: "s6", part, delta: "x" },
} satisfies Payload
expect(filter(live)).toBe(false)
expect(
filter(
sync({
type: "sync",
name: "message.part.updated.1",
id: "e13",
seq: 5,
aggregateID: "s6",
data: { sessionID: "s6", part, time: 0 },
}),
),
).toBe(true)
})

it("keeps replay-only compatibility envelopes", () => {
const filter = createDuplicateEventFilter()
expect(
filter(
sync({
type: "sync",
name: "session.next.model.switched.1",
id: "e14",
seq: 6,
aggregateID: "s6",
data: { sessionID: "s6", messageID: "m1", model: { id: "test", providerID: "kilo" } },
}),
),
).toBe(false)
})

it("keeps session updates because the provider consumes their sync metadata", () => {
const filter = createDuplicateEventFilter()
const live = {
id: "e15",
type: "session.updated",
properties: { sessionID: "s6", info: { id: "s6", time: { created: 0, updated: 1 } } },
} as Payload
expect(filter(live)).toBe(false)
expect(
filter(
sync({
type: "sync",
name: "session.updated.1",
id: "e15",
seq: 7,
aggregateID: "s6",
data: { sessionID: "s6", info: { id: "s6", time: { created: 0, updated: 1 } } },
}),
),
).toBe(false)
})
})
Loading