fix(auto): initialize notification store during bootstrap
This commit is contained in:
parent
0a332f4cba
commit
09ea553b6d
2 changed files with 103 additions and 4 deletions
|
|
@ -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?.(),
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue