Skip to main content

Module watch

Module watch 

Source
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=0 disables; OCSERVE_CONFIG_POLL_MS tunes (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§

WatchOpts
Watcher knobs. Tests construct these directly (env reads are confined to from_env so parallel tests never race set_var).
WatchState
Watched-file state. paths set once at boot (≤6 files — bounded by the set Runtime::load_for reads); observed tracks last-seen (mtime,len)+content-hash so the watcher fires only on real CONTENT changes. The hash matters: their opencode rewrites models.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.