| author | |
| committer | |
| log | 0d8a91a25a35618c7cae1e02791e491afa1f0a00 |
| tree | 085fa6f4380cb0f1dba93483dd1264dc14bc7ab6 |
| parent | 6349a0e68a958cc51b7b5198683c1d1db88b8d28 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 29 | import { |
| 30 | 30 | buildInitialCursorProviderSnapshot, |
| 31 | 31 | checkCursorProviderStatus, |
| 32 | makeCursorModelDiscovery, | |
| 32 | 33 | enrichCursorSnapshot, |
| 33 | 34 | } from "../Layers/CursorProvider.ts"; |
| 34 | 35 | import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; |
| ... | ... | @@ -136,7 +137,12 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 136 | 137 | }); |
| 137 | 138 | const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv); |
| 138 | 139 | |
| 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 | 146 | Effect.map(stampIdentity), |
| 141 | 147 | Effect.provideService(Crypto.Crypto, crypto), |
| 142 | 148 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
apps/server/src/provider/Layers/CursorProvider.test.ts+37| ... | ... | @@ -16,6 +16,7 @@ import { |
| 16 | 16 | buildCursorCapabilitiesFromConfigOptions, |
| 17 | 17 | checkCursorProviderStatus, |
| 18 | 18 | discoverCursorModelsViaAcp, |
| 19 | makeCursorModelDiscovery, | |
| 19 | 20 | getCursorParameterizedModelPickerUnsupportedMessage, |
| 20 | 21 | parseCursorAboutOutput, |
| 21 | 22 | parseCursorCliConfigChannel, |
| ... | ... | @@ -627,6 +628,42 @@ describe("checkCursorProviderStatus", () => { |
| 627 | 628 | }); |
| 628 | 629 | |
| 629 | 630 | describe("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 | 667 | it("keeps the ACP probe runtime alive long enough to discover models", async () => { |
| 631 | 668 | const wrapperPath = await runNode(makeMockAgentWrapper()); |
| 632 | 669 |
apps/server/src/provider/Layers/CursorProvider.ts+24-3| ... | ... | @@ -10,6 +10,8 @@ import type { |
| 10 | 10 | } from "@t3tools/contracts"; |
| 11 | 11 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 12 | 12 | import { causeErrorTag } from "@t3tools/shared/observability"; |
| 13 | import * as Cache from "effect/Cache"; | |
| 14 | import * as Duration from "effect/Duration"; | |
| 13 | 15 | import * as Crypto from "effect/Crypto"; |
| 14 | 16 | import * as DateTime from "effect/DateTime"; |
| 15 | 17 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -572,6 +574,23 @@ export const discoverCursorModelsViaAcp = ( |
| 572 | 574 | environment?: NodeJS.ProcessEnv, |
| 573 | 575 | ) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment); |
| 574 | 576 | |
| 577 | // Each driver instance owns its cache; version and account changes invalidate it. | |
| 578 | export 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 | ||
| 575 | 594 | function getCursorFallbackModels( |
| 576 | 595 | cursorSettings: Pick<CursorSettings, "customModels">, |
| 577 | 596 | ): ReadonlyArray<ServerProviderModel> { |
| ... | ... | @@ -989,6 +1008,7 @@ const runCursorAboutCommand = (cursorSettings: CursorSettings, environment?: Nod |
| 989 | 1008 | export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(function* ( |
| 990 | 1009 | cursorSettings: CursorSettings, |
| 991 | 1010 | environment?: NodeJS.ProcessEnv, |
| 1011 | discoverModels?: (about: CursorAboutResult) => ReturnType<typeof discoverCursorModelsViaAcp>, | |
| 992 | 1012 | ): Effect.fn.Return< |
| 993 | 1013 | ServerProviderDraft, |
| 994 | 1014 | never, |
| ... | ... | @@ -1086,9 +1106,10 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( |
| 1086 | 1106 | let discoveryWarning: string | undefined; |
| 1087 | 1107 | if (parsed.auth.status !== "unauthenticated") { |
| 1088 | 1108 | const discoveryExit = yield* Effect.exit( |
| 1089 | discoverCursorModelsViaAcp(cursorSettings, environment).pipe( | |
| 1090 | Effect.timeoutOption(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS), | |
| 1091 | ), | |
| 1109 | (discoverModels | |
| 1110 | ? discoverModels(parsed) | |
| 1111 | : discoverCursorModelsViaAcp(cursorSettings, environment) | |
| 1112 | ).pipe(Effect.timeoutOption(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS)), | |
| 1092 | 1113 | ); |
| 1093 | 1114 | if (Exit.isFailure(discoveryExit)) { |
| 1094 | 1115 | yield* Effect.logWarning("Cursor ACP model discovery failed", { |