Skip to main content

AppState

Struct AppState 

Source
pub struct AppState {
Show 25 fields pub version: String, pub registry: Arc<PalaceRegistry>, pub data_root: PathBuf, pub embedder: Arc<OnceCell<Arc<FastEmbedder>>>, pub default_palace: Option<String>, pub chat_provider: Arc<OnceCell<Option<Arc<dyn ChatProvider>>>>, pub session_stores: Arc<DashMap<String, Arc<ChatSessionStore>>>, pub events: Arc<Sender<DaemonEvent>>, pub started_at: Instant, pub log_buffer: LogBuffer, pub error_store: Option<ErrorStore>, pub disk_bytes: Arc<AtomicU64>, pub sys_metrics: Arc<Mutex<SysMetrics>>, pub bound_addr: Arc<OnceLock<SocketAddr>>, pub prompt_context_cache: Arc<RwLock<PromptFactsCache>>, pub activity_log: Arc<ActivityLog>, pub bm25_client: Option<Arc<Bm25Client>>, pub bm25_supervisor: Option<Arc<Bm25Supervisor>>, pub palace_write_locks: Arc<DashMap<String, Arc<Mutex<()>>>>, pub pending_activity_writes: Arc<AtomicUsize>, pub palace_names: Arc<DashMap<String, String>>, pub pin_project_map: Arc<DashMap<String, PathBuf>>, pub bm25_index_tx: Sender<Bm25IndexRequest>, pub update_available: Arc<Mutex<Option<String>>>, pub daemon_readiness: Arc<AtomicU8>,
}
Expand description

Shared application state passed to every request handler.

Why: The stdio loop and HTTP server need the same handles to the registry, data root, and embedder so MCP tools can perform real reads/writes against the live trusty-memory core. The embedder is heavy (loads ONNX weights) so we hold it behind a OnceCell and initialize lazily on first use. What: Clone-able via Arc fields. The registry / data root are eager; embedder is Arc<OnceCell<Arc<FastEmbedder>>> so concurrent first-use races resolve to a single shared instance. Test: app_state_default_constructs confirms construction without panic.

Fields§

§version: String§registry: Arc<PalaceRegistry>§data_root: PathBuf§embedder: Arc<OnceCell<Arc<FastEmbedder>>>§default_palace: Option<String>

Optional default palace applied to MCP tool calls when the caller omits the palace argument. Set via trusty-memory serve --palace.

§chat_provider: Arc<OnceCell<Option<Arc<dyn ChatProvider>>>>

Active chat provider selected at startup. None means no upstream is configured (no Ollama detected and no OpenRouter key) — callers must degrade gracefully (chat endpoint returns 412).

§session_stores: Arc<DashMap<String, Arc<ChatSessionStore>>>

Per-palace chat-session stores, opened lazily so cold-start cost is paid only when chat-history endpoints are hit.

§events: Arc<Sender<DaemonEvent>>

Broadcast sender for live DaemonEvent pushes to SSE subscribers.

Why: Lets mutating handlers emit events that any connected dashboard receives instantly. Cap of 128 buffers transient slow readers; if a receiver lags it gets RecvError::Lagged and we emit a lag frame.

§started_at: Instant

Instant the daemon started, used to compute uptime_secs on /health.

Why (issue #35): GET /health reports how long the daemon has been up. Capturing a monotonic Instant at AppState construction lets the handler compute the elapsed seconds cheaply and without a clock-skew hazard. What: a wall-monotonic Instant; AppState::new stamps it at startup. Test: health_endpoint_includes_resource_fields.

§log_buffer: LogBuffer

In-memory ring buffer of recent tracing log lines (issue #35).

Why: the GET /api/v1/logs/tail endpoint serves the last N log lines so operators can inspect a running daemon without tailing a file. The buffer is shared between the tracing LogBufferLayer (writer) and the HTTP handler (reader). What: a cheap Arc-backed clone of the buffer the subscriber writes to. Defaults to an empty buffer for states that never install the layer (tests, the stdio path). Test: logs_tail_returns_recent_lines.

§error_store: Option<ErrorStore>

Bug-capture ERROR store (bug-reporting #478, Phase 1).

Why: Phase 2 MCP / HTTP endpoints need to query captured errors; stashing the ErrorStore handle here lets any handler reach it cheaply without a second global or per-request construction. What: populated by run_serve from the init_tracing_with_buffer_and_capture result; the layer writes to this store automatically so every tracing::error! call site contributes without any changes to call sites. None in states that do not install the layer (tests, the stdio path). Test: compile-presence is verified by the trusty-memory build; Phase 2 will add query tests in web.rs.

§disk_bytes: Arc<AtomicU64>

Most recent on-disk footprint of data_root, in bytes (issue #35).

Why: GET /health reports disk_bytes. Walking the data directory on every health request would make a frequent health poll do unbounded I/O; a background task recomputes it every 10 s and stores it here so the handler reads it lock-free. What: an AtomicU64 updated by the ticker spawned in run_http_on. 0 until the first walk completes. Test: health_endpoint_includes_resource_fields.

§sys_metrics: Arc<Mutex<SysMetrics>>

Per-process RSS + CPU sampler, refreshed on each /health request (issue #35).

Why: CPU usage is a delta between two sysinfo refreshes, so the sampler must persist between requests — hence the shared Mutex. What: a tokio::sync::Mutex<SysMetrics> so the async health handler can sample without blocking the runtime. Test: health_endpoint_includes_resource_fields.

§bound_addr: Arc<OnceLock<SocketAddr>>

HTTP listener address the daemon bound to, once run_http_on is running.

Why: clients (and /health responses) need to advertise the live host:port even though port selection happens dynamically (7070–7079 walk + OS fallback). Stashing it on AppState lets request handlers surface the discovery value without re-querying the listener. What: a OnceLock<SocketAddr> so run_http_on writes it exactly once at bind time and every handler reads it lock-free thereafter. Empty (None from get()) on the stdio path where no listener exists. Test: health_endpoint_reports_bound_addr (added below).

§prompt_context_cache: Arc<RwLock<PromptFactsCache>>

Cached prompt-facts surface served by the MCP get_prompt_context tool (issue #42).

Why: The original session-init prompts/get design loaded context once per connection; switching to a per-message tool lets the model pull fresh, query-filtered context on demand. The cache holds both the raw triples (for filtered lookups) and a pre-formatted Markdown block (for the unfiltered hot path) so neither code path re-walks the KG. The cache is rebuilt by prompt_facts::rebuild_prompt_cache after any write that touches a hot predicate (kg_assert, add_alias, remove_prompt_fact). What: An Arc<tokio::sync::RwLock<PromptFactsCache>> so the hot read path takes a brief read lock and clones the cache; rebuilds take a write lock for the assignment only. The async-aware lock (issue #229) yields to the tokio runtime instead of blocking a runtime thread for the rebuild duration. An empty triples vec ↔ “no context stored yet” (the tool handler renders a hint). Test: get_prompt_context_returns_cached_or_hint, get_prompt_context_filters_by_query.

§activity_log: Arc<ActivityLog>

Persistent activity log (issue #96).

Why: the dashboard activity feed used to be a pure live-stream over /sse — opening the UI showed an empty feed and any mutation from the MCP path was invisible. Holding an ActivityLog on AppState lets emit record an entry on every push so the GET /api/v1/activity handler can return historical rows on mount and the live SSE stream can continue prepending events on top of the loaded history. None on builds that opt out (tests that use AppState::new get a real log under their tempdir so behaviour matches production). What: an Arc<ActivityLog> shared with every emitter. Test: web::tests::activity_endpoint_lists_recent_emits.

§bm25_client: Option<Arc<Bm25Client>>

Optional per-palace BM25 lexical search lane (issue #156).

Why: in-process BM25 would serialise the recall hot path on disk I/O during writes and contend with the redb/usearch locks. Delegating to the trusty-bm25-daemon subprocess (one socket per palace) keeps BM25 ingestion and search off the critical path while still feeding hits into the recall RRF fusion. What: Some(client) only when TRUSTY_BM25_DAEMON=1 at startup — every code path that uses this field is gated on is_some() and falls back to vector-only behaviour otherwise so existing deployments see zero behavioural change. Test: bm25_client_disabled_by_default, bm25_client_enabled_when_env_set.

§bm25_supervisor: Option<Arc<Bm25Supervisor>>

Optional per-palace BM25 daemon spawn supervisor (issue #193).

Why: without an in-process supervisor the BM25 daemon must be launched out-of-band (launchd, manual trusty-bm25-daemon), which is the same UX trap PR #190 fixed for trusty-embedderd. Holding a supervisor here lets us spawn the daemon on first BM25 use for a palace, restart it if it dies, and reap it on clean shutdown. Some only when TRUSTY_BM25_DAEMON=1 at startup — the same gate that enables bm25_client. When set but TRUSTY_BM25_EXTERNAL=1, the supervisor’s ensure_running becomes a no-op that just returns the canonical socket path so operators can keep using their own process manager. Test: covered by bm25_supervisor_present_when_env_set and the bm25_supervisor::tests unit tests.

§palace_write_locks: Arc<DashMap<String, Arc<Mutex<()>>>>

Per-palace write serialisation locks (issue #230).

Why: the dedup gate in tools.rs previously read a snapshot of existing drawers, checked for near-duplicates via Jaro-Winkler, and then issued the write — a classic time-of-check/time-of-use race. Two concurrent memory_remember calls with the same content could both see the pre-write snapshot, both pass the gate, and both land duplicate drawers. Serialising the gate-then-write sequence per palace closes the window: while one task holds the mutex, any concurrent writer for the same palace blocks until the first write finishes and is visible to list_drawers. The lock is per palace (not global) so writes to different palaces continue to run in parallel. What: a DashMap keyed by palace id, where each entry is an Arc<tokio::sync::Mutex<()>>. The mutex is constructed lazily by palace_write_lock on first access. Arc lets callers hold a clone of the lock past the lifetime of the DashMap entry so the map never needs to be held across an .await. Test: tools::tests::dedup_gate_blocks_concurrent_duplicate_writes.

§pending_activity_writes: Arc<AtomicUsize>

Counter of in-flight activity-log writes spawned by emit (issue #232).

Why: emit offloads the synchronous redb append to the tokio blocking pool via spawn_blocking so the async runtime is never parked waiting on fsync. The write is fire-and-forget — emit returns immediately after spawning. Tests that observe the activity log right after a burst of emit calls need a deterministic synchronization point; holding an in-flight counter lets flush_activity_writes poll until every spawned append has settled, which keeps the assertions race-free without forcing every caller to .await. What: an Arc<AtomicUsize> incremented before each spawn_blocking and decremented inside the closure (after the append completes, even if it errored). The counter is cheap (one atomic add per emit) and stays at zero in steady-state production traffic. Test: web::tests::activity_endpoint_lists_recent_emits and tests::emit_persists_mutations_but_skips_status_changed call flush_activity_writes to drain the counter before reading the log.

§palace_names: Arc<DashMap<String, String>>

In-memory cache mapping palace id → Palace.name (issue #228).

Why: every memory_remember / memory_note write used to call PalaceRegistry::list_palaces (a synchronous filesystem walk of the data root) just to resolve a friendly palace name for the SSE DrawerAdded event. With N palaces on disk the cost was O(N) opendirs plus palace.json reads on every write, blocking the async runtime. Caching the name in-memory turns the lookup into a DashMap::get. What: DashMap<String, String> populated by create_palace and load_palaces_from_disk, kept in sync by rename / delete paths. Missing entries are treated as “name unknown” so callers fall back to the palace id and the emit path never fails. Test: palace_name_cache_populated_after_hydration and palace_name_cache_updates_on_create.

§pin_project_map: Arc<DashMap<String, PathBuf>>

Single-pass startup pin-file map: palace id → project root path (issue #470).

Why: after daemon startup we have no record of which on-disk project directories correspond to which palace ids — that information only existed inside the pin files on disk. Eager-opening every palace on startup is too expensive. This field captures the scan-only result of startup_scan::scan_pin_map so handlers that want to locate a project by its palace id (e.g. future cwd-inference, project-health checks) can do a single DashMap::get instead of a filesystem walk. Populated once, shortly after load_palaces_from_disk returns, by spawn_startup_tasks. Never mutated after population — it is a snapshot of what the filesystem looked like at startup. What: DashMap<String (palace_id), PathBuf (project root)>. The outer Arc lets spawn_startup_tasks (which holds only a clone of AppState) write to the same backing map that request handlers read. Population is asynchronous so callers must treat an absent entry as “not yet scanned” (or “no pin found”), never as “palace unknown”. Test: startup_scan::tests::scan_pin_map_* validate the underlying scanner function; the wiring in spawn_startup_tasks is covered by the integration-test daemon start path.

§bm25_index_tx: Sender<Bm25IndexRequest>

Bounded sender for the BM25 index worker (issue #231).

Why: the previous fire-and-forget design tokio::spawned one task per memory_remember / memory_note call, so a write burst against a slow or unreachable BM25 daemon grew an unbounded in-flight task queue. A single long-lived worker draining a bounded mpsc channel caps that back-pressure: writers try_send (never block), full-queue requests are dropped with a warn!, and the worker exits cleanly when the last sender is dropped on shutdown. What: an mpsc::Sender cloned to every AppState clone (cheap). The matching receiver is consumed by the worker spawned in AppState::new via tools::spawn_bm25_index_worker. Capacity is tools::BM25_INDEX_QUEUE_CAPACITY (256). Test: bm25_index_queue_drops_when_full exercises the full-queue branch via bm25_index_enqueue.

§update_available: Arc<Mutex<Option<String>>>

Cached result of the startup update check (issue #537).

Why: /health should report update_available without hitting crates.io on every probe. A single background check at daemon startup stores the result here; the health handler reads it lock-free (well, a brief mutex lock) without a network call. What: None = up-to-date or check not yet done; Some("x.y.z") = newer version available. The field is populated by a tokio::spawn in spawn_startup_tasks (main.rs) after the daemon binds. Test: indirectly by the /health endpoint tests in web.rs.

§daemon_readiness: Arc<AtomicU8>

Two-phase readiness state — Warming until the embedder is initialised, then Ready (issues #910 / #911).

Why: AppState::embedder() used to call FastEmbedder::new() without any timeout, so the first memory_recall/memory_remember that arrived before CoreML finished compiling would block for 5–11 hours until the OnceCell resolved (issue #910). Exposing this state lets the preflight guards in tools.rs return an explicit fast error immediately — "trusty-memory is warming up, retry shortly" — instead of queueing behind an open-ended init. What: An AtomicU8 starting at DaemonReadiness::Warming (0) and flipped to DaemonReadiness::Ready (1) by spawn_startup_tasks after the embedder warm-up succeeds. The transition is one-way and lock-free. Test: daemon_readiness_transitions_warming_to_ready.

Implementations§

Source§

impl AppState

Source

pub fn new(data_root: PathBuf) -> Self

Construct an AppState rooted at the given on-disk data directory.

Why: The CLI (serve) and integration tests need to point the MCP server at different roots — production at dirs::data_dir, tests at a tempfile::tempdir(). What: Builds an empty PalaceRegistry, captures the version, and allocates an empty OnceCell for the embedder. default_palace is None; use with_default_palace to set it. Test: tools::tests::dispatch_palace_create_persists constructs an AppState pointed at a tempdir and round-trips a palace through it.

Source

pub fn palace_write_lock(&self, palace_id: &str) -> Arc<Mutex<()>>

Acquire (lazily, then clone) the per-palace write mutex.

Why (issue #230): the dedup-check + remember_with_options write sequence in tools.rs must be atomic per palace to prevent two concurrent identical writes from both passing the dedup gate. Callers hold the returned Arc<Mutex<()>>’s guard across the gate check and the write so the second writer blocks until the first write is visible to list_drawers. Returning a clone of the Arc rather than a borrow into the DashMap lets the caller .await while holding the lock without risking a deadlock against any future map mutation (DashMap shards are sync mutexes). What: looks up the palace id in palace_write_locks and returns a clone of the existing mutex; on the first call for a palace, inserts a freshly-constructed tokio::sync::Mutex<()> first. The DashMap::entry().or_insert_with API guarantees the lazy construction is racy-safe — only one mutex is ever inserted per palace id. Test: tools::tests::dedup_gate_blocks_concurrent_duplicate_writes.

Source

pub fn pinned_project_path(&self, palace_id: &str) -> Option<PathBuf>

Look up a project root path by palace id in the startup pin-scan map.

Why: provides a stable, cheap accessor so handlers do not reach directly into the DashMap field and so the accessor can be mocked in future tests without touching AppState internals. The map is populated asynchronously by spawn_startup_tasks — an absent entry means either the scan has not completed yet or no pin file claimed that id. What: returns Some(project_path) when the palace id was found during startup scan; None otherwise. Test: covered indirectly via the startup-scan integration path; the underlying map data is validated by startup_scan::tests.

Source

pub fn with_bm25_client_from_env(self) -> Self

Builder-style: opt-in to the BM25 lexical lane (issue #156).

Why: the BM25 subprocess is gated behind TRUSTY_BM25_DAEMON=1 so the default cargo install trusty-memory / launchd plist deployment stays vector-only and existing test fixtures keep passing without having to provision a daemon. Reading the env var here keeps the gating logic in one place (the helper in main.rs just plumbs the result through). What: when TRUSTY_BM25_DAEMON=1, constructs one Bm25Client per palace by lazy-resolving the socket path the first time the palace id is observed. Currently we install a shared default client up front and re-key on the palace id at the call site — palaces with no daemon socket simply see search/index errors which we log + ignore. Returns self unchanged when the env var is unset or set to anything other than 1. Test: bm25_client_disabled_by_default, bm25_client_enabled_when_env_set.

Source

pub async fn load_palaces_from_disk(&self) -> Result<usize>

Scan the palace registry directory and re-register every persisted palace into the in-memory PalaceRegistry.

Why: AppState::new builds an empty registry, so after a daemon restart palace_list / the dashboard reported zero palaces even though dozens existed on disk — palace metadata was persisted by palace_create but never re-hydrated on startup. This method closes that gap by walking the on-disk layout (each subdirectory holding a palace.json is one palace) and rebuilding a live PalaceHandle for each, so recall paths see the full set immediately after a restart. What: runs the blocking filesystem walk + per-palace PalaceHandle::open on a spawn_blocking thread (so it never stalls the async runtime), registers each successfully opened palace via register_arc, logs every load at debug!, and returns the count loaded. A palace that fails to open (corrupt index, unreadable kg.db, etc.) is logged at warn! and skipped — one bad palace must not abort startup or crash the daemon. data_root is expected to already be the palace registry directory — main.rs resolves it via resolve_palace_registry_dir before constructing the AppState, so the flat / legacy-palaces/ layout difference is handled exactly once. Test: tests::load_palaces_from_disk_rehydrates_registry writes two palaces into a tempdir, constructs an AppState, calls this method, and asserts the returned count and registry contents.

Source

pub fn with_log_buffer(self, buffer: LogBuffer) -> Self

Builder-style: attach the daemon’s shared [LogBuffer] so the GET /api/v1/logs/tail endpoint serves the same lines the tracing subscriber captures (issue #35).

Why: main builds the buffer (via init_tracing_with_buffer) before constructing the AppState, then hands a clone here so the HTTP handler and the tracing layer observe the same ring. What: replaces the empty default buffer with the supplied one. Test: logs_tail_returns_recent_lines.

Source

pub fn with_writer_intent(self) -> Self

Builder-style: mark this daemon as the sole palace writer so palace redb files open with OpenIntent::Writer (issue #1487).

Why: The HTTP daemon owns the write lock on every palace’s kg.redb and index.usearch.redb. Before this fix, when a second daemon instance opened the same store it silently degraded to a read-only snapshot and rejected every memory_remember for its lifetime — effectively silent data loss when an MCP client routed a write to the rogue instance. Opening as Writer makes the second instance fail loud (after a short handoff-retry window that absorbs a graceful launchd bootoutbootstrap overlap) instead of serving broken reads-only. CLI, stdio-proxy, and test code paths never call this, so they keep the snapshot read-fallback (issue #59). What: Replaces self.registry with a fresh PalaceRegistry carrying OpenIntent::Writer.

Invariant: MUST be called on a fresh, unhydrated, unshared registry — during startup, before spawn_startup_tasks/load_palaces_from_disk registers any PalaceHandle and before the AppState (hence its Arc<PalaceRegistry>) is cloned to a handler. Replacing the registry discards the prior Arc; doing so after hydration would silently drop live handles (data loss), and doing so after the state is shared would leave other clones on the stale read-only registry. The guard is a debug_assert! on the strongest cheap signals the registry exposes — is_empty() (no handles hydrated) and Arc::strong_count == 1 (not yet shared) — so an ordering violation fails fast as the programmer error it is (the call site is startup-only and fixed). Release builds elide the assert; the real call site (run_serve) always satisfies it. Test: with_writer_intent_marks_registry_writer and with_writer_intent_panics_on_hydrated_registry in lib_tests.

Source

pub fn with_error_store(self, store: ErrorStore) -> Self

Builder-style: attach the bug-capture ErrorStore handle (bug-reporting #478).

Why: Phase 2 MCP / HTTP endpoints need a handle to the in-memory error ring so they can serve recent_errors / errors_by_fingerprint without disk I/O on the hot path. Installing it here — rather than adding it as a separate global — keeps the state graph explicit and lets tests skip it by never calling this method. What: stores Some(store) in AppState::error_store; the BugCaptureLayer that writes to this store is already installed in the tracing subscriber by init_tracing_with_buffer_and_capture. The store is Clone (cheap Arc clone internally) so both the layer and this field share the same underlying ring. Test: Phase 2 will add error_store_captures_and_queries in web.rs.

Source

pub fn emit(&self, event: DaemonEvent)

Send a DaemonEvent to all connected SSE subscribers and persist it to the activity log when the variant carries a source.

Why: Mutating handlers call this after a successful write so the dashboard can update without polling. The send is best-effort — broadcast::Sender::send returns Err only when there are no live receivers, which is fine (no listeners == no work to do). Issue #96 additionally writes the entry to the persistent activity log so the feed can serve historical rows on page load and so MCP / HTTP / Hook origins are visible to the operator. Persistence is also best-effort — a write failure is logged but never blocks the SSE broadcast.

Issue #232: the activity-log append is a synchronous redb write + fsync. Calling it directly on the async caller’s task parked a tokio worker thread on disk I/O for every SSE event. We now offload the append to the blocking thread pool via spawn_blocking and return immediately — emit stays synchronous so every existing caller (including the sync dispatch_hook_fired JSON-RPC handler) keeps compiling unchanged. The fire-and-forget pattern matches the pre-fix semantics (best-effort, never blocks the SSE broadcast) while freeing the async runtime to do real work during the write. What: serialises the event for the log (skipping StatusChanged which is a recomputed aggregate, not a mutation), spawns the redb append on tokio::task::spawn_blocking keyed by a clone of the Arc<ActivityLog> and the cloned event, then sends the event over the broadcast channel. A pending_activity_writes counter is bumped before the spawn and decremented inside the closure so Self::flush_activity_writes can drain in tests. Test: web::tests::sse_stream_receives_palace_created confirms a subscriber observes the emitted event; activity_endpoint_lists_recent_emits confirms persistence via flush_activity_writes.

Source

pub async fn flush_activity_writes(&self)

Block (asynchronously) until every in-flight activity-log write spawned by Self::emit has settled.

Why: emit offloads its redb append to tokio::task::spawn_blocking and returns immediately (issue #232). Tests that observe the activity log right after a burst of emits would otherwise race the blocking-pool worker; this helper gives them a deterministic synchronization point. Production code never needs to call this — the dashboard reads through GET /api/v1/activity, which already tolerates writes settling asynchronously. What: spins on pending_activity_writes with a 1 ms yield until the counter is zero. Cheap: tests typically emit a handful of events and the loop exits within a single scheduler tick. Test: covered indirectly by emit_persists_mutations_but_skips_status_changed and web::tests::activity_endpoint_lists_recent_emits.

Source

pub fn session_store(&self, palace_id: &str) -> Result<Arc<ChatSessionStore>>

Open (or return cached) the chat-session store for a palace.

Why: Chat session persistence lives in a dedicated redb file under the palace’s data dir (chat_sessions.redb) so it doesn’t intermingle with the KG’s transactional load. The store is cheap to clone via Arc but the underlying connection should be reused, so cache by id. What: Creates the palace data dir if missing, opens (or reuses) a ChatSessionStore and stashes an Arc in the DashMap. Test: Indirectly via the session HTTP handlers in web::tests.

Source

pub fn with_default_palace(self, name: Option<String>) -> Self

Builder-style setter for the default palace name.

Why: serve --palace <name> wants to bind every tool call to a project-scoped namespace without forcing every MCP request to repeat the palace argument. What: Returns self with default_palace = Some(name). Test: default_palace_used_when_arg_omitted covers the resolution path; this setter is exercised there.

Source

pub async fn chat_provider(&self) -> Option<Arc<dyn ChatProvider>>

Resolve (or initialize) the shared embedder.

Why: FastEmbedder load is expensive — we share one instance across all tool calls; the OnceCell ensures concurrent first-use races collapse to a single load. What: Returns Arc<FastEmbedder> on success. Errors propagate from the underlying ONNX load. Test: Indirectly via dispatch_remember_then_recall. Resolve the active chat provider, auto-detecting on first call.

Why: Provider selection depends on filesystem-loaded config plus a network probe (Ollama liveness), so it must be lazily initialised at runtime. Caching the choice in a OnceCell keeps it stable across concurrent requests without re-probing on every chat call. What: On first use loads ~/.trusty-memory/config.toml, prefers an auto-detected Ollama instance (when local_model.enabled), and falls back to OpenRouter when an API key is set. Returns Ok(None) when neither is available so the caller can emit a 412. Test: web::tests::providers_endpoint_returns_payload covers the detection path indirectly through /api/v1/chat/providers.

Source

pub fn spawn_alias_discovery(&self, palace: String, project_root: PathBuf)

Spawn a fire-and-forget background task that auto-discovers project aliases under project_root and asserts new ones into palace.

Why (issue #42): Projects carry implicit shorthand — cargo package names that differ from their directory, binary names that differ from packages, first-letter abbreviations — that should be surfaced without a user ever calling add_alias. Running discovery as a detached task on palace-open keeps startup latency unchanged: the daemon binds and starts serving immediately while the discovery scan completes in the background, and any newly-asserted aliases land in the prompt cache before the model’s next get_prompt_context call. What: clones self (cheap; Arc-backed), spawns a tokio task that invokes the discover_aliases tool handler directly so the dedup + cache-rebuild logic runs exactly the same path as the MCP tool call. Errors are logged at warn!; one failed discovery never destabilises the daemon. Test: not unit-tested (timing-dependent fire-and-forget); the underlying discover_aliases dispatch is covered by dispatch_discover_aliases_inserts_new_and_dedupes in tools::tests.

Source

pub fn readiness(&self) -> DaemonReadiness

Return the current readiness state.

Why: tool handlers and the /health endpoint need a cheap, lock-free way to check whether the embedder has been initialised yet. What: loads daemon_readiness with Acquire ordering so the caller sees all writes the startup task made before setting the state. Test: daemon_readiness_transitions_warming_to_ready.

Source

pub fn set_ready(&self)

Flip the readiness state from Warming to Ready.

Why: called by spawn_startup_tasks in main.rs once the embedder warm-up succeeds — this is the single state-transition site. What: store(Ready, Release) so subsequent Acquire loads in handlers observe a consistent state. Idempotent: calling it multiple times is harmless. Test: daemon_readiness_transitions_warming_to_ready.

Source

pub fn readiness_check(&self) -> Result<()>

Return Ok(()) when Ready, or an explicit Err with the warming message when still Warming.

Why: the preflight in every bounded handler calls this and returns the error immediately so no embedding / redb I/O is attempted while the daemon is still initialising (tracks #911 internally). What: cheaply reads daemon_readiness; returns the fast error string on Warming. Zero allocation on the happy path. Test: covered by tools::tests::remember_returns_warming_error_while_state_is_warming.

Source

pub async fn embedder(&self) -> Result<Arc<FastEmbedder>>

Obtain the shared FastEmbedder instance, initialising it on first call.

Why: centralises lazy embedder access so every tool handler goes through one bounded init path (tracks #910 internally). What: wraps OnceCell::get_or_try_init with a timeout so a slow CoreML/CUDA first-compile cannot block a handler indefinitely. On timeout the OnceCell is left unresolved and the next caller retries.

Callers on the request path MUST call readiness_check() before this method. The four guarded handlers (memory_remember, memory_recall, memory_recall_deep, memory_note) do so; any new handler that calls embedder() must follow the same pattern. Reaching this method while still Warming is not a bug — the warm-up task itself calls embedder() while in Warming state — but request handlers should have short-circuited before here via readiness_check().

The readiness_check() preflight is the PRIMARY guard (fast rejection with no I/O). This timeout is the last-resort backstop in case a handler bypasses the preflight or the warm-up task itself hits a pathological init delay. If this timeout fires the OnceCell is left in the unresolved state and the next call retries from scratch.

Trait Implementations§

Source§

impl Clone for AppState

Source§

fn clone(&self) -> AppState

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for AppState

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more