docs(sessions): document background catalog refresh and contain offline env window

Finding 6: serialize createOfflineModelRuntime so overlapping calls cannot
interleave their PI_OFFLINE save/restore pairs and leave the process offline,
and name the process-wide visibility of that window in the docblock.

Finding 7: assert the offline construction through the public refresh seam via
reloadConfig() — the request path that regressed — instead of reading upstream's
private modelNetworkEnabled field.

Finding 8.4/8.5: document the background provider-catalog refresh in
docs/config.md and docs/config.html (cadence, timeout, single retry, offline
opt-out via PI_WEB_OFFLINE / PI_OFFLINE only), and update the changeset to
match the behavior after the earlier fixes.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-25 13:07:02 +02:00
parent c5af390ab9
commit 90dd7ce401
5 changed files with 132 additions and 10 deletions
+31 -2
View File
@@ -31,6 +31,14 @@ interface AuthChangeContext {
const noopLogger: AuthServiceLogger = { error() { /* no-op */ } };
/**
* Serializes the `PI_OFFLINE` windows below, which is what keeps their
* save/restore pairs properly nested. Two overlapping calls would otherwise both
* capture the forced `"1"` and restore it, leaving the whole process offline
* permanently. Owned by this module only; nothing else may mutate it.
*/
let offlineRuntimeCreations: Promise<unknown> = Promise.resolve();
/**
* Create the shared model runtime with runtime-owned network refreshes disabled.
*
@@ -42,12 +50,33 @@ const noopLogger: AuthServiceLogger = { error() { /* no-op */ } };
* 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).
*
* `modelNetworkEnabled` is computed once from the environment inside
* `ModelRuntime.create()`, so the env var is the only lever upstream exposes.
* Calls are queued so their env windows never overlap.
*/
async function createOfflineModelRuntime(options: CreateModelRuntimeOptions): Promise<ModelRuntime> {
function createOfflineModelRuntime(options: CreateModelRuntimeOptions): Promise<ModelRuntime> {
const created = offlineRuntimeCreations.then(() => forceOfflineWhile(() => ModelRuntime.create(options)));
// A failed creation must not poison the queue; the caller still sees the rejection.
offlineRuntimeCreations = created.then(() => undefined, () => undefined);
return created;
}
/**
* Force `PI_OFFLINE` for the duration of `create`, then restore what was there.
*
* `process.env` is process-wide, so this window is a real global side effect:
* anything reading `PI_OFFLINE` while `create` awaits observes offline mode,
* including upstream's package manager, tools manager, and version check. That
* is acceptable because pi-web only builds runtimes during daemon startup and in
* tests, and the window is one `ModelRuntime.create()` call — but it is why
* callers must stay serialized rather than run concurrently.
*/
async function forceOfflineWhile<T>(create: () => Promise<T>): Promise<T> {
const previous = process.env["PI_OFFLINE"];
process.env["PI_OFFLINE"] = "1";
try {
return await ModelRuntime.create(options);
return await create();
} finally {
if (previous === undefined) delete process.env["PI_OFFLINE"];
else process.env["PI_OFFLINE"] = previous;