fix(sessions): move provider catalog network refreshes off request paths

The shared ModelRuntime was constructed with network refreshes enabled, so
reloadConfig()/login()/logout() — called on the model picker, session model
changes, and auth dialogs — performed unbounded provider-catalog fetches.
A single stalled fetch blocked those requests for minutes and, through pi's
coalesced per-provider refresh, dragged session creation along with it.

Construct the runtime with PI_OFFLINE forced so every runtime-driven refresh
stays local, and add ModelCatalogRefresher as the single deliberate network
path: bounded by an abort timeout, serialized through one in-flight run,
scheduled in the background, and triggered after provider auth changes.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-24 23:44:35 +02:00
parent 83e9014a8a
commit ed9c2f65bb
6 changed files with 358 additions and 9 deletions
+27 -5
View File
@@ -1,5 +1,5 @@
import { join } from "node:path";
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
import { ModelRuntime, type CreateModelRuntimeOptions } from "@earendil-works/pi-coding-agent";
import type { AuthInteraction } from "@earendil-works/pi-ai";
import type { AuthProvidersResponse, AuthType, OAuthFlowState } from "../../shared/apiTypes.js";
import { getLoginProviderOptions, getLogoutProviderOptions } from "./authProviderOptions.js";
@@ -31,11 +31,33 @@ interface AuthChangeContext {
const noopLogger: AuthServiceLogger = { error() { /* no-op */ } };
export function createModelRuntimeForAgentDir(agentDir: string, allowModelNetwork?: boolean): Promise<ModelRuntime> {
return ModelRuntime.create({
/**
* Create the shared model runtime with runtime-owned network refreshes disabled.
*
* Upstream `ModelRuntime.reloadConfig()`, `login()`, and `logout()` always refresh
* with `allowNetwork: modelNetworkEnabled` and accept no abort signal. With the
* default (`PI_OFFLINE` unset) a single stalled provider-catalog fetch can block
* those call paths for minutes — and, because pi-web shares one runtime and pi
* coalesces per-provider refreshes, session creation joins the same stalled
* fetch. Forcing `PI_OFFLINE` during construction makes every runtime-driven
* refresh local-only; pi-web performs its own bounded catalog refreshes in the
* background instead (see modelCatalogRefresher.ts).
*/
async function createOfflineModelRuntime(options: CreateModelRuntimeOptions): Promise<ModelRuntime> {
const previous = process.env["PI_OFFLINE"];
process.env["PI_OFFLINE"] = "1";
try {
return await ModelRuntime.create(options);
} finally {
if (previous === undefined) delete process.env["PI_OFFLINE"];
else process.env["PI_OFFLINE"] = previous;
}
}
export function createModelRuntimeForAgentDir(agentDir: string): Promise<ModelRuntime> {
return createOfflineModelRuntime({
authPath: join(agentDir, "auth.json"),
modelsPath: join(agentDir, "models.json"),
...(allowModelNetwork === undefined ? {} : { allowModelNetwork }),
});
}
@@ -52,7 +74,7 @@ export class AuthService {
}
static async create(deps: AuthServiceDependencies = {}): Promise<AuthService> {
const runtime = deps.runtime ?? (deps.agentDir === undefined ? await ModelRuntime.create({}) : await createModelRuntimeForAgentDir(deps.agentDir));
const runtime = deps.runtime ?? (deps.agentDir === undefined ? await createOfflineModelRuntime({}) : await createModelRuntimeForAgentDir(deps.agentDir));
const logger = deps.logger ?? noopLogger;
const authFlows = deps.authFlows ?? new OAuthLoginFlowService({ logger });
return new AuthService(runtime, authFlows, logger);