fix(auto): initialize notification store during bootstrap

This commit is contained in:
Mikael Hugo 2026-05-15 14:42:02 +02:00
parent 0a332f4cba
commit 09ea553b6d
2 changed files with 103 additions and 4 deletions

View file

@ -103,6 +103,7 @@ import {
} from "./uok/unit-runtime.js";
import { safeSetWidget } from "./widget-safe.js";
import { logError, logWarning } from "./workflow-logger.js";
import { initNotificationStore } from "./notification-store.js";
import {
captureIntegrationBranch,
@ -344,6 +345,10 @@ export async function bootstrapAutoSession(
lockBase,
buildResolver,
} = deps;
// Defensive: ensure notification store is initialized before any bootstrap
// notify calls. session_start also initializes it, but this closes the gap
// in case the bootstrap path runs before the lifecycle hook fires.
initNotificationStore(base);
let lockResult = acquireSessionLock(base, {
sessionId: ctx.sessionManager?.getSessionId?.(),
sessionFile: ctx.sessionManager?.getSessionFile?.(),

View file

@ -6,11 +6,17 @@
* with a 6-hour TTL so the model picker and preference validation see fresh
* model IDs without blocking the event loop.
*
* For providers that the SDK's built-in discovery adapter registry does NOT
* cover with supportsDiscovery=true (opencode, opencode-go, kimi-coding, xiaomi),
* results are ALSO written to ~/.sf/agent/discovery-cache.json so that
* `sf --list-models --discover` includes them in the discovery output.
*
* Provider configuration (base URL, auth format, model filter patterns) comes
* from provider-catalog-config.js not hardcoded here.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { sfHome } from "./sf-home.js";
import { sfRuntimeRoot } from "./paths.js";
import {
DISCOVERABLE_PROVIDER_IDS,
@ -20,6 +26,24 @@ import {
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
/**
* Providers whose live model lists are already handled by the SDK's native
* discoverModels() adapter (supportsDiscovery=true in model-discovery.js).
* For these, the SF layer does NOT need to write to the SDK discovery cache
* --discover will populate it via the SDK adapter path.
* All other DISCOVERABLE_PROVIDER_IDS are "SF-managed" and need SF to write
* their results into discovery-cache.json so --list-models --discover sees them.
*/
const SDK_NATIVE_DISCOVERY_PROVIDERS = new Set([
"ollama-cloud",
"openrouter",
"zai",
"minimax",
"mistral",
]);
const SDK_DISCOVERY_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour — matches DISCOVERY_TTLS defaults
function cacheDirPath(basePath) {
return join(sfRuntimeRoot(basePath), "model-catalog");
}
@ -58,6 +82,56 @@ function writeCacheEntry(basePath, providerId, modelIds) {
}
}
/**
* Write fetched model IDs for an SF-managed provider into the SDK discovery
* cache (~/.sf/agent/discovery-cache.json) so that `--list-models --discover`
* includes them. Only called for providers not covered by the SDK's native
* discovery adapters (i.e. not in SDK_NATIVE_DISCOVERY_PROVIDERS).
*
* Model objects are minimal: {id, name, provider, api, baseUrl} enough for
* the model registry to register and display them.
*/
function writeSdkDiscoveryCacheEntry(providerId, modelIds) {
try {
const cfg = getProviderCatalogConfig(providerId);
if (!cfg) return;
const cachePath = join(sfHome(), "agent", "discovery-cache.json");
let cache = { version: 1, entries: {} };
if (existsSync(cachePath)) {
try {
cache = JSON.parse(readFileSync(cachePath, "utf-8"));
if (cache.version !== 1 || typeof cache.entries !== "object") {
cache = { version: 1, entries: {} };
}
} catch {
cache = { version: 1, entries: {} };
}
}
const api =
cfg.type === "anthropic" ? "anthropic-messages" : "openai-completions";
const models = modelIds.map((id) => ({
id,
name: id,
provider: providerId,
api,
baseUrl: cfg.baseUrl,
input: ["text"],
}));
cache.entries[providerId] = {
models,
fetchedAt: Date.now(),
ttlMs: SDK_DISCOVERY_CACHE_TTL_MS,
};
const dir = join(sfHome(), "agent");
mkdirSync(dir, { recursive: true });
const tmp = cachePath + ".tmp";
writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
renameSync(tmp, cachePath);
} catch {
// Best-effort — never fail the caller.
}
}
function buildAuthHeaders(cfg, apiKey) {
switch (cfg.auth.type) {
case "bearer":
@ -109,6 +183,10 @@ function applyModelFilter(providerId, modelIds) {
/**
* Fetch the live model list for one provider and update the disk cache.
* Returns the filtered model ID array on success, null on any error.
*
* For SF-managed providers (not in SDK_NATIVE_DISCOVERY_PROVIDERS), also
* writes results into the SDK discovery cache so --list-models --discover
* includes them.
*/
export async function refreshProviderCatalog(basePath, providerId, apiKey) {
const cfg = getProviderCatalogConfig(providerId);
@ -125,6 +203,13 @@ export async function refreshProviderCatalog(basePath, providerId, apiKey) {
if (!raw) return null;
const modelIds = applyModelFilter(providerId, raw);
writeCacheEntry(basePath, providerId, modelIds);
// Also populate the SDK discovery cache for providers that the SDK's
// built-in discovery adapters do not cover (opencode, opencode-go,
// kimi-coding, xiaomi, etc.). Without this, --list-models --discover
// silently omits them because their SDK adapter has supportsDiscovery=false.
if (!SDK_NATIVE_DISCOVERY_PROVIDERS.has(providerId)) {
writeSdkDiscoveryCacheEntry(providerId, modelIds);
}
return modelIds;
} catch {
return null;
@ -144,9 +229,18 @@ export function scheduleModelCatalogRefresh(basePath, auth) {
const apiKey = creds.find((c) => c.type === "api_key" && c.key)?.key;
if (!apiKey) continue;
if (readCachedModelIds(basePath, providerId) !== null) continue;
await refreshProviderCatalog(basePath, providerId, apiKey);
} catch {
// Per-provider failures are silently swallowed.
const result = await refreshProviderCatalog(basePath, providerId, apiKey);
if (result === null) {
// Surface per-provider fetch failures so they don't silently disappear.
process.stderr.write(
`[model-catalog-cache] refresh failed for provider: ${providerId}\n`,
);
}
} catch (err) {
// Per-provider failures must not crash the refresh loop, but should be visible.
process.stderr.write(
`[model-catalog-cache] unexpected error for provider ${providerId}: ${err?.message ?? err}\n`,
);
}
}
});