refactor(openhuman): codify canonical module shape, tidy offenders (#2986)

This commit is contained in:
Steven Enamakel
2026-05-29 14:31:44 -07:00
committed by GitHub
parent 886c4a6342
commit 97bf23c53a
10 changed files with 423 additions and 384 deletions
+1 -1
View File
@@ -317,7 +317,7 @@ Deep link plugin is registered where supported; behavior is platform-specific (s
- **Controller schema contract**: Shared controller metadata types live in `src/core/types.rs` / `src/core/mod.rs` (`ControllerSchema`, `FieldSchema`, `TypeSchema`) and are consumed by adapters (RPC/CLI).
- **Domain schema files**: For each domain, define controller schema metadata in a dedicated module inside the domain folder (example: `src/openhuman/cron/schemas.rs`) and export from the domain `mod.rs`.
- **Controller-only exposure rule**: Expose domain functionality to **CLI and JSON-RPC through the controller registry** (`schemas.rs` + registered handlers wired into `src/core/all.rs`). Do **not** add domain-specific branches in `src/core/cli.rs` or `src/core/jsonrpc.rs`.
- **Light `mod.rs` rule**: Keep domain `mod.rs` files light and export-focused. Put operational code in sibling files (`ops.rs`, `store.rs`, `schedule.rs`, `types.rs`, `bus.rs`).
- **Light `mod.rs` rule**: Keep domain `mod.rs` files light and export-focused. Put operational code in sibling files (`ops.rs`, `store.rs`, `schedule.rs`, `types.rs`, `bus.rs`). See the **Canonical module shape** table in [`CLAUDE.md`](CLAUDE.md) for the full per-file contract (it is the single source of truth for module structure).
- **`src/core/`** — Transport only: Axum/HTTP, JSON-RPC envelope, CLI parsing, **dispatch** (`src/core/dispatch.rs`), auth, observability, event bus. **No** heavy business logic here. (Older docs that say `core_server` mean this directory; there is no `src/core_server/`.)
- **Layering**: Implementation in `openhuman::<domain>/`, controllers in `openhuman::<domain>/rpc.rs`, routes/dispatch in `src/core/`.
+20 -1
View File
@@ -225,9 +225,28 @@ Watch out for Tauri plugins that inject JS by default. `tauri-plugin-opener` shi
- **Controller schema contract**: shared types in `src/core/types.rs` / `src/core/mod.rs` (`ControllerSchema`, `FieldSchema`, `TypeSchema`).
- **Domain schema files**: per-domain `schemas.rs` (e.g. `src/openhuman/cron/schemas.rs`), exported from domain `mod.rs`.
- **Controller-only exposure**: expose features to CLI and JSON-RPC via the controller registry. **Do not** add domain branches in `src/core/cli.rs` / `src/core/jsonrpc.rs`.
- **Light `mod.rs`**: keep domain `mod.rs` export-focused. Operational code in `ops.rs`, `store.rs`, `types.rs`, etc.
- **Light `mod.rs`**: keep domain `mod.rs` export-focused. Operational code in `ops.rs`, `store.rs`, `types.rs`, etc. See **Canonical module shape** below for the full per-file contract.
- **`src/core/`** — Transport only. Modules: `all`, `all_tests`, `auth`, `autocomplete_cli_adapter`, `cli`, `cli_tests`, `dispatch`, `event_bus/`, `jsonrpc`, `jsonrpc_tests`, `legacy_aliases`, `logging`, `memory_cli`, `observability`, `rpc_log`, `shutdown`, `socketio`, `types`, plus `agent_cli`. No heavy domain logic here. (There is no `src/core_server/` — older docs that reference `core_server` mean `src/core/`.)
### Canonical module shape
Each high-level domain under `src/openhuman/<domain>/` should follow this file contract. Only `mod.rs` and tests are universal; the rest exist **only when applicable** — do not create empty placeholder files (e.g. a stateless domain has no `store.rs`, a domain that exposes no agent tools has no `tools.rs`).
| File | When | Role |
| --- | --- | --- |
| `mod.rs` | always | Export-focused **only**: module docstring + `mod`/`pub mod` decls + `pub use` re-exports, plus the `all_<domain>_controller_schemas` / `all_<domain>_registered_controllers` pair when RPC-facing. **No business logic, no domain-state statics, no domain `impl` blocks.** |
| `types.rs` | domain has its own types | Serde domain types. |
| `store.rs` | domain persists state | Persistence layer. |
| `ops.rs` | domain has logic / handlers | Business logic + entry points returning `RpcOutcome<T>`. **Canonical handler file** (`ops.rs` is the majority convention; `rpc.rs` is legal only where a domain separates a pure-domain API from ops, e.g. `cron` does `pub use ops as rpc`). |
| `schemas.rs` | RPC-facing | Controller schemas + `handle_*` fns delegating to `ops.rs` (see **Controller migration checklist**). |
| `tools.rs` | domain owns agent tools | Domain-owned tool impls live here (+ optional `tools/` submodules), re-exported via `src/openhuman/tools/mod.rs` (see AGENTS.md "Tool ownership rule"). Only genuinely cross-cutting tool families (filesystem, browser/computer, generic system/network) stay in `src/openhuman/tools/impl/`. |
| `bus.rs` | domain has event subscribers | `EventHandler` impls (see **Event bus**). |
| tests | new/changed behavior | Inline `#[cfg(test)] mod tests` (small modules) **or** a sibling `<file>_tests.rs` via `#[cfg(test)] #[path = "<file>_tests.rs"] mod tests;` (large suites). Both are legal. |
Two clarifications:
- **Inline tests do not count against "light `mod.rs`".** A `mod.rs` whose non-test body is pure re-exports is already compliant; moving a large inline suite into a sibling `mod_tests.rs` is tidiness, not a correctness requirement.
- **Narrow thin-facade exception:** pure dispatch forwarders (pick an implementation and forward — no domain state, no I/O of their own) MAY stay in `mod.rs` when being that facade is the module's whole purpose (e.g. `cwd_jail::spawn` / `spawn_with` / `default_backend`). Justify it in the module docstring.
### Controller migration checklist
- `src/openhuman/<domain>/mod.rs`: add `mod schemas;`, re-export `all_controller_schemas as all_<domain>_controller_schemas` and `all_registered_controllers as all_<domain>_registered_controllers`.
+13
View File
@@ -72,6 +72,19 @@ impl Jail {
}
Ok(())
}
/// Best-effort canonicalize that swallows errors and logs them. Most
/// callers should use the validating [`Jail::canonicalize`] path that
/// [`crate::openhuman::cwd_jail::spawn`] runs automatically.
pub fn canonicalize_or_log(&mut self) {
if let Err(e) = self.canonicalize() {
log::warn!(
"[cwd_jail] failed to canonicalize jail root {}: {}",
self.root.display(),
e
);
}
}
}
/// OS-specific enforcement of a [`Jail`].
-15
View File
@@ -92,21 +92,6 @@ pub fn spawn_with(backend: &dyn JailBackend, jail: &Jail, cmd: Command) -> std::
backend.spawn(&jail, cmd)
}
impl Jail {
/// Best-effort canonicalize that swallows errors and logs them. Most
/// callers should use the validating [`Jail::canonicalize`] path that
/// [`jail`] runs automatically.
pub fn canonicalize_or_log(&mut self) {
if let Err(e) = self.canonicalize() {
log::warn!(
"[cwd_jail] failed to canonicalize jail root {}: {}",
self.root.display(),
e
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+2 -226
View File
@@ -49,229 +49,5 @@ pub use schemas::{
};
#[cfg(test)]
mod tests {
use super::*;
// ── Trait default method ─────────────────────────────────
#[test]
fn noop_name_and_dims() {
let p = NoopEmbedding;
assert_eq!(p.name(), "none");
assert_eq!(p.model_id(), "none");
assert_eq!(p.dimensions(), 0);
assert_eq!(p.signature(), "provider=none;model=none;dims=0");
}
#[tokio::test]
async fn noop_embed_returns_empty() {
let p = NoopEmbedding;
let result = p.embed(&["hello"]).await.unwrap();
assert!(result.is_empty());
}
#[tokio::test]
async fn noop_embed_one_returns_error() {
// embed returns empty vec → pop() returns None → error from default impl
let p = NoopEmbedding;
let err = p.embed_one("hello").await.unwrap_err();
assert!(err.to_string().contains("Empty embedding result"));
}
#[tokio::test]
async fn noop_embed_empty_batch() {
let p = NoopEmbedding;
let result = p.embed(&[]).await.unwrap();
assert!(result.is_empty());
}
// ── Factory — success ────────────────────────────────────
#[test]
fn factory_ollama() {
let p = create_embedding_provider("ollama", DEFAULT_OLLAMA_MODEL, 768).unwrap();
assert_eq!(p.name(), "ollama");
assert_eq!(p.model_id(), DEFAULT_OLLAMA_MODEL);
assert_eq!(p.dimensions(), 768);
assert_eq!(p.signature(), "provider=ollama;model=bge-m3;dims=768");
}
#[test]
fn factory_openai() {
let p = create_embedding_provider("openai", "text-embedding-3-small", 1536).unwrap();
assert_eq!(p.name(), "openai");
assert_eq!(p.model_id(), "text-embedding-3-small");
assert_eq!(p.dimensions(), 1536);
}
#[test]
fn factory_custom_url() {
let p = create_embedding_provider("custom:http://localhost:1234", "model", 768).unwrap();
assert_eq!(p.name(), "openai"); // OpenAI-compatible under the hood
assert_eq!(p.dimensions(), 768);
}
#[test]
fn factory_custom_empty_url() {
let p = create_embedding_provider("custom:", "model", 768).unwrap();
assert_eq!(p.name(), "openai");
}
#[test]
fn factory_none() {
let p = create_embedding_provider("none", "", 0).unwrap();
assert_eq!(p.name(), "none");
assert_eq!(p.dimensions(), 0);
}
#[test]
fn factory_voyage() {
let p = create_embedding_provider("voyage", "voyage-3-large", 1024).unwrap();
assert_eq!(p.name(), "voyage");
assert_eq!(p.dimensions(), 1024);
}
#[test]
fn factory_cohere() {
let p = create_embedding_provider("cohere", "embed-english-v3.0", 1024).unwrap();
assert_eq!(p.name(), "cohere");
assert_eq!(p.dimensions(), 1024);
}
// ── Factory — errors ─────────────────────────────────────
#[test]
fn factory_unknown_provider_errors() {
let result = create_embedding_provider("deepseek", "model", 1536);
let msg = result.err().expect("should be an error").to_string();
assert!(
msg.contains("deepseek"),
"should include provider name: {msg}"
);
assert!(msg.contains("unknown"), "should say unknown: {msg}");
}
#[test]
fn factory_empty_string_errors() {
let result = create_embedding_provider("", "model", 1536);
assert!(result
.err()
.expect("should error")
.to_string()
.contains("unknown"));
}
#[test]
fn factory_fastembed_errors() {
let result = create_embedding_provider("fastembed", "BGESmallENV15", 384);
assert!(result
.err()
.expect("should error")
.to_string()
.contains("fastembed"));
}
#[test]
fn factory_cloud() {
let p = create_embedding_provider(
"cloud",
DEFAULT_CLOUD_EMBEDDING_MODEL,
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
)
.unwrap();
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
#[test]
fn factory_managed() {
let p = create_embedding_provider(
"managed",
DEFAULT_CLOUD_EMBEDDING_MODEL,
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
)
.unwrap();
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
// ── Default provider ─────────────────────────────────────
#[test]
fn default_provider_uses_cloud() {
let p = default_embedding_provider();
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
#[test]
fn default_local_provider_uses_ollama() {
let p = default_local_embedding_provider();
assert_eq!(p.name(), "ollama");
assert_eq!(p.dimensions(), DEFAULT_OLLAMA_DIMENSIONS);
}
// ── create_embedding_provider_with_credentials ───────────
#[test]
fn factory_with_credentials_voyage() {
let p = factory::create_embedding_provider_with_credentials(
"voyage",
"voyage-3-large",
1024,
"voyage-test-key",
None,
)
.expect("voyage with key");
assert_eq!(p.name(), "voyage");
assert_eq!(p.model_id(), "voyage-3-large");
assert_eq!(p.dimensions(), 1024);
}
#[test]
fn factory_with_credentials_cohere() {
let p = factory::create_embedding_provider_with_credentials(
"cohere",
"embed-english-v3.0",
1024,
"cohere-test-key",
None,
)
.expect("cohere with key");
assert_eq!(p.name(), "cohere");
assert_eq!(p.model_id(), "embed-english-v3.0");
assert_eq!(p.dimensions(), 1024);
}
#[test]
fn factory_with_credentials_custom() {
let p = factory::create_embedding_provider_with_credentials(
"custom",
"custom-model",
768,
"custom-key",
Some("http://localhost:9999"),
)
.expect("custom provider with endpoint");
// Custom is backed by OpenAiEmbedding
assert_eq!(p.name(), "openai");
assert_eq!(p.dimensions(), 768);
}
#[test]
fn factory_with_credentials_managed_ignores_key() {
// Managed/cloud provider does not use the API key — it routes through
// the OpenHuman backend. Creating it with an arbitrary key must succeed
// and produce the cloud provider.
let p = factory::create_embedding_provider_with_credentials(
"managed",
DEFAULT_CLOUD_EMBEDDING_MODEL,
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
"should-be-ignored",
None,
)
.expect("managed ignores key");
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
}
#[path = "mod_tests.rs"]
mod tests;
+230
View File
@@ -0,0 +1,230 @@
//! Tests for the embeddings module root (provider factory + trait defaults).
//!
//! Split out of `mod.rs` to keep the module root export-focused. Declared via
//! `#[path = "mod_tests.rs"] mod tests;` so `super::*` still resolves to the
//! `embeddings` module.
use super::*;
// ── Trait default method ─────────────────────────────────
#[test]
fn noop_name_and_dims() {
let p = NoopEmbedding;
assert_eq!(p.name(), "none");
assert_eq!(p.model_id(), "none");
assert_eq!(p.dimensions(), 0);
assert_eq!(p.signature(), "provider=none;model=none;dims=0");
}
#[tokio::test]
async fn noop_embed_returns_empty() {
let p = NoopEmbedding;
let result = p.embed(&["hello"]).await.unwrap();
assert!(result.is_empty());
}
#[tokio::test]
async fn noop_embed_one_returns_error() {
// embed returns empty vec → pop() returns None → error from default impl
let p = NoopEmbedding;
let err = p.embed_one("hello").await.unwrap_err();
assert!(err.to_string().contains("Empty embedding result"));
}
#[tokio::test]
async fn noop_embed_empty_batch() {
let p = NoopEmbedding;
let result = p.embed(&[]).await.unwrap();
assert!(result.is_empty());
}
// ── Factory — success ────────────────────────────────────
#[test]
fn factory_ollama() {
let p = create_embedding_provider("ollama", DEFAULT_OLLAMA_MODEL, 768).unwrap();
assert_eq!(p.name(), "ollama");
assert_eq!(p.model_id(), DEFAULT_OLLAMA_MODEL);
assert_eq!(p.dimensions(), 768);
assert_eq!(p.signature(), "provider=ollama;model=bge-m3;dims=768");
}
#[test]
fn factory_openai() {
let p = create_embedding_provider("openai", "text-embedding-3-small", 1536).unwrap();
assert_eq!(p.name(), "openai");
assert_eq!(p.model_id(), "text-embedding-3-small");
assert_eq!(p.dimensions(), 1536);
}
#[test]
fn factory_custom_url() {
let p = create_embedding_provider("custom:http://localhost:1234", "model", 768).unwrap();
assert_eq!(p.name(), "openai"); // OpenAI-compatible under the hood
assert_eq!(p.dimensions(), 768);
}
#[test]
fn factory_custom_empty_url() {
let p = create_embedding_provider("custom:", "model", 768).unwrap();
assert_eq!(p.name(), "openai");
}
#[test]
fn factory_none() {
let p = create_embedding_provider("none", "", 0).unwrap();
assert_eq!(p.name(), "none");
assert_eq!(p.dimensions(), 0);
}
#[test]
fn factory_voyage() {
let p = create_embedding_provider("voyage", "voyage-3-large", 1024).unwrap();
assert_eq!(p.name(), "voyage");
assert_eq!(p.dimensions(), 1024);
}
#[test]
fn factory_cohere() {
let p = create_embedding_provider("cohere", "embed-english-v3.0", 1024).unwrap();
assert_eq!(p.name(), "cohere");
assert_eq!(p.dimensions(), 1024);
}
// ── Factory — errors ─────────────────────────────────────
#[test]
fn factory_unknown_provider_errors() {
let result = create_embedding_provider("deepseek", "model", 1536);
let msg = result.err().expect("should be an error").to_string();
assert!(
msg.contains("deepseek"),
"should include provider name: {msg}"
);
assert!(msg.contains("unknown"), "should say unknown: {msg}");
}
#[test]
fn factory_empty_string_errors() {
let result = create_embedding_provider("", "model", 1536);
assert!(result
.err()
.expect("should error")
.to_string()
.contains("unknown"));
}
#[test]
fn factory_fastembed_errors() {
let result = create_embedding_provider("fastembed", "BGESmallENV15", 384);
assert!(result
.err()
.expect("should error")
.to_string()
.contains("fastembed"));
}
#[test]
fn factory_cloud() {
let p = create_embedding_provider(
"cloud",
DEFAULT_CLOUD_EMBEDDING_MODEL,
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
)
.unwrap();
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
#[test]
fn factory_managed() {
let p = create_embedding_provider(
"managed",
DEFAULT_CLOUD_EMBEDDING_MODEL,
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
)
.unwrap();
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
// ── Default provider ─────────────────────────────────────
#[test]
fn default_provider_uses_cloud() {
let p = default_embedding_provider();
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
#[test]
fn default_local_provider_uses_ollama() {
let p = default_local_embedding_provider();
assert_eq!(p.name(), "ollama");
assert_eq!(p.dimensions(), DEFAULT_OLLAMA_DIMENSIONS);
}
// ── create_embedding_provider_with_credentials ───────────
#[test]
fn factory_with_credentials_voyage() {
let p = factory::create_embedding_provider_with_credentials(
"voyage",
"voyage-3-large",
1024,
"voyage-test-key",
None,
)
.expect("voyage with key");
assert_eq!(p.name(), "voyage");
assert_eq!(p.model_id(), "voyage-3-large");
assert_eq!(p.dimensions(), 1024);
}
#[test]
fn factory_with_credentials_cohere() {
let p = factory::create_embedding_provider_with_credentials(
"cohere",
"embed-english-v3.0",
1024,
"cohere-test-key",
None,
)
.expect("cohere with key");
assert_eq!(p.name(), "cohere");
assert_eq!(p.model_id(), "embed-english-v3.0");
assert_eq!(p.dimensions(), 1024);
}
#[test]
fn factory_with_credentials_custom() {
let p = factory::create_embedding_provider_with_credentials(
"custom",
"custom-model",
768,
"custom-key",
Some("http://localhost:9999"),
)
.expect("custom provider with endpoint");
// Custom is backed by OpenAiEmbedding
assert_eq!(p.name(), "openai");
assert_eq!(p.dimensions(), 768);
}
#[test]
fn factory_with_credentials_managed_ignores_key() {
// Managed/cloud provider does not use the API key — it routes through
// the OpenHuman backend. Creating it with an arbitrary key must succeed
// and produce the cloud provider.
let p = factory::create_embedding_provider_with_credentials(
"managed",
DEFAULT_CLOUD_EMBEDDING_MODEL,
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
"should-be-ignored",
None,
)
.expect("managed ignores key");
assert_eq!(p.name(), "cloud");
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
}
+2 -48
View File
@@ -16,51 +16,5 @@ pub use types::{
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_scope_equality() {
assert_eq!(ToolScope::All, ToolScope::All);
assert_ne!(ToolScope::All, ToolScope::CliRpcOnly);
assert_ne!(ToolScope::AgentOnly, ToolScope::CliRpcOnly);
}
#[test]
fn backend_response_deserializes() {
let json = r#"{"success": true, "data": {"foo": 42}}"#;
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.data.unwrap()["foo"], 42);
}
#[test]
fn backend_response_without_data() {
let json = r#"{"success": true}"#;
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert!(resp.data.is_none());
}
#[test]
fn integration_pricing_defaults_on_missing_fields() {
let json = r#"{"integrations": {}}"#;
let pricing: IntegrationPricing = serde_json::from_str(json).unwrap();
assert!(pricing.integrations.apify.is_none());
assert!(pricing.integrations.twilio.is_none());
assert!(pricing.integrations.google_places.is_none());
assert!(pricing.integrations.parallel.is_none());
assert!(pricing.integrations.tinyfish.is_none());
}
#[test]
fn build_client_returns_none_when_no_auth_token() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = crate::openhuman::config::Config {
workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"),
..crate::openhuman::config::Config::default()
};
assert!(build_client(&config).is_none());
}
}
#[path = "mod_tests.rs"]
mod tests;
+52
View File
@@ -0,0 +1,52 @@
//! Tests for the integrations module root (types + client construction).
//!
//! Split out of `mod.rs` to keep the module root export-focused. Declared via
//! `#[path = "mod_tests.rs"] mod tests;` so `super::*` still resolves to the
//! `integrations` module.
use super::*;
#[test]
fn tool_scope_equality() {
assert_eq!(ToolScope::All, ToolScope::All);
assert_ne!(ToolScope::All, ToolScope::CliRpcOnly);
assert_ne!(ToolScope::AgentOnly, ToolScope::CliRpcOnly);
}
#[test]
fn backend_response_deserializes() {
let json = r#"{"success": true, "data": {"foo": 42}}"#;
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.data.unwrap()["foo"], 42);
}
#[test]
fn backend_response_without_data() {
let json = r#"{"success": true}"#;
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert!(resp.data.is_none());
}
#[test]
fn integration_pricing_defaults_on_missing_fields() {
let json = r#"{"integrations": {}}"#;
let pricing: IntegrationPricing = serde_json::from_str(json).unwrap();
assert!(pricing.integrations.apify.is_none());
assert!(pricing.integrations.twilio.is_none());
assert!(pricing.integrations.google_places.is_none());
assert!(pricing.integrations.parallel.is_none());
assert!(pricing.integrations.tinyfish.is_none());
}
#[test]
fn build_client_returns_none_when_no_auth_token() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = crate::openhuman::config::Config {
workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"),
..crate::openhuman::config::Config::default()
};
assert!(build_client(&config).is_none());
}
+2 -93
View File
@@ -31,6 +31,7 @@
//! `openhuman::memory` re-exports it as `memory::jobs` during the migration.
pub(crate) mod handlers;
mod ops;
mod redact;
pub mod scheduler;
pub mod store;
@@ -38,82 +39,7 @@ pub mod testing;
pub mod types;
pub(crate) mod worker;
use std::sync::atomic::{AtomicBool, Ordering};
/// #1574 §6 / #1365: set while a re-embed backfill chain has work pending.
///
/// Read by the first-person / subconscious retrieval layer so an empty
/// vector-search result during the backfill window is interpreted as
/// "not searched yet" rather than "no such memory" — preventing the agent
/// from confidently asserting false self-ignorance mid-re-embed. Set true
/// when a backfill is enqueued / still has rows; cleared when the chain
/// drains. Process-global (resets to false on restart; the worker re-sets
/// it on the next backfill tick — acceptable for v1).
static BACKFILL_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
/// Mark whether a re-embed backfill currently has pending work.
pub fn set_backfill_in_progress(v: bool) {
BACKFILL_IN_PROGRESS.store(v, Ordering::Relaxed);
}
/// True while a re-embed backfill chain still has rows to process. The
/// #1365 absence-reasoning consumer checks this before treating an empty
/// semantic-recall result as "no memory exists".
pub fn backfill_in_progress() -> bool {
BACKFILL_IN_PROGRESS.load(Ordering::Relaxed)
}
/// #1574 §4: ensure a re-embed backfill chain exists for the **current**
/// active signature, if (and only if) there is uncovered work.
///
/// This is the switch-path trigger: call it after the embedder config
/// changes (a new signature → every prior row is missing at it). The §7
/// migration is one-shot (`user_version`-gated) so it does NOT fire on a
/// later model switch — without this, switching silently blinds prior
/// memory. Standalone (own connection); the §7 migration keeps its own
/// in-tx enqueue (atomic with the copy). Idempotent + non-fatal: the
/// per-signature dedupe key means at most one chain per space, and a
/// covered space enqueues nothing. Errors are logged, never propagated —
/// a failed enqueue must not fail the user's settings save.
pub fn ensure_reembed_backfill(config: &crate::openhuman::config::Config) {
let sig = crate::openhuman::memory_store::chunks::store::tree_active_signature(config);
let result = crate::openhuman::memory_store::chunks::store::with_connection(config, |conn| {
Ok(crate::openhuman::memory_store::chunks::store::has_uncovered_reembed_work(conn, &sig)?)
});
match result {
Ok(true) => {
let job = match types::NewJob::reembed_backfill(&types::ReembedBackfillPayload {
signature: sig.clone(),
}) {
Ok(j) => j,
Err(e) => {
log::warn!("[memory::jobs] ensure_reembed_backfill: build job failed: {e}");
return;
}
};
match store::enqueue(config, &job) {
Ok(_) => {
set_backfill_in_progress(true);
log::info!(
"[memory::jobs] ensure_reembed_backfill: enqueued chain for sig={sig}"
);
}
Err(e) => log::warn!(
"[memory::jobs] ensure_reembed_backfill: enqueue failed for sig={sig}: {e}"
),
}
}
Ok(false) => {
log::debug!(
"[memory::jobs] ensure_reembed_backfill: sig={sig} fully covered; nothing to do"
);
}
Err(e) => log::warn!(
"[memory::jobs] ensure_reembed_backfill: coverage probe failed for sig={sig}: {e}"
),
}
}
pub use ops::{backfill_in_progress, ensure_reembed_backfill, set_backfill_in_progress};
pub use scheduler::{backfill_missing_digests, trigger_digest};
pub use store::{
claim_next, count_by_status, count_total, enqueue, enqueue_tx, get_job, mark_deferred,
@@ -125,20 +51,3 @@ pub use types::{
Job, JobKind, JobOutcome, JobStatus, NewJob, NodeRef, SealPayload, TopicRoutePayload,
};
pub use worker::{start, wake_workers};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backfill_flag_roundtrip() {
set_backfill_in_progress(false);
assert!(!backfill_in_progress());
set_backfill_in_progress(true);
assert!(backfill_in_progress());
set_backfill_in_progress(false);
assert!(!backfill_in_progress());
}
}
+101
View File
@@ -0,0 +1,101 @@
//! Memory-queue operations: backfill-progress signalling and the re-embed
//! backfill switch-path trigger.
//!
//! Split out of `mod.rs` so the module root stays export-focused. Public paths
//! are preserved via re-exports in [`super`], so callers keep using
//! `crate::openhuman::memory_queue::<fn>`.
use std::sync::atomic::{AtomicBool, Ordering};
use super::{store, types};
/// #1574 §6 / #1365: set while a re-embed backfill chain has work pending.
///
/// Read by the first-person / subconscious retrieval layer so an empty
/// vector-search result during the backfill window is interpreted as
/// "not searched yet" rather than "no such memory" — preventing the agent
/// from confidently asserting false self-ignorance mid-re-embed. Set true
/// when a backfill is enqueued / still has rows; cleared when the chain
/// drains. Process-global (resets to false on restart; the worker re-sets
/// it on the next backfill tick — acceptable for v1).
static BACKFILL_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
/// Mark whether a re-embed backfill currently has pending work.
pub fn set_backfill_in_progress(v: bool) {
BACKFILL_IN_PROGRESS.store(v, Ordering::Relaxed);
}
/// True while a re-embed backfill chain still has rows to process. The
/// #1365 absence-reasoning consumer checks this before treating an empty
/// semantic-recall result as "no memory exists".
pub fn backfill_in_progress() -> bool {
BACKFILL_IN_PROGRESS.load(Ordering::Relaxed)
}
/// #1574 §4: ensure a re-embed backfill chain exists for the **current**
/// active signature, if (and only if) there is uncovered work.
///
/// This is the switch-path trigger: call it after the embedder config
/// changes (a new signature → every prior row is missing at it). The §7
/// migration is one-shot (`user_version`-gated) so it does NOT fire on a
/// later model switch — without this, switching silently blinds prior
/// memory. Standalone (own connection); the §7 migration keeps its own
/// in-tx enqueue (atomic with the copy). Idempotent + non-fatal: the
/// per-signature dedupe key means at most one chain per space, and a
/// covered space enqueues nothing. Errors are logged, never propagated —
/// a failed enqueue must not fail the user's settings save.
pub fn ensure_reembed_backfill(config: &crate::openhuman::config::Config) {
let sig = crate::openhuman::memory_store::chunks::store::tree_active_signature(config);
let result = crate::openhuman::memory_store::chunks::store::with_connection(config, |conn| {
Ok(crate::openhuman::memory_store::chunks::store::has_uncovered_reembed_work(conn, &sig)?)
});
match result {
Ok(true) => {
let job = match types::NewJob::reembed_backfill(&types::ReembedBackfillPayload {
signature: sig.clone(),
}) {
Ok(j) => j,
Err(e) => {
log::warn!("[memory::jobs] ensure_reembed_backfill: build job failed: {e}");
return;
}
};
match store::enqueue(config, &job) {
Ok(_) => {
set_backfill_in_progress(true);
log::info!(
"[memory::jobs] ensure_reembed_backfill: enqueued chain for sig={sig}"
);
}
Err(e) => log::warn!(
"[memory::jobs] ensure_reembed_backfill: enqueue failed for sig={sig}: {e}"
),
}
}
Ok(false) => {
log::debug!(
"[memory::jobs] ensure_reembed_backfill: sig={sig} fully covered; nothing to do"
);
}
Err(e) => log::warn!(
"[memory::jobs] ensure_reembed_backfill: coverage probe failed for sig={sig}: {e}"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backfill_flag_roundtrip() {
set_backfill_in_progress(false);
assert!(!backfill_in_progress());
set_backfill_in_progress(true);
assert!(backfill_in_progress());
set_backfill_in_progress(false);
assert!(!backfill_in_progress());
}
}