diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts index 1ed6361549202..09bc3b88e63a9 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts @@ -5641,3 +5641,17 @@ it.effect("ProviderSessionManagerV2 gives a delegated child its project's device assert.isTrue(child?.capabilities?.has("device")); }), ); + +it("ProviderSessionManagerV2 reads its idle timeout from the host environment", () => { + const read = (value: string | undefined) => + ProviderSessionManager.idleTimeoutFromEnvironment({ + [ProviderSessionManager.IDLE_TIMEOUT_MINUTES_ENV]: value, + }); + assert.isUndefined(read(undefined)); + assert.isUndefined(read(" ")); + assert.deepStrictEqual(read("90"), { idleTimeoutMs: 90 * 60 * 1000 }); + assert.deepStrictEqual(read(" 1440 "), { idleTimeoutMs: 1440 * 60 * 1000 }); + for (const value of ["0", "1441", "1.5", "-5", "30m", "1e3"]) { + assert.deepStrictEqual(read(value), { invalid: value }, value); + } +}); diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.ts index 2e9adb9c8a54c..46ed48977e8c1 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.ts @@ -1,4 +1,5 @@ import { AgentScope } from "@t3tools/shared/AgentScope"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; import { @@ -68,6 +69,13 @@ import * as ProjectionStore from "./ProjectionStore.ts"; const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; const DEFAULT_MAX_IDLE_PIN_MS = 4 * 60 * 60 * 1000; +/** + * Lets a host keep idle provider processes longer than the default, for example so a + * prompt-cache keep-alive running inside the provider process outlives the release timer. + * Each idle process holds its memory until released, so the default stays unchanged. + */ +export const IDLE_TIMEOUT_MINUTES_ENV = "T3CODE_PROVIDER_IDLE_TIMEOUT_MINUTES"; +const MAX_IDLE_TIMEOUT_MINUTES = 24 * 60; const RELEASE_SCOPE_CLOSE_TIMEOUT_MS = 30 * 1000; /** Past adapters' own 10 s stop fallbacks, so they settle first when the provider answers at all. */ export const PROVIDER_INTERRUPT_TIMEOUT_MS = 15 * 1000; @@ -3424,4 +3432,27 @@ export const layerWithOptions = ( }), ); -export const layer = layerWithOptions(); +/** `undefined` keeps the default; `invalid` is a value to warn about and ignore. */ +export function idleTimeoutFromEnvironment( + env: NodeJS.ProcessEnv, +): { readonly idleTimeoutMs: number } | { readonly invalid: string } | undefined { + const raw = env[IDLE_TIMEOUT_MINUTES_ENV]?.trim(); + if (raw === undefined || raw === "") return undefined; + const minutes = /^\d+$/.test(raw) ? Number(raw) : Number.NaN; + if (!(minutes >= 1 && minutes <= MAX_IDLE_TIMEOUT_MINUTES)) return { invalid: raw }; + return { idleTimeoutMs: minutes * 60 * 1000 }; +} + +export const layer = Layer.unwrap( + Effect.gen(function* () { + const configured = idleTimeoutFromEnvironment(yield* HostProcess.Environment); + if (configured !== undefined && "invalid" in configured) { + yield* Effect.logWarning( + `Ignoring ${IDLE_TIMEOUT_MINUTES_ENV}: expected whole minutes from 1 to ${MAX_IDLE_TIMEOUT_MINUTES}`, + { value: configured.invalid }, + ); + return layerWithOptions(); + } + return layerWithOptions(configured); + }), +); diff --git a/docs/user/background-service.md b/docs/user/background-service.md index 1bc41addf8c96..7440b1aa367e4 100644 --- a/docs/user/background-service.md +++ b/docs/user/background-service.md @@ -81,6 +81,14 @@ A missing file is ignored; other read failures stop service startup. If the file contains credentials, restrict access to that file with `chmod 600` (for the default T3 home, `chmod 600 ~/.t3/service.env`). +T3 Code stops an agent's process after its thread has been idle for 30 minutes +and resumes it on the next message. To keep idle processes longer, for example +for a tool that keeps a provider's prompt cache warm from inside that process, +set `T3CODE_PROVIDER_IDLE_TIMEOUT_MINUTES` to a whole number of minutes from 1 +to 1440. Every idle process keeps its memory until it stops, so a longer value +uses more memory when many threads sit idle. An invalid value is logged and +ignored. + ## Platform support Linux needs systemd user services. Setup enables lingering so T3 Code starts at