From 1582028902af8687b567bd411e59d313ba31ee73 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 21 Apr 2026 22:39:46 +0700 Subject: [PATCH] Cache available editor discovery --- package-lock.json | 41 ++++++++++++ packages/server/package.json | 3 +- packages/server/src/server/session.ts | 25 +++++++- .../src/server/session.workspaces.test.ts | 64 ++++++++++++++++++- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6874d53c..034f12eb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28729,6 +28729,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -30117,6 +30129,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-memoize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-8.0.0.tgz", + "integrity": "sha512-jdZ10MCxavHoIHwJ5oweOtYy6ElPixEHaMkz0AuaEMovR1MRpVvYFzIEHRxgMEpXYzNpRVByFAniAzwmd1/uug==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1", + "type-fest": "^4.41.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/p-memoize?sponsor=1" + } + }, + "node_modules/p-memoize/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -37859,6 +37899,7 @@ "onnxruntime-node": "^1.23.0", "openai": "^4.20.0", "p-limit": "^7.3.0", + "p-memoize": "^8.0.0", "pino": "^10.2.0", "pino-pretty": "^13.1.3", "qrcode": "^1.5.4", diff --git a/packages/server/package.json b/packages/server/package.json index c7a93b899..1d9e9bfe2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -63,8 +63,8 @@ "@getpaseo/highlight": "0.1.60-beta.1", "@getpaseo/relay": "0.1.60-beta.1", "@isaacs/ttlcache": "^2.1.4", - "@modelcontextprotocol/sdk": "^1.20.1", "@mariozechner/pi-coding-agent": "^0.67.68", + "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", "@sctg/sentencepiece-js": "^1.1.0", "@xterm/headless": "^6.0.0", @@ -80,6 +80,7 @@ "onnxruntime-node": "^1.23.0", "openai": "^4.20.0", "p-limit": "^7.3.0", + "p-memoize": "^8.0.0", "pino": "^10.2.0", "pino-pretty": "^13.1.3", "qrcode": "^1.5.4", diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 1321b14b4..69fd6adaa 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1,5 +1,7 @@ import equal from "fast-deep-equal"; import { v4 as uuidv4 } from "uuid"; +import { TTLCache } from "@isaacs/ttlcache"; +import pMemoize from "p-memoize"; import type { FSWatcher } from "node:fs"; import { resolve, sep } from "path"; import { homedir } from "node:os"; @@ -448,6 +450,8 @@ const MIN_STREAMING_SEGMENT_BYTES = Math.round( ); const AgentIdSchema = z.string().uuid(); const VOICE_INTERRUPT_CONFIRMATION_MS = 500; +const AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS = 60_000; +const AVAILABLE_EDITOR_TARGETS_CACHE_KEY = "available"; type VoiceModeBaseConfig = { systemPrompt?: string; @@ -705,6 +709,21 @@ export class Session { private nextTerminalSlot = 0; private inflightRequests = 0; private peakInflightRequests = 0; + private readonly availableEditorTargetsCache = new TTLCache< + string, + EditorTargetDescriptorPayload[] + >({ + ttl: AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS, + max: 1, + checkAgeOnGet: true, + }); + private readonly getMemoizedAvailableEditorTargets = pMemoize( + async () => this.resolveAvailableEditorTargets(), + { + cache: this.availableEditorTargetsCache, + cacheKey: () => AVAILABLE_EDITOR_TARGETS_CACHE_KEY, + }, + ); private readonly checkoutDiffSubscriptions = new Map void>(); private readonly workspaceGitWatchTargets = new Map(); private readonly workspaceSetupSnapshots = new Map(); @@ -6590,8 +6609,12 @@ export class Session { }); } + async resolveAvailableEditorTargets(): Promise { + return listAvailableEditorTargets(); + } + async getAvailableEditorTargets() { - return this.filterEditorsForClient(await listAvailableEditorTargets()); + return this.filterEditorsForClient(await this.getMemoizedAvailableEditorTargets()); } async openEditorTarget(options: { editorId: EditorTargetId; path: string }): Promise { diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 7ac35f712..7f1ccdcfa 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -4,7 +4,11 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, test, vi } from "vitest"; import { Session } from "./session.js"; -import type { AgentSnapshotPayload } from "../shared/messages.js"; +import type { + AgentSnapshotPayload, + EditorTargetDescriptorPayload, + SessionOutboundMessage, +} from "../shared/messages.js"; import type { WorkspaceGitRuntimeSnapshot } from "./workspace-git-service.js"; import { createPersistedProjectRecord, @@ -188,6 +192,7 @@ function createWorkspaceRuntimeSnapshot( function createSessionForWorkspaceTests( options: { appVersion?: string | null; + onMessage?: (message: SessionOutboundMessage) => void; workspaceGitService?: ReturnType; } = {}, ): Session { @@ -203,7 +208,7 @@ function createSessionForWorkspaceTests( const session = new Session({ clientId: "test-client", appVersion: options.appVersion ?? null, - onMessage: vi.fn(), + onMessage: options.onMessage ?? vi.fn(), logger: logger as any, downloadTokenStore: {} as any, pushTokenStore: {} as any, @@ -1555,6 +1560,61 @@ describe("workspace aggregation", () => { ]); }); + test("list_available_editors_request coalesces concurrent discovery", async () => { + const emitted: SessionOutboundMessage[] = []; + const session = createSessionForWorkspaceTests({ + appVersion: "0.1.50", + onMessage: (message) => emitted.push(message), + }); + let resolveDiscovery: (editors: EditorTargetDescriptorPayload[]) => void = () => {}; + let discoveryCalls = 0; + + vi.spyOn(session, "resolveAvailableEditorTargets").mockImplementation(async () => { + discoveryCalls += 1; + return await new Promise((resolve) => { + resolveDiscovery = resolve; + }); + }); + + const first = session.handleMessage({ + type: "list_available_editors_request", + requestId: "req-editors-1", + }); + const second = session.handleMessage({ + type: "list_available_editors_request", + requestId: "req-editors-2", + }); + + await Promise.resolve(); + expect(discoveryCalls).toBe(1); + + resolveDiscovery([{ id: "cursor", label: "Cursor" }]); + await Promise.all([first, second]); + + const responses = emitted.filter( + ( + message, + ): message is Extract => + message.type === "list_available_editors_response", + ); + expect(responses).toHaveLength(2); + expect(responses.map((response) => response.payload.requestId)).toEqual([ + "req-editors-1", + "req-editors-2", + ]); + expect(responses.map((response) => response.payload.editors)).toEqual([ + [{ id: "cursor", label: "Cursor" }], + [{ id: "cursor", label: "Cursor" }], + ]); + + await session.handleMessage({ + type: "list_available_editors_request", + requestId: "req-editors-3", + }); + + expect(discoveryCalls).toBe(1); + }); + test("list_available_editors_request filters unsupported ids for legacy clients", async () => { const emitted: Array<{ type: string; payload: unknown }> = []; const session = createSessionForWorkspaceTests({ appVersion: "0.1.49" }) as any;