Skip to main content

MemoryService

Struct MemoryService 

Source
pub struct MemoryService { /* private fields */ }
Expand description

Wraps AppState and exposes one async method per logical operation.

Why: see module docs. Lets HTTP handlers stay thin and lets non-HTTP callers (chat tool dispatch, RPC bridges) reuse the same code paths. What: Clone (cheap — only the inner AppState is shared); construct with MemoryService::new(state). Test: every method is covered by the corresponding handler test in web::tests.

Implementations§

Source§

impl MemoryService

Source

pub fn new(state: AppState) -> Self

Construct a new service wrapper.

Why: handlers cheaply re-wrap their AppState on every request; the cost is just an Arc clone, so we don’t bother caching the wrapper. What: stores the AppState for later method calls. Test: trivial — covered indirectly by every handler test.

Source

pub fn state(&self) -> &AppState

Borrow the inner AppState.

Why: some handlers still need direct access (SSE broadcaster, session store, etc.) while we incrementally extract code into the service. What: returns a borrowed reference to the wrapped AppState. Test: not directly tested; surface-level accessor.

Source

pub async fn status(&self) -> StatusPayload

Build the aggregate /api/v1/status payload.

Why: dashboard widgets and the MCP get_status tool need the same roll-up; centralising avoids drift between the two surfaces. What: walks every persisted palace for palace_count, then sums drawer/vector/triple counts across the cache-resident subset and returns the StatusPayload. Why (issue #4637): this used to open every persisted palace to sum those three counts. With 5,794 palaces on disk against a 64-slot LRU that is ~5,730 cold opens of ~1s each — the endpoint measurably never responded. palace_count still reflects the true on-disk total (the directory walk is cheap and now runs on the blocking pool); the totals cover only cache-resident palaces and say so via cached_palace_count. Test: status_endpoint_returns_payload, status_does_not_open_uncached_palaces.

Source

pub fn aggregate_status_event(&self) -> DaemonEvent

Compute the aggregate StatusChanged event used by SSE consumers.

Why: mutating handlers — and the periodic status ticker — push a refreshed status snapshot so dashboards stay in sync without an extra /api/v1/status request. Why (issue #228): this used to call PalaceRegistry::list_palaces (a synchronous disk walk) + open_palace (more disk I/O on first call) for every palace on every emit. Since every persisted palace is already loaded into the in-memory registry by AppState::load_palaces_from_disk at startup (and every create_palace keeps it in sync), iterating the in-memory registry returns the same counts without touching disk. What: iterates state.registry.list() (a DashMap snapshot) and sums the live handle stats via [collect_palace_stats]. Returns a DaemonEvent::StatusChanged. Palaces that fail to resolve in the registry (race during shutdown) are silently skipped — the next emit will catch them. Test: indirectly via SSE integration tests; the math is identical to the disk-walk implementation and the status_endpoint_returns_payload test still passes against status() (which keeps the disk view for the dedicated endpoint).

Source

pub async fn list_palaces(&self) -> ServiceResult<Vec<PalaceInfo>>

List every palace on disk, enriched with live handle stats.

Why: shared between the HTTP handler and the chat tool dispatcher; both want the same PalaceInfo shape. Issue #185 added the reserved-prefix filter so internal “system” palaces (e.g. the __health_probe__ palace used by /health) never surface in the admin UI, TUI, or any user-facing roster. What: walks the registry, drops any palace whose id starts with the reserved __ prefix, and builds a PalaceInfo per remaining row. Why (issue #4637): this used to call open_palace per row purely to enrich it with counts. At 5,794 palaces against a 64-slot LRU that is ~90 minutes of cold, blocking disk I/O inline on the async executor — and it evicted the entire working set on every call. Rows now come from PalaceRegistry::peek (zero I/O, no LRU promotion, mirroring the #1924 fix in console_metrics.rs). Uncached rows carry cached: false and zero counts; a client that needs live counts for one palace should fetch GET /api/v1/palaces/{id}, which still opens it. Test: palace_list_includes_richer_counts, palace_list_includes_graph_counts, health_probe_palace_is_invisible (in web::tests), list_palaces_does_not_open_uncached_palaces.

Source

pub async fn create_palace( &self, body: CreatePalaceBody, source: ActivitySource, ) -> ServiceResult<String>

Create a new palace and emit the corresponding activity event.

Why: trims duplicated work between the HTTP handler and any future non-HTTP creation flow. What: validates the name, builds the Palace row, calls PalaceRegistry::create_palace, and emits PalaceCreated. Returns the new palace id. Test: covered indirectly by palace_list_includes_richer_counts (which posts a palace through the HTTP layer then reads it back).

Source

pub async fn delete_palace( &self, palace_id: &str, force: bool, ) -> ServiceResult<()>

Delete a palace from disk, optionally rejecting non-empty palaces.

Why: Issue #180 — operators need a way to drop an entire palace without going through drawer-by-drawer deletion. Defaulting to a “must be empty” guard prevents fat-finger destruction of populated palaces; force=true is the explicit opt-in to the destructive path. What: 1) confirms the palace exists on disk (else NotFound), 2) when !force, lists drawers via the live handle and returns BadRequest("Palace has drawers; pass force=true to delete") if the palace is non-empty, 3) drops the in-memory registry entry so future opens hit the (now-missing) disk state, 4) removes <data_root>/<palace_id>/ recursively via tokio::fs::remove_dir_all, and 5) emits an aggregate StatusChanged so dashboards refresh. Test: delete_palace_removes_dir_when_empty, delete_palace_refuses_when_drawers_present, delete_palace_force_removes_populated_palace, delete_palace_returns_not_found_for_missing_id in web::tests.

Source

pub async fn update_palace_name( &self, palace_id: &str, name: &str, ) -> Result<Value>

Rename a palace’s display name without touching its data.

Why: Operators need to fix typos and rebrand palaces without dropping the underlying drawers / vectors / KG. The palace id (the directory name on disk) is immutable — only the human-readable name field in palace.json changes — so cached PalaceHandles stay valid and no registry invalidation is required. What: 1) loads the palace via PalaceStore::load_palace (404 when the directory or palace.json is missing), 2) trims the new name and returns BadRequest when empty, 3) mutates palace.name and writes the metadata back through the atomic PalaceStore::save_palace (tmp file + rename), 4) emits an aggregate StatusChanged so dashboards re-render the relabelled palace, 5) returns the updated palace as JSON (enriched with the live handle stats, so callers see drawer/vector/KG counts in the same shape as GET /palaces/{id}). Test: update_palace_name_renames_palace, update_palace_name_rejects_empty_name, update_palace_name_returns_not_found_for_missing_id in web::tests.

Source

pub async fn update_palace_name_typed( &self, palace_id: &str, name: &str, ) -> ServiceResult<Value>

Typed variant of Self::update_palace_name used by the HTTP handler.

Why: HTTP needs to distinguish 400 (empty name) from 404 (missing palace) so the right status code is emitted; the chat / MCP tool only cares about a Result<Value> because both errors are surfaced as opaque MCP error strings. Keeping a typed variant alongside the untyped one keeps the wire shape correct on both surfaces without asking either caller to parse error strings. What: same as Self::update_palace_name but returns ServiceError::BadRequest for empty names and ServiceError::NotFound for missing palace metadata. Test: update_palace_name_renames_palace, update_palace_name_rejects_empty_name, update_palace_name_returns_not_found_for_missing_id.

Source

pub async fn get_palace(&self, id: &str) -> ServiceResult<PalaceInfo>

Look up a single palace by id and enrich with live handle stats.

Why: distinct 404 vs. 500 path is needed by both HTTP and chat callers. What: returns NotFound when the id is unknown, otherwise a fully populated PalaceInfo. Test: indirectly via health_endpoint_round_trip_with_palace_is_ok.

Source

pub async fn list_drawers( &self, id: &str, q: ListDrawersQuery, ) -> ServiceResult<Value>

List drawers in a palace with optional room/tag filters and pagination.

Why: deduplicates the open-handle + listing path between HTTP and chat, and (issue #184) lets the TUI activity panel page through drawers in creation-date order without breaking the importance-sorted default the legacy callers rely on. What: opens the palace handle, fetches a window of drawers, optionally re-sorts by created_at descending when sort = "created_desc" (leaving the importance-desc default untouched), then drops the leading offset rows and keeps limit. For created_desc the window must cover the full filtered set (otherwise the importance pre-sort hides truly-recent low-importance drawers), so the window is widened to a sane ceiling (MAX_DRAWER_WINDOW); the default importance path keeps a tight limit+offset window. Returns the serialised JSON array. Test: service::tests::list_drawers_creates_desc_paginates.

Source

pub async fn create_drawer( &self, id: &str, body: CreateDrawerBody, creator: CreatorInfo, source: ActivitySource, ) -> ServiceResult<Uuid>

Store a new drawer and emit the matching activity events.

Why: HTTP and chat both need the auto-KG-extraction follow-up; this method keeps that side-effect chain in one place. What: opens the palace, stores the drawer via PalaceHandle::remember_with_options (issue #3225: body.force threads through as RememberOptions::force, letting a caller bypass the QUALITY gates only — allow_secret_like is left at its default false, so secret detection always still runs, force or not), emits DrawerAdded + StatusChanged, then triggers tools::auto_extract_and_assert. Returns the new drawer id. Test: http_create_drawer_runs_auto_kg_extraction, create_drawer_rejects_json_content_without_force, create_drawer_force_bypasses_quality_gate_for_json_content.

Source

pub async fn delete_drawer( &self, id: &str, drawer_id: &str, source: ActivitySource, ) -> ServiceResult<()>

Forget (delete) a drawer and emit the matching events.

Why: same dedup story as create_drawer. #5231: DELETE on a drawer id that was never stored used to answer 204 No Content, the same as a real delete — this now 404s, matching delete_palace. What: parses the drawer UUID, calls PalaceHandle::forget, maps [ForgetOutcome::NotFound] to ServiceError::not_found, and emits DrawerDeleted only when a drawer was actually removed. Test: delete_drawer_404s_for_an_unknown_drawer_id.

Source

pub async fn recall( &self, id: &str, query: &str, top_k: usize, deep: bool, ) -> ServiceResult<Value>

Per-palace recall (semantic search), optionally with deep retrieval.

Why: HTTP and chat tools both perform the same fan-out logic. What: opens the palace handle and dispatches to the shallow or deep recall helper. Returns a JSON array of flattened drawer rows (the recall_entry_json shape from issue #69). Test: recall_entry_json_hoists_drawer_fields.

Source

pub async fn recall_all(&self, query: &str, top_k: usize, deep: bool) -> Value

Cross-palace recall.

Why: shared between /api/v1/recall and the memory_recall_all chat tool. Encapsulating the open-everything-fanout-merge dance avoids drift. What: lists every palace, opens handles (skipping failures with a tracing::warn!), delegates to recall_across_palaces_with_default_embedder. Returns a JSON array. Why (issue #4637): unlike list_palaces/status, this route is NOT converted to peek(). A cross-palace recall that answered from cache-resident palaces only would silently omit ~98.9% of the corpus — a wrong answer that looks like a right one, which is strictly worse than a slow correct one. The open loop keeps opening every palace; it just no longer does so inline on a tokio worker thread. Making this route actually fast needs a different design (a shared cross-palace index, or an explicit palace-scoped query), not a cache-only read. Test: indirectly via recall_across_palaces_merges_results and the MCP memory_recall_all integration paths; open_palaces_blocking_opens_every_palace pins that uncached palaces are still searched.

Source§

impl MemoryService

Source

pub async fn kg_query( &self, id: &str, subject: &str, ) -> ServiceResult<Vec<Triple>>

Query the KG for all active triples whose subject matches.

Source

pub async fn kg_assert(&self, id: &str, body: KgAssertBody) -> ServiceResult<()>

Assert a triple in the KG.

#4888: this is the HTTP counterpart of the kg_assert MCP tool (POST /api/v1/palaces/{id}/kg) and accepts an arbitrary predicate, so it can write a hot predicate and must carry the same Tier S gate. It is not a hypothetical path — trusty-mpm’s provisioner seeds its identity fact through exactly this endpoint. A gate on the MCP tools alone would read as protection while leaving the surface writable.

Source

pub async fn kg_retract_triple( &self, id: &str, subject: &str, predicate: &str, ) -> ServiceResult<bool>

Retract the single active triple identified by (subject, predicate).

Why: Issue #278 — the DELETE /kg/triples/<id> HTTP endpoint needs a service-layer method so the HTTP handler stays a thin adapter. What: Opens the palace handle, calls KnowledgeGraph::retract, and maps the closed count to a 204/404 signal: Ok(true) when at least one interval was closed, Ok(false) when no active triple matched. Test: Covered by kg_delete_triple_returns_204_on_success in web::tests.

Source

pub async fn kg_list_subjects( &self, id: &str, limit: usize, ) -> ServiceResult<Vec<String>>

List distinct subjects in the KG.

Source

pub async fn kg_list_subjects_with_counts( &self, id: &str, limit: usize, ) -> ServiceResult<Vec<(String, u64)>>

List distinct subjects in the KG paired with their active-triple count.

Source

pub async fn kg_list_all( &self, id: &str, limit: usize, offset: usize, ) -> ServiceResult<Vec<Triple>>

Page through every active triple.

Source

pub async fn kg_count(&self, id: &str) -> ServiceResult<usize>

Return the count of currently-active triples.

#5384: a failed count read is a 500, not {"active": 0} — the badge this feeds cannot tell those apart.

Source

pub async fn kg_graph(&self, id: &str) -> ServiceResult<KgGraphPayload>

Build the per-palace visual graph payload.

Why (issue #4670): the node_count / edge_count / community_count here are computed over the FULL adjacency while triples is capped at [KG_GRAPH_MAX_TRIPLES]. That mismatch used to be invisible — the UI rendered 5,000 triples under a “9,311 nodes” badge — and because list_active orders by valid_from DESC the dropped triples were silently the oldest. The payload now reports what it actually returned alongside what exists, so truncation is machine-detectable. What: unchanged query; adds returned_triple_count, active_triple_count, and the derived truncated flag. Test: kg_graph_signals_truncation, kg_graph_returns_active_triples.

Source

pub async fn kg_graph_with_cap( &self, id: &str, max_triples: usize, ) -> ServiceResult<KgGraphPayload>

Self::kg_graph with an explicit triple cap.

Why (issue #4670): the truncation-signalling branch is only reachable above KG_GRAPH_MAX_TRIPLES (5,000), and seeding 5,001 triples costs ~90 s of test time — expensive enough that the branch would in practice go untested. Taking the cap as a parameter makes it provable with five triples and a cap of three, at no cost to the production call path. What: the real implementation; kg_graph is a thin wrapper that passes the production constant. Test: kg_graph_signals_truncation.

Source

pub async fn kg_graph_seed( &self, id: &str, limit: usize, ) -> ServiceResult<KgSeedPayload>

Top-limit nodes by degree plus the edges among them (issue #4670).

Why: first paint must show the graph’s skeleton, not 9,311 nodes in an O(n²) layout. Measured on the live 8,266-triple palace, 90.2% of nodes are degree-1 leaves and only 7.2% have degree >= 5, so a top-degree slice carries essentially all of the visible structure and everything else stays one click away. What: runs [KnowledgeGraph::top_degree_subgraph] over the resident adjacency (O(V log V + E), no disk I/O) and pairs the result with the palace-wide totals the header needs to report honestly. Test: kg_graph_seed_ranks_by_degree, kg_graph_seed_clamps_limit.

Source

pub async fn kg_neighbors( &self, id: &str, node: &str, direction: ExpandDirection, max_hops: usize, ) -> ServiceResult<KgNeighborsPayload>

Direction-aware, hop-bounded expansion around one node (issue #4670).

Why: click-to-expand needs “what points AT this node”, which no HTTP endpoint could answer — kg_query is a subject prefix scan. Bounding the hops keeps one click on a hub from pulling the whole palace. What: delegates to [KnowledgeGraph::expand_neighbors]. direction and max_hops are already validated/clamped by the HTTP layer; they are echoed back so the client can see what actually ran. Test: kg_neighbors_returns_incoming_edges, kg_neighbors_clamps_max_hops.

Source

pub async fn dream_status_aggregate(&self) -> DreamStatusPayload

Aggregate dream stats across every persisted palace.

Source

pub async fn dream_status_for_palace( &self, id: &str, ) -> ServiceResult<DreamStatusPayload>

Per-palace dream stats snapshot.

Source

pub async fn dream_run(&self) -> ServiceResult<DreamStatusPayload>

Run a dream cycle across every palace.

Why (issue #4637): like recall_all, this route is deliberately NOT converted to PalaceRegistry::peek. Dreaming is a maintenance pass — consolidating only the 64 palaces that happen to be cache-resident would silently stop maintaining the other ~5,730, which is a worse failure than a slow run because nothing reports it. Opening every palace stays correct; what changes is that the blocking open no longer runs inline on a tokio worker thread. A long dream run is expected — this is an explicitly-triggered POST, not a page load. What: lists palaces on the blocking pool, then per palace hops to spawn_blocking for the open before awaiting the async dream cycle. Test: dream_run_aggregates_stats.

Source

pub async fn list_activity( &self, filter: ActivityFilter, limit: usize, offset: usize, ) -> ServiceResult<(Vec<ActivityEntry>, u64)>

Paginated activity-log read.

Source

pub fn open_handle(&self, id: &str) -> ServiceResult<Arc<PalaceHandle>>

Open the named palace, returning ServiceError::NotFound on failure.

Trait Implementations§

Source§

impl Clone for MemoryService

Source§

fn clone(&self) -> MemoryService

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

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