pub struct AppState {
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 disk_bytes: Arc<AtomicU64>,
pub sys_metrics: Arc<Mutex<SysMetrics>>,
}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: InstantInstant 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: LogBufferIn-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.
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.
Implementations§
Source§impl AppState
impl AppState
Sourcepub fn new(data_root: PathBuf) -> Self
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.
Sourcepub async fn load_palaces_from_disk(&self) -> Result<usize>
pub async fn load_palaces_from_disk(&self) -> Result<usize>
Scan data_root/palaces/ 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.
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.
Sourcepub fn with_log_buffer(self, buffer: LogBuffer) -> Self
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.
Sourcepub fn emit(&self, event: DaemonEvent)
pub fn emit(&self, event: DaemonEvent)
Send a DaemonEvent to all connected SSE subscribers.
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).
What: Drops the result, so callers don’t need to care whether anyone
is listening.
Test: web::tests::sse_stream_receives_palace_created confirms a
subscriber observes the emitted event.
Sourcepub fn session_store(&self, palace_id: &str) -> Result<Arc<ChatSessionStore>>
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 SQLite file under
the palace’s data dir (chat_sessions.db) so it doesn’t intermingle
with the KG’s transactional load. The store is cheap to clone via
Arc but the underlying r2d2 pool 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.
Sourcepub fn with_default_palace(self, name: Option<String>) -> Self
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.
Sourcepub async fn chat_provider(&self) -> Option<Arc<dyn ChatProvider>>
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.
pub async fn embedder(&self) -> Result<Arc<FastEmbedder>>
Trait Implementations§
Auto Trait Implementations§
impl Freeze for AppState
impl !RefUnwindSafe for AppState
impl Send for AppState
impl Sync for AppState
impl Unpin for AppState
impl UnsafeUnpin for AppState
impl !UnwindSafe for AppState
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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