diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 2239fafca..81999797e 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -1819,6 +1819,20 @@ async fn run_server_inner( ), Err(e) => log::warn!("[boot] whatsapp_data::global init failed: {e}"), } + // Seed the people store so people controllers + `people_*` + // tools can read/write. Without this the process-global stays + // empty and every call fails with "people store not + // initialised" (Sentry TAURI-RUST-8NM). Sits inside this + // Ok(cfg) arm so it inherits the wrong-workspace guard above + // (never seed against a Config::default fallback). + match crate::openhuman::people::store::init_from_workspace(&cfg.workspace_dir).await + { + Ok(_) => log::info!( + "[boot] people::store initialized (workspace={})", + cfg.workspace_dir.display() + ), + Err(e) => log::warn!("[boot] people::store init failed: {e}"), + } // Prune legacy bundled skills (dev-workflow / github-issue-crusher // / pr-review-shepherd) that older builds seeded into // /skills/. OpenHuman no longer ships bundled defaults; diff --git a/src/openhuman/people/README.md b/src/openhuman/people/README.md index f5e087e95..b7e694bca 100644 --- a/src/openhuman/people/README.md +++ b/src/openhuman/people/README.md @@ -58,7 +58,7 @@ Dedicated SQLite DB managed by `PeopleStore` (open via `open_at(path)` or `open_ - `handle_aliases` — `(kind, value)` primary key → `person_id` (FK, `ON DELETE CASCADE`); `value` is the canonicalized form. This table *is* the resolver index. - `interactions` — `(person_id, ts, is_outbound, length)` rows the scorer aggregates; indexed by `(person_id, ts DESC)` and `ts DESC`. -Migrations are tracked in `_people_migrations` and applied idempotently in a transaction. The store is exposed process-globally through a `tokio::sync::OnceCell` (`init` once at startup, `get` from controller handlers). Note: no `people::store::init` call was found outside the module — controller handlers will return "people store not initialised" until something seeds it. +Migrations are tracked in `_people_migrations` and applied idempotently in a transaction. The store is exposed process-globally through a `tokio::sync::OnceCell` (`get` from controller handlers). Core boot seeds it via `store::init_from_workspace(workspace_dir)` (`src/core/jsonrpc.rs`, alongside `memory::global` and `whatsapp_data::global`), opening `/people/people.db`; tests construct stores directly with `open_in_memory`. ## Dependencies diff --git a/src/openhuman/people/store.rs b/src/openhuman/people/store.rs index 179214515..008cfa20e 100644 --- a/src/openhuman/people/store.rs +++ b/src/openhuman/people/store.rs @@ -29,6 +29,33 @@ pub async fn init(store: Arc) -> Result<(), &'static str> { .map_err(|_| "people store already initialised") } +/// Seed the process-global store from a workspace directory, opening the +/// on-disk db at `/people/people.db` (schema migrations run on +/// open). Idempotent: a second call — or a concurrent boot path — is a no-op +/// that returns the already-seeded store rather than erroring. +/// +/// Wired into core boot (`src/core/jsonrpc.rs`) alongside `memory::global` and +/// `whatsapp_data::global`. Without this seed the global stays empty and every +/// people controller / `people_*` tool fails with "people store not +/// initialised" (Sentry TAURI-RUST-8NM). +pub async fn init_from_workspace( + workspace_dir: &std::path::Path, +) -> Result, String> { + if let Some(existing) = GLOBAL.get() { + log::debug!("[people:store] already initialised"); + return Ok(existing.clone()); + } + let db_path = workspace_dir.join("people").join("people.db"); + let store = Arc::new( + PeopleStore::open_at(&db_path).map_err(|e| format!("people store open failed: {e}"))?, + ); + // Race-resolve: another caller may have seeded while we were opening. + match GLOBAL.set(store.clone()) { + Ok(()) => Ok(store), + Err(_) => Ok(GLOBAL.get().cloned().unwrap_or(store)), + } +} + pub fn get() -> Result, &'static str> { GLOBAL .get() diff --git a/src/openhuman/people/tests.rs b/src/openhuman/people/tests.rs index 4a7a5828b..7608e5cb5 100644 --- a/src/openhuman/people/tests.rs +++ b/src/openhuman/people/tests.rs @@ -1,5 +1,7 @@ //! Cross-file integration tests for the people domain. +use std::sync::Arc; + use chrono::Utc; use crate::openhuman::people::address_book; @@ -57,6 +59,31 @@ fn schema_exposes_four_controllers() { assert_eq!(names.len(), 4); } +/// Regression for Sentry TAURI-RUST-8NM: the process-global people store was +/// never seeded at boot, so `store::get()` (and every `people_*` tool / +/// controller) always failed with "people store not initialised". Boot now +/// calls `init_from_workspace`; verify it seeds the global, creates the on-disk +/// db, and is idempotent. +#[tokio::test] +async fn init_from_workspace_seeds_global_store() { + use crate::openhuman::people::store; + + let tmp = tempfile::tempdir().unwrap(); + let store = store::init_from_workspace(tmp.path()).await.unwrap(); + assert!( + tmp.path().join("people").join("people.db").exists(), + "boot seed must create /people/people.db" + ); + + // The previously-dead global is now reachable — this is the fix. + let via_global = store::get().expect("people store reachable after boot seed"); + assert!(Arc::ptr_eq(&store, &via_global)); + + // Idempotent: a second seed returns the same instance, never errors. + let again = store::init_from_workspace(tmp.path()).await.unwrap(); + assert!(Arc::ptr_eq(&store, &again)); +} + #[test] fn person_id_uuid_format() { let id = PersonId::new();