Expand description
H1: config hot-reload for EXTERNAL edits (vim/TUI writing the shared files) + the single reconcile pipeline that PATCH/auth paths also use.
Divergence D-CONFIG-1 (TESTING §1.6): upstream has NO file watcher —
config is Effect.cachedInvalidateWithTTL(Duration.infinity) invalidated
only on its own write (v1 config/config.ts:295,302,678), so an external
edit does not apply until restart. ocserve applies it within one poll
interval. Shapes/routes unchanged; replay unaffected (file state is fixed
during a recording).
Design (antagonism decisions):
- Poll, not notify/inotify: editors save via tmp+rename (inotify on
the file loses the watch; dir-watch needs debounce + a dependency);
stat-ing ≤6 known files every interval is bounded, rename-proof,
zero-dependency. Interval clamped ≥100ms;
OCSERVE_CONFIG_WATCH=0disables;OCSERVE_CONFIG_POLL_MStunes (default 2000). - Fail-safe at runtime, fail-fast at boot (SRE §1): a broken config
keeps the OLD payloads/registry serving +
result="error"metric and self-heals on the next successful load; boot still refuses to start. - Serialized: PATCH/auth paths and the watcher task share one
reconcile_lock(tokio Mutex — held across the reload + MCP awaits, never a parking_lot guard). - Observed tuples recorded only after a successful load → failed loads retry every tick until the file is valid (no lost updates, no write side effects → no feedback loop).
- MCP section diffed against the previously SERVED config: added → upsert_cfg + connect; changed → connect (replaces the client) with the new cfg; removed/disabled → disconnect + drop_cfg; any change rescans tool schemas (boot parity: connect-time trust scan).
Structs§
- Watch
Opts - Watcher knobs. Tests construct these directly (env reads are confined to
from_envso parallel tests never raceset_var). - Watch
State - Watched-file state.
pathsset once at boot (≤6 files — bounded by the setRuntime::load_forreads);observedtracks last-seen (mtime,len)+content-hash so the watcher fires only on real CONTENT changes. The hash matters: their opencode rewritesmodels.json~hourly with IDENTICAL bytes (fresh-TTL fetch) — mtime churn without a content change used to trigger a full reload (the proven warm-kill spike: 7 OOM kills on 2026-10-05, journal 17:01:33) for zero benefit.
Functions§
- changed
- True when any watched path’s CONTENT differs from the last observed
state. (mtime,len) is the fast path; when only mtime churned (same
bytes — their hourly identical catalog rewrite) the tuple is advanced
in place,
result="skipped"is counted, and NO reload is requested. Empty watch set → false (no-op). - observe
- Record current tuples for every watched path (call after each successful load AND once at boot so the first poll tick doesn’t re-load fresh state).
- reconcile
- Rebuild payloads + LLM registry from disk and (re)apply both, then diff the MCP section against what was previously served.
- trim_
heap - Return freed-but-retained glibc heap pages to the OS (
malloc_trim(0)). - watch_
loop - Watcher task (spawned by serve): poll → changed → reconcile. Owns no state beyond the Arc; exits only on abort/process end.