/** * On-disk cache of the workspace's deployed retrieval services. * * Landing spot: `${DSH_HOME:-~/.dsh}/cache/bailian-kb/services-.json`. * Per-workspace files are required: an api key is scoped to one workspace, and * the panel's "autofill" button exists to switch accounts, so one shared file * would blend services from different accounts. * * Why not `ctx.storage`: the storage hub is absent from every shipped agent * preset, so `inject(['storage'])` may never fire for a third-party plugin, and * the JSON backend's on-disk location is decided by its own `root` config — the * plugin could not tell anyone where the data went. Why not `settings.yaml`: that * document is the user's, and it hot-reloads, so writing machine-refreshed data * there both fights the user for the file and republishes configuration for no * reason. * * Every read is best-effort and total: a missing, malformed, foreign, or * stale-schema file reads as a miss. Callers run inside `agent/pre-step`, where a * throw fails the user's step. */ import type { KbClient } from "./client.js"; import { type ServiceEntry } from "./services.js"; /** Refresh interval. Evaluated per `agent/pre-step`, so a short window genuinely takes effect. */ export declare const CACHE_TTL_MS: number; /** * Refresh interval applied when the cached list is EMPTY. * * An empty list is almost never a settled fact — it is the intermediate state of * a workspace being set up. Caching that negative result for the full TTL breaks * the standard first-run path: configure the plugin against a fresh workspace (0 * services) → create a knowledge base and a service → and then wait up to half an * hour before the catalog appears. Re-asking every minute while the answer is * "nothing yet" has a bounded cost and removes that trap. */ export declare const EMPTY_CACHE_TTL_MS: number; /** The stored document. */ export interface ServiceCacheDocument { version: number; /** Epoch millis of the fetch that produced `entries`. */ fetchedAt: number; /** Guards against reading a file written for another account or region. */ workspaceId: string; endpointHost: string; entries: ServiceEntry[]; total: number; truncated: boolean; } /** * Build the cache path for one workspace. * @param workspaceId - the resolved Bailian workspace id. * @param home - override for tests; defaults to `$DSH_HOME` or `~/.dsh`. * @returns the absolute file path. */ export declare function serviceCachePath(workspaceId: string, home?: string): string; /** * Read a cache document, validating it belongs to this workspace and schema. * @param path - the cache file path. * @param workspaceId - the workspace the caller is serving. * @param endpointHost - the host the caller is serving. * @returns the document, or undefined for any miss (absent, malformed, foreign, or wrong version). */ export declare function readServiceCache(path: string, workspaceId: string, endpointHost: string): ServiceCacheDocument | undefined; /** * Publish a cache document atomically: a reader either sees the previous file or * the complete new one, never a half-written mix. * @param path - the cache file path. * @param doc - the document to store. */ export declare function writeServiceCache(path: string, doc: ServiceCacheDocument): void; /** What the settings panel shows about the cache; see {@link ServiceCache.status}. */ export interface ServiceCacheStatus { workspaceId: string; /** Epoch millis of the last successful fetch; absent when nothing is cached. */ fetchedAt?: number; searchCount: number; chatCount: number; /** Server-reported total, which exceeds the counts above when the fetch was capped. */ total: number; truncated: boolean; stale: boolean; } export interface ServiceCacheOptions { client: KbClient; /** Resolves the current workspace id; a failure means "not configured yet". */ resolveWorkspaceId: () => Promise; endpointHost: string; /** Reports refresh failures without escalating them. */ warn: (message: string) => void; /** Test seams. */ home?: string; now?: () => number; } /** * The service cache: a synchronous in-memory view over the on-disk document, * plus a deduplicated background refresh. */ export declare class ServiceCache { private readonly opts; /** Last document read or written; undefined until one is available. */ private document; /** The in-flight refresh, if any. One per instance: `pre-step` asks on every model request. */ private inFlight; /** Workspace of {@link document}, so a workspace switch invalidates in memory too. */ private loadedFor; constructor(opts: ServiceCacheOptions); private get now(); /** * The cached entries for a workspace, loading the file on first use. * Synchronous and total — safe to call from `agent/pre-step`. * @param workspaceId - the workspace being served. * @returns the document, or undefined when nothing usable is cached. */ peek(workspaceId: string): ServiceCacheDocument | undefined; /** * Whether the cached document is missing or older than its TTL. * An empty list expires on the much shorter {@link EMPTY_CACHE_TTL_MS}. * @param workspaceId - the workspace being served. * @returns true when a refresh is due. */ isStale(workspaceId: string): boolean; /** Drop the in-memory view and force the next `peek` to re-read from disk. */ invalidate(): void; /** * A diagnostic snapshot for the settings panel. * * The panel exists because this cache's staleness is otherwise invisible: a * developer whose agent silently stops retrieving cannot tell an empty * workspace from a stale list without reading the JSON file. `fetchedAt` plus * the per-scene counts answer that in one glance. * @param workspaceId - the workspace being served. * @returns the snapshot; `fetchedAt` is undefined when nothing is cached. */ status(workspaceId: string): ServiceCacheStatus; /** * The cached entries of one scene, most recently modified first. * Backs the panel's service picker, which exists so a default service can be * chosen by name instead of by pasting a 36-character hex id. * @param workspaceId - the workspace being served. * @param scene - `search` or `chat`. * @returns the entries, newest first. */ entriesFor(workspaceId: string, scene: ServiceEntry["scene"]): ServiceEntry[]; /** * Fetch and store the current service list. * Never rejects: failures are warned and leave the previous document in place. * Concurrent calls share one request. * @returns a promise resolving once the attempt finishes. */ refresh(): Promise; /** A mutation must not reuse a discovery request that began before it completed. */ refreshAfterMutation(): Promise; private runRefresh; }