/** * config-routing.ts, the ownership-aware read and write path used by the * `goodvibes_settings` and `goodvibes_context` tools. * * A setting has exactly one owning runtime (see `config/config-ownership.ts`). * The tools must therefore READ and WRITE through that runtime, not through * whichever process happens to be holding the model conversation: * * - daemon-owned key (`surfaces.*`, control-plane binding, watchers, device * pairing, provisioning, retention) → the daemon's config. * - client-owned key (rendering, transcript display, this installation's own * lifecycle) → this client's own config. * - user-level key → the cross-client shared tier. * * Both directions matter and they failed together. A Telegram bot username was * written into the agent's own settings file, reported as a success, and * configured nothing, because Telegram runs in the daemon. When the same value * was later asked for, the agent read its own store, found nothing, and said the * setting was not set, after it had been set. A write that routes correctly * paired with a read that does not is still a system that reports fiction. * * So: one source of truth per key, read live, and every reported value carries * the store it came from. When the owning runtime exists but cannot be reached, * that is stated as a failure. A default presented as the current setting is * indistinguishable from a lie, so it is never presented as one. */ import type { ConfigKey } from '../../config/schema.js'; import type { ConfigManager } from '../../config/manager.js'; import { type DaemonConfigRouterDeps } from '../../config/daemon-config-route.js'; /** * Host-supplied routing facts. * * Deliberately the router's OWN dependency shape with `hostsDaemon` relaxed to * optional, rather than a parallel hand-copied field list. A host builds these * once (the agent does it in `config/daemon-config-routing.ts`) and hands the * same object to both the tools and its own config paths, so there is one * routing table. Enumerating the fields here instead would silently drop any * the router later grows, and a dropped discovery hook is not a type error, * it is a daemon that looks absent while it is running. */ export type ConfigRoutingOptions = Omit & { readonly hostsDaemon?: boolean | undefined; /** * The surface whose store owns CLIENT-owned keys for this turn, when that is * not the host's own surface. * * A daemon-hosted session runs inside the host process but belongs to the * surface that asked for it. Client-owned keys, rendering, transcript * display, wake-word selection, are that surface's, and they live in that * surface's file. Without this the host's own ConfigManager answered, and a * hosted AGENT conversation read and wrote `~/.goodvibes/tui/settings.json` * for its whole life: asked to enable the wake word, it wrote the TUI's * store, read the value back out of the same store, and reported success, * while the live agent process went on reading a file nobody had touched. * * Only the FILE is redirected, never a whole ConfigManager: constructing one * for another surface runs that surface's migrations and shared-config * bootstrap, which is a non-owner rewriting a store it does not own. */ readonly clientOwnedStore?: ClientOwnedStore | undefined; }; /** The originating surface's own settings file, for client-owned keys. */ export interface ClientOwnedStore { /** Surface root, `agent`, `tui`, `webui`. Reported so a value names its owner. */ readonly surface: string; /** Absolute path to that surface's `settings.json`. */ readonly settingsPath: string; } /** Where a reported value actually came from. */ export interface ConfigValueOrigin { /** `daemon` = read live over the control plane; `file` = read from a store. */ readonly readFrom: 'daemon' | 'file' | 'memory'; /** Absolute path or daemon base URL. */ readonly source: string; /** Which runtime owns the key. */ readonly scope: 'daemon' | 'client' | 'user'; /** Plain-language ownership statement, for the user-facing report. */ readonly ownership: string; } /** A successfully resolved read. */ export interface RoutedConfigRead extends ConfigValueOrigin { readonly key: string; readonly value: unknown; readonly available: true; } /** A read that could not be answered honestly. */ export interface UnavailableConfigRead extends Pick { readonly key: string; readonly available: false; /** Why no value can be reported, in words the user can act on. */ readonly reason: string; readonly source: string; } export type ConfigReadResult = RoutedConfigRead | UnavailableConfigRead; /** A completed, verified write. */ export interface RoutedConfigWrite { readonly key: string; readonly scope: 'daemon' | 'client' | 'user'; readonly ownership: string; /** Which runtime applied it. */ readonly appliedBy: 'daemon' | 'local'; /** The file or endpoint the value is stored in. */ readonly persistedTo: string; /** The value the OWNING store reports holding after the write. */ readonly value: unknown; } /** * Fill in the routing deps the daemon router needs. The daemon home is derived * from the daemon config store's own directory, so a host that configured * `GOODVIBES_DAEMON_HOME` gets that home without having to repeat it here. */ export declare function resolveRouterDeps(configManager: Pick, options?: ConfigRoutingOptions): DaemonConfigRouterDeps; /** * The store a LOCAL write to `key` lands in. `applyConfigWrite` reports the * daemon tier for daemon-owned keys and the surface file otherwise; user-level * keys ride the shared tier, so that case is resolved here rather than reporting * a surface path the value is not in. */ export declare function localStorePathForKey(configManager: Pick, key: string, options?: ConfigRoutingOptions): string; /** * Apply a write to the runtime that owns the key, then confirm the OWNING store * holds it. * * The read-back is the point. `configManager.get()` after a set reports the * in-process object, so it says "yes, that's the value" even when the bytes * never landed, landed in a store nothing reads, or will be shadowed on reload. * Here the value is re-read from the file (or from the daemon that applied it) * and a write that did not stick is returned as a failure. * * @throws {Error} when the value cannot be confirmed in the owning store, and * whatever `applyConfigWrite` throws when the daemon is unreachable or refuses. */ export declare function applyRoutedConfigWrite(configManager: ConfigManager, key: ConfigKey, value: unknown, options?: ConfigRoutingOptions): Promise; /** * Read `key` from the runtime that owns it, and say where the answer came from. * * A daemon-owned key is read live over the control plane when a daemon is * running, because a running daemon may hold state it has not flushed. With no * daemon running, the daemon's own store file is the answer, it is the same * store the daemon will read at startup. When a daemon IS expected and cannot be * reached, this returns `available: false` with the reason instead of the local * default: reporting a default as the current setting is the failure mode this * whole path exists to remove. */ export declare function readRoutedConfigValue(configManager: ConfigManager, key: ConfigKey, options?: ConfigRoutingOptions): Promise; /** * Read many keys at once, fetching the daemon's config a single time. Used by * the settings LIST view so daemon-owned rows carry daemon values and * client-owned rows carry local ones, each labelled with its store. */ export declare function readRoutedConfigValues(configManager: ConfigManager, keys: readonly ConfigKey[], options?: ConfigRoutingOptions): Promise; //# sourceMappingURL=config-routing.d.ts.map