perf(sessions): archive a worktree's sessions through one server batch

Removing a worktree archived its sessions one SDK call at a time and
then re-rendered the whole sidebar once per streamed session.updated
echo. On a worktree with 121 sessions that meant 14.8s of main-thread
work, 121 requests, and 328 localStorage writes.

- Add POST /api/openchamber/sessions/archive: validates a batch (max
  500 ids, per-request archivedAt), archives sequentially, and reports
  partial failures instead of dropping the batch. VS Code serves no
  such route and answers 501; the shared UI then falls back to the
  per-session path.
- Plan batches from the sessions this client actually holds, live
  directory stores first, so worktree-only sessions still batch.
- Claim (id, archivedAt) pairs before the request and consume the
  matching session.updated echoes, so the server's own confirmations
  no longer fan out into 121 store publications. Runtime-scoped, TTL
  30s, released on response or fallback; non-matching updates pass.
- Make the managed-chats persistence a real trailing debounce instead
  of a 50ms throttle, so a burst of publications coalesces into one
  localStorage write.

Benchmark (121 sessions, production build, real Chrome): 14785ms ->
~1030ms, long tasks 100 -> 1, global store publications 236 -> 1,
persistence writes 328 -> 3.
This commit is contained in:
Iuliia Ivashko
2026-09-04 16:50:03 +03:00
parent 0d8709a72e
commit 0ef189f3c8
11 changed files with 769 additions and 15 deletions
@@ -45,7 +45,15 @@ mock.module("@/lib/runtime-switch", () => ({
return () => undefined
},
}))
import { applySessionEventsToGlobalSessions, applySessionEventToGlobalSessions } from "../session-event-router"
import {
applySessionEventsToGlobalSessions,
applySessionEventToGlobalSessions,
} from "../session-event-router"
import {
registerBulkArchiveEchoes,
releaseBulkArchiveEchoes,
shouldConsumeBulkArchiveEcho,
} from "../bulk-archive-echo"
const buildSession = (title: string, time: Session["time"]): Session => ({
id: "ses_1",
@@ -146,4 +154,35 @@ describe("applySessionEventToGlobalSessions", () => {
expect(mutationCalls).toBe(1)
expect(upsertedSessions).toHaveLength(1_000)
})
test("consumes only the matching bulk archive echo", () => {
registerBulkArchiveEchoes(runtimeKey, [{ id: "ses_1", archivedAt: 20 }], 100)
expect(shouldConsumeBulkArchiveEcho(buildEvent(buildSession("Initial", {
created: 1,
updated: 20,
archived: 20,
})), runtimeKey, 101)).toBe(true)
expect(shouldConsumeBulkArchiveEcho(buildEvent(buildSession("Initial", {
created: 1,
updated: 21,
archived: 21,
})), runtimeKey, 101)).toBe(false)
expect(shouldConsumeBulkArchiveEcho(buildEvent(buildSession("Initial", {
created: 1,
updated: 20,
archived: 20,
})), "runtime-b", 101)).toBe(false)
})
test("does not consume an expired or released bulk archive echo", () => {
registerBulkArchiveEchoes(runtimeKey, [{ id: "ses_1", archivedAt: 20 }], 100)
const event = buildEvent(buildSession("Initial", { created: 1, updated: 20, archived: 20 }))
expect(shouldConsumeBulkArchiveEcho(event, runtimeKey, 30_101)).toBe(false)
registerBulkArchiveEchoes(runtimeKey, [{ id: "ses_1", archivedAt: 20 }], 100)
releaseBulkArchiveEchoes(runtimeKey, ["ses_1"])
expect(shouldConsumeBulkArchiveEcho(event, runtimeKey, 101)).toBe(false)
})
})