diff --git a/apps/server/src/provider/AntigravityProvider.ts b/apps/server/src/provider/AntigravityProvider.ts index ed047331e73ee..b140c7511ca14 100644 --- a/apps/server/src/provider/AntigravityProvider.ts +++ b/apps/server/src/provider/AntigravityProvider.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { ANTIGRAVITY_DEFAULT_MODEL, ProviderDriverKind, @@ -40,7 +41,7 @@ import { antigravityThinkingConfig } from "./acp/AntigravityAcpSupport.ts"; const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] }); const MAX_WORKSPACE_SNAPSHOTS = 32; -const HEALTH_CHECK_TIMEOUT = "90 seconds"; +const HEALTH_CHECK_TIMEOUT_MS = 90_000; const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity."; const AUTH_UNCHECKED_MESSAGE = "Antigravity is installed. Google account access is not checked yet."; @@ -155,6 +156,7 @@ interface AntigravityProviderState { } interface AntigravityProviderOptions { + readonly environment?: NodeJS.ProcessEnv; readonly stampIdentity: (snapshot: ServerProviderDraft) => Effect.Effect; readonly probe: Effect.Effect< EffectAcpSchema.InitializeResponse, @@ -271,10 +273,12 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func const checkProvider = Effect.fn("checkAntigravityProvider")(function* () { if (!settings.enabled) return yield* getSnapshot; const before = yield* SubscriptionRef.get(metadata); - const result = yield* options.probe.pipe( - Effect.timeoutOption(HEALTH_CHECK_TIMEOUT), - Effect.result, + const timeoutMs = resolveProviderProbeTimeoutMs( + "antigravity", + HEALTH_CHECK_TIMEOUT_MS, + options.environment, ); + const result = yield* options.probe.pipe(Effect.timeoutOption(timeoutMs), Effect.result); const initialized = Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : undefined; const version = initialized?.agentInfo?.version; @@ -289,7 +293,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func ? "Antigravity ACP runtime is not installed or its executable could not be found." : failure ? "Antigravity could not complete its local health check." - : `Antigravity did not respond to its local health check within ${HEALTH_CHECK_TIMEOUT}.`; + : `Antigravity did not respond to its local health check within ${timeoutMs / 1_000} seconds.`; const supportsTextGeneration = initialized !== undefined ? yield* options.supportsTextGeneration : false; const updatedAt = DateTime.formatIso(yield* DateTime.now); diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index eafd128d610fb..f969544fad064 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { type ClaudeSettings, type ModelCapabilities, @@ -484,7 +485,9 @@ const probeClaudeCapabilities = ( catch: (cause) => new ClaudeCapabilitiesProbeSdkError({ operation: "initialize", cause }), }); }).pipe( - Effect.timeout(CAPABILITIES_PROBE_TIMEOUT_MS), + Effect.timeout( + resolveProviderProbeTimeoutMs("claude", CAPABILITIES_PROBE_TIMEOUT_MS, environment), + ), Effect.flatMap(({ q, init, graceMs }) => Effect.gen(function* () { // Usage has its own deadline so a slow optional request cannot discard initialization. @@ -651,7 +654,12 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( ["--version"], providerProbeEnvironment(resolvedEnvironment), cwd, - ).pipe(Effect.timeoutOption(CLAUDE_VERSION_CHECK_TIMEOUT_MS), Effect.result); + ).pipe( + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("claude", CLAUDE_VERSION_CHECK_TIMEOUT_MS, resolvedEnvironment), + ), + Effect.result, + ); if (Result.isFailure(versionProbe)) { const error = versionProbe.failure; @@ -756,7 +764,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( auth: { status: "unknown" }, message: probeFailure ? Cause.isTimeoutError(probeFailure) - ? `Timed out after ${CAPABILITIES_PROBE_TIMEOUT_MS / 1_000}s while checking Claude account status.` + ? `Timed out after ${resolveProviderProbeTimeoutMs("claude", CAPABILITIES_PROBE_TIMEOUT_MS, resolvedEnvironment) / 1_000}s while checking Claude account status.` : "Claude Agent CLI failed while checking account status." : "Could not verify Claude authentication status from initialization result.", }, diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index 197eed97ff820..ab2cda61e53ee 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { pipe } from "effect/Function"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; @@ -692,6 +693,7 @@ const readCodexRateLimits = ( ); const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: { + readonly probeTimeoutMs: number; readonly binaryPath: string; readonly homePath?: string; readonly launchArgs?: string; @@ -716,7 +718,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun } // Optional discovery must finish before the outer timeout can discard the account result. - const deadlineMs = spawnedAtMs + CODEX_AUTH_PROBE_TIMEOUT_MS - CODEX_DISCOVERY_MARGIN_MS; + const deadlineMs = spawnedAtMs + input.probeTimeoutMs - CODEX_DISCOVERY_MARGIN_MS; const [skillsResponse, models, { config, nativeUsage, rateLimits }] = yield* Effect.all( [ withCodexDiscoveryDeadline(client.request("skills/list", { cwds: [input.cwd] }), deadlineMs), @@ -800,7 +802,12 @@ const probeCodexCliVersion = Effect.fn("probeCodexCliVersion")( if (result.code !== 0) return null; return CODEX_CLI_VERSION_PATTERN.exec(result.stdout)?.[1] ?? null; }, - Effect.timeoutOption(Duration.millis(DEFAULT_TIMEOUT_MS)), + (effect, input) => + effect.pipe( + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("codex", DEFAULT_TIMEOUT_MS, input.environment), + ), + ), Effect.map(Option.getOrNull), Effect.catch((cause) => Effect.logDebug("Codex --version fallback failed.", { cause }).pipe(Effect.as(null)), @@ -909,6 +916,7 @@ function accountProbeStatus( export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(function* ( codexSettings: CodexSettings, probe: (input: { + readonly probeTimeoutMs: number; readonly binaryPath: string; readonly homePath?: string; readonly launchArgs?: string; @@ -948,7 +956,13 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu }); } + const probeTimeoutMs = resolveProviderProbeTimeoutMs( + "codex", + CODEX_AUTH_PROBE_TIMEOUT_MS, + resolvedEnvironment, + ); const probeResult = yield* probe({ + probeTimeoutMs, binaryPath: codexSettings.binaryPath, homePath: codexSettings.homePath, launchArgs: resolveCodexLaunchArgs(codexSettings.launchArgs, resolvedEnvironment), @@ -957,7 +971,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu ...(managedAuth ? { skipNativeUsage: true } : {}), }).pipe( // Process teardown can outlast the work budget without discarding a completed snapshot. - Effect.timeoutOption(Duration.millis(CODEX_AUTH_PROBE_TIMEOUT_MS)), + Effect.timeoutOption(Duration.millis(probeTimeoutMs)), Effect.scoped, Effect.result, ); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 154020e0b64bd..75bca5acc87be 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -434,6 +434,7 @@ export const AntigravityDriver: ProviderDriver< Effect.provideService(HttpClient.HttpClient, httpClient), ); const provider = yield* makeAntigravityProvider(settings, { + environment: processEnvironment, stampIdentity: classifyModels, probe, probeUsage: usage.probe, diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 50fbbc7024b99..f2bfa4082dcbf 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts"; import * as Digest from "effect/crypto/Digest"; import * as SecureRandom from "effect/crypto/SecureRandom"; @@ -333,7 +334,7 @@ export const CodexDriver: ProviderDriver< environment: processEnv, }).pipe( Effect.scoped, - Effect.timeout("20 seconds"), + Effect.timeout(resolveProviderProbeTimeoutMs("codex", 20_000, processEnv)), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), ), ]).pipe( diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index c0f60b9d77e66..0601714dd618e 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -504,6 +504,20 @@ it.layer( }), ); + it.effect("allows a slow successful Codex probe with a configured budget", () => + Effect.gen(function* () { + const fiber = yield* checkCodexProviderStatus( + defaultCodexSettings, + () => Effect.sleep("22 seconds").pipe(Effect.as(makeCodexProbeSnapshot())), + { T3CODE_CODEX_PROBE_TIMEOUT_MS: "60000" }, + ).pipe(Effect.forkChild); + yield* TestClock.adjust("22 seconds"); + const status = yield* Fiber.join(fiber); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.auth.status, "authenticated"); + }), + ); + it.effect("labels the plan the usage read reports over the cached login plan", () => Effect.gen(function* () { const statusWithUsagePlan = (planType: string | null) => diff --git a/docs/user/install.md b/docs/user/install.md index ac0913f18d119..b594894efe377 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -247,6 +247,47 @@ For provider-specific setup and accounts, see [Codex](./providers-codex.md), [Antigravity](./providers-antigravity.md), [Kiro](./providers-kiro.md), [Pi](./providers-pi.md), [Muse Code](./providers-muse.md), and [fx](./providers-fx.md). +### Provider check timeouts + +If a provider works in its CLI but T3 Code reports a timeout while checking it, +allow more time in the T3 server's environment. For example, give Codex and +Claude readiness checks 60 seconds: + +```bash +T3CODE_CODEX_PROBE_TIMEOUT_MS=60000 T3CODE_CLAUDE_PROBE_TIMEOUT_MS=60000 t3 serve +``` + +For a remote environment, set these on the machine running the T3 server. +Keep them in your launcher or service configuration to retain them across +updates, then restart the server. For systemd, add +`Environment=T3CODE_CODEX_PROBE_TIMEOUT_MS=60000` under `[Service]` in a service +override, reload systemd, and restart the service. Provider instances can also +set their own overrides in Settings → Providers. + +Each variable extends that provider's readiness-probe deadlines; a phase whose +default is already longer keeps its default. Providers with several phases can +take longer than one deadline in total. Values must be whole milliseconds no +greater than 2147483647; unset or invalid values keep the defaults. + +| Provider | Environment variable | +| ------------------- | -------------------------------------- | +| Codex | `T3CODE_CODEX_PROBE_TIMEOUT_MS` | +| Claude | `T3CODE_CLAUDE_PROBE_TIMEOUT_MS` | +| Cursor | `T3CODE_CURSOR_PROBE_TIMEOUT_MS` | +| Antigravity | `T3CODE_ANTIGRAVITY_PROBE_TIMEOUT_MS` | +| Grok | `T3CODE_GROK_PROBE_TIMEOUT_MS` | +| Kiro | `T3CODE_KIRO_PROBE_TIMEOUT_MS` | +| fx | `T3CODE_FX_PROBE_TIMEOUT_MS` | +| Pi | `T3CODE_PI_PROBE_TIMEOUT_MS` | +| Muse Code | `T3CODE_MUSE_PROBE_TIMEOUT_MS` | +| OpenCode | `T3CODE_OPENCODE_PROBE_TIMEOUT_MS` | +| ACP Registry agents | `T3CODE_ACP_REGISTRY_PROBE_TIMEOUT_MS` | + +Longer deadlines also take longer to report a stalled check. These overrides +cover readiness and discovery, including OpenCode server startup. Usage and +config enrichment retain their own shorter limits. Turn execution, package +installation, update checks, and shutdown limits keep their existing behavior. + ## Next steps - [Working with threads](./thread-sidebar.md): start tasks and organize parallel work. diff --git a/docs/user/providers-codex.md b/docs/user/providers-codex.md index 63f7f9ac95b1e..f7389f75b4175 100644 --- a/docs/user/providers-codex.md +++ b/docs/user/providers-codex.md @@ -33,6 +33,13 @@ Running `codex login` does not change a managed ChatGPT connection. In and complete the wizard. Then select that instance from the thread's model picker. See [Use multiple accounts](#use-multiple-accounts). +## Allow more time for provider checks + +If an authenticated Codex installation reports “Timed out while checking Codex +app-server provider status,” set `T3CODE_CODEX_PROBE_TIMEOUT_MS=60000` in the +T3 server's environment and restart it. See [Provider check timeouts](./install.md#provider-check-timeouts) +for configuration and other providers. + ## Use multiple accounts Add another ChatGPT account in **Settings → Providers**, then select the account diff --git a/packages/provider-acp-registry/src/server/probe.ts b/packages/provider-acp-registry/src/server/probe.ts index b450fe28de1e6..2fcf80cbce4d9 100644 --- a/packages/provider-acp-registry/src/server/probe.ts +++ b/packages/provider-acp-registry/src/server/probe.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { AcpRegistryListProvidersResult, AcpRegistryOperationError, @@ -42,8 +43,7 @@ const MAX_ID_LENGTH = 128; const MAX_NAME_LENGTH = 160; const MAX_DESCRIPTION_LENGTH = 1_024; const MAX_COMMAND_LINE_LENGTH = 2_048; -const PROBE_TIMEOUT_SECONDS = 60; -const PROBE_TIMEOUT = `${PROBE_TIMEOUT_SECONDS} seconds`; +const PROBE_TIMEOUT_MS = 60_000; const COMMAND_ADVERTISEMENT_GRACE = "500 millis"; const PROBE_SESSION_DELETE_TIMEOUT = "5 seconds"; @@ -332,6 +332,11 @@ export const probeAcpRegistryConfiguration = Effect.fn("AcpRegistryProbe.probeCo | SecureRandom.SecureRandom > { const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog; + const probeTimeoutMs = resolveProviderProbeTimeoutMs( + "acp_registry", + PROBE_TIMEOUT_MS, + input.environment, + ); const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const random = yield* SecureRandom.SecureRandom; const pty = yield* Effect.serviceOption(PtyAdapter.PtyAdapter); @@ -444,12 +449,12 @@ export const probeAcpRegistryConfiguration = Effect.fn("AcpRegistryProbe.probeCo }).pipe( Effect.scoped, Effect.timeoutOrElse({ - duration: PROBE_TIMEOUT, + duration: probeTimeoutMs, orElse: () => Effect.fail( new AcpRegistryOperationError({ reason: "probe_failed", - message: `The ACP agent did not resolve and create a test session within ${PROBE_TIMEOUT_SECONDS} seconds. Package installation or agent startup may be slow; this check retries on the next provider refresh.`, + message: `The ACP agent did not resolve and create a test session within ${probeTimeoutMs / 1_000} seconds. Package installation or agent startup may be slow; this check retries on the next provider refresh.`, }), ), }), diff --git a/packages/provider-core/src/server/snapshotProbe.test.ts b/packages/provider-core/src/server/snapshotProbe.test.ts index 26b66751516c5..e2d1e166c2031 100644 --- a/packages/provider-core/src/server/snapshotProbe.test.ts +++ b/packages/provider-core/src/server/snapshotProbe.test.ts @@ -12,6 +12,7 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { isCommandMissingCause, + resolveProviderProbeTimeoutMs, parseGenericCliVersion, providerModelsFromSettings, spawnAndCollect, @@ -161,6 +162,37 @@ describe("ProviderCommandNotFoundError", () => { }); }); +describe("resolveProviderProbeTimeoutMs", () => { + it.each([ + "codex", + "claude", + "cursor", + "antigravity", + "grok", + "kiro", + "fx", + "pi", + "muse", + "opencode", + "acp_registry", + ])("uses only %s's readiness override", (provider) => { + const key = `T3CODE_${provider.toUpperCase()}_PROBE_TIMEOUT_MS`; + expect(resolveProviderProbeTimeoutMs(provider, 25000, { [key]: "60000" })).toBe(60000); + expect(resolveProviderProbeTimeoutMs(provider, 25000, { [key]: "1000" })).toBe(25000); + expect( + resolveProviderProbeTimeoutMs(provider, 25000, { T3CODE_OTHER_PROBE_TIMEOUT_MS: "60000" }), + ).toBe(25000); + }); + it.each([undefined, "", " ", "bad", "0", "-1", "1.5", "Infinity", "2147483648"])( + "retains the phase default for invalid value %s", + (value) => { + expect( + resolveProviderProbeTimeoutMs("claude", 25000, { T3CODE_CLAUDE_PROBE_TIMEOUT_MS: value }), + ).toBe(25000); + }, + ); +}); + describe("spawnAndCollect", () => { // regression: #35262 - without forceKillAfter a timed-out probe whose CLI ignores // SIGTERM waits for it to exit, so the caller's timeout never returns. diff --git a/packages/provider-core/src/server/snapshotProbe.ts b/packages/provider-core/src/server/snapshotProbe.ts index a30b69ac98661..d86879091b6d8 100644 --- a/packages/provider-core/src/server/snapshotProbe.ts +++ b/packages/provider-core/src/server/snapshotProbe.ts @@ -45,6 +45,19 @@ export const isWindowsCommandNotFound = Effect.fn("isWindowsCommandNotFound")(fu export const DEFAULT_TIMEOUT_MS = 4_000; export const AUTH_PROBE_TIMEOUT_MS = 10_000; +export const resolveProviderProbeTimeoutMs = ( + provider: string, + defaultTimeoutMs: number, + environment: NodeJS.ProcessEnv = process.env, +): number => { + const timeoutMs = Number(environment[`T3CODE_${provider.toUpperCase()}_PROBE_TIMEOUT_MS`]); + // Overrides only extend: callers nest these deadlines and size optional work inside them + // against the defaults. Node timers overflow above a signed 32-bit delay. + return Number.isInteger(timeoutMs) && timeoutMs > defaultTimeoutMs && timeoutMs <= 2_147_483_647 + ? timeoutMs + : defaultTimeoutMs; +}; + export const COMPACT_SLASH_COMMAND = { name: "compact", description: "Summarize the conversation and reduce context usage", diff --git a/packages/provider-cursor/src/server/status.test.ts b/packages/provider-cursor/src/server/status.test.ts index ddbddcac1f1eb..9854fb7c26978 100644 --- a/packages/provider-cursor/src/server/status.test.ts +++ b/packages/provider-cursor/src/server/status.test.ts @@ -197,6 +197,33 @@ describe("checkCursorProviderStatus", () => { }), ); + it.effect("allows slow SDK discovery with a configured readiness timeout", () => + Effect.gen(function* () { + const fiber = yield* checkCursorProviderStatus(baseCursorSettings, { + CURSOR_API_KEY: "test-key", + T3CODE_CURSOR_PROBE_TIMEOUT_MS: "30000", + }).pipe( + Effect.provide( + CursorSdkCatalog.layerTest(() => + Effect.sleep("20 seconds").pipe( + Effect.as({ + user: { + apiKeyName: "test", + userEmail: "cursor@example.com", + createdAt: "2026-01-01", + }, + models: [sdkParameterizedModel], + }), + ), + ), + ), + Effect.forkChild, + ); + yield* TestClock.adjust("20 seconds"); + expect((yield* Fiber.join(fiber)).status).toBe("ready"); + }), + ); + it.effect("surfaces SDK authentication failures", () => Effect.gen(function* () { const provider = yield* checkCursorProviderStatus(baseCursorSettings, { diff --git a/packages/provider-cursor/src/server/status.ts b/packages/provider-cursor/src/server/status.ts index 0330fcaca0bc1..0c4d8ae031aaf 100644 --- a/packages/provider-cursor/src/server/status.ts +++ b/packages/provider-cursor/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import type { SDKModel, SDKUser } from "@cursor/sdk"; import type { ModelCapabilities, @@ -286,8 +287,13 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( } const sdkCatalog = yield* CursorSdkCatalog.CursorSdkCatalog; + const timeoutMs = resolveProviderProbeTimeoutMs( + "cursor", + CURSOR_SDK_CATALOG_TIMEOUT_MS, + environment, + ); const readCatalog = Effect.suspend(() => sdkCatalog.read(sdkApiKey)).pipe( - Effect.timeoutOption(CURSOR_SDK_CATALOG_TIMEOUT_MS), + Effect.timeoutOption(timeoutMs), ); // api.cursor.com latency is intermittent; one slow read should not flip the provider to error. const catalogResult = yield* readCatalog.pipe( @@ -338,7 +344,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( version: null, status: "error", auth: { status: "unknown" }, - message: `Cursor SDK catalog request timed out after ${CURSOR_SDK_CATALOG_TIMEOUT_MS}ms.`, + message: `Cursor SDK catalog request timed out after ${timeoutMs}ms.`, }, }); } diff --git a/packages/provider-fx/src/server/status.ts b/packages/provider-fx/src/server/status.ts index d2196dd9c787e..581b9549199e3 100644 --- a/packages/provider-fx/src/server/status.ts +++ b/packages/provider-fx/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { type ServerProviderAuth, type ServerProviderModel } from "@t3tools/contracts"; import type { FxSettings } from "../settings.ts"; import { createModelCapabilities } from "@t3tools/shared/model"; @@ -166,7 +167,9 @@ export const checkFxProviderStatus = Effect.fn("checkFxProviderStatus")(function const probeEnvironment = providerProbeEnvironment(environment); const versionResult = yield* runFxCli(settings, ["--version"], probeEnvironment).pipe( - Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("fx", VERSION_PROBE_TIMEOUT_MS, environment), + ), Effect.result, ); if (Result.isFailure(versionResult) || Option.isNone(versionResult.success)) { @@ -193,7 +196,7 @@ export const checkFxProviderStatus = Effect.fn("checkFxProviderStatus")(function const read = (args: ReadonlyArray) => runFxCli(settings, args, probeEnvironment).pipe( - Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.timeoutOption(resolveProviderProbeTimeoutMs("fx", AUTH_PROBE_TIMEOUT_MS, environment)), Effect.map(Option.filter((output) => output.code === 0)), Effect.map(Option.map((output) => output.stdout)), Effect.orElseSucceed(() => Option.none()), diff --git a/packages/provider-grok/src/server/skills.ts b/packages/provider-grok/src/server/skills.ts index f06eaa140ba21..a87545c09b905 100644 --- a/packages/provider-grok/src/server/skills.ts +++ b/packages/provider-grok/src/server/skills.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; // The CLI catalog honors ignored/disabled skills and nested plugin skills that flat scans miss. import type { ServerProviderSkill } from "@t3tools/contracts"; import type { GrokSettings } from "../settings.ts"; @@ -112,7 +113,9 @@ export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* ( cause, }), ), - Effect.timeoutOption(GROK_SKILLS_PROBE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("grok", GROK_SKILLS_PROBE_TIMEOUT_MS, environment), + ), ); if (Option.isNone(inspectResult)) { diff --git a/packages/provider-grok/src/server/status.ts b/packages/provider-grok/src/server/status.ts index aea0f8120bd5e..5c3d0042ce176 100644 --- a/packages/provider-grok/src/server/status.ts +++ b/packages/provider-grok/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { type CustomModelSetting, type ModelCapabilities, @@ -313,7 +314,7 @@ const probeGrokModels = Effect.fn("GrokProviderStatus.probeGrokModels")(function environment: NodeJS.ProcessEnv, ) { const modelsResult = yield* runGrokCliCommand(grokSettings, ["models"], environment).pipe( - Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.timeoutOption(resolveProviderProbeTimeoutMs("grok", AUTH_PROBE_TIMEOUT_MS, environment)), Effect.result, ); // Only a clean exit is parsed. Failed invocations print help or error text that @@ -429,7 +430,12 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func grokSettings, ["--version"], probeEnvironment, - ).pipe(Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), Effect.result); + ).pipe( + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("grok", VERSION_PROBE_TIMEOUT_MS, environment), + ), + Effect.result, + ); if (Result.isFailure(versionResult)) { const error = versionResult.failure; @@ -517,7 +523,9 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func ); const acpExit = yield* discoverGrokMetadataViaAcpInitialize(grokSettings, probeEnvironment).pipe( - Effect.timeoutOption(GROK_ACP_INITIALIZE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("grok", GROK_ACP_INITIALIZE_TIMEOUT_MS, environment), + ), Effect.exit, ); const acpMetadata = Exit.isSuccess(acpExit) ? Option.getOrUndefined(acpExit.value) : undefined; diff --git a/packages/provider-kiro/src/server/status.ts b/packages/provider-kiro/src/server/status.ts index 93298551527d5..6c1549e27e6e3 100644 --- a/packages/provider-kiro/src/server/status.ts +++ b/packages/provider-kiro/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { type ModelCapabilities, ProviderDriverKind, @@ -376,7 +377,9 @@ export const checkKiroProviderStatus = Effect.fn("checkKiroProviderStatus")(func const probeEnvironment = providerProbeEnvironment(environment); const versionResult = yield* runKiroCli(settings, ["--version"], probeEnvironment).pipe( - Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("kiro", VERSION_PROBE_TIMEOUT_MS, environment), + ), Effect.result, ); if (Result.isFailure(versionResult) || Option.isNone(versionResult.success)) { @@ -407,7 +410,10 @@ export const checkKiroProviderStatus = Effect.fn("checkKiroProviderStatus")(func settings, ["whoami", "--format", "json"], probeEnvironment, - ).pipe(Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), Effect.result); + ).pipe( + Effect.timeoutOption(resolveProviderProbeTimeoutMs("kiro", AUTH_PROBE_TIMEOUT_MS, environment)), + Effect.result, + ); const auth = kiroAuthFromWhoami( Result.isSuccess(whoamiResult) ? Option.getOrUndefined(whoamiResult.success) : undefined, environment, @@ -433,12 +439,16 @@ export const checkKiroProviderStatus = Effect.fn("checkKiroProviderStatus")(func ["chat", "--list-models", "--format", "json"], probeEnvironment, ).pipe( - Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("kiro", AUTH_PROBE_TIMEOUT_MS, environment), + ), Effect.map((output) => kiroModelsFromList(Option.getOrUndefined(output))), Effect.orElseSucceed(() => undefined), ), probeKiroAgents(settings, probeEnvironment).pipe( - Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("kiro", AUTH_PROBE_TIMEOUT_MS, environment), + ), Effect.map(Option.getOrUndefined), Effect.orElseSucceed(() => undefined), ), diff --git a/packages/provider-muse/src/server/modelCatalog.ts b/packages/provider-muse/src/server/modelCatalog.ts index b8a62297ddf59..a99deca5354db 100644 --- a/packages/provider-muse/src/server/modelCatalog.ts +++ b/packages/provider-muse/src/server/modelCatalog.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import type { SendUserTurnOptions } from "@muse-code/sdk"; import { ModelCapabilities, ServerProviderModel } from "@t3tools/contracts"; import { MuseSettings } from "../settings.ts"; @@ -11,6 +12,9 @@ import { type MuseSdkHost, } from "./sdk.ts"; +export const resolveMuseStartupTimeoutMs = (environment?: NodeJS.ProcessEnv) => + resolveProviderProbeTimeoutMs("muse", 8_000, environment); + type MuseReasoningEffort = NonNullable["reasoningEffort"]>; const REASONING_EFFORT_LABELS = { @@ -111,7 +115,7 @@ export const discoverMuseModels = Effect.fn("discoverMuseModels")(function* ( ...(environment ? { environment } : {}), ...(cwd ? { cwd } : {}), readOnly: true, - startupTimeoutMs: 8_000, + startupTimeoutMs: resolveMuseStartupTimeoutMs(environment), }, createHost, ), diff --git a/packages/provider-muse/src/server/status.ts b/packages/provider-muse/src/server/status.ts index ef4940df11641..344ae1d1a684e 100644 --- a/packages/provider-muse/src/server/status.ts +++ b/packages/provider-muse/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { ServerProviderModel } from "@t3tools/contracts"; import { MuseSettings } from "../settings.ts"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -9,7 +10,11 @@ import * as ChildProcess from "effect/process/ChildProcess"; import { createMuseSdkHost, makeMuseEnvironment } from "./sdk.ts"; import { parseMuseVersion } from "./maintenance.ts"; -import { discoverMuseModels, museModelCapabilities } from "./modelCatalog.ts"; +import { + discoverMuseModels, + museModelCapabilities, + resolveMuseStartupTimeoutMs, +} from "./modelCatalog.ts"; import { buildServerProvider, COMPACT_SLASH_COMMAND, @@ -83,7 +88,12 @@ export const checkMuseProviderStatus = Effect.fn("checkMuseProviderStatus")(func ...(cwd ? { cwd } : {}), }), ); - }).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result); + }).pipe( + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("muse", DEFAULT_TIMEOUT_MS, museEnvironment), + ), + Effect.result, + ); if (Result.isFailure(versionResult)) { const missing = isCommandMissingCause(versionResult.failure); @@ -124,7 +134,8 @@ export const checkMuseProviderStatus = Effect.fn("checkMuseProviderStatus")(func const catalog = yield* discoverMuseModels(settings, museEnvironment, cwd, createHost).pipe( Effect.scoped, - Effect.timeoutOption(12_000), + // Startup shares the override, so the catalog keeps its 4 seconds on top of it. + Effect.timeoutOption(resolveMuseStartupTimeoutMs(museEnvironment) + 4_000), Effect.result, ); if (Result.isFailure(catalog) || Option.isNone(catalog.success)) { diff --git a/packages/provider-opencode/src/server/OpenCodeRuntime.ts b/packages/provider-opencode/src/server/OpenCodeRuntime.ts index a99e8cd7daece..88ff243eccd47 100644 --- a/packages/provider-opencode/src/server/OpenCodeRuntime.ts +++ b/packages/provider-opencode/src/server/OpenCodeRuntime.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import * as NodeURL from "node:url"; import type { ChatAttachment, ProviderApprovalDecision } from "@t3tools/contracts"; @@ -42,7 +43,7 @@ const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonStri const OPENCODE_EMPTY_CONFIG_CONTENT = "{}"; export const MINIMUM_OPENCODE_VERSION = "1.14.19"; -const OPENCODE_HEALTH_TIMEOUT = "5 seconds"; +const OPENCODE_HEALTH_TIMEOUT_MS = 5_000; const OpenCodeHealthSchema = Schema.Struct({ healthy: Schema.Literal(true), @@ -163,10 +164,15 @@ export const runOpenCodeSdk = ( export const verifyOpenCodeServerVersion = Effect.fn("verifyOpenCodeServerVersion")(function* ( client: OpencodeClient, + environment?: NodeJS.ProcessEnv, ) { const healthOption = yield* runOpenCodeSdk("global.health", (signal) => client.global.health({ signal }), - ).pipe(Effect.timeoutOption(OPENCODE_HEALTH_TIMEOUT)); + ).pipe( + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("opencode", OPENCODE_HEALTH_TIMEOUT_MS, environment), + ), + ); if (Option.isNone(healthOption)) { return yield* new OpenCodeRuntimeError({ operation: "global.health", @@ -578,7 +584,13 @@ const makeOpenCodeRuntime = Effect.gen(function* () { }), ), )); - const timeoutMs = input.timeoutMs ?? DEFAULT_OPENCODE_SERVER_TIMEOUT_MS; + const timeoutMs = + input.timeoutMs ?? + resolveProviderProbeTimeoutMs( + "opencode", + DEFAULT_OPENCODE_SERVER_TIMEOUT_MS, + input.environment, + ); const args = ["serve", `--hostname=${hostname}`, `--port=${port}`]; const launch = yield* agentScope.wrap({ command: input.binaryPath, @@ -749,6 +761,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { directory: input.directory, ...(serverPassword !== undefined ? { serverPassword } : {}), }), + input.environment, ) ); @@ -801,6 +814,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { directory: input.directory, ...(serverPassword !== undefined ? { serverPassword } : {}), }), + input.environment, ).pipe( Effect.map((version) => ({ url: serverUrl, diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index a245744eda2f8..9b39fed2546b5 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { ProviderDriverKind, settingsSecretLiteral, @@ -398,6 +399,7 @@ export const OpenCodeDriver: ProviderDriver< }); const loadOpenCode2Models = yield* makeOpenCode2ModelLoader( openCode2Server.withConnection(({ client }) => loadOpenCode2Catalog(client, host.paths.cwd)), + processEnv, ); const listOpenCode2Workspace = (cwd: string) => openCode2Server.withConnection(({ client, events }) => @@ -441,6 +443,7 @@ export const OpenCodeDriver: ProviderDriver< ), // OpenCode answers provider.list for a location only once its skill scan has ended. client.provider.list({ location }).pipe(Effect.timeout("10 seconds"), Effect.ignore), + processEnv, ); }).pipe(Effect.scoped), ); @@ -514,6 +517,7 @@ export const OpenCodeDriver: ProviderDriver< host.paths.cwd, runtimeProbe.refresh, loadOpenCode2Models, + processEnv, ).pipe( Effect.annotateLogs({ providerInstanceId: instanceId }), Effect.annotateSpans({ providerInstanceId: instanceId }), @@ -542,7 +546,7 @@ export const OpenCodeDriver: ProviderDriver< { skills: openCodeRuntime.loadOpenCodeSkills(client), commandInventory: OpenCodeRuntime.loadOpenCodeCommands(client).pipe( - Effect.timeoutOption("10 seconds"), + Effect.timeoutOption(resolveProviderProbeTimeoutMs("opencode", 10_000, processEnv)), Effect.map((commands) => ({ commands: Option.getOrElse(commands, () => []), slashCommandsPending: Option.isNone(commands), @@ -706,6 +710,7 @@ export const OpenCodeDriver: ProviderDriver< openCode2Server.withConnection(({ client }) => loadOpenCode2DirectoryCatalog(client, cwd), ), + processEnv, ).pipe( Effect.map( Option.map((catalog) => @@ -714,7 +719,10 @@ export const OpenCodeDriver: ProviderDriver< ), ), listOpenCode2Workspace(cwd).pipe( - Effect.timeout("20 seconds"), + // Leaves room for the reads around the scan wait, which uses the same override. + Effect.timeout( + 2 * resolveProviderProbeTimeoutMs("opencode", 10_000, processEnv), + ), Effect.mapError( (cause) => new ProviderDriverError({ @@ -740,7 +748,12 @@ export const OpenCodeDriver: ProviderDriver< ), v1: Effect.all([ snapshot.getSnapshot, - loadWorkspaceForCwd(cwd).pipe(Effect.timeout("20 seconds")), + loadWorkspaceForCwd(cwd).pipe( + // Leaves room for connection setup before the command fallback below. + Effect.timeout( + 2 * resolveProviderProbeTimeoutMs("opencode", 10_000, processEnv), + ), + ), ]).pipe( Effect.map( ([machineSnapshot, { skills, models, commands, slashCommandsPending }]) => ({ diff --git a/packages/provider-opencode/src/server/status.ts b/packages/provider-opencode/src/server/status.ts index 26f6380dc0ff6..0f90b9df28940 100644 --- a/packages/provider-opencode/src/server/status.ts +++ b/packages/provider-opencode/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { type ModelCapabilities, type ServerProviderModel, @@ -517,10 +518,11 @@ const settleOpenCode2Catalog = ( ); }); -const OPENCODE_2_CATALOG_SETTLE_TIMEOUT = "5 seconds"; +const OPENCODE_2_CATALOG_SETTLE_TIMEOUT_MS = 5_000; export const makeOpenCode2ModelLoader = ( list: Effect.Effect, E>, + environment?: NodeJS.ProcessEnv, ) => Effect.sync(() => { let lastLoaded: ReadonlyArray = []; @@ -537,7 +539,13 @@ export const makeOpenCode2ModelLoader = ( return tracked.pipe( Effect.flatMap((first) => settleOpenCode2Catalog(tracked, first).pipe( - Effect.timeoutOption(OPENCODE_2_CATALOG_SETTLE_TIMEOUT), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs( + "opencode", + OPENCODE_2_CATALOG_SETTLE_TIMEOUT_MS, + environment, + ), + ), ), ), Effect.catch((error) => @@ -556,9 +564,12 @@ export const makeOpenCode2ModelLoader = ( /** None when the reads fail or never agree, so a partial catalog never passes for a settled one. */ export const readSettledOpenCode2Catalog = ( list: Effect.Effect, E>, + environment?: NodeJS.ProcessEnv, ) => settleOpenCode2Catalog(list).pipe( - Effect.timeoutOption(OPENCODE_2_CATALOG_SETTLE_TIMEOUT), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("opencode", OPENCODE_2_CATALOG_SETTLE_TIMEOUT_MS, environment), + ), Effect.orElseSucceed(() => Option.none>()), ); @@ -587,12 +598,15 @@ export const loadOpenCode2Workspace = Effect.fn("OpencodeProviderStatus.loadOpen scanned: Effect.Effect, loaded: Effect.Effect, settled: Effect.Effect = Effect.void, + environment?: NodeJS.ProcessEnv, ) { const wasLoaded = yield* loaded; yield* settled; const first = yield* read; if (wasLoaded && first.commands.length > 0) return first; - yield* scanned.pipe(Effect.timeoutOption("10 seconds")); + yield* scanned.pipe( + Effect.timeoutOption(resolveProviderProbeTimeoutMs("opencode", 10_000, environment)), + ); return yield* read; }, ); @@ -749,6 +763,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu ReadonlyArray, OpenCodeRuntime.OpenCodeRuntimeError >, + environment?: NodeJS.ProcessEnv, ): Effect.fn.Return< ServerProviderDraft, never, @@ -865,6 +880,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu ...(settingsSecretLiteral(openCodeSettings.serverPassword) ? { serverPassword: settingsSecretLiteral(openCodeSettings.serverPassword) } : {}), + ...(environment !== undefined ? { environment } : {}), }) .pipe(Effect.flatMap(loadInventory), Effect.scoped) : serverOwner.withServer(loadInventory); diff --git a/packages/provider-opencode/src/server/v2/OpenCode2Server.ts b/packages/provider-opencode/src/server/v2/OpenCode2Server.ts index 1670fc3a93081..ced84482b48d4 100644 --- a/packages/provider-opencode/src/server/v2/OpenCode2Server.ts +++ b/packages/provider-opencode/src/server/v2/OpenCode2Server.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; // One OpenCode 2 process serves all directories; share it across instance threads. import type { OpenCodeClient } from "@opencode/client/effect"; import * as Context from "effect/Context"; @@ -12,7 +13,7 @@ import * as OpenCodeRuntime from "../OpenCodeRuntime.ts"; import * as OpenCodeServerOwner from "../OpenCodeServerOwner.ts"; import * as OpenCode2Client from "./OpenCode2Client.ts"; -const INFO_TIMEOUT = "5 seconds"; +const INFO_TIMEOUT_MS = 5_000; export interface OpenCode2Connection extends OpenCode2Client.OpenCode2Api { readonly url: string; @@ -79,10 +80,10 @@ const describeInfoFailure = (cause: unknown) => { }; /** Only server info proves readiness; keep credential-bearing failure details out of client text. */ -export const verifyServer = (client: OpenCodeClient) => +export const verifyServer = (client: OpenCodeClient, environment?: NodeJS.ProcessEnv) => client.server.info().pipe( Effect.timeoutOrElse({ - duration: INFO_TIMEOUT, + duration: resolveProviderProbeTimeoutMs("opencode", INFO_TIMEOUT_MS, environment), orElse: () => Effect.fail( new OpenCodeRuntime.OpenCodeRuntimeError({ @@ -127,7 +128,7 @@ export const make = Effect.fn("OpenCode2Server.make")(function* (input: { external: boolean, ) { const api = yield* opencode.connect({ baseUrl: url, password }); - const version = yield* verifyServer(api.client); + const version = yield* verifyServer(api.client, input.environment); return { ...api, url, version, external } satisfies OpenCode2Connection; }); let latest: OpenCode2Connection | undefined; diff --git a/packages/provider-opencode/src/server/versionProbe.ts b/packages/provider-opencode/src/server/versionProbe.ts index 8da0b38f81641..705cfe1cabf4e 100644 --- a/packages/provider-opencode/src/server/versionProbe.ts +++ b/packages/provider-opencode/src/server/versionProbe.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { parseSemver } from "@t3tools/shared/semver"; import * as Cache from "effect/Cache"; import * as Duration from "effect/Duration"; @@ -17,8 +18,8 @@ export interface ProbedOpenCode { } // Meant to stop a hung `--version`; wrapped installs can take several seconds to start cold. -const OPENCODE_VERSION_PROBE_TIMEOUT = "10 seconds"; -const OPENCODE_SERVER_PROBE_TIMEOUT = "5 seconds"; +const OPENCODE_VERSION_PROBE_TIMEOUT_MS = 10_000; +const OPENCODE_SERVER_PROBE_TIMEOUT_MS = 5_000; // 2.x's own CLI decodes `{version, pid}` from `/api/info`; requiring both keeps unrelated JSON out. const decodeApiInfo = Schema.decodeUnknownOption( Schema.fromJsonString(Schema.Struct({ version: Schema.String, pid: Schema.Int })), @@ -60,6 +61,11 @@ export const probeOpenCodeBinary = Effect.fn("probeOpenCodeBinary")(function* ( environment: NodeJS.ProcessEnv | undefined, ) { const runtime = yield* OpenCodeRuntime.OpenCodeRuntime; + const timeoutMs = resolveProviderProbeTimeoutMs( + "opencode", + OPENCODE_VERSION_PROBE_TIMEOUT_MS, + environment, + ); const { stdout } = yield* runtime .runOpenCodeCommand({ binaryPath, @@ -68,12 +74,12 @@ export const probeOpenCodeBinary = Effect.fn("probeOpenCodeBinary")(function* ( }) .pipe( Effect.timeoutOrElse({ - duration: OPENCODE_VERSION_PROBE_TIMEOUT, + duration: timeoutMs, orElse: () => Effect.fail( new OpenCodeRuntime.OpenCodeRuntimeError({ operation: "probeOpenCodeBinary", - detail: `OpenCode CLI version probe timed out after ${OPENCODE_VERSION_PROBE_TIMEOUT}.`, + detail: `OpenCode CLI version probe timed out after ${timeoutMs / 1_000} seconds.`, }), ), }), @@ -90,6 +96,7 @@ export const probeOpenCodeBinary = Effect.fn("probeOpenCodeBinary")(function* ( const probeOpenCodeServer = Effect.fn("probeOpenCodeServer")(function* ( serverUrl: string, serverPassword: string, + environment?: NodeJS.ProcessEnv, ) { const client = yield* HttpClient.HttpClient; const baseUrl = URL.parse(serverUrl.trim()); @@ -132,7 +139,11 @@ const probeOpenCodeServer = Effect.fn("probeOpenCodeServer")(function* ( }), ), Effect.timeoutOrElse({ - duration: OPENCODE_SERVER_PROBE_TIMEOUT, + duration: resolveProviderProbeTimeoutMs( + "opencode", + OPENCODE_SERVER_PROBE_TIMEOUT_MS, + environment, + ), orElse: () => Effect.fail( new OpenCodeRuntime.OpenCodeRuntimeError({ @@ -169,7 +180,7 @@ export const probeOpenCodeRuntime = ( HttpClient.HttpClient | OpenCodeRuntime.OpenCodeRuntime > => settings.serverUrl.trim().length > 0 - ? probeOpenCodeServer(settings.serverUrl, settings.serverPassword) + ? probeOpenCodeServer(settings.serverUrl, settings.serverPassword, environment) : probeOpenCodeBinary(settings.binaryPath, environment); // Only 2.x is kept: 1.x can be upgraded in place while T3 runs, and 2.x converts the shared diff --git a/packages/provider-pi/src/server/status.ts b/packages/provider-pi/src/server/status.ts index 01047b00eaf26..9361bc204eba5 100644 --- a/packages/provider-pi/src/server/status.ts +++ b/packages/provider-pi/src/server/status.ts @@ -1,3 +1,4 @@ +import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe"; import { type CustomModelSetting, type ServerProvider, @@ -199,6 +200,7 @@ const discoverPiViaRpc = Effect.fn("PiProviderStatus.discoverPiViaRpc")(function launchArgs: ReadonlyArray, cwd?: string, ) { + const timeoutMs = resolveProviderProbeTimeoutMs("pi", PI_RPC_DISCOVERY_TIMEOUT_MS, environment); // This optional probe must finish before the normal discovery deadline so // it cannot discard an otherwise usable catalog and authentication state. const textGenerationModelSlugsFiber = yield* discoverPiTextGenerationModelSlugs( @@ -221,10 +223,10 @@ const discoverPiViaRpc = Effect.fn("PiProviderStatus.discoverPiViaRpc")(function Effect.forkScoped, ); const connection = yield* makePiDiscoveryConnection(piSettings, environment, launchArgs, cwd); - const stateData = yield* connection.request({ type: "get_state" }); - const modelsData = yield* connection.request({ type: "get_available_models" }); + const stateData = yield* connection.request({ type: "get_state" }, timeoutMs); + const modelsData = yield* connection.request({ type: "get_available_models" }, timeoutMs); const commandsData = yield* connection - .request({ type: "get_commands" }) + .request({ type: "get_commands" }, timeoutMs) .pipe(Effect.orElseSucceed(() => undefined)); const discoveredModels = yield* Effect.fromResult(parseDiscoveredModels(modelsData, stateData)); const { slashCommands, skills } = parsePiDiscoveredCommands(commandsData); @@ -269,18 +271,23 @@ export const discoverPiCommandsForCwd = Effect.fn("discoverPiCommandsForCwd")( cwd, ); // A failed read must not replace a previously usable workspace catalog with an empty one. - const commandsData = yield* connection.request({ type: "get_commands" }); + const commandsData = yield* connection.request( + { type: "get_commands" }, + resolveProviderProbeTimeoutMs("pi", PI_RPC_DISCOVERY_TIMEOUT_MS, environment), + ); const { slashCommands, skills } = parsePiDiscoveredCommands(commandsData); return { slashCommands: withPiBuiltinSlashCommands(slashCommands), skills }; }, Effect.scoped, - Effect.timeoutOrElse({ - duration: PI_RPC_DISCOVERY_TIMEOUT_MS, - orElse: () => - Effect.fail( - new PiRpcTimeoutError({ operation: "discovery", timeoutMs: PI_RPC_DISCOVERY_TIMEOUT_MS }), - ), - }), + (effect, _settings, environment) => { + const timeoutMs = resolveProviderProbeTimeoutMs("pi", PI_RPC_DISCOVERY_TIMEOUT_MS, environment); + return effect.pipe( + Effect.timeoutOrElse({ + duration: timeoutMs, + orElse: () => Effect.fail(new PiRpcTimeoutError({ operation: "discovery", timeoutMs })), + }), + ); + }, ); const runPiVersionCommand = Effect.fn("PiProviderStatus.runPiVersionCommand")(function* ( @@ -337,7 +344,7 @@ const describeUnavailablePiModel = Effect.fn("PiProviderStatus.describeUnavailab }), ), ), - Effect.timeout(DEFAULT_TIMEOUT_MS), + Effect.timeout(resolveProviderProbeTimeoutMs("pi", DEFAULT_TIMEOUT_MS, environment)), Effect.option, ); const missingCredentials = Option.exists( @@ -411,7 +418,9 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function const probeEnvironment = providerProbeEnvironment(environment); const versionResult = yield* runPiVersionCommand(piSettings, probeEnvironment).pipe( - Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("pi", VERSION_PROBE_TIMEOUT_MS, environment), + ), Effect.result, ); @@ -523,7 +532,12 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function probeEnvironment, resolvedLaunchArgs.args, cwd, - ).pipe(Effect.timeoutOption(PI_RPC_DISCOVERY_TIMEOUT_MS), Effect.exit); + ).pipe( + Effect.timeoutOption( + resolveProviderProbeTimeoutMs("pi", PI_RPC_DISCOVERY_TIMEOUT_MS, environment), + ), + Effect.exit, + ); if (Exit.isFailure(discoveryExit)) { yield* Effect.logWarning("Pi RPC discovery failed.", { errorTag: causeErrorTag(discoveryExit.cause),