authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-05 14:56:43-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 14:56:43-04:00
log0d8a91a25a35618c7cae1e02791e491afa1f0a00
tree085fa6f4380cb0f1dba93483dd1264dc14bc7ab6
parent6349a0e68a958cc51b7b5198683c1d1db88b8d28
signature Signed by PGP key B5690EEEBB952194

fix(cursor): cache successful model discovery between refreshes (#9918)


3 files changed, 68 insertions(+), 4 deletions(-)

apps/server/src/provider/Drivers/CursorDriver.ts+7-1
...@@ -29,6 +29,7 @@ import { makeCursorAdapter } from "../Layers/CursorAdapter.ts";...@@ -29,6 +29,7 @@ import { makeCursorAdapter } from "../Layers/CursorAdapter.ts";
29import {29import {
30 buildInitialCursorProviderSnapshot,30 buildInitialCursorProviderSnapshot,
31 checkCursorProviderStatus,31 checkCursorProviderStatus,
32 makeCursorModelDiscovery,
32 enrichCursorSnapshot,33 enrichCursorSnapshot,
33} from "../Layers/CursorProvider.ts";34} from "../Layers/CursorProvider.ts";
34import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";35import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
...@@ -136,7 +137,12 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {...@@ -136,7 +137,12 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
136 });137 });
137 const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv);138 const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv);
138139
139 const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).pipe(140 const discoverModels = yield* makeCursorModelDiscovery(effectiveConfig, processEnv);
141 const checkProvider = checkCursorProviderStatus(
142 effectiveConfig,
143 processEnv,
144 discoverModels,
145 ).pipe(
140 Effect.map(stampIdentity),146 Effect.map(stampIdentity),
141 Effect.provideService(Crypto.Crypto, crypto),147 Effect.provideService(Crypto.Crypto, crypto),
142 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),148 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
apps/server/src/provider/Layers/CursorProvider.test.ts+37
...@@ -16,6 +16,7 @@ import {...@@ -16,6 +16,7 @@ import {
16 buildCursorCapabilitiesFromConfigOptions,16 buildCursorCapabilitiesFromConfigOptions,
17 checkCursorProviderStatus,17 checkCursorProviderStatus,
18 discoverCursorModelsViaAcp,18 discoverCursorModelsViaAcp,
19 makeCursorModelDiscovery,
19 getCursorParameterizedModelPickerUnsupportedMessage,20 getCursorParameterizedModelPickerUnsupportedMessage,
20 parseCursorAboutOutput,21 parseCursorAboutOutput,
21 parseCursorCliConfigChannel,22 parseCursorCliConfigChannel,
...@@ -627,6 +628,42 @@ describe("checkCursorProviderStatus", () => {...@@ -627,6 +628,42 @@ describe("checkCursorProviderStatus", () => {
627});628});
628629
629describe("discoverCursorModelsViaAcp", () => {630describe("discoverCursorModelsViaAcp", () => {
631 it("reuses successful discovery until the CLI version or account changes", async () => {
632 await runNode(
633 Effect.gen(function* () {
634 const { requestLogPath, wrapperPath } = yield* makeProviderStatusEnvFixture();
635 const fileSystem = yield* FileSystem.FileSystem;
636 const settings = {
637 enabled: true,
638 binaryPath: wrapperPath,
639 apiEndpoint: "",
640 customModels: [],
641 };
642 const discover = yield* makeCursorModelDiscovery(settings, {
643 ...process.env,
644 T3_ACP_REQUEST_LOG_PATH: requestLogPath,
645 });
646 const about = {
647 version: "2026.08.11",
648 auth: { status: "authenticated" as const, label: "first@example.test" },
649 };
650 const first = yield* discover(about);
651 expect(first.length).toBeGreaterThan(0);
652 yield* fileSystem.writeFileString(requestLogPath, "");
653 expect(yield* discover(about)).toEqual(first);
654 expect(yield* fileSystem.readFileString(requestLogPath)).toBe("");
655 yield* discover({ ...about, version: "2026.08.12" });
656 expect(yield* fileSystem.readFileString(requestLogPath)).toContain("initialize");
657 yield* fileSystem.writeFileString(requestLogPath, "");
658 yield* discover({
659 version: "2026.08.12",
660 auth: { ...about.auth, label: "second@example.test" },
661 });
662 expect(yield* fileSystem.readFileString(requestLogPath)).toContain("initialize");
663 }),
664 );
665 });
666
630 it("keeps the ACP probe runtime alive long enough to discover models", async () => {667 it("keeps the ACP probe runtime alive long enough to discover models", async () => {
631 const wrapperPath = await runNode(makeMockAgentWrapper());668 const wrapperPath = await runNode(makeMockAgentWrapper());
632669
apps/server/src/provider/Layers/CursorProvider.ts+24-3
...@@ -10,6 +10,8 @@ import type {...@@ -10,6 +10,8 @@ import type {
10} from "@t3tools/contracts";10} from "@t3tools/contracts";
11import type * as EffectAcpSchema from "effect-acp/schema";11import type * as EffectAcpSchema from "effect-acp/schema";
12import { causeErrorTag } from "@t3tools/shared/observability";12import { causeErrorTag } from "@t3tools/shared/observability";
13import * as Cache from "effect/Cache";
14import * as Duration from "effect/Duration";
13import * as Crypto from "effect/Crypto";15import * as Crypto from "effect/Crypto";
14import * as DateTime from "effect/DateTime";16import * as DateTime from "effect/DateTime";
15import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
...@@ -572,6 +574,23 @@ export const discoverCursorModelsViaAcp = (...@@ -572,6 +574,23 @@ export const discoverCursorModelsViaAcp = (
572 environment?: NodeJS.ProcessEnv,574 environment?: NodeJS.ProcessEnv,
573) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment);575) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment);
574576
577// Each driver instance owns its cache; version and account changes invalidate it.
578export const makeCursorModelDiscovery = Effect.fn("makeCursorModelDiscovery")(function* (
579 cursorSettings: CursorSettings,
580 environment?: NodeJS.ProcessEnv,
581) {
582 const cache = yield* Cache.makeWith(
583 (_key: string) => discoverCursorModelsViaAcp(cursorSettings, environment),
584 {
585 capacity: 1,
586 timeToLive: (exit) =>
587 Exit.isSuccess(exit) && exit.value.length > 0 ? Duration.minutes(30) : Duration.zero,
588 },
589 );
590 return (about: Pick<CursorAboutResult, "version" | "auth">) =>
591 Cache.get(cache, JSON.stringify([about.version, about.auth]));
592});
593
575function getCursorFallbackModels(594function getCursorFallbackModels(
576 cursorSettings: Pick<CursorSettings, "customModels">,595 cursorSettings: Pick<CursorSettings, "customModels">,
577): ReadonlyArray<ServerProviderModel> {596): ReadonlyArray<ServerProviderModel> {
...@@ -989,6 +1008,7 @@ const runCursorAboutCommand = (cursorSettings: CursorSettings, environment?: Nod...@@ -989,6 +1008,7 @@ const runCursorAboutCommand = (cursorSettings: CursorSettings, environment?: Nod
989export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(function* (1008export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(function* (
990 cursorSettings: CursorSettings,1009 cursorSettings: CursorSettings,
991 environment?: NodeJS.ProcessEnv,1010 environment?: NodeJS.ProcessEnv,
1011 discoverModels?: (about: CursorAboutResult) => ReturnType<typeof discoverCursorModelsViaAcp>,
992): Effect.fn.Return<1012): Effect.fn.Return<
993 ServerProviderDraft,1013 ServerProviderDraft,
994 never,1014 never,
...@@ -1086,9 +1106,10 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(...@@ -1086,9 +1106,10 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
1086 let discoveryWarning: string | undefined;1106 let discoveryWarning: string | undefined;
1087 if (parsed.auth.status !== "unauthenticated") {1107 if (parsed.auth.status !== "unauthenticated") {
1088 const discoveryExit = yield* Effect.exit(1108 const discoveryExit = yield* Effect.exit(
1089 discoverCursorModelsViaAcp(cursorSettings, environment).pipe(1109 (discoverModels
1090 Effect.timeoutOption(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS),1110 ? discoverModels(parsed)
1091 ),1111 : discoverCursorModelsViaAcp(cursorSettings, environment)
1112 ).pipe(Effect.timeoutOption(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS)),
1092 );1113 );
1093 if (Exit.isFailure(discoveryExit)) {1114 if (Exit.isFailure(discoveryExit)) {
1094 yield* Effect.logWarning("Cursor ACP model discovery failed", {1115 yield* Effect.logWarning("Cursor ACP model discovery failed", {