From 861ce7f12b65ba404bc3fabfef170b0ec33349a7 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 24 Aug 2026 14:04:04 +0000 Subject: [PATCH 1/6] fix(provider): load Claude SDK only when needed --- .../Layers/ClaudeAdapter.lazy.test.ts | 13 +++++ .../src/provider/Layers/ClaudeAdapter.ts | 53 +++++++++---------- .../src/provider/Layers/ClaudeProvider.ts | 14 ++--- 3 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 apps/server/src/provider/Layers/ClaudeAdapter.lazy.test.ts diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.lazy.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.lazy.test.ts new file mode 100644 index 0000000000000..be50143d93d36 --- /dev/null +++ b/apps/server/src/provider/Layers/ClaudeAdapter.lazy.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +vi.mock("@anthropic-ai/claude-agent-sdk", () => { + throw new Error("Claude SDK loaded eagerly."); +}); + +describe("ClaudeAdapter module loading", () => { + it("does not load the Claude SDK while importing the adapter", async () => { + await expect(import("./ClaudeAdapter.ts")).resolves.toMatchObject({ + makeClaudeAdapter: expect.any(Function), + }); + }); +}); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 02d73e372d2b0..23fb73ca14589 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -6,19 +6,18 @@ * * @module ClaudeAdapterLive */ -import { - type CanUseTool, - query, - type Options as ClaudeQueryOptions, - type PermissionMode, - type PermissionResult, - type PermissionUpdate, - type SDKMessage, - type SDKControlGetContextUsageResponse, - type SDKResultMessage, - type SettingSource, - type SDKUserMessage, - type ModelUsage, +import type { + CanUseTool, + Options as ClaudeQueryOptions, + PermissionMode, + PermissionResult, + PermissionUpdate, + SDKMessage, + SDKControlGetContextUsageResponse, + SDKResultMessage, + SettingSource, + SDKUserMessage, + ModelUsage, } from "@anthropic-ai/claude-agent-sdk"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; import { @@ -1674,16 +1673,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const managedNativeEventLogger = options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; - const createQuery = - options?.createQuery ?? - ((input: { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - }) => - query({ - prompt: input.prompt, - options: input.options, - }) as ClaudeQueryRuntime); + const createQuery = options?.createQuery; const sessions = new Map(); const runtimeEventQueue = yield* Queue.unbounded(); @@ -4220,12 +4210,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( "claude.query.path_to_executable": claudeBinaryPath, }); - const queryRuntime = yield* Effect.try({ - try: () => - createQuery({ - prompt, - options: queryOptions, - }), + const queryRuntime = yield* Effect.tryPromise({ + try: async () => + createQuery + ? createQuery({ + prompt, + options: queryOptions, + }) + : ((await import(/* @vite-ignore */ "@anthropic-ai/claude-agent-sdk")).query({ + prompt, + options: queryOptions, + }) as ClaudeQueryRuntime), catch: (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 806f7e19b9050..5b6c7f54081a9 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -20,12 +20,11 @@ import { } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { compareSemverVersions } from "@t3tools/shared/semver"; -import { - query as claudeQuery, - type Options as ClaudeQueryOptions, - type SlashCommand as ClaudeSlashCommand, - type SDKUserMessage, - type SettingSource, +import type { + Options as ClaudeQueryOptions, + SlashCommand as ClaudeSlashCommand, + SDKUserMessage, + SettingSource, } from "@anthropic-ai/claude-agent-sdk"; import { @@ -742,6 +741,9 @@ const probeClaudeCapabilities = ( claudeEnvironment, ); return yield* Effect.tryPromise(async () => { + const { query: claudeQuery } = await import( + /* @vite-ignore */ "@anthropic-ai/claude-agent-sdk" + ); const q = claudeQuery({ // Never yield — we only need initialization data, not a conversation. // This prevents any prompt from reaching the Anthropic API. From 67dac5284f44dd4116ecd612b339773c5672ba77 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 24 Aug 2026 14:28:47 +0000 Subject: [PATCH 2/6] fix(provider): cancel delayed Claude startup --- .../src/provider/Layers/ClaudeAdapter.test.ts | 64 +++++++++++++++++++ .../src/provider/Layers/ClaudeAdapter.ts | 36 ++++++++--- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 6ec0a1ab62884..7d4a1674059ca 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -23,6 +23,7 @@ import { } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; import { assert, describe, it } from "@effect/vitest"; +import { vi } from "vite-plus/test"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -40,6 +41,34 @@ import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); +const lazyClaudeSdk = vi.hoisted(() => { + let markImportStarted!: () => void; + let markImportFinished!: () => void; + let releaseImport!: () => void; + return { + importFinished: new Promise((resolve) => { + markImportFinished = resolve; + }), + importStarted: new Promise((resolve) => { + markImportStarted = resolve; + }), + markImportFinished: () => markImportFinished(), + markImportStarted: () => markImportStarted(), + query: vi.fn(), + releaseImport: () => releaseImport(), + waitForRelease: new Promise((resolve) => { + releaseImport = resolve; + }), + }; +}); + +vi.mock("@anthropic-ai/claude-agent-sdk", async () => { + lazyClaudeSdk.markImportStarted(); + await lazyClaudeSdk.waitForRelease; + lazyClaudeSdk.markImportFinished(); + return { query: lazyClaudeSdk.query }; +}); + // Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`. class ClaudeAdapter extends Context.Service()( "t3/provider/Layers/ClaudeAdapter.test/ClaudeAdapter", @@ -1662,6 +1691,41 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect("does not start a Claude runtime after lazy loading is interrupted", () => { + const layer = Layer.effect( + ClaudeAdapter, + Effect.gen(function* () { + const claudeConfig = decodeClaudeSettings({}); + return yield* makeClaudeAdapter(claudeConfig); + }), + ).pipe( + Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), + Layer.provideMerge(ServerSettingsService.layerTest()), + Layer.provideMerge(NodeServices.layer), + ); + + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + const startFiber = yield* Effect.forkChild( + adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + runtimeMode: "full-access", + }), + ); + + yield* Effect.promise(() => lazyClaudeSdk.importStarted); + yield* Fiber.interrupt(startFiber); + lazyClaudeSdk.releaseImport(); + yield* Effect.promise(() => lazyClaudeSdk.importFinished); + + assert.equal(lazyClaudeSdk.query.mock.calls.length, 0); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(layer), + ); + }); + it.effect("keeps the session available when process close fails", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 23fb73ca14589..cde491d31f7cd 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -4211,16 +4211,32 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( }); const queryRuntime = yield* Effect.tryPromise({ - try: async () => - createQuery - ? createQuery({ - prompt, - options: queryOptions, - }) - : ((await import(/* @vite-ignore */ "@anthropic-ai/claude-agent-sdk")).query({ - prompt, - options: queryOptions, - }) as ClaudeQueryRuntime), + try: async (signal) => { + const query = + createQuery ?? + (await import(/* @vite-ignore */ "@anthropic-ai/claude-agent-sdk")).query; + signal.throwIfAborted(); + const runtime = query({ + prompt, + options: queryOptions, + }) as ClaudeQueryRuntime; + let closed = false; + const close = () => { + if (closed) return; + closed = true; + try { + runtime.close(); + } catch { + // Interruption remains authoritative when late runtime cleanup fails. + } + }; + signal.addEventListener("abort", close, { once: true }); + if (signal.aborted) { + close(); + signal.throwIfAborted(); + } + return runtime; + }, catch: (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, From 679a037d57186cccc3cc449f64f5903dabe53654 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 26 Aug 2026 08:24:06 +0000 Subject: [PATCH 3/6] fix(provider): make Claude SDK opt-in --- packages/provider-runtime/package.json | 17 ++++++++++++++++- pnpm-lock.yaml | 6 +++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/provider-runtime/package.json b/packages/provider-runtime/package.json index 7eb15408f527e..04f461c50b5f8 100644 --- a/packages/provider-runtime/package.json +++ b/packages/provider-runtime/package.json @@ -2,7 +2,13 @@ "name": "@t3tools/provider-runtime", "version": "0.0.33", "description": "Embeddable T3 Code provider runtime.", + "keywords": [], + "homepage": "https://github.com/vite-hub/t3code#readme", + "bugs": { + "url": "https://github.com/vite-hub/t3code/issues" + }, "license": "MIT", + "author": "", "repository": { "type": "git", "url": "git+https://github.com/vite-hub/t3code.git", @@ -12,6 +18,7 @@ "dist" ], "type": "module", + "main": "index.js", "types": "./dist/index.d.mts", "exports": { ".": { @@ -25,16 +32,24 @@ "test": "vp test run" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.170", "@effect/platform-node": "catalog:", "@t3tools/contracts": "https://pkg.pr.new/vite-hub/t3code/@t3tools/contracts@4934f87487ab50a956c4abc030063ebc1b0ba8d6", "effect": "catalog:" }, "devDependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.170", "@effect/vitest": "catalog:", "@types/node": "catalog:", "vite-plus": "catalog:" }, + "peerDependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.170" + }, + "peerDependenciesMeta": { + "@anthropic-ai/claude-agent-sdk": { + "optional": true + } + }, "engines": { "node": "^22.16 || ^23.11 || >=24.10" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0247d5ef1b22..ae4ec4b341902 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -839,9 +839,6 @@ importers: packages/provider-runtime: dependencies: - '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.170 - version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@effect/platform-node': specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(ioredis@5.11.0)(utf-8-validate@6.0.6) @@ -852,6 +849,9 @@ importers: specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6) devDependencies: + '@anthropic-ai/claude-agent-sdk': + specifier: ^0.3.170 + version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@effect/vitest': specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6)) From 78dbd5224d79e3964ab35988d66c74dd70c723b7 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 26 Aug 2026 08:24:53 +0000 Subject: [PATCH 4/6] chore(provider): preserve package metadata --- packages/provider-runtime/package.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/provider-runtime/package.json b/packages/provider-runtime/package.json index 04f461c50b5f8..fcbb5f862ce60 100644 --- a/packages/provider-runtime/package.json +++ b/packages/provider-runtime/package.json @@ -2,13 +2,7 @@ "name": "@t3tools/provider-runtime", "version": "0.0.33", "description": "Embeddable T3 Code provider runtime.", - "keywords": [], - "homepage": "https://github.com/vite-hub/t3code#readme", - "bugs": { - "url": "https://github.com/vite-hub/t3code/issues" - }, "license": "MIT", - "author": "", "repository": { "type": "git", "url": "git+https://github.com/vite-hub/t3code.git", @@ -18,7 +12,6 @@ "dist" ], "type": "module", - "main": "index.js", "types": "./dist/index.d.mts", "exports": { ".": { From ace4e6c2730220e4557a85720377436ff38a1bda Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 26 Aug 2026 08:34:59 +0000 Subject: [PATCH 5/6] fix(provider): avoid pnpm peer auto-install --- packages/provider-runtime/package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/provider-runtime/package.json b/packages/provider-runtime/package.json index fcbb5f862ce60..38158d94f9381 100644 --- a/packages/provider-runtime/package.json +++ b/packages/provider-runtime/package.json @@ -35,14 +35,6 @@ "@types/node": "catalog:", "vite-plus": "catalog:" }, - "peerDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.170" - }, - "peerDependenciesMeta": { - "@anthropic-ai/claude-agent-sdk": { - "optional": true - } - }, "engines": { "node": "^22.16 || ^23.11 || >=24.10" } From 1248f5ddb9319be23980bb04d5a51ab297c69eaa Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 27 Aug 2026 22:58:34 +0000 Subject: [PATCH 6/6] fix(desktop): stage lazy Claude SDK --- scripts/build-desktop-artifact.test.ts | 1 + scripts/lib/cli-external-packages.test.ts | 13 ++++++++++--- scripts/lib/cli-external-packages.ts | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index a7bb1b9b7c4c9..883000f2cbd61 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -560,6 +560,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { fffNodeVersion: "0.9.4", }), { + "@anthropic-ai/claude-agent-sdk": "^0.3.170", "@ff-labs/fff-node": "0.9.4", "msgpackr-extract": "3.0.4", "node-pty": "1.1.0", diff --git a/scripts/lib/cli-external-packages.test.ts b/scripts/lib/cli-external-packages.test.ts index 754cd646f17d2..d247f66f90874 100644 --- a/scripts/lib/cli-external-packages.test.ts +++ b/scripts/lib/cli-external-packages.test.ts @@ -69,25 +69,32 @@ describe("shouldBundleCliDependency", () => { }); describe("selectCliRuntimeExternalDependencies", () => { - it("keeps only runtime-external dependency roots for the Windows sidecar", () => { + it("keeps desktop runtime dependency roots for the Windows sidecar", () => { assert.deepStrictEqual( selectCliRuntimeExternalDependencies({ + "@anthropic-ai/claude-agent-sdk": "3.0.0", "@effect/platform-bun": "1.0.0", "@ff-labs/fff-node": "2.0.0", effect: "3.0.0", "node-pty": "4.0.0", }), { + "@anthropic-ai/claude-agent-sdk": "3.0.0", "@ff-labs/fff-node": "2.0.0", "node-pty": "4.0.0", }, ); }); - it("selects every external root declared by the server", () => { + it("selects every staged runtime root declared by the server", () => { assert.deepStrictEqual( Object.keys(selectCliRuntimeExternalDependencies(serverPackageJson.dependencies)).sort(), - ["@ff-labs/fff-node", "msgpackr-extract", "node-pty"], + [ + "@anthropic-ai/claude-agent-sdk", + "@ff-labs/fff-node", + "msgpackr-extract", + "node-pty", + ], ); }); }); diff --git a/scripts/lib/cli-external-packages.ts b/scripts/lib/cli-external-packages.ts index d7a89bc408a4e..aae4809cb783a 100644 --- a/scripts/lib/cli-external-packages.ts +++ b/scripts/lib/cli-external-packages.ts @@ -63,6 +63,12 @@ export const CLI_BUILD_ONLY_EXTERNAL_PREFIXES = [ "@effect/sql-sqlite-bun", ] as const; +// Computed imports do not reach the bundler's package hooks, but desktop +// artifacts still need their package roots in the staged production install. +const CLI_RUNTIME_STAGE_ONLY_DEPENDENCIES = [ + "@anthropic-ai/claude-agent-sdk", +] as const; + export const CLI_EXTERNAL_PACKAGE_PREFIXES = [ ...CLI_RUNTIME_EXTERNAL_PREFIXES, ...CLI_BUILD_ONLY_EXTERNAL_PREFIXES, @@ -92,12 +98,16 @@ export function shouldBundleCliDependency(id: string): boolean { return !isExternalCliDependency(id); } -/** Select direct dependency roots whose runtime closure belongs in the sidecar. */ +/** Select direct dependency roots whose runtime closure belongs in desktop artifacts. */ export function selectCliRuntimeExternalDependencies( dependencies: Readonly>, ): Record { return Object.fromEntries( - Object.entries(dependencies).filter(([name]) => isRuntimeExternalCliDependency(name)), + Object.entries(dependencies).filter( + ([name]) => + isRuntimeExternalCliDependency(name) || + CLI_RUNTIME_STAGE_ONLY_DEPENDENCIES.some((dependency) => dependency === name), + ), ); }