From d73a9396df40b564b60ad49045c2faf26c99de08 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:44:47 +0300 Subject: [PATCH 01/18] chore: restructure repository into a Cargo workspace Moved all crates into a top-level `crates/` directory and removed several unused test and module files to clean up the project structure. This change consolidates the codebase into a standard Cargo workspace layout without altering any runtime behaviour. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .../store/namespace_store/documents_tests.rs | 1508 ----------------- core/src/store/namespace_store/fts5.rs | 610 ------- core/src/store/safety/mod.rs | 340 ---- core/src/store/write_gate_tests.rs | 176 -- {api => crates/tinymemory-api}/Cargo.toml | 0 .../tinymemory-api}/src/capabilities.rs | 0 .../tinymemory-api}/src/capabilities_tests.rs | 0 {api => crates/tinymemory-api}/src/chunks.rs | 0 .../tinymemory-api}/src/chunks_tests.rs | 0 {api => crates/tinymemory-api}/src/drivers.rs | 0 {api => crates/tinymemory-api}/src/error.rs | 0 .../tinymemory-api}/src/error_tests.rs | 0 {api => crates/tinymemory-api}/src/goals.rs | 0 .../tinymemory-api}/src/goals_tests.rs | 0 {api => crates/tinymemory-api}/src/health.rs | 0 .../tinymemory-api}/src/health_tests.rs | 0 .../src/host/cloud_providers.rs | 0 .../tinymemory-api}/src/host/composio.rs | 0 .../tinymemory-api}/src/host/config.rs | 0 .../src/host/embedding_host.rs | 0 .../tinymemory-api}/src/host/embeddings.rs | 0 .../src/host/error_reporter.rs | 0 .../tinymemory-api}/src/host/events.rs | 0 .../tinymemory-api}/src/host/evidence.rs | 0 .../tinymemory-api}/src/host/local_ai.rs | 0 .../tinymemory-api}/src/host/mod.rs | 0 .../tinymemory-api}/src/host/nlp.rs | 0 .../tinymemory-api}/src/host/routes.rs | 0 .../src/host/scheduler_gate.rs | 0 .../src/host/storage_memory.rs | 0 .../tinymemory-api}/src/host/subsystems.rs | 0 .../tinymemory-api}/src/host/test_support.rs | 0 .../tinymemory-api}/src/host/usage.rs | 0 {api => crates/tinymemory-api}/src/lib.rs | 0 .../tinymemory-api}/src/mandatory/mod.rs | 0 .../tinymemory-api}/src/mandatory/provider.rs | 0 .../tinymemory-api}/src/mandatory/test.rs | 0 {api => crates/tinymemory-api}/src/null.rs | 0 .../tinymemory-api}/src/null_tests.rs | 0 .../tinymemory-api}/src/provider/audit.rs | 0 .../src/provider/audit_tests.rs | 0 .../tinymemory-api}/src/provider/chunks.rs | 0 .../tinymemory-api}/src/provider/content.rs | 0 .../tinymemory-api}/src/provider/driver.rs | 0 .../tinymemory-api}/src/provider/episodic.rs | 0 .../tinymemory-api}/src/provider/knowledge.rs | 0 .../tinymemory-api}/src/provider/mandatory.rs | 0 .../tinymemory-api}/src/provider/mod.rs | 0 .../tinymemory-api}/src/provider/people.rs | 0 .../tinymemory-api}/src/provider/profile.rs | 0 .../tinymemory-api}/src/provider/records.rs | 0 .../tinymemory-api}/src/provider/retrieval.rs | 0 .../tinymemory-api}/src/provider/types.rs | 0 .../src/provider/types_tests.rs | 0 {api => crates/tinymemory-api}/src/recall.rs | 0 .../tinymemory-api}/src/recall_tests.rs | 0 .../tinymemory-api}/src/tool_memory.rs | 0 .../tinymemory-api}/src/tool_memory_tests.rs | 0 {api => crates/tinymemory-api}/src/traits.rs | 0 {api => crates/tinymemory-api}/src/tree.rs | 0 .../tinymemory-api}/src/tree_tests.rs | 0 {api => crates/tinymemory-api}/src/types.rs | 0 .../tinymemory-api}/src/types_tests.rs | 0 {api => crates/tinymemory-api}/src/version.rs | 0 .../tinymemory-api}/src/version_tests.rs | 0 {api => crates/tinymemory-api}/src/wire.rs | 0 .../tinymemory-api}/src/wire_tests.rs | 0 .../tinymemory-conformance}/Cargo.toml | 0 .../tinymemory-conformance}/src/lib.rs | 0 .../src/reference/mod.rs | 0 .../tinymemory-conformance}/src/suite/mod.rs | 0 .../tests/reference_drivers.rs | 0 {core => crates/tinymemory-core}/Cargo.toml | 0 {core => crates/tinymemory-core}/src/chat.rs | 0 .../tinymemory-core}/src/chat_host.rs | 0 .../tinymemory-core}/src/composio_host.rs | 0 .../tinymemory-core}/src/config_loader.rs | 0 .../src/conversations/blocking.rs | 0 .../src/conversations/blocking_tests.rs | 0 .../tinymemory-core}/src/conversations/mod.rs | 0 .../tinymemory-core}/src/diff/mod.rs | 0 .../tinymemory-core}/src/diff/ops.rs | 0 .../tinymemory-core}/src/diff/source.rs | 0 .../tinymemory-core}/src/diff/stub.rs | 0 .../tinymemory-core}/src/embedding_adapter.rs | 0 .../tinymemory-core}/src/embedding_host.rs | 0 .../tinymemory-core}/src/engine/backend.rs | 0 .../tinymemory-core}/src/engine/chat.rs | 0 .../tinymemory-core}/src/engine/config.rs | 0 .../tinymemory-core}/src/engine/embeddings.rs | 0 .../tinymemory-core}/src/engine/ingest.rs | 0 .../tinymemory-core}/src/engine/mod.rs | 0 .../tinymemory-core}/src/engine/parity.rs | 0 .../tinymemory-core}/src/engine/persona.rs | 0 .../src/engine/queue_driver.rs | 0 .../tinymemory-core}/src/engine/seal.rs | 0 .../tinymemory-core}/src/engine/summariser.rs | 0 .../tinymemory-core}/src/engine/sync.rs | 0 .../tinymemory-core}/src/events.rs | 0 .../tinymemory-core}/src/global.rs | 0 .../tinymemory-core}/src/ingest_pipeline.rs | 0 .../tinymemory-core}/src/ingestion/README.md | 0 .../tinymemory-core}/src/ingestion/mod.rs | 0 .../tinymemory-core}/src/ingestion/queue.rs | 0 .../tinymemory-core}/src/ingestion/state.rs | 0 .../tinymemory-core}/src/ingestion/tests.rs | 0 .../src/learning_candidate.rs | 0 {core => crates/tinymemory-core}/src/lib.rs | 0 .../tinymemory-core}/src/nlp_host.rs | 0 .../tinymemory-core}/src/observability.rs | 0 .../tinymemory-core}/src/people/README.md | 0 .../tinymemory-core}/src/people/mod.rs | 0 .../tinymemory-core}/src/preferences.rs | 0 .../tinymemory-core}/src/queue/README.md | 0 .../tinymemory-core}/src/queue/mod.rs | 0 .../tinymemory-core}/src/queue/ops.rs | 0 .../tinymemory-core}/src/queue/scheduler.rs | 0 .../tinymemory-core}/src/queue/store.rs | 0 .../tinymemory-core}/src/queue/testing.rs | 0 .../tinymemory-core}/src/queue/types.rs | 0 .../tinymemory-core}/src/queue/worker.rs | 0 .../tinymemory-core}/src/remember.rs | 0 .../tinymemory-core}/src/rpc_models.rs | 0 .../tinymemory-core}/src/rpc_models_tests.rs | 0 .../tinymemory-core}/src/scheduler_gate.rs | 0 .../tinymemory-core}/src/search/mod.rs | 0 .../tinymemory-core}/src/shutdown.rs | 0 .../tinymemory-core}/src/source_scope.rs | 0 .../tinymemory-core}/src/sources/README.md | 0 .../tinymemory-core}/src/sources/mod.rs | 0 .../src/sources/readers/composio.rs | 0 .../src/sources/readers/conversation.rs | 0 .../src/sources/readers/folder.rs | 0 .../src/sources/readers/github.rs | 0 .../src/sources/readers/mod.rs | 0 .../src/sources/readers/rss.rs | 0 .../src/sources/readers/twitter.rs | 0 .../src/sources/readers/web_page.rs | 0 .../tinymemory-core}/src/sources/reconcile.rs | 0 .../tinymemory-core}/src/sources/registry.rs | 0 .../tinymemory-core}/src/sources/status.rs | 0 .../tinymemory-core}/src/sources/sync.rs | 0 .../tinymemory-core}/src/sources/types.rs | 0 .../tinymemory-core}/src/store/README.md | 0 .../src/store/chunks/connection.rs | 0 .../src/store/chunks/embeddings.rs | 0 .../tinymemory-core}/src/store/chunks/mod.rs | 0 .../src/store/chunks/raw_refs.rs | 0 .../src/store/chunks/semantic.rs | 0 .../src/store/chunks/store.rs | 0 .../src/store/chunks/types.rs | 0 .../tinymemory-core}/src/store/client.rs | 0 .../src/store/client_tests.rs | 0 .../src/store/content/README.md | 0 .../tinymemory-core}/src/store/content/mod.rs | 0 .../src/store/content/read.rs | 0 .../src/store/content/tags.rs | 0 .../tinymemory-core}/src/store/entities.rs | 0 .../tinymemory-core}/src/store/factories.rs | 0 .../src/store/factories_provider_test.rs | 0 .../tinymemory-core}/src/store/kinds.rs | 0 .../tinymemory-core}/src/store/kv.rs | 0 .../src/store/memory_trait.rs | 0 .../tinymemory-core}/src/store/mod.rs | 0 .../src/store/namespace_store/README.md | 0 .../src/store/namespace_store/documents.rs | 0 .../src/store/namespace_store/events.rs | 0 .../src/store/namespace_store/events_tests.rs | 0 .../src/store/namespace_store/graph.rs | 0 .../src/store/namespace_store/helpers.rs | 0 .../src/store/namespace_store/init.rs | 0 .../src/store/namespace_store/mod.rs | 0 .../src/store/namespace_store/profile.rs | 0 .../store/namespace_store/profile_tests.rs | 0 .../src/store/namespace_store/query.rs | 0 .../src/store/namespace_store/query_tests.rs | 0 .../src/store/namespace_store/segments.rs | 0 .../store/namespace_store/segments_tests.rs | 0 .../src/store/profile_store.rs | 0 .../src/store/profile_store_tests.rs | 0 .../src/store/recall_policy.rs | 0 .../src/store/retrieval/mod.rs | 0 .../tinymemory-core}/src/store/safety/pii.rs | 0 .../tinymemory-core}/src/store/traits.rs | 0 .../src/store/trees/hotness.rs | 0 .../tinymemory-core}/src/store/trees/mod.rs | 0 .../src/store/trees/registry.rs | 0 .../tinymemory-core}/src/store/trees/store.rs | 0 .../src/store/trees/store_tests.rs | 0 .../tinymemory-core}/src/store/trees/types.rs | 0 .../tinymemory-core}/src/store/types.rs | 0 .../tinymemory-core}/src/store/write_gate.rs | 0 .../tinymemory-core}/src/sync/README.md | 0 .../tinymemory-core}/src/sync/audit.rs | 0 .../tinymemory-core}/src/sync/composio/mod.rs | 0 .../src/sync/composio/periodic.rs | 0 .../src/sync/composio/providers/catalogs.rs | 0 .../composio/providers/catalogs_business.rs | 0 .../composio/providers/catalogs_google.rs | 0 .../composio/providers/catalogs_messaging.rs | 0 .../composio/providers/catalogs_microsoft.rs | 0 .../providers/catalogs_productivity.rs | 0 .../providers/catalogs_social_media.rs | 0 .../sync/composio/providers/clickup/mod.rs | 0 .../composio/providers/clickup/provider.rs | 0 .../sync/composio/providers/clickup/tests.rs | 0 .../sync/composio/providers/clickup/tools.rs | 0 .../sync/composio/providers/descriptions.rs | 0 .../src/sync/composio/providers/github/mod.rs | 0 .../composio/providers/github/provider.rs | 0 .../sync/composio/providers/github/tests.rs | 0 .../sync/composio/providers/github/tools.rs | 0 .../src/sync/composio/providers/gmail/mod.rs | 0 .../sync/composio/providers/gmail/provider.rs | 0 .../sync/composio/providers/gmail/tests.rs | 0 .../sync/composio/providers/gmail/tools.rs | 0 .../src/sync/composio/providers/helpers.rs | 0 .../src/sync/composio/providers/linear/mod.rs | 0 .../composio/providers/linear/provider.rs | 0 .../sync/composio/providers/linear/tests.rs | 0 .../sync/composio/providers/linear/tools.rs | 0 .../src/sync/composio/providers/mod.rs | 0 .../src/sync/composio/providers/notion/mod.rs | 0 .../composio/providers/notion/provider.rs | 0 .../sync/composio/providers/notion/tests.rs | 0 .../sync/composio/providers/notion/tools.rs | 0 .../src/sync/composio/providers/profile.rs | 0 .../src/sync/composio/providers/profile_md.rs | 0 .../src/sync/composio/providers/registry.rs | 0 .../sync/composio/providers/scope_lookup.rs | 0 .../src/sync/composio/providers/slack/mod.rs | 0 .../sync/composio/providers/slack/provider.rs | 0 .../sync/composio/providers/slack/types.rs | 0 .../src/sync/composio/providers/sync_state.rs | 0 .../src/sync/composio/providers/tool_scope.rs | 0 .../src/sync/composio/providers/traits.rs | 0 .../src/sync/composio/providers/types.rs | 0 .../sync/composio/providers/user_scopes.rs | 0 .../composio/providers/user_scopes_tests.rs | 0 .../tinymemory-core}/src/sync/mcp/mod.rs | 0 .../tinymemory-core}/src/sync/mod.rs | 0 .../src/sync/pipelines/composio/client.rs | 0 .../src/sync/pipelines/composio/connect.rs | 0 .../sync/pipelines/composio/connect_tests.rs | 0 .../src/sync/pipelines/composio/gmail.rs | 0 .../sync/pipelines/composio/gmail_tests.rs | 0 .../src/sync/pipelines/composio/mod.rs | 0 .../sync/pipelines/composio/orchestrator.rs | 0 .../pipelines/composio/orchestrator_tests.rs | 0 .../src/sync/pipelines/composio/page_size.rs | 0 .../pipelines/composio/page_size_tests.rs | 0 .../pipelines/composio/providers/clickup.rs | 0 .../pipelines/composio/providers/common.rs | 0 .../pipelines/composio/providers/github.rs | 0 .../composio/providers/google_calendar.rs | 0 .../composio/providers/google_docs.rs | 0 .../composio/providers/google_drive.rs | 0 .../composio/providers/google_sheets.rs | 0 .../pipelines/composio/providers/linear.rs | 0 .../sync/pipelines/composio/providers/mod.rs | 0 .../pipelines/composio/providers/notion.rs | 0 .../pipelines/composio/providers/outlook.rs | 0 .../pipelines/composio/providers/slack.rs | 0 .../composio/providers/slack_parse.rs | 0 .../pipelines/composio/providers/todoist.rs | 0 .../src/sync/pipelines/dispatcher.rs | 0 .../src/sync/pipelines/dispatcher_tests.rs | 0 .../src/sync/pipelines/host.rs | 0 .../src/sync/pipelines/mod.rs | 0 .../src/sync/pipelines/traits.rs | 0 .../src/sync/sync_status/mod.rs | 0 .../src/sync/workspace/mod.rs | 0 .../src/sync/workspace/periodic.rs | 0 .../src/sync/workspace/watcher.rs | 0 .../tinymemory-core}/src/sync_events.rs | 0 .../tinymemory-core}/src/test_env_lock.rs | 0 .../tinymemory-core}/src/test_seams.rs | 0 .../tinymemory-core}/src/thread_context.rs | 0 .../src/tool_memory/README.md | 0 .../tinymemory-core}/src/tool_memory/mod.rs | 0 .../tinymemory-core}/src/tool_memory/store.rs | 0 .../src/tool_memory/test_helpers.rs | 0 .../tinymemory-core}/src/traits.rs | 0 .../tinymemory-core}/src/tree/README.md | 0 .../tinymemory-core}/src/tree/graph/bfs.rs | 0 .../tinymemory-core}/src/tree/graph/mod.rs | 0 .../tinymemory-core}/src/tree/graph/store.rs | 0 .../src/tree/health/doctor.rs | 0 .../tinymemory-core}/src/tree/health/mod.rs | 0 .../src/tree/health/user_error.rs | 0 .../tinymemory-core}/src/tree/ingest.rs | 0 .../tinymemory-core}/src/tree/mod.rs | 0 .../tinymemory-core}/src/tree/nlp/mod.rs | 0 .../src/tree/retrieval/README.md | 0 .../src/tree/retrieval/benchmarks.rs | 0 .../src/tree/retrieval/cover.rs | 0 .../src/tree/retrieval/drill_down.rs | 0 .../src/tree/retrieval/engine.rs | 0 .../src/tree/retrieval/fast.rs | 0 .../src/tree/retrieval/fetch.rs | 0 .../src/tree/retrieval/integration_tests.rs | 0 .../src/tree/retrieval/mod.rs | 0 .../src/tree/retrieval/search.rs | 0 .../src/tree/retrieval/source.rs | 0 .../src/tree/retrieval/source_scope_tests.rs | 0 .../src/tree/retrieval/types.rs | 0 .../tinymemory-core}/src/tree/score/README.md | 0 .../src/tree/score/embed/README.md | 0 .../src/tree/score/embed/factory.rs | 0 .../src/tree/score/embed/inert.rs | 0 .../src/tree/score/embed/mod.rs | 0 .../src/tree/score/embed/openai_compat.rs | 0 .../src/tree/score/extract/README.md | 0 .../src/tree/score/extract/mod.rs | 0 .../tinymemory-core}/src/tree/score/mod.rs | 0 .../src/tree/score/signals/README.md | 0 .../tinymemory-core}/src/tree/score/store.rs | 0 .../tinymemory-core}/src/tree/summarise.rs | 0 .../src/tree/tree/bucket_seal.rs | 0 .../tinymemory-core}/src/tree/tree/factory.rs | 0 .../tinymemory-core}/src/tree/tree/flush.rs | 0 .../tinymemory-core}/src/tree/tree/mod.rs | 0 .../src/tree/tree/registry.rs | 0 .../src/tree/tree_runtime/engine.rs | 0 .../src/tree/tree_runtime/mod.rs | 0 .../src/tree/tree_runtime/store.rs | 0 .../tinymemory-core}/src/tree_policy.rs | 0 .../tinymemory-core}/src/tree_source/file.rs | 0 .../tinymemory-core}/src/tree_source/mod.rs | 0 .../src/tree_source/registry.rs | 0 .../tinymemory-core}/src/util/README.md | 0 .../tinymemory-core}/src/util/mod.rs | 0 .../tinymemory-core}/src/util/redact.rs | 0 .../composio_gmail_non_tinycortex_e2e.rs | 0 .../tests/fixtures/ingestion/README.md | 0 .../ingestion/gmail_thread_example.txt | 0 .../ingestion/notion_page_example.txt | 0 .../tinymemory-remote}/Cargo.toml | 0 .../examples/conformance.rs | 0 .../tinymemory-remote}/src/cognee.rs | 0 .../tinymemory-remote}/src/cognee_test.rs | 0 .../tinymemory-remote}/src/common.rs | 0 .../src/conformance_test.rs | 0 .../tinymemory-remote}/src/failure_test.rs | 0 .../tinymemory-remote}/src/lib.rs | 0 .../tinymemory-remote}/src/mem0.rs | 0 .../tinymemory-remote}/src/mem0_test.rs | 0 .../tinymemory-remote}/src/supermemory.rs | 0 .../src/supermemory_test.rs | 0 .../tinymemory-sources}/Cargo.toml | 0 .../tinymemory-sources}/src/lib.rs | 0 .../tinymemory-sources}/src/raw_kind.rs | 0 .../src/readers/conversation.rs | 0 .../src/readers/conversation_tests.rs | 0 .../tinymemory-sources}/src/readers/folder.rs | 0 .../src/readers/folder_tests.rs | 0 .../tinymemory-sources}/src/readers/github.rs | 0 .../src/readers/github/api.rs | 0 .../src/readers/github/git.rs | 0 .../src/readers/github/git_tests.rs | 0 .../src/readers/github/issues.rs | 0 .../src/readers/github/types.rs | 0 .../src/readers/github_tests.rs | 0 .../tinymemory-sources}/src/readers/mod.rs | 0 .../tinymemory-sources}/src/readers/rss.rs | 0 .../src/readers/rss/types.rs | 0 .../src/readers/rss_tests.rs | 0 .../tinymemory-sources}/src/readers/ssrf.rs | 0 .../src/readers/ssrf_tests.rs | 0 .../src/readers/web_page.rs | 0 .../src/readers/web_page/types.rs | 0 .../src/readers/web_page_tests.rs | 0 .../tinymemory-sources}/src/registry.rs | 0 .../tinymemory-sources}/src/registry_tests.rs | 0 .../tinymemory-sources}/src/types.rs | 0 .../tinymemory-sources}/src/types_tests.rs | 0 .../tinymemory-sources}/src/validation.rs | 0 .../src/validation_tests.rs | 0 {sync => crates/tinymemory-sync}/Cargo.toml | 0 .../tinymemory-sync}/src/clickup.rs | 0 .../tinymemory-sync}/src/clickup_tests.rs | 0 .../tinymemory-sync}/src/email_clean.rs | 0 .../tinymemory-sync}/src/email_clean_tests.rs | 0 .../tinymemory-sync}/src/email_markdown.rs | 0 .../tinymemory-sync}/src/github.rs | 0 .../tinymemory-sync}/src/github_tests.rs | 0 .../src/gmail_post_process.rs | 0 .../src/gmail_post_process_tests.rs | 0 .../tinymemory-sync}/src/helpers.rs | 0 .../tinymemory-sync}/src/helpers_tests.rs | 0 {sync => crates/tinymemory-sync}/src/lib.rs | 0 .../tinymemory-sync}/src/linear.rs | 0 .../tinymemory-sync}/src/linear_tests.rs | 0 .../tinymemory-sync}/src/notion.rs | 0 .../tinymemory-sync}/src/notion_tests.rs | 0 .../src/slack_post_process.rs | 0 .../src/slack_post_process_tests.rs | 0 .../tinymemory-tinycortex}/Cargo.toml | 0 .../src/conformance_test.rs | 0 .../tinymemory-tinycortex}/src/engine/mod.rs | 0 .../tinymemory-tinycortex}/src/engine/test.rs | 0 .../tinymemory-tinycortex}/src/lib.rs | 0 .../tinymemory-tinycortex}/src/memory.rs | 0 .../tinymemory-tinycortex}/src/memory_test.rs | 0 .../tests/full_provider_conformance.rs | 0 .../tinymemory/examples}/basic.rs | 0 .../tinymemory/examples}/tinycortex.rs | 0 {src => crates/tinymemory/src}/lib.rs | 0 .../tinymemory/src}/registry/class.rs | 0 .../tinymemory/src}/registry/mod.rs | 0 .../tinymemory/src}/registry/test.rs | 0 .../tests}/capability_negotiation.rs | 0 .../tinymemory/tests}/driver_selection.rs | 0 .../tinymemory/tests}/null_provider.rs | 0 .../tests}/sync_on_a_foreign_driver.rs | 0 .../tinymemory/tests}/taint_end_to_end.rs | 0 416 files changed, 2634 deletions(-) delete mode 100644 core/src/store/namespace_store/documents_tests.rs delete mode 100644 core/src/store/namespace_store/fts5.rs delete mode 100644 core/src/store/safety/mod.rs delete mode 100644 core/src/store/write_gate_tests.rs rename {api => crates/tinymemory-api}/Cargo.toml (100%) rename {api => crates/tinymemory-api}/src/capabilities.rs (100%) rename {api => crates/tinymemory-api}/src/capabilities_tests.rs (100%) rename {api => crates/tinymemory-api}/src/chunks.rs (100%) rename {api => crates/tinymemory-api}/src/chunks_tests.rs (100%) rename {api => crates/tinymemory-api}/src/drivers.rs (100%) rename {api => crates/tinymemory-api}/src/error.rs (100%) rename {api => crates/tinymemory-api}/src/error_tests.rs (100%) rename {api => crates/tinymemory-api}/src/goals.rs (100%) rename {api => crates/tinymemory-api}/src/goals_tests.rs (100%) rename {api => crates/tinymemory-api}/src/health.rs (100%) rename {api => crates/tinymemory-api}/src/health_tests.rs (100%) rename {api => crates/tinymemory-api}/src/host/cloud_providers.rs (100%) rename {api => crates/tinymemory-api}/src/host/composio.rs (100%) rename {api => crates/tinymemory-api}/src/host/config.rs (100%) rename {api => crates/tinymemory-api}/src/host/embedding_host.rs (100%) rename {api => crates/tinymemory-api}/src/host/embeddings.rs (100%) rename {api => crates/tinymemory-api}/src/host/error_reporter.rs (100%) rename {api => crates/tinymemory-api}/src/host/events.rs (100%) rename {api => crates/tinymemory-api}/src/host/evidence.rs (100%) rename {api => crates/tinymemory-api}/src/host/local_ai.rs (100%) rename {api => crates/tinymemory-api}/src/host/mod.rs (100%) rename {api => crates/tinymemory-api}/src/host/nlp.rs (100%) rename {api => crates/tinymemory-api}/src/host/routes.rs (100%) rename {api => crates/tinymemory-api}/src/host/scheduler_gate.rs (100%) rename {api => crates/tinymemory-api}/src/host/storage_memory.rs (100%) rename {api => crates/tinymemory-api}/src/host/subsystems.rs (100%) rename {api => crates/tinymemory-api}/src/host/test_support.rs (100%) rename {api => crates/tinymemory-api}/src/host/usage.rs (100%) rename {api => crates/tinymemory-api}/src/lib.rs (100%) rename {api => crates/tinymemory-api}/src/mandatory/mod.rs (100%) rename {api => crates/tinymemory-api}/src/mandatory/provider.rs (100%) rename {api => crates/tinymemory-api}/src/mandatory/test.rs (100%) rename {api => crates/tinymemory-api}/src/null.rs (100%) rename {api => crates/tinymemory-api}/src/null_tests.rs (100%) rename {api => crates/tinymemory-api}/src/provider/audit.rs (100%) rename {api => crates/tinymemory-api}/src/provider/audit_tests.rs (100%) rename {api => crates/tinymemory-api}/src/provider/chunks.rs (100%) rename {api => crates/tinymemory-api}/src/provider/content.rs (100%) rename {api => crates/tinymemory-api}/src/provider/driver.rs (100%) rename {api => crates/tinymemory-api}/src/provider/episodic.rs (100%) rename {api => crates/tinymemory-api}/src/provider/knowledge.rs (100%) rename {api => crates/tinymemory-api}/src/provider/mandatory.rs (100%) rename {api => crates/tinymemory-api}/src/provider/mod.rs (100%) rename {api => crates/tinymemory-api}/src/provider/people.rs (100%) rename {api => crates/tinymemory-api}/src/provider/profile.rs (100%) rename {api => crates/tinymemory-api}/src/provider/records.rs (100%) rename {api => crates/tinymemory-api}/src/provider/retrieval.rs (100%) rename {api => crates/tinymemory-api}/src/provider/types.rs (100%) rename {api => crates/tinymemory-api}/src/provider/types_tests.rs (100%) rename {api => crates/tinymemory-api}/src/recall.rs (100%) rename {api => crates/tinymemory-api}/src/recall_tests.rs (100%) rename {api => crates/tinymemory-api}/src/tool_memory.rs (100%) rename {api => crates/tinymemory-api}/src/tool_memory_tests.rs (100%) rename {api => crates/tinymemory-api}/src/traits.rs (100%) rename {api => crates/tinymemory-api}/src/tree.rs (100%) rename {api => crates/tinymemory-api}/src/tree_tests.rs (100%) rename {api => crates/tinymemory-api}/src/types.rs (100%) rename {api => crates/tinymemory-api}/src/types_tests.rs (100%) rename {api => crates/tinymemory-api}/src/version.rs (100%) rename {api => crates/tinymemory-api}/src/version_tests.rs (100%) rename {api => crates/tinymemory-api}/src/wire.rs (100%) rename {api => crates/tinymemory-api}/src/wire_tests.rs (100%) rename {conformance => crates/tinymemory-conformance}/Cargo.toml (100%) rename {conformance => crates/tinymemory-conformance}/src/lib.rs (100%) rename {conformance => crates/tinymemory-conformance}/src/reference/mod.rs (100%) rename {conformance => crates/tinymemory-conformance}/src/suite/mod.rs (100%) rename {conformance => crates/tinymemory-conformance}/tests/reference_drivers.rs (100%) rename {core => crates/tinymemory-core}/Cargo.toml (100%) rename {core => crates/tinymemory-core}/src/chat.rs (100%) rename {core => crates/tinymemory-core}/src/chat_host.rs (100%) rename {core => crates/tinymemory-core}/src/composio_host.rs (100%) rename {core => crates/tinymemory-core}/src/config_loader.rs (100%) rename {core => crates/tinymemory-core}/src/conversations/blocking.rs (100%) rename {core => crates/tinymemory-core}/src/conversations/blocking_tests.rs (100%) rename {core => crates/tinymemory-core}/src/conversations/mod.rs (100%) rename {core => crates/tinymemory-core}/src/diff/mod.rs (100%) rename {core => crates/tinymemory-core}/src/diff/ops.rs (100%) rename {core => crates/tinymemory-core}/src/diff/source.rs (100%) rename {core => crates/tinymemory-core}/src/diff/stub.rs (100%) rename {core => crates/tinymemory-core}/src/embedding_adapter.rs (100%) rename {core => crates/tinymemory-core}/src/embedding_host.rs (100%) rename {core => crates/tinymemory-core}/src/engine/backend.rs (100%) rename {core => crates/tinymemory-core}/src/engine/chat.rs (100%) rename {core => crates/tinymemory-core}/src/engine/config.rs (100%) rename {core => crates/tinymemory-core}/src/engine/embeddings.rs (100%) rename {core => crates/tinymemory-core}/src/engine/ingest.rs (100%) rename {core => crates/tinymemory-core}/src/engine/mod.rs (100%) rename {core => crates/tinymemory-core}/src/engine/parity.rs (100%) rename {core => crates/tinymemory-core}/src/engine/persona.rs (100%) rename {core => crates/tinymemory-core}/src/engine/queue_driver.rs (100%) rename {core => crates/tinymemory-core}/src/engine/seal.rs (100%) rename {core => crates/tinymemory-core}/src/engine/summariser.rs (100%) rename {core => crates/tinymemory-core}/src/engine/sync.rs (100%) rename {core => crates/tinymemory-core}/src/events.rs (100%) rename {core => crates/tinymemory-core}/src/global.rs (100%) rename {core => crates/tinymemory-core}/src/ingest_pipeline.rs (100%) rename {core => crates/tinymemory-core}/src/ingestion/README.md (100%) rename {core => crates/tinymemory-core}/src/ingestion/mod.rs (100%) rename {core => crates/tinymemory-core}/src/ingestion/queue.rs (100%) rename {core => crates/tinymemory-core}/src/ingestion/state.rs (100%) rename {core => crates/tinymemory-core}/src/ingestion/tests.rs (100%) rename {core => crates/tinymemory-core}/src/learning_candidate.rs (100%) rename {core => crates/tinymemory-core}/src/lib.rs (100%) rename {core => crates/tinymemory-core}/src/nlp_host.rs (100%) rename {core => crates/tinymemory-core}/src/observability.rs (100%) rename {core => crates/tinymemory-core}/src/people/README.md (100%) rename {core => crates/tinymemory-core}/src/people/mod.rs (100%) rename {core => crates/tinymemory-core}/src/preferences.rs (100%) rename {core => crates/tinymemory-core}/src/queue/README.md (100%) rename {core => crates/tinymemory-core}/src/queue/mod.rs (100%) rename {core => crates/tinymemory-core}/src/queue/ops.rs (100%) rename {core => crates/tinymemory-core}/src/queue/scheduler.rs (100%) rename {core => crates/tinymemory-core}/src/queue/store.rs (100%) rename {core => crates/tinymemory-core}/src/queue/testing.rs (100%) rename {core => crates/tinymemory-core}/src/queue/types.rs (100%) rename {core => crates/tinymemory-core}/src/queue/worker.rs (100%) rename {core => crates/tinymemory-core}/src/remember.rs (100%) rename {core => crates/tinymemory-core}/src/rpc_models.rs (100%) rename {core => crates/tinymemory-core}/src/rpc_models_tests.rs (100%) rename {core => crates/tinymemory-core}/src/scheduler_gate.rs (100%) rename {core => crates/tinymemory-core}/src/search/mod.rs (100%) rename {core => crates/tinymemory-core}/src/shutdown.rs (100%) rename {core => crates/tinymemory-core}/src/source_scope.rs (100%) rename {core => crates/tinymemory-core}/src/sources/README.md (100%) rename {core => crates/tinymemory-core}/src/sources/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/composio.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/conversation.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/folder.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/github.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/rss.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/twitter.rs (100%) rename {core => crates/tinymemory-core}/src/sources/readers/web_page.rs (100%) rename {core => crates/tinymemory-core}/src/sources/reconcile.rs (100%) rename {core => crates/tinymemory-core}/src/sources/registry.rs (100%) rename {core => crates/tinymemory-core}/src/sources/status.rs (100%) rename {core => crates/tinymemory-core}/src/sources/sync.rs (100%) rename {core => crates/tinymemory-core}/src/sources/types.rs (100%) rename {core => crates/tinymemory-core}/src/store/README.md (100%) rename {core => crates/tinymemory-core}/src/store/chunks/connection.rs (100%) rename {core => crates/tinymemory-core}/src/store/chunks/embeddings.rs (100%) rename {core => crates/tinymemory-core}/src/store/chunks/mod.rs (100%) rename {core => crates/tinymemory-core}/src/store/chunks/raw_refs.rs (100%) rename {core => crates/tinymemory-core}/src/store/chunks/semantic.rs (100%) rename {core => crates/tinymemory-core}/src/store/chunks/store.rs (100%) rename {core => crates/tinymemory-core}/src/store/chunks/types.rs (100%) rename {core => crates/tinymemory-core}/src/store/client.rs (100%) rename {core => crates/tinymemory-core}/src/store/client_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/content/README.md (100%) rename {core => crates/tinymemory-core}/src/store/content/mod.rs (100%) rename {core => crates/tinymemory-core}/src/store/content/read.rs (100%) rename {core => crates/tinymemory-core}/src/store/content/tags.rs (100%) rename {core => crates/tinymemory-core}/src/store/entities.rs (100%) rename {core => crates/tinymemory-core}/src/store/factories.rs (100%) rename {core => crates/tinymemory-core}/src/store/factories_provider_test.rs (100%) rename {core => crates/tinymemory-core}/src/store/kinds.rs (100%) rename {core => crates/tinymemory-core}/src/store/kv.rs (100%) rename {core => crates/tinymemory-core}/src/store/memory_trait.rs (100%) rename {core => crates/tinymemory-core}/src/store/mod.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/README.md (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/documents.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/events.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/events_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/graph.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/helpers.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/init.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/mod.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/profile.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/profile_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/query.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/query_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/segments.rs (100%) rename {core => crates/tinymemory-core}/src/store/namespace_store/segments_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/profile_store.rs (100%) rename {core => crates/tinymemory-core}/src/store/profile_store_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/recall_policy.rs (100%) rename {core => crates/tinymemory-core}/src/store/retrieval/mod.rs (100%) rename {core => crates/tinymemory-core}/src/store/safety/pii.rs (100%) rename {core => crates/tinymemory-core}/src/store/traits.rs (100%) rename {core => crates/tinymemory-core}/src/store/trees/hotness.rs (100%) rename {core => crates/tinymemory-core}/src/store/trees/mod.rs (100%) rename {core => crates/tinymemory-core}/src/store/trees/registry.rs (100%) rename {core => crates/tinymemory-core}/src/store/trees/store.rs (100%) rename {core => crates/tinymemory-core}/src/store/trees/store_tests.rs (100%) rename {core => crates/tinymemory-core}/src/store/trees/types.rs (100%) rename {core => crates/tinymemory-core}/src/store/types.rs (100%) rename {core => crates/tinymemory-core}/src/store/write_gate.rs (100%) rename {core => crates/tinymemory-core}/src/sync/README.md (100%) rename {core => crates/tinymemory-core}/src/sync/audit.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/periodic.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs_business.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs_google.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs_messaging.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs_microsoft.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs_productivity.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/catalogs_social_media.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/clickup/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/clickup/provider.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/clickup/tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/clickup/tools.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/descriptions.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/github/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/github/provider.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/github/tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/github/tools.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/gmail/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/gmail/provider.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/gmail/tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/gmail/tools.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/helpers.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/linear/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/linear/provider.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/linear/tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/linear/tools.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/notion/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/notion/provider.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/notion/tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/notion/tools.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/profile.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/profile_md.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/registry.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/scope_lookup.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/slack/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/slack/provider.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/slack/types.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/sync_state.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/tool_scope.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/traits.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/types.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/user_scopes.rs (100%) rename {core => crates/tinymemory-core}/src/sync/composio/providers/user_scopes_tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/mcp/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/client.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/connect.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/connect_tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/gmail.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/gmail_tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/orchestrator.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/orchestrator_tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/page_size.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/page_size_tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/clickup.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/common.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/github.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/google_calendar.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/google_docs.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/google_drive.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/google_sheets.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/linear.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/notion.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/outlook.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/slack.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/slack_parse.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/composio/providers/todoist.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/dispatcher.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/dispatcher_tests.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/host.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/pipelines/traits.rs (100%) rename {core => crates/tinymemory-core}/src/sync/sync_status/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/workspace/mod.rs (100%) rename {core => crates/tinymemory-core}/src/sync/workspace/periodic.rs (100%) rename {core => crates/tinymemory-core}/src/sync/workspace/watcher.rs (100%) rename {core => crates/tinymemory-core}/src/sync_events.rs (100%) rename {core => crates/tinymemory-core}/src/test_env_lock.rs (100%) rename {core => crates/tinymemory-core}/src/test_seams.rs (100%) rename {core => crates/tinymemory-core}/src/thread_context.rs (100%) rename {core => crates/tinymemory-core}/src/tool_memory/README.md (100%) rename {core => crates/tinymemory-core}/src/tool_memory/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tool_memory/store.rs (100%) rename {core => crates/tinymemory-core}/src/tool_memory/test_helpers.rs (100%) rename {core => crates/tinymemory-core}/src/traits.rs (100%) rename {core => crates/tinymemory-core}/src/tree/README.md (100%) rename {core => crates/tinymemory-core}/src/tree/graph/bfs.rs (100%) rename {core => crates/tinymemory-core}/src/tree/graph/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/graph/store.rs (100%) rename {core => crates/tinymemory-core}/src/tree/health/doctor.rs (100%) rename {core => crates/tinymemory-core}/src/tree/health/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/health/user_error.rs (100%) rename {core => crates/tinymemory-core}/src/tree/ingest.rs (100%) rename {core => crates/tinymemory-core}/src/tree/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/nlp/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/README.md (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/benchmarks.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/cover.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/drill_down.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/engine.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/fast.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/fetch.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/integration_tests.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/search.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/source.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/source_scope_tests.rs (100%) rename {core => crates/tinymemory-core}/src/tree/retrieval/types.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/README.md (100%) rename {core => crates/tinymemory-core}/src/tree/score/embed/README.md (100%) rename {core => crates/tinymemory-core}/src/tree/score/embed/factory.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/embed/inert.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/embed/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/embed/openai_compat.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/extract/README.md (100%) rename {core => crates/tinymemory-core}/src/tree/score/extract/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/score/signals/README.md (100%) rename {core => crates/tinymemory-core}/src/tree/score/store.rs (100%) rename {core => crates/tinymemory-core}/src/tree/summarise.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree/bucket_seal.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree/factory.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree/flush.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree/registry.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree_runtime/engine.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree_runtime/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree/tree_runtime/store.rs (100%) rename {core => crates/tinymemory-core}/src/tree_policy.rs (100%) rename {core => crates/tinymemory-core}/src/tree_source/file.rs (100%) rename {core => crates/tinymemory-core}/src/tree_source/mod.rs (100%) rename {core => crates/tinymemory-core}/src/tree_source/registry.rs (100%) rename {core => crates/tinymemory-core}/src/util/README.md (100%) rename {core => crates/tinymemory-core}/src/util/mod.rs (100%) rename {core => crates/tinymemory-core}/src/util/redact.rs (100%) rename {core => crates/tinymemory-core}/tests/composio_gmail_non_tinycortex_e2e.rs (100%) rename {core => crates/tinymemory-core}/tests/fixtures/ingestion/README.md (100%) rename {core => crates/tinymemory-core}/tests/fixtures/ingestion/gmail_thread_example.txt (100%) rename {core => crates/tinymemory-core}/tests/fixtures/ingestion/notion_page_example.txt (100%) rename {adapters/remote => crates/tinymemory-remote}/Cargo.toml (100%) rename {adapters/remote => crates/tinymemory-remote}/examples/conformance.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/cognee.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/cognee_test.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/common.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/conformance_test.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/failure_test.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/lib.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/mem0.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/mem0_test.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/supermemory.rs (100%) rename {adapters/remote => crates/tinymemory-remote}/src/supermemory_test.rs (100%) rename {sources => crates/tinymemory-sources}/Cargo.toml (100%) rename {sources => crates/tinymemory-sources}/src/lib.rs (100%) rename {sources => crates/tinymemory-sources}/src/raw_kind.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/conversation.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/conversation_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/folder.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/folder_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github/api.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github/git.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github/git_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github/issues.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github/types.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/github_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/mod.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/rss.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/rss/types.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/rss_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/ssrf.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/ssrf_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/web_page.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/web_page/types.rs (100%) rename {sources => crates/tinymemory-sources}/src/readers/web_page_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/registry.rs (100%) rename {sources => crates/tinymemory-sources}/src/registry_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/types.rs (100%) rename {sources => crates/tinymemory-sources}/src/types_tests.rs (100%) rename {sources => crates/tinymemory-sources}/src/validation.rs (100%) rename {sources => crates/tinymemory-sources}/src/validation_tests.rs (100%) rename {sync => crates/tinymemory-sync}/Cargo.toml (100%) rename {sync => crates/tinymemory-sync}/src/clickup.rs (100%) rename {sync => crates/tinymemory-sync}/src/clickup_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/email_clean.rs (100%) rename {sync => crates/tinymemory-sync}/src/email_clean_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/email_markdown.rs (100%) rename {sync => crates/tinymemory-sync}/src/github.rs (100%) rename {sync => crates/tinymemory-sync}/src/github_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/gmail_post_process.rs (100%) rename {sync => crates/tinymemory-sync}/src/gmail_post_process_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/helpers.rs (100%) rename {sync => crates/tinymemory-sync}/src/helpers_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/lib.rs (100%) rename {sync => crates/tinymemory-sync}/src/linear.rs (100%) rename {sync => crates/tinymemory-sync}/src/linear_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/notion.rs (100%) rename {sync => crates/tinymemory-sync}/src/notion_tests.rs (100%) rename {sync => crates/tinymemory-sync}/src/slack_post_process.rs (100%) rename {sync => crates/tinymemory-sync}/src/slack_post_process_tests.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/Cargo.toml (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/src/conformance_test.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/src/engine/mod.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/src/engine/test.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/src/lib.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/src/memory.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/src/memory_test.rs (100%) rename {adapters/tinycortex => crates/tinymemory-tinycortex}/tests/full_provider_conformance.rs (100%) rename {examples => crates/tinymemory/examples}/basic.rs (100%) rename {examples => crates/tinymemory/examples}/tinycortex.rs (100%) rename {src => crates/tinymemory/src}/lib.rs (100%) rename {src => crates/tinymemory/src}/registry/class.rs (100%) rename {src => crates/tinymemory/src}/registry/mod.rs (100%) rename {src => crates/tinymemory/src}/registry/test.rs (100%) rename {tests => crates/tinymemory/tests}/capability_negotiation.rs (100%) rename {tests => crates/tinymemory/tests}/driver_selection.rs (100%) rename {tests => crates/tinymemory/tests}/null_provider.rs (100%) rename {tests => crates/tinymemory/tests}/sync_on_a_foreign_driver.rs (100%) rename {tests => crates/tinymemory/tests}/taint_end_to_end.rs (100%) diff --git a/core/src/store/namespace_store/documents_tests.rs b/core/src/store/namespace_store/documents_tests.rs deleted file mode 100644 index 7b2f7ea5..00000000 --- a/core/src/store/namespace_store/documents_tests.rs +++ /dev/null @@ -1,1508 +0,0 @@ -//! Tests for the `documents` module — upsert / list / delete / clear-namespace. - -use std::sync::Arc; - -use serde_json::json; -use tempfile::TempDir; - -use crate::store::{NamespaceDocumentInput, UnifiedMemory}; -use tinymemory_api::host::NoopEmbedding; - -fn make_doc_input( - namespace: &str, - key: &str, - title: &str, - content: &str, -) -> NamespaceDocumentInput { - NamespaceDocumentInput { - namespace: namespace.to_string(), - key: key.to_string(), - title: title.to_string(), - content: content.to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - } -} - -fn count_vector_chunks(memory: &UnifiedMemory, namespace: &str, document_id: &str) -> i64 { - let conn = memory.conn.lock(); - conn.query_row( - "SELECT COUNT(*) FROM vector_chunks WHERE namespace = ?1 AND document_id = ?2", - rusqlite::params![UnifiedMemory::sanitize_namespace(namespace), document_id], - |row| row.get(0), - ) - .unwrap() -} - -#[tokio::test] -async fn list_documents_without_namespace_returns_all_docs_across_namespaces() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input("test:one", "doc-a", "Doc A", "A body")) - .await - .unwrap(); - memory - .upsert_document(make_doc_input("test:two", "doc-b", "Doc B", "B body")) - .await - .unwrap(); - - let docs = memory.list_documents(None).await.unwrap(); - assert_eq!(docs["count"].as_u64().unwrap(), 2); - let namespaces: std::collections::BTreeSet<_> = docs["documents"] - .as_array() - .unwrap() - .iter() - .filter_map(|doc| doc["namespace"].as_str()) - .collect(); - assert!(namespaces.contains("test_one")); - assert!(namespaces.contains("test_two")); -} - -#[tokio::test] -async fn list_namespaces_returns_distinct_sorted_sanitized_namespaces() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input("team alpha/#1", "doc-a", "Doc A", "A body")) - .await - .unwrap(); - memory - .upsert_document(make_doc_input("team alpha/#1", "doc-b", "Doc B", "B body")) - .await - .unwrap(); - memory - .upsert_document(make_doc_input("zeta", "doc-c", "Doc C", "C body")) - .await - .unwrap(); - - let namespaces = memory.list_namespaces().await.unwrap(); - assert_eq!( - namespaces, - vec!["team_alpha/_1".to_string(), "zeta".to_string()] - ); -} - -#[tokio::test] -async fn list_documents_with_namespace_filters_by_sanitized_namespace_and_orders_newest_first() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input( - "team alpha/#1", - "doc-a", - "Older Doc", - "A body", - )) - .await - .unwrap(); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - memory - .upsert_document(make_doc_input( - "team alpha/#1", - "doc-b", - "Newer Doc", - "B body", - )) - .await - .unwrap(); - memory - .upsert_document(make_doc_input("other", "doc-c", "Other Doc", "C body")) - .await - .unwrap(); - - let docs = memory.list_documents(Some("team alpha/#1")).await.unwrap(); - let documents = docs["documents"].as_array().unwrap(); - - assert_eq!(docs["count"].as_u64().unwrap(), 2); - assert_eq!(documents[0]["namespace"], json!("team_alpha/_1")); - assert_eq!(documents[0]["key"], json!("doc-b")); - assert_eq!(documents[1]["key"], json!("doc-a")); -} - -#[tokio::test] -async fn load_documents_for_scope_defaults_invalid_json_fields_from_persisted_rows() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - let namespace = UnifiedMemory::sanitize_namespace("broken/json"); - - { - let conn = memory.conn.lock(); - conn.execute( - "INSERT INTO memory_docs - (document_id, namespace, key, title, content, source_type, priority, tags_json, metadata_json, category, session_id, created_at, updated_at, markdown_rel_path) - VALUES - (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - rusqlite::params![ - "doc-invalid-json", - namespace, - "doc-a", - "Doc A", - "Body", - "doc", - "medium", - "{not json", - "also not json", - "core", - Option::::None, - 10.0_f64, - 20.0_f64, - "memory/namespaces/broken_json/docs/doc-invalid-json.md" - ], - ) - .unwrap(); - } - - let docs = memory - .load_documents_for_scope("broken/json") - .await - .unwrap(); - assert_eq!(docs.len(), 1); - assert!( - docs[0].tags.is_empty(), - "invalid tags_json should fall back to []" - ); - assert_eq!( - docs[0].metadata, - json!({}), - "invalid metadata_json should fall back to an empty object" - ); -} - -#[tokio::test] -async fn upsert_document_metadata_only_reuses_document_id_for_same_namespace_and_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let first_id = memory - .upsert_document_metadata_only(make_doc_input( - "test:meta", - "doc-a", - "Doc A", - "Initial body", - )) - .await - .unwrap(); - let second_id = memory - .upsert_document_metadata_only(make_doc_input( - "test:meta", - "doc-a", - "Doc A v2", - "Updated body", - )) - .await - .unwrap(); - - assert_eq!( - first_id, second_id, - "metadata-only upsert should reuse the document id" - ); - let docs = memory.load_documents_for_scope("test:meta").await.unwrap(); - assert_eq!(docs.len(), 1); - assert_eq!(docs[0].document_id, first_id); - assert_eq!(docs[0].title, "Doc A v2"); - assert_eq!(docs[0].content, "Updated body"); - assert_eq!( - count_vector_chunks(&memory, "test:meta", &first_id), - 0, - "metadata-only writes must not enqueue vector chunks" - ); -} - -#[tokio::test] -async fn upsert_document_metadata_only_preserves_created_at_and_rewrites_sidecar() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let first_id = memory - .upsert_document_metadata_only(make_doc_input( - "test:meta-sidecar", - "doc-a", - "Doc A", - "Initial body", - )) - .await - .unwrap(); - let first_doc = memory - .load_documents_for_scope("test:meta-sidecar") - .await - .unwrap()[0] - .clone(); - let sidecar = tmp.path().join(&first_doc.markdown_rel_path); - let first_markdown = std::fs::read_to_string(&sidecar).unwrap(); - assert!(first_markdown.contains("Initial body")); - - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - let second_id = memory - .upsert_document_metadata_only(make_doc_input( - "test:meta-sidecar", - "doc-a", - "Doc A v2", - "Updated body", - )) - .await - .unwrap(); - - assert_eq!(first_id, second_id); - let updated_doc = memory - .load_documents_for_scope("test:meta-sidecar") - .await - .unwrap()[0] - .clone(); - assert_eq!(updated_doc.created_at, first_doc.created_at); - assert!(updated_doc.updated_at >= first_doc.updated_at); - let updated_markdown = std::fs::read_to_string(sidecar).unwrap(); - assert!(updated_markdown.contains("Updated body")); - assert!(updated_markdown.contains("Doc A v2")); -} - -#[tokio::test] -async fn upsert_document_metadata_only_over_existing_document_preserves_vector_chunks() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let document_id = memory - .upsert_document(make_doc_input( - "test:meta-preserve-chunks", - "doc-a", - "Doc A", - &"alpha ".repeat(400), - )) - .await - .unwrap(); - let original_chunk_count = - count_vector_chunks(&memory, "test:meta-preserve-chunks", &document_id); - assert!(original_chunk_count > 0); - - let updated_id = memory - .upsert_document_metadata_only(make_doc_input( - "test:meta-preserve-chunks", - "doc-a", - "Doc A v2", - "Updated body without re-embedding", - )) - .await - .unwrap(); - - assert_eq!(updated_id, document_id); - let docs = memory - .load_documents_for_scope("test:meta-preserve-chunks") - .await - .unwrap(); - assert_eq!(docs.len(), 1); - assert_eq!(docs[0].content, "Updated body without re-embedding"); - assert_eq!( - count_vector_chunks(&memory, "test:meta-preserve-chunks", &document_id), - original_chunk_count, - "metadata-only writes should not delete existing semantic chunks" - ); -} - -#[tokio::test] -async fn upsert_document_after_metadata_only_reuses_document_id_and_adds_vector_chunks() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let metadata_only_id = memory - .upsert_document_metadata_only(make_doc_input( - "test:meta-then-full", - "doc-a", - "Doc A", - "Short body", - )) - .await - .unwrap(); - assert_eq!( - count_vector_chunks(&memory, "test:meta-then-full", &metadata_only_id), - 0 - ); - - let full_id = memory - .upsert_document(make_doc_input( - "test:meta-then-full", - "doc-a", - "Doc A Embedded", - &"beta ".repeat(400), - )) - .await - .unwrap(); - - assert_eq!(full_id, metadata_only_id); - let docs = memory - .load_documents_for_scope("test:meta-then-full") - .await - .unwrap(); - assert_eq!(docs.len(), 1); - assert_eq!(docs[0].title, "Doc A Embedded"); - assert!( - count_vector_chunks(&memory, "test:meta-then-full", &full_id) > 0, - "full upsert should backfill chunks for a metadata-only document" - ); -} - -#[tokio::test] -async fn upsert_document_writes_vector_chunks_for_chunked_content() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let long_body = "alpha ".repeat(400); - let document_id = memory - .upsert_document(make_doc_input("test:vector", "doc-a", "Doc A", &long_body)) - .await - .unwrap(); - - assert!( - count_vector_chunks(&memory, "test:vector", &document_id) > 0, - "full document upsert should replace vector chunks for semantic retrieval" - ); -} - -/// Embedder that records how many times `embed` is invoked and returns one -/// fixed-dimension vector per input text. Used to prove `upsert_document` -/// embeds all chunks in a SINGLE batch call rather than one call per chunk. -struct CountingEmbedder { - calls: std::sync::atomic::AtomicUsize, -} - -#[async_trait::async_trait] -impl tinymemory_api::host::EmbeddingProvider for CountingEmbedder { - fn name(&self) -> &str { - "counting" - } - - fn model_id(&self) -> &str { - "counting-test" - } - - fn dimensions(&self) -> usize { - 3 - } - - async fn embed(&self, texts: &[&str]) -> anyhow::Result>> { - self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - Ok(texts.iter().map(|_| vec![0.1, 0.2, 0.3]).collect()) - } -} - -#[tokio::test] -async fn upsert_document_batch_embeds_all_chunks_in_one_call() { - let tmp = TempDir::new().unwrap(); - let embedder = Arc::new(CountingEmbedder { - calls: std::sync::atomic::AtomicUsize::new(0), - }); - let memory = UnifiedMemory::new(tmp.path(), embedder.clone(), None).unwrap(); - - // Long enough to chunk into several pieces (chunk size is 225 chars). - let long_body = "alpha ".repeat(400); - let document_id = memory - .upsert_document(make_doc_input("test:batch", "doc-a", "Doc A", &long_body)) - .await - .unwrap(); - - let chunk_count = count_vector_chunks(&memory, "test:batch", &document_id); - assert!( - chunk_count >= 3, - "test body should chunk into >=3 pieces, got {chunk_count}" - ); - assert_eq!( - embedder.calls.load(std::sync::atomic::Ordering::SeqCst), - 1, - "all chunks must be embedded in a single batch call, not one call per chunk" - ); -} - -#[tokio::test] -async fn upsert_document_reuses_document_id_preserves_created_at_and_replaces_vector_chunks() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let first_id = memory - .upsert_document(make_doc_input( - "test:update", - "doc-a", - "Doc A", - &"alpha ".repeat(400), - )) - .await - .unwrap(); - let first_doc = memory - .load_documents_for_scope("test:update") - .await - .unwrap()[0] - .clone(); - let first_chunk_count = count_vector_chunks(&memory, "test:update", &first_id); - assert!(first_chunk_count > 0); - - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - let second_id = memory - .upsert_document(make_doc_input( - "test:update", - "doc-a", - "Doc A v2", - &"beta ".repeat(40), - )) - .await - .unwrap(); - - assert_eq!( - first_id, second_id, - "upsert should reuse the existing document id" - ); - let updated_doc = memory - .load_documents_for_scope("test:update") - .await - .unwrap()[0] - .clone(); - assert_eq!(updated_doc.document_id, first_id); - assert_eq!(updated_doc.created_at, first_doc.created_at); - assert!(updated_doc.updated_at >= first_doc.updated_at); - assert_eq!(updated_doc.title, "Doc A v2"); - assert_eq!(updated_doc.content, "beta ".repeat(40)); - let second_chunk_count = count_vector_chunks(&memory, "test:update", &second_id); - assert!(second_chunk_count > 0); - assert!( - second_chunk_count <= first_chunk_count, - "replacing with shorter content should not leave stale vector chunks behind" - ); -} - -#[tokio::test] -async fn delete_document_removes_doc_sidecar_and_is_idempotent() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let document_id = memory - .upsert_document(make_doc_input("test:delete", "doc-a", "Doc A", "Delete me")) - .await - .unwrap(); - - let docs = memory - .load_documents_for_scope("test:delete") - .await - .unwrap(); - assert_eq!(docs.len(), 1); - let sidecar = tmp.path().join(&docs[0].markdown_rel_path); - assert!(sidecar.exists(), "sidecar should exist before delete"); - - memory - .graph_upsert_namespace( - "test:delete", - "Alice", - "OWNS", - "Phoenix", - &json!({ - "document_id": document_id.clone(), - "chunk_id": format!("{document_id}:0") - }), - ) - .await - .unwrap(); - - let deleted = memory - .delete_document("test:delete", &document_id) - .await - .unwrap(); - assert_eq!(deleted["deleted"], json!(true)); - assert_eq!(deleted["documentId"], json!(document_id.clone())); - assert!(!sidecar.exists(), "sidecar should be removed on delete"); - assert!(memory - .load_documents_for_scope("test:delete") - .await - .unwrap() - .is_empty()); - assert!( - memory - .graph_relations_namespace("test:delete", None, None) - .await - .unwrap() - .is_empty(), - "document-linked graph relations should be pruned" - ); - - let second = memory - .delete_document("test:delete", &document_id) - .await - .unwrap(); - assert_eq!(second["deleted"], json!(false)); - assert_eq!(second["documentId"], json!(document_id)); -} - -#[tokio::test] -async fn delete_document_succeeds_when_sidecar_is_already_missing() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let document_id = memory - .upsert_document(make_doc_input( - "test:delete-missing-sidecar", - "doc-a", - "Doc A", - "Delete me", - )) - .await - .unwrap(); - - let docs = memory - .load_documents_for_scope("test:delete-missing-sidecar") - .await - .unwrap(); - assert_eq!(docs.len(), 1); - let sidecar = tmp.path().join(&docs[0].markdown_rel_path); - assert!(sidecar.exists()); - std::fs::remove_file(&sidecar).unwrap(); - assert!(!sidecar.exists()); - - let deleted = memory - .delete_document("test:delete-missing-sidecar", &document_id) - .await - .unwrap(); - assert_eq!(deleted["deleted"], json!(true)); - assert!(memory - .load_documents_for_scope("test:delete-missing-sidecar") - .await - .unwrap() - .is_empty()); -} - -#[tokio::test] -async fn delete_document_accepts_unsanitized_namespace_and_removes_chunks() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let document_id = memory - .upsert_document(make_doc_input( - "Team Alpha/#1", - "doc-a", - "Doc A", - &"delete ".repeat(300), - )) - .await - .unwrap(); - assert!(count_vector_chunks(&memory, "Team Alpha/#1", &document_id) > 0); - - let deleted = memory - .delete_document("Team Alpha/#1", &document_id) - .await - .unwrap(); - assert_eq!(deleted["deleted"], json!(true)); - assert_eq!(deleted["namespace"], json!("Team_Alpha/_1")); - assert_eq!( - count_vector_chunks(&memory, "Team Alpha/#1", &document_id), - 0 - ); - assert!(memory - .load_documents_for_scope("Team Alpha/#1") - .await - .unwrap() - .is_empty()); -} - -#[tokio::test] -async fn clear_namespace_removes_all_data_and_preserves_other_namespaces() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - // --- Populate "test:cleanup" namespace --- - - // 3 documents - memory - .upsert_document(make_doc_input( - "test:cleanup", - "doc-a", - "Document A", - "Content of document A for cleanup.", - )) - .await - .unwrap(); - memory - .upsert_document(make_doc_input( - "test:cleanup", - "doc-b", - "Document B", - "Content of document B for cleanup.", - )) - .await - .unwrap(); - memory - .upsert_document(make_doc_input( - "test:cleanup", - "doc-c", - "Document C", - "Content of document C for cleanup.", - )) - .await - .unwrap(); - - // 2 KV entries - memory - .kv_set_namespace("test:cleanup", "pref-1", &json!({"theme": "dark"})) - .await - .unwrap(); - memory - .kv_set_namespace("test:cleanup", "pref-2", &json!({"lang": "en"})) - .await - .unwrap(); - - // 2 graph relations - memory - .graph_upsert_namespace( - "test:cleanup", - "Alice", - "knows", - "Bob", - &json!({"source": "test"}), - ) - .await - .unwrap(); - memory - .graph_upsert_namespace( - "test:cleanup", - "Bob", - "works_at", - "Acme", - &json!({"source": "test"}), - ) - .await - .unwrap(); - - // --- Populate "test:other" namespace (control) --- - - memory - .upsert_document(make_doc_input( - "test:other", - "other-doc", - "Other Document", - "Content of document in the other namespace.", - )) - .await - .unwrap(); - memory - .kv_set_namespace("test:other", "other-key", &json!({"value": true})) - .await - .unwrap(); - memory - .graph_upsert_namespace( - "test:other", - "X", - "relates_to", - "Y", - &json!({"source": "other"}), - ) - .await - .unwrap(); - - // --- Verify pre-conditions --- - - let cleanup_docs = memory.list_documents(Some("test:cleanup")).await.unwrap(); - assert_eq!( - cleanup_docs["count"].as_u64().unwrap(), - 3, - "test:cleanup should have 3 documents before clear" - ); - - let cleanup_kv = memory.kv_list_namespace("test:cleanup").await.unwrap(); - assert_eq!( - cleanup_kv.len(), - 2, - "test:cleanup should have 2 KV entries before clear" - ); - - let cleanup_graph = memory - .graph_relations_namespace("test:cleanup", None, None) - .await - .unwrap(); - assert_eq!( - cleanup_graph.len(), - 2, - "test:cleanup should have 2 graph relations before clear" - ); - - let other_docs = memory.list_documents(Some("test:other")).await.unwrap(); - assert_eq!( - other_docs["count"].as_u64().unwrap(), - 1, - "test:other should have 1 document before clear" - ); - - // --- Execute clear_namespace --- - - memory.clear_namespace("test:cleanup").await.unwrap(); - - // --- Assert: "test:cleanup" is empty --- - - let cleanup_docs_after = memory.list_documents(Some("test:cleanup")).await.unwrap(); - assert_eq!( - cleanup_docs_after["count"].as_u64().unwrap(), - 0, - "test:cleanup documents should be empty after clear" - ); - - let cleanup_kv_after = memory.kv_list_namespace("test:cleanup").await.unwrap(); - assert!( - cleanup_kv_after.is_empty(), - "test:cleanup KV entries should be empty after clear" - ); - - let cleanup_graph_after = memory - .graph_relations_namespace("test:cleanup", None, None) - .await - .unwrap(); - assert!( - cleanup_graph_after.is_empty(), - "test:cleanup graph relations should be empty after clear" - ); - - // --- Assert: "test:other" is untouched (critical) --- - - let other_docs_after = memory.list_documents(Some("test:other")).await.unwrap(); - assert_eq!( - other_docs_after["count"].as_u64().unwrap(), - 1, - "test:other document must still exist after clearing test:cleanup" - ); - - let other_kv_after = memory.kv_list_namespace("test:other").await.unwrap(); - assert_eq!( - other_kv_after.len(), - 1, - "test:other KV entry must still exist after clearing test:cleanup" - ); - - let other_graph_after = memory - .graph_relations_namespace("test:other", None, None) - .await - .unwrap(); - assert_eq!( - other_graph_after.len(), - 1, - "test:other graph relation must still exist after clearing test:cleanup" - ); -} - -#[tokio::test] -async fn clear_namespace_on_empty_namespace_is_noop() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - // Clearing a namespace that has never been used should succeed without error. - memory.clear_namespace("nonexistent").await.unwrap(); - - let docs = memory.list_documents(Some("nonexistent")).await.unwrap(); - assert_eq!(docs["count"].as_u64().unwrap(), 0); -} - -#[tokio::test] -async fn clear_namespace_removes_on_disk_markdown_files() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input( - "test:diskcheck", - "disk-doc", - "Disk Doc", - "This doc has a markdown file on disk.", - )) - .await - .unwrap(); - - let docs_dir = tmp - .path() - .join("memory") - .join("namespaces") - .join("test_diskcheck") - .join("docs"); - assert!( - docs_dir.exists(), - "docs directory should exist after upsert" - ); - - memory.clear_namespace("test:diskcheck").await.unwrap(); - - assert!( - !docs_dir.exists(), - "docs directory should be removed after clear_namespace" - ); -} - -#[tokio::test] -async fn clear_namespace_accepts_unsanitized_namespace_and_removes_sanitized_docs_dir() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input( - "Team Alpha/#1", - "doc-a", - "Doc A", - "Namespace cleanup body", - )) - .await - .unwrap(); - memory - .kv_set_namespace("Team Alpha/#1", "pref-1", &json!({"theme": "dark"})) - .await - .unwrap(); - - let docs_dir = tmp - .path() - .join("memory") - .join("namespaces") - .join("Team_Alpha/_1") - .join("docs"); - assert!(docs_dir.exists()); - - memory.clear_namespace("Team Alpha/#1").await.unwrap(); - - assert!(memory - .load_documents_for_scope("Team Alpha/#1") - .await - .unwrap() - .is_empty()); - assert!(memory - .kv_list_namespace("Team Alpha/#1") - .await - .unwrap() - .is_empty()); - assert!(!docs_dir.exists()); -} - -#[tokio::test] -async fn list_namespaces_skips_blank_rows_inserted_outside_normal_writes() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - { - let conn = memory.conn.lock(); - conn.execute( - "INSERT INTO memory_docs - (document_id, namespace, key, title, content, source_type, priority, tags_json, metadata_json, category, session_id, created_at, updated_at, markdown_rel_path) - VALUES - (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - rusqlite::params![ - "doc-blank-ns", - " ", - "doc-a", - "Doc A", - "Body", - "doc", - "medium", - "[]", - "{}", - "core", - Option::::None, - 10.0_f64, - 20.0_f64, - "memory/namespaces/blank/docs/doc-blank-ns.md" - ], - ) - .unwrap(); - } - memory - .upsert_document(make_doc_input("valid/ns", "doc-b", "Doc B", "Body")) - .await - .unwrap(); - - let namespaces = memory.list_namespaces().await.unwrap(); - assert_eq!(namespaces, vec!["valid/ns".to_string()]); -} - -#[tokio::test] -async fn upsert_document_redacts_secret_like_content_before_persisting() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(NamespaceDocumentInput { - namespace: "safe".to_string(), - key: "secret-note".to_string(), - title: "Bearer abcdefghijklmnop".to_string(), - content: "token=abc123\n-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----" - .to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec!["sk-1234567890123456789012345".to_string()], - metadata: json!({ - "token": "raw", - "notes": "api_key=really-secret" - }), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .unwrap(); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - assert_eq!(docs.len(), 1); - let doc = &docs[0]; - assert!(!doc.title.contains("abcdefghijklmnop")); - assert!(doc.title.contains("[REDACTED]")); - assert!(!doc.content.contains("BEGIN PRIVATE KEY")); - assert!(doc.content.contains("[REDACTED_PRIVATE_KEY]")); - assert_eq!(doc.metadata["token"], json!("[REDACTED_SECRET]")); - assert_eq!(doc.metadata["notes"], json!("api_key=[REDACTED]")); - assert_eq!(doc.tags[0], "[REDACTED]"); - - let markdown = std::fs::read_to_string(tmp.path().join(&doc.markdown_rel_path)).unwrap(); - assert!(!markdown.contains("BEGIN PRIVATE KEY")); - assert!(markdown.contains("[REDACTED_PRIVATE_KEY]")); -} - -#[tokio::test] -async fn kv_set_namespace_redacts_secret_like_payloads() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .kv_set_namespace( - "safe", - "key-1", - &json!({ - "token": "super-secret", - "note": "Bearer abcdefghijklmnop" - }), - ) - .await - .unwrap(); - - let rows = memory.kv_list_namespace("safe").await.unwrap(); - assert_eq!(rows.len(), 1); - assert_eq!(rows[0]["key"], json!("key-1")); - assert_eq!(rows[0]["value"]["token"], json!("[REDACTED_SECRET]")); - assert_eq!(rows[0]["value"]["note"], json!("Bearer [REDACTED]")); -} - -#[tokio::test] -async fn kv_set_namespace_rejects_secret_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let err = memory - .kv_set_namespace( - "safe", - "api_key=sk-1234567890123456789012345", - &json!({"value": "ok"}), - ) - .await - .expect_err("secret-like key should be rejected"); - assert!(err.contains("cannot contain secrets")); -} - -#[tokio::test] -async fn kv_set_namespace_rejects_secret_like_namespace() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let err = memory - .kv_set_namespace( - "Bearer abcdefghijklmnop", - "safe-key", - &json!({"value": "ok"}), - ) - .await - .expect_err("secret-like namespace should be rejected"); - assert!(err.contains("cannot contain secrets")); -} - -#[tokio::test] -async fn kv_set_global_rejects_secret_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let err = memory - .kv_set_global( - "authorization=Bearer abcdefghijklmnop", - &json!({"value": "ok"}), - ) - .await - .expect_err("secret-like global key should be rejected"); - assert!(err.contains("cannot contain secrets")); -} - -#[tokio::test] -async fn upsert_document_rejects_secret_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let err = memory - .upsert_document(NamespaceDocumentInput { - namespace: "safe".to_string(), - key: "api_key=sk-1234567890123456789012345".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect_err("secret-like key should be rejected"); - assert!(err.contains("cannot contain secrets")); -} - -#[tokio::test] -async fn upsert_document_rejects_secret_like_namespace() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let err = memory - .upsert_document(NamespaceDocumentInput { - namespace: "Bearer abcdefghijklmnop".to_string(), - key: "k1".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect_err("secret-like namespace should be rejected"); - assert!(err.contains("cannot contain secrets")); -} - -#[tokio::test] -async fn upsert_document_metadata_only_rejects_secret_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let err = memory - .upsert_document_metadata_only(NamespaceDocumentInput { - namespace: "safe".to_string(), - key: "refresh_token=abcdef".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect_err("secret-like key should be rejected"); - assert!(err.contains("cannot contain secrets")); -} - -// --------------------------------------------------------------------------- -// Personal-identifier (PII) at the namespace/key boundary — auto-sanitize. -// -// Rather than rejecting writes with PII-like keys/namespaces (which caused -// unthrottled retry loops, see #5164), the store now auto-sanitizes the -// namespace and key using `redact_pii` before persisting. These tests verify -// the upsert succeeds and the stored key/namespace contains redacted tokens. -// --------------------------------------------------------------------------- - -#[tokio::test] -async fn kv_set_global_auto_sanitizes_pii_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - // SSN-like key should be auto-sanitized, not rejected. - memory - .kv_set_global("ssn-123-45-6789", &json!({"value": "ok"})) - .await - .expect("PII-like global key should be auto-sanitized, not rejected"); - - // ... and the caller must be able to read it back with the identifier it - // wrote. The canonicalization is a storage-address transform, so the read - // path applies the same one (#5164). A miss here is what made the caller - // write again, which is the loop the issue was reported for. - let stored = memory.kv_get_global("ssn-123-45-6789").await.unwrap(); - assert_eq!( - stored, - Some(json!({"value": "ok"})), - "a canonicalized KV key must stay readable by its original identifier" - ); - assert!( - memory.kv_delete_global("ssn-123-45-6789").await.unwrap(), - "delete must address the same canonicalized row the write created" - ); -} - -#[tokio::test] -async fn kv_set_namespace_auto_sanitizes_pii_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .kv_set_namespace("safe", "ssn-123-45-6789", &json!({"value": "ok"})) - .await - .expect("PII-like key should be auto-sanitized, not rejected"); - - let records = memory.kv_records_namespace("safe").await.unwrap(); - // The record should still exist; the key gets redacted internally. - assert_eq!(records.len(), 1); - assert_eq!( - memory - .kv_get_namespace("safe", "ssn-123-45-6789") - .await - .unwrap(), - Some(json!({"value": "ok"})), - "a canonicalized KV key must stay readable by its original identifier" - ); -} - -#[tokio::test] -async fn kv_set_namespace_auto_sanitizes_pii_like_namespace() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .kv_set_namespace("user/111.444.777-35", "safe-key", &json!({"value": "ok"})) - .await - .expect("PII-like namespace should be auto-sanitized, not rejected"); - - assert_eq!( - memory - .kv_get_namespace("user/111.444.777-35", "safe-key") - .await - .unwrap(), - Some(json!({"value": "ok"})), - "a canonicalized KV namespace must stay readable by its original value" - ); -} - -#[tokio::test] -async fn upsert_document_auto_sanitizes_pii_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let doc_id = memory - .upsert_document(NamespaceDocumentInput { - namespace: "safe".to_string(), - key: "cuit-20-11111111-2".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect("PII-like key should be auto-sanitized, not rejected"); - - // The document was stored with a redacted key. - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); - assert!(!doc.key.contains("20-11111111-2"), "key should be redacted"); - assert!( - doc.key.contains("REDACTED"), // matches [REDACTED_PII_CUIT] - "key should contain a redaction token, got: {}", - doc.key - ); -} - -#[tokio::test] -async fn upsert_document_auto_sanitizes_pii_like_namespace() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let doc_id = memory - .upsert_document(NamespaceDocumentInput { - namespace: "cliente-RFC-VECJ880326XK4".to_string(), - key: "k1".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect("PII-like namespace should be auto-sanitized, not rejected"); - - // Look up the document by sanitized namespace (note sanitize_namespace - // normalises special chars, so `[` becomes `_`). - let docs = memory - .load_documents_for_scope("cliente-RFC-VECJ880326XK4") - .await - .unwrap(); - let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); - assert!( - doc.namespace.contains("REDACTED"), // [REDACTED_PII_RFC] after sanitize_namespace - "namespace should contain a redaction token, got: {}", - doc.namespace - ); -} - -#[tokio::test] -async fn upsert_document_metadata_only_auto_sanitizes_pii_like_key() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let doc_id = memory - .upsert_document_metadata_only(NamespaceDocumentInput { - namespace: "safe".to_string(), - key: "ssn-123-45-6789".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect("PII-like key should be auto-sanitized, not rejected"); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); - assert!( - doc.key.contains("REDACTED"), // [REDACTED_PII_SSN] - "key should contain a redaction token, got: {}", - doc.key - ); -} - -#[tokio::test] -async fn upsert_document_metadata_only_auto_sanitizes_pii_like_namespace() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - let doc_id = memory - .upsert_document_metadata_only(NamespaceDocumentInput { - namespace: "user/111.444.777-35".to_string(), - key: "safe-key".to_string(), - title: "Title".to_string(), - content: "Body".to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - }) - .await - .expect("PII-like namespace should be auto-sanitized, not rejected"); - - let docs = memory - .load_documents_for_scope("user/111.444.777-35") - .await - .unwrap(); - let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); - assert!( - doc.namespace.contains("REDACTED"), // [REDACTED_PII_CPF] after sanitize_namespace - "namespace should contain a redaction token, got: {}", - doc.namespace - ); -} - -// --------------------------------------------------------------------------- -// #5164 — the identifier canonicalization has to be symmetric, and it has to -// leave scanner-built identifiers alone. -// -// Canonicalizing a namespace/key rewrites the row's *address*. Two failure -// modes follow, and both re-create the unthrottled write loop the issue was -// filed for (silently, this time): -// -// 1. a read path that addresses the raw caller identifier never finds the -// canonicalized row, so the caller writes it again; -// 2. canonicalizing with the lenient *content* scrubber maps every -// phone-shaped identifier onto one placeholder, so distinct chats collapse -// onto one `(namespace, key)` and the upsert's `ON CONFLICT … DO UPDATE` -// has one contact's document overwrite another's. -// --------------------------------------------------------------------------- - -#[tokio::test] -async fn pii_like_document_key_round_trips_through_get_and_forget() { - use crate::traits::Memory; - - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input( - "clients", - "ssn-123-45-6789", - "Title", - "Body", - )) - .await - .expect("PII-like key should be canonicalized, not rejected"); - - let entry = memory - .get("clients", "ssn-123-45-6789") - .await - .unwrap() - .expect("a canonicalized key must stay readable by its original identifier"); - assert_eq!(entry.content, "Body"); - assert!( - !entry.key.contains("123-45-6789"), - "the SSN must not be persisted as the storage address, got: {}", - entry.key - ); - - assert!( - memory.forget("clients", "ssn-123-45-6789").await.unwrap(), - "forget must address the same canonicalized row the write created" - ); - assert!(memory - .get("clients", "ssn-123-45-6789") - .await - .unwrap() - .is_none()); -} - -#[tokio::test] -async fn pii_like_namespace_round_trips_through_get_and_list() { - use crate::traits::Memory; - - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document(make_doc_input( - "cliente-RFC-VECJ880326XK4", - "notes", - "Title", - "Body", - )) - .await - .expect("PII-like namespace should be canonicalized, not rejected"); - - assert!( - memory - .get("cliente-RFC-VECJ880326XK4", "notes") - .await - .unwrap() - .is_some(), - "a canonicalized namespace must stay readable by its original value" - ); - let listed = memory - .list(Some("cliente-RFC-VECJ880326XK4"), None, None) - .await - .unwrap(); - assert_eq!( - listed.len(), - 1, - "list() must canonicalize its namespace the same way the write did" - ); -} - -#[tokio::test] -async fn scanner_built_phone_shaped_keys_stay_distinct_documents() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - // Two different WhatsApp contacts, same day. The digit runs differ only in - // the phone number — the lenient content scrubber replaces both with one - // `[REDACTED_PII_PHONE]` token, which would collapse them onto a single row. - for (key, content) in [ - ("12025551234@c.us:2026-05-30", "alice thread"), - ("12025559999@c.us:2026-05-30", "bob thread"), - ] { - memory - .upsert_document(make_doc_input("whatsapp-web", key, "Chat", content)) - .await - .unwrap(); - } - - let docs = memory - .load_documents_for_scope("whatsapp-web") - .await - .unwrap(); - assert_eq!( - docs.len(), - 2, - "scanner-built phone-shaped keys must stay distinct documents, got: {:?}", - docs.iter().map(|d| d.key.clone()).collect::>() - ); - for key in ["12025551234@c.us:2026-05-30", "12025559999@c.us:2026-05-30"] { - assert!( - docs.iter().any(|d| d.key == key), - "key {key} must be stored verbatim, got: {:?}", - docs.iter().map(|d| d.key.clone()).collect::>() - ); - } -} - -#[tokio::test] -async fn scanner_built_identifiers_are_preserved_verbatim() { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - // The strict boundary predicate deliberately tolerates these shapes - // (WhatsApp group JID, iMessage E.164 chat id, padded ms timestamp); the - // content scrubber does not. Canonicalization must follow the strict set, - // or every scanner rewrites its own storage addresses. - for key in [ - "12025551234-1543890267@g.us:2026-05-30", - "imessage:+12025551234:2026-05-30", - "accepted:000001747729035001", - ] { - let doc_id = memory - .upsert_document(make_doc_input("scanner", key, "Title", "Body")) - .await - .unwrap(); - let docs = memory.load_documents_for_scope("scanner").await.unwrap(); - let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); - assert_eq!( - doc.key, key, - "scanner-built identifier must not be rewritten" - ); - } -} - -#[tokio::test] -async fn metadata_only_write_round_trips_through_pii_like_key() { - use crate::traits::Memory; - - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - - memory - .upsert_document_metadata_only(make_doc_input( - "clients", - "cuit-20-11111111-2", - "Title", - "Body", - )) - .await - .expect("PII-like key should be canonicalized, not rejected"); - - assert!( - memory - .get("clients", "cuit-20-11111111-2") - .await - .unwrap() - .is_some(), - "the metadata-only path must canonicalize keys the same way the full upsert does" - ); -} diff --git a/core/src/store/namespace_store/fts5.rs b/core/src/store/namespace_store/fts5.rs deleted file mode 100644 index 2bb495a7..00000000 --- a/core/src/store/namespace_store/fts5.rs +++ /dev/null @@ -1,610 +0,0 @@ -//! FTS5 episodic memory — full-text search over past sessions. -//! -//! Adds an FTS5 virtual table backed by an `episodic_log` table for storing -//! turn-level records with optional extracted lessons. The Archivist uses -//! this for post-session knowledge extraction and the `search_memory` tool -//! uses it for episodic recall. - -use parking_lot::Mutex; -use rusqlite::Connection; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::store::safety; - -/// A single episodic record (one turn or event). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EpisodicEntry { - pub id: Option, - pub session_id: String, - pub timestamp: f64, - pub role: String, - pub content: String, - pub lesson: Option, - pub tool_calls_json: Option, - pub cost_microdollars: u64, -} - -/// SQL to create the episodic tables. Called during `UnifiedMemory` init. -pub const EPISODIC_INIT_SQL: &str = r#" -CREATE TABLE IF NOT EXISTS episodic_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - timestamp REAL NOT NULL, - role TEXT NOT NULL, - content TEXT NOT NULL, - lesson TEXT, - tool_calls_json TEXT, - cost_microdollars INTEGER DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS idx_episodic_session - ON episodic_log(session_id, timestamp); - -CREATE VIRTUAL TABLE IF NOT EXISTS episodic_fts USING fts5( - session_id, - role, - content, - lesson, - content=episodic_log, - content_rowid=id, - tokenize='porter unicode61' -); - --- Triggers to keep FTS5 in sync with the backing table. -CREATE TRIGGER IF NOT EXISTS episodic_ai AFTER INSERT ON episodic_log BEGIN - INSERT INTO episodic_fts(rowid, session_id, role, content, lesson) - VALUES (new.id, new.session_id, new.role, new.content, new.lesson); -END; - -CREATE TRIGGER IF NOT EXISTS episodic_ad AFTER DELETE ON episodic_log BEGIN - INSERT INTO episodic_fts(episodic_fts, rowid, session_id, role, content, lesson) - VALUES ('delete', old.id, old.session_id, old.role, old.content, old.lesson); -END; - -CREATE TRIGGER IF NOT EXISTS episodic_au AFTER UPDATE ON episodic_log BEGIN - INSERT INTO episodic_fts(episodic_fts, rowid, session_id, role, content, lesson) - VALUES ('delete', old.id, old.session_id, old.role, old.content, old.lesson); - INSERT INTO episodic_fts(rowid, session_id, role, content, lesson) - VALUES (new.id, new.session_id, new.role, new.content, new.lesson); -END; -"#; - -/// Insert an episodic entry. -/// Insert one episodic turn, returning the row id it was assigned. -/// -/// # Why the id comes back from here -/// -/// Callers used to insert and then issue `SELECT last_insert_rowid()`. That is -/// **connection-local** state: this store hands the same `Arc>` -/// to several writers, so an interleaved insert between the two statements -/// returns the wrong id — and the caller files the turn under the wrong -/// conversation segment. Reading it here, still under the lock taken for the -/// insert, is the only place it can be read correctly. -pub fn episodic_insert( - conn: &Arc>, - entry: &EpisodicEntry, -) -> anyhow::Result { - if safety::has_likely_secret(&entry.session_id) || safety::has_likely_secret(&entry.role) { - tracing::warn!( - "[memory:safety] episodic insert rejected secret-like session/role session_chars={} role_chars={}", - entry.session_id.chars().count(), - entry.role.chars().count() - ); - anyhow::bail!("episodic session_id/role cannot contain secrets"); - } - - let content = safety::sanitize_text(&entry.content); - let lesson = entry - .lesson - .as_ref() - .map(|value| safety::sanitize_text(value)); - let tool_calls_json = entry.tool_calls_json.as_ref().map(|value| { - if let Ok(parsed) = serde_json::from_str::(value) { - let sanitized = safety::sanitize_json(&parsed); - safety::Sanitized { - value: sanitized.value.to_string(), - report: sanitized.report, - } - } else { - safety::sanitize_text(value) - } - }); - - let report = content - .report - .merge( - lesson - .as_ref() - .map(|value| value.report) - .unwrap_or_default(), - ) - .merge( - tool_calls_json - .as_ref() - .map(|value| value.report) - .unwrap_or_default(), - ); - if report.changed() { - tracing::warn!( - "[memory:safety] episodic insert sanitized session_chars={} role_chars={} text_redactions={} key_redactions={} blocked_secret_hits={} depth_redactions={} pii_redactions={}", - entry.session_id.chars().count(), - entry.role.chars().count(), - report.text_redactions, - report.key_redactions, - report.blocked_secret_hits, - report.depth_redactions, - report.pii_redactions - ); - } - - let conn = conn.lock(); - conn.execute( - "INSERT INTO episodic_log (session_id, timestamp, role, content, lesson, tool_calls_json, cost_microdollars) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - rusqlite::params![ - &entry.session_id, - entry.timestamp, - &entry.role, - content.value, - lesson.map(|value| value.value), - tool_calls_json.map(|value| value.value), - entry.cost_microdollars as i64, - ], - )?; - // Still holding the lock taken above — see the doc comment. - let id = conn.last_insert_rowid(); - tracing::debug!( - "[fts5] inserted episodic entry: session={}, role={}, id={id}", - entry.session_id, - entry.role - ); - Ok(id) -} - -/// Full-text search over episodic entries. -pub fn episodic_search( - conn: &Arc>, - query: &str, - limit: usize, -) -> anyhow::Result> { - let conn = conn.lock(); - let trimmed = query.trim(); - if trimmed.is_empty() { - tracing::debug!("[fts5] search skipped — empty query"); - return Ok(Vec::new()); - } - let phrase_query = sanitize_fts_query(trimmed); - if phrase_query.is_empty() { - tracing::debug!("[fts5] search skipped — sanitised query is empty"); - return Ok(Vec::new()); - } - - let mut stmt = conn.prepare( - "SELECT el.id, el.session_id, el.timestamp, el.role, el.content, el.lesson, - el.tool_calls_json, el.cost_microdollars - FROM episodic_fts AS ef - JOIN episodic_log AS el ON ef.rowid = el.id - WHERE episodic_fts MATCH ?1 - ORDER BY rank - LIMIT ?2", - )?; - - let rows = stmt - .query_map(rusqlite::params![phrase_query, limit as i64], |row| { - Ok(EpisodicEntry { - id: row.get(0)?, - session_id: row.get(1)?, - timestamp: row.get(2)?, - role: row.get(3)?, - content: row.get(4)?, - lesson: row.get(5)?, - tool_calls_json: row.get(6)?, - cost_microdollars: row.get::<_, i64>(7)? as u64, - }) - })? - .collect::, _>>()?; - - tracing::debug!("[fts5] search returned {} results", rows.len()); - Ok(rows) -} - -/// FTS5 search across **all** sessions, optionally excluding one session -/// from the result set. Used by [`crate`] to surface -/// cross-chat conversational context for the same user/workspace (issue -/// #1505) without leaking the current chat's own history into the -/// "other chats" block. -/// -/// `exclude_session` should be the active session_id when the caller is -/// also pulling same-session entries via [`episodic_session_entries`] — -/// passing `None` returns hits from every session indexed in this DB. -/// -/// Workspace/user scope is enforced at the connection level: the SQLite -/// database lives at `/memory/...` so one DB == one workspace. -/// This helper cannot cross that boundary. -pub fn episodic_cross_session_search( - conn: &Arc>, - query: &str, - limit: usize, - exclude_session: Option<&str>, -) -> anyhow::Result> { - let conn = conn.lock(); - let trimmed = query.trim(); - if trimmed.is_empty() { - tracing::debug!("[fts5] cross-session search skipped — empty query"); - return Ok(Vec::new()); - } - - // FTS5 MATCH expressions are picky about syntax — bare phrases with - // punctuation can fail to parse. Wrap the query in double quotes so - // it's treated as a phrase (FTS5 will still tokenize it). This mirrors - // how the unified store sanitises queries before MATCH. - let phrase_query = sanitize_fts_query(trimmed); - if phrase_query.is_empty() { - tracing::debug!("[fts5] cross-session search skipped — sanitised query is empty"); - return Ok(Vec::new()); - } - - let mut stmt = match exclude_session { - Some(_) => conn.prepare( - "SELECT el.id, el.session_id, el.timestamp, el.role, el.content, el.lesson, - el.tool_calls_json, el.cost_microdollars - FROM episodic_fts AS ef - JOIN episodic_log AS el ON ef.rowid = el.id - WHERE episodic_fts MATCH ?1 AND el.session_id != ?2 - ORDER BY rank - LIMIT ?3", - )?, - None => conn.prepare( - "SELECT el.id, el.session_id, el.timestamp, el.role, el.content, el.lesson, - el.tool_calls_json, el.cost_microdollars - FROM episodic_fts AS ef - JOIN episodic_log AS el ON ef.rowid = el.id - WHERE episodic_fts MATCH ?1 - ORDER BY rank - LIMIT ?2", - )?, - }; - - let map_row = |row: &rusqlite::Row<'_>| -> rusqlite::Result { - Ok(EpisodicEntry { - id: row.get(0)?, - session_id: row.get(1)?, - timestamp: row.get(2)?, - role: row.get(3)?, - content: row.get(4)?, - lesson: row.get(5)?, - tool_calls_json: row.get(6)?, - cost_microdollars: row.get::<_, i64>(7)? as u64, - }) - }; - - let rows: Vec = match exclude_session { - Some(sid) => stmt - .query_map(rusqlite::params![phrase_query, sid, limit as i64], map_row)? - .collect::, _>>()?, - None => stmt - .query_map(rusqlite::params![phrase_query, limit as i64], map_row)? - .collect::, _>>()?, - }; - - // Never log the raw query string — may contain secrets / PII. Emit a - // stable non-reversible hash + length instead so cross-session - // diagnostics stay grep-friendly without leaking user content. - let query_hash = { - use std::hash::{Hash, Hasher}; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - trimmed.hash(&mut hasher); - hasher.finish() - }; - tracing::debug!( - "[fts5] cross-session search query_hash={:016x} query_len={} (exclude={:?}) returned {} results", - query_hash, - trimmed.chars().count(), - exclude_session, - rows.len() - ); - Ok(rows) -} - -/// Best-effort FTS5 query sanitiser: split user text on punctuation and -/// symbols that break the MATCH grammar, then quote each surviving token -/// so FTS5 treats it as literal text. Returns an empty string when -/// nothing usable survives — callers short-circuit to "no hits". -pub(super) fn sanitize_fts_query(query: &str) -> String { - let cleaned: String = query - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '_' { - c - } else { - ' ' - } - }) - .collect(); - let tokens: Vec = cleaned - .split_whitespace() - .filter(|tok| !tok.is_empty()) - .take(8) - .map(|tok| format!("\"{tok}\"")) - .collect(); - tokens.join(" ") -} - -/// Get all entries for a session (for post-session summary). -pub fn episodic_session_entries( - conn: &Arc>, - session_id: &str, -) -> anyhow::Result> { - let conn = conn.lock(); - let mut stmt = conn.prepare( - "SELECT id, session_id, timestamp, role, content, lesson, tool_calls_json, cost_microdollars - FROM episodic_log - WHERE session_id = ?1 - ORDER BY timestamp ASC", - )?; - - let rows = stmt - .query_map(rusqlite::params![session_id], |row| { - Ok(EpisodicEntry { - id: row.get(0)?, - session_id: row.get(1)?, - timestamp: row.get(2)?, - role: row.get(3)?, - content: row.get(4)?, - lesson: row.get(5)?, - tool_calls_json: row.get(6)?, - cost_microdollars: row.get::<_, i64>(7)? as u64, - }) - })? - .collect::, _>>()?; - - Ok(rows) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn setup_db() -> Arc> { - let conn = Connection::open_in_memory().unwrap(); - conn.execute_batch(EPISODIC_INIT_SQL).unwrap(); - Arc::new(Mutex::new(conn)) - } - - #[test] - fn insert_and_search() { - let conn = setup_db(); - let entry = EpisodicEntry { - id: None, - session_id: "s1".into(), - timestamp: 1000.0, - role: "user".into(), - content: "How do I deploy to production?".into(), - lesson: Some("User frequently asks about deployment".into()), - tool_calls_json: None, - cost_microdollars: 100, - }; - episodic_insert(&conn, &entry).unwrap(); - - let results = episodic_search(&conn, "deploy production", 10).unwrap(); - assert_eq!(results.len(), 1); - assert_eq!(results[0].session_id, "s1"); - assert!(results[0].content.contains("deploy")); - } - - #[test] - fn session_entries() { - let conn = setup_db(); - for i in 0..3 { - episodic_insert( - &conn, - &EpisodicEntry { - id: None, - session_id: "s2".into(), - timestamp: 1000.0 + i as f64, - role: if i % 2 == 0 { "user" } else { "assistant" }.into(), - content: format!("Turn {i} content"), - lesson: None, - tool_calls_json: None, - cost_microdollars: 0, - }, - ) - .unwrap(); - } - - let entries = episodic_session_entries(&conn, "s2").unwrap(); - assert_eq!(entries.len(), 3); - assert!(entries[0].timestamp < entries[2].timestamp); - } - - #[test] - fn empty_search_returns_empty() { - let conn = setup_db(); - let results = episodic_search(&conn, "nonexistent query", 10).unwrap(); - assert!(results.is_empty()); - } - - #[test] - fn insert_redacts_secret_like_content() { - let conn = setup_db(); - episodic_insert( - &conn, - &EpisodicEntry { - id: None, - session_id: "s1".into(), - timestamp: 1000.0, - role: "user".into(), - content: "Bearer abcdefghijklmnop".into(), - lesson: Some("token=abc123".into()), - tool_calls_json: Some("{\"api_key\":\"sk-1234567890123456789012345\"}".into()), - cost_microdollars: 0, - }, - ) - .unwrap(); - - let rows = episodic_session_entries(&conn, "s1").unwrap(); - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].content, "Bearer [REDACTED]"); - assert_eq!(rows[0].lesson.as_deref(), Some("[REDACTED]")); - assert_eq!( - rows[0].tool_calls_json.as_deref(), - Some("{\"api_key\":\"[REDACTED_SECRET]\"}") - ); - } - - #[test] - fn insert_rejects_secret_like_session_id() { - let conn = setup_db(); - let err = episodic_insert( - &conn, - &EpisodicEntry { - id: None, - session_id: "Bearer abcdefghijklmnop".into(), - timestamp: 1000.0, - role: "user".into(), - content: "hello".into(), - lesson: None, - tool_calls_json: None, - cost_microdollars: 0, - }, - ) - .expect_err("secret-like session_id should be rejected"); - assert!(err.to_string().contains("cannot contain secrets")); - } - - // ── Cross-session search (#1505) ───────────────────────────────────── - - fn insert_turn(conn: &Arc>, session_id: &str, ts: f64, content: &str) { - episodic_insert( - conn, - &EpisodicEntry { - id: None, - session_id: session_id.into(), - timestamp: ts, - role: "user".into(), - content: content.into(), - lesson: None, - tool_calls_json: None, - cost_microdollars: 0, - }, - ) - .unwrap(); - } - - #[test] - fn cross_session_search_surfaces_other_sessions_excluding_current() { - let conn = setup_db(); - // Chat A — user shared the durable fact - insert_turn( - &conn, - "session-a", - 1000.0, - "I prefer Postgres for new services", - ); - // Chat B — current chat, where the question is being asked - insert_turn( - &conn, - "session-b", - 2000.0, - "What database should I use today?", - ); - // Chat C — yet another chat with a related fact - insert_turn(&conn, "session-c", 1500.0, "Postgres timezone is UTC"); - - // Asking from chat B: should see session-a + session-c (not session-b) - let hits = episodic_cross_session_search(&conn, "Postgres", 10, Some("session-b")).unwrap(); - assert!( - !hits.is_empty(), - "cross-session search must surface hits from other sessions" - ); - for hit in &hits { - assert_ne!( - hit.session_id, "session-b", - "current session must be excluded from cross-session sweep, got {}", - hit.session_id - ); - } - let session_ids: std::collections::HashSet<&str> = - hits.iter().map(|h| h.session_id.as_str()).collect(); - assert!(session_ids.contains("session-a")); - assert!(session_ids.contains("session-c")); - } - - #[test] - fn cross_session_search_returns_empty_for_unknown_query() { - let conn = setup_db(); - insert_turn(&conn, "session-a", 1000.0, "I prefer Postgres"); - let hits = episodic_cross_session_search(&conn, "kubernetes", 10, None).unwrap(); - assert!( - hits.is_empty(), - "no FTS match should produce zero hits, not all rows" - ); - } - - #[test] - fn cross_session_search_handles_empty_query() { - let conn = setup_db(); - insert_turn(&conn, "session-a", 1000.0, "anything"); - let hits = episodic_cross_session_search(&conn, " ", 10, None).unwrap(); - assert!(hits.is_empty(), "empty query short-circuits to zero hits"); - } - - #[test] - fn cross_session_search_sanitises_punctuation_safely() { - let conn = setup_db(); - insert_turn(&conn, "session-a", 1000.0, "Postgres deployment notes"); - // Query with FTS5-hostile punctuation — should not panic. Tokens - // shared with the indexed row should still match (FTS5 phrase - // ANDs every quoted token, so we use words that all appear in - // the row to avoid AND-mismatch false negatives). - let hits = - episodic_cross_session_search(&conn, "\"Postgres\" (deployment)?", 10, None).unwrap(); - assert!( - !hits.is_empty(), - "punctuated query whose surviving tokens match the indexed row must still surface it" - ); - assert!(hits[0].content.contains("Postgres")); - } - - #[test] - fn episodic_search_sanitises_punctuation_safely() { - let conn = setup_db(); - insert_turn(&conn, "session-a", 1000.0, "Postgres deployment notes"); - - let hits = episodic_search(&conn, "\"Postgres\",(deployment)?", 10) - .expect("punctuated user query should not trip FTS5 syntax errors"); - - assert!( - !hits.is_empty(), - "punctuated query whose surviving tokens match the indexed row must still surface it" - ); - assert!(hits[0].content.contains("Postgres")); - } - - #[test] - fn cross_session_search_does_not_panic_on_pure_punctuation() { - let conn = setup_db(); - insert_turn(&conn, "session-a", 1000.0, "Postgres deployment notes"); - // All-punctuation query should normalise to empty and produce - // zero hits without panicking. - let hits = episodic_cross_session_search(&conn, "()*\":", 10, None).unwrap(); - assert!( - hits.is_empty(), - "punctuation-only query must produce zero hits" - ); - } - - #[test] - fn cross_session_search_no_exclusion_includes_all_matches() { - let conn = setup_db(); - insert_turn(&conn, "session-a", 1000.0, "Postgres preference"); - insert_turn(&conn, "session-b", 2000.0, "Postgres setup"); - - let hits = episodic_cross_session_search(&conn, "Postgres", 10, None).unwrap(); - let session_ids: std::collections::HashSet<&str> = - hits.iter().map(|h| h.session_id.as_str()).collect(); - assert!(session_ids.contains("session-a")); - assert!(session_ids.contains("session-b")); - } -} diff --git a/core/src/store/safety/mod.rs b/core/src/store/safety/mod.rs deleted file mode 100644 index fe021a09..00000000 --- a/core/src/store/safety/mod.rs +++ /dev/null @@ -1,340 +0,0 @@ -//! Secret-detection and redaction for memory writes — thin host shim over -//! `crate::engine::backend::store::safety` (W3). -//! -//! The conservative secret + PII scrubbers (`has_likely_secret`, -//! `has_likely_pii`, `sanitize_text`, `sanitize_json`) + the -//! `SanitizationReport`/`Sanitized` types are the crate's — now including the -//! full multilingual national-ID PII module (ported into the crate so the crate -//! `sanitize_text` matches this host's byte-for-byte). The host keeps only -//! [`sanitize_document_input`], which scrubs the host-specific -//! [`NamespaceDocumentInput`] shape by delegating each field to the crate -//! scrubbers. The retained test suite doubles as a byte-parity guard: it asserts -//! the crate scrubber still redacts every secret/PII pattern the host relied on. - -pub mod pii; - -use crate::store::types::NamespaceDocumentInput; - -pub use crate::engine::backend::store::safety::{ - has_likely_pii, has_likely_secret, sanitize_json, sanitize_text, SanitizationReport, Sanitized, -}; - -/// Canonical storage form of a caller-supplied memory **identifier** — a -/// namespace, a document key, or a KV key. -/// -/// An identifier is an address, not content: whatever this returns is what the -/// row is stored under, so every read / update / delete that addresses a row by -/// identifier has to canonicalize through this same function, or it looks up a -/// row the write never created (#5164). -/// -/// Two properties make that safe, and both follow the split the crate's PII -/// module documents between its **strict boundary predicate** and its **lenient -/// content scrubber**: -/// -/// * **Strict gating.** Only identifiers that trip [`has_likely_pii`] — -/// formatted / keyword-gated national IDs (`ssn-123-45-6789`, -/// `cliente-RFC-VECJ880326XK4`, `cuit-20-11111111-2`) — are rewritten. -/// `redact_pii` on its own also rewrites bare digit-run shapes, and the -/// scanners legitimately build identifiers out of those: WhatsApp JIDs -/// (`12025551234-1543890267@g.us`), iMessage `+1…` chat ids, millisecond -/// timestamps, padded counters. Rewriting those maps two distinct contacts -/// onto one `(namespace, key)`, where the upsert's `ON CONFLICT … DO UPDATE` -/// has one contact's document silently overwrite the other's. -/// * **Idempotence.** The `[REDACTED_PII_*]` placeholders carry no PII pattern -/// of their own, so canonicalizing an already-canonical identifier is a -/// no-op — which is what lets read paths canonicalize unconditionally. -pub fn canonical_identifier(value: &str) -> String { - if !has_likely_pii(value) { - return value.to_string(); - } - pii::redact_pii(value).value -} - -/// Canonical storage form of a document key: the exact transform -/// `upsert_document` / `upsert_document_metadata_only` apply before writing the -/// `memory_docs.key` column (trim, then [`canonical_identifier`]). -/// -/// Single-sourced so the by-key read paths (`Memory::get`, `Memory::forget`) -/// cannot drift from the write path. Drift there is invisible — the lookup -/// simply misses, the caller treats the row as absent and writes again, which -/// is the unthrottled loop #5164 was reported for. -pub fn canonical_document_key(key: &str) -> String { - canonical_identifier(key.trim()) -} - -/// Scrub a namespace-document input, field by field, via the crate scrubbers. -/// -/// Sanitization is content-cleaning only; provenance `taint` survives untouched -/// so the write gate's taint check still sees the real source signal. -pub fn sanitize_document_input(input: NamespaceDocumentInput) -> Sanitized { - let mut report = SanitizationReport::default(); - - let title = sanitize_text(&input.title); - report = report.merge(title.report); - let content = sanitize_text(&input.content); - report = report.merge(content.report); - - let mut tags = Vec::with_capacity(input.tags.len()); - for tag in input.tags { - let sanitized = sanitize_text(&tag); - report = report.merge(sanitized.report); - tags.push(sanitized.value); - } - - let metadata = sanitize_json(&input.metadata); - report = report.merge(metadata.report); - - Sanitized { - value: NamespaceDocumentInput { - namespace: input.namespace, - key: input.key, - title: title.value, - content: content.value, - source_type: input.source_type, - priority: input.priority, - tags, - metadata: metadata.value, - category: input.category, - session_id: input.session_id, - document_id: input.document_id, - taint: input.taint, - }, - report, - } -} - -#[cfg(test)] -mod tests { - //! Byte-parity guard over the crate scrubber: every secret/PII pattern the - //! host used to redact must still be redacted after the port. - use super::*; - use serde_json::json; - - const REDACTED_SECRET: &str = "[REDACTED_SECRET]"; - const REDACTED_PRIVATE_KEY: &str = "[REDACTED_PRIVATE_KEY]"; - const MAX_JSON_SANITIZE_DEPTH: usize = 128; - - #[test] - fn sanitize_text_redacts_bearer_and_openai_key() { - let input = "Authorization: Bearer abcdefghijklmnop and sk-1234567890123456789012345"; - let sanitized = sanitize_text(input); - assert!(sanitized.value.contains("Bearer [REDACTED]")); - assert!(!sanitized.value.contains("sk-1234567890123456789012345")); - assert!(sanitized.report.text_redactions >= 2); - } - - #[test] - fn sanitize_text_blocks_private_key_blocks() { - let input = "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----"; - let sanitized = sanitize_text(input); - assert!(sanitized.value.contains(REDACTED_PRIVATE_KEY)); - assert!(sanitized.report.blocked_secret_hits >= 1); - } - - #[test] - fn sanitize_json_redacts_sensitive_keys_and_nested_strings() { - let input = json!({ - "token": "abc123", - "nested": { "notes": "Bearer supersecretvalue", "ok": "hello" }, - "arr": ["sk-1234567890123456789012345", "safe"] - }); - let sanitized = sanitize_json(&input); - assert_eq!(sanitized.value["token"], json!(REDACTED_SECRET)); - assert_eq!(sanitized.value["nested"]["ok"], json!("hello")); - assert!(sanitized.value["nested"]["notes"] - .as_str() - .unwrap_or_default() - .contains("[REDACTED]")); - assert!(sanitized.report.key_redactions >= 1); - assert!(sanitized.report.text_redactions >= 2); - } - - #[test] - fn sanitize_json_redacts_common_sensitive_key_variants() { - let input = json!({ - "db_password": "p@ss", "secret_key": "abc123", - "api_secret": "def456", "monkey": "banana" - }); - let sanitized = sanitize_json(&input); - assert_eq!(sanitized.value["db_password"], json!(REDACTED_SECRET)); - assert_eq!(sanitized.value["secret_key"], json!(REDACTED_SECRET)); - assert_eq!(sanitized.value["api_secret"], json!(REDACTED_SECRET)); - assert_eq!(sanitized.value["monkey"], json!(REDACTED_SECRET)); - assert!(sanitized.report.key_redactions >= 4); - } - - #[test] - fn has_likely_secret_detects_common_patterns() { - assert!(has_likely_secret("api_key=abc123")); - assert!(has_likely_secret("Bearer abcdefghijklmnopqrstuvwxyz")); - assert!(has_likely_secret("xoxb-1234567890-abcdef-ghijklmnop")); - assert!(has_likely_secret("glpat-aaaaaaaaaaaaaaaaaaaa")); - assert!(has_likely_secret("SG.aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbb")); - assert!(!has_likely_secret("I prefer rust")); - } - - #[test] - fn sanitize_text_redacts_more_provider_secrets() { - let input = "auth=Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== stripe=sk_live_12345678901234567890 npm=npm_abcdefghijklmnopqrstuvwxyz"; - let sanitized = sanitize_text(input); - assert!(!sanitized.value.contains("sk_live_12345678901234567890")); - assert!(!sanitized.value.contains("npm_abcdefghijklmnopqrstuvwxyz")); - assert!(sanitized.value.contains("[REDACTED]")); - assert!(sanitized.report.text_redactions >= 2); - } - - #[test] - fn sanitize_text_redacts_oauth_url_style_params() { - let input = "https://example.com/callback?access_token=abcd1234&refresh_token=efgh5678&id_token=jwt"; - let sanitized = sanitize_text(input); - assert!(!sanitized.value.contains("abcd1234")); - assert!(!sanitized.value.contains("efgh5678")); - assert!(!sanitized.value.contains("id_token=jwt")); - assert!(sanitized.report.text_redactions >= 3); - } - - #[test] - fn sanitize_text_redacts_multiline_private_key_blocks() { - let input = "BEGIN\n-----BEGIN OPENSSH PRIVATE KEY-----\nline1\nline2\n-----END OPENSSH PRIVATE KEY-----\nEND"; - let sanitized = sanitize_text(input); - assert!(!sanitized.value.contains("OPENSSH PRIVATE KEY")); - assert!(sanitized.value.contains(REDACTED_PRIVATE_KEY)); - assert!(sanitized.report.blocked_secret_hits >= 1); - } - - #[test] - fn sanitize_text_also_redacts_pii_after_secrets() { - let input = "Token sk-abcdefghijklmnopqrstuvwxyz; CPF 111.444.777-35; phone +15551234567"; - let sanitized = sanitize_text(input); - assert!(!sanitized.value.contains("sk-abcdefghijklmnopqrstuvwxyz")); - assert!(!sanitized.value.contains("111.444.777-35")); - assert!(!sanitized.value.contains("+15551234567")); - assert!(sanitized.value.contains("[REDACTED_PII_CPF]")); - assert!(sanitized.value.contains("[REDACTED_PII_PHONE]")); - assert!(sanitized.report.text_redactions >= 1); - assert_eq!(sanitized.report.pii_redactions, 2); - } - - #[test] - fn sanitize_json_propagates_pii_redaction_into_nested_strings() { - let input = json!({ - "note": "Cliente RFC VECJ880326XK4 confirmado", - "meta": { "cuit": "20-11111111-2" } - }); - let sanitized = sanitize_json(&input); - assert!(sanitized.value["note"] - .as_str() - .unwrap_or_default() - .contains("[REDACTED_PII_RFC]")); - assert!(sanitized.value["meta"]["cuit"] - .as_str() - .unwrap_or_default() - .contains("[REDACTED_PII_CUIT]")); - assert!(sanitized.report.pii_redactions >= 2); - } - - #[test] - fn sanitize_json_redacts_values_beyond_max_depth() { - let mut nested = json!("leaf"); - for _ in 0..(MAX_JSON_SANITIZE_DEPTH + 2) { - nested = json!({ "nested": nested }); - } - let sanitized = sanitize_json(&nested); - assert!(sanitized.report.depth_redactions >= 1); - assert!(sanitized - .value - .to_string() - .contains(&format!("\"{REDACTED_SECRET}\""))); - } - - /// #5164: identifiers are storage addresses, so canonicalization follows - /// the **strict** boundary predicate. Formatted / keyword-gated national IDs - /// are rewritten; the bare digit-run shapes the scanners build identifiers - /// out of are left alone (rewriting those maps distinct contacts onto one - /// `(namespace, key)` and the upsert silently overwrites). - #[test] - fn canonical_identifier_rewrites_only_strict_pii() { - for identifier in [ - "ssn-123-45-6789", - "cliente-RFC-VECJ880326XK4", - "cuit-20-11111111-2", - "user/111.444.777-35", - ] { - let canonical = canonical_identifier(identifier); - assert_ne!( - canonical, identifier, - "strict PII identifier must be canonicalized: {identifier}" - ); - assert!( - canonical.contains("[REDACTED_PII_"), - "expected a redaction placeholder, got: {canonical}" - ); - } - - for identifier in [ - // WhatsApp group JID / 1:1 JID / broadcast, iMessage E.164 chat id, - // telegram numeric peer id, padded ms timestamp, plain namespaces. - "12025551234-1543890267@g.us:2026-05-30", - "12025551234@c.us:2026-05-30", - "imessage:+12025551234:2026-05-30", - "4123456789:2026-05-30", - "accepted:000001747729035001", - "memory/global/preferences", - "skill-gmail", - ] { - assert_eq!( - canonical_identifier(identifier), - identifier, - "scanner-built identifier must keep its identity: {identifier}" - ); - } - } - - /// Read paths canonicalize unconditionally, so the transform has to be a - /// fixed point on its own output. - #[test] - fn canonical_identifier_is_idempotent() { - for identifier in ["ssn-123-45-6789", "cliente-RFC-VECJ880326XK4", "safe-key"] { - let once = canonical_identifier(identifier); - assert_eq!(canonical_identifier(&once), once, "not idempotent: {once}"); - } - } - - /// `canonical_document_key` single-sources the write-path transform, trim - /// included — otherwise `Memory::get` would address an untrimmed key that - /// `upsert_document` never wrote. - #[test] - fn canonical_document_key_trims_before_canonicalizing() { - assert_eq!(canonical_document_key(" doc-a "), "doc-a"); - assert_eq!( - canonical_document_key(" ssn-123-45-6789 "), - canonical_identifier("ssn-123-45-6789") - ); - assert_eq!(canonical_document_key(" "), ""); - } - - #[test] - fn sanitize_document_input_preserves_taint() { - let input = NamespaceDocumentInput { - namespace: "ns".into(), - key: "k".into(), - title: "Bearer secret123456789 visible title".into(), - content: "content with sk-abcdefghijklmnopqrstuvwxyz".into(), - source_type: "sync".into(), - priority: "normal".into(), - tags: vec!["tag1".into()], - metadata: json!({"safe": "value"}), - category: "core".into(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::ExternalSync, - }; - let sanitized = sanitize_document_input(input); - assert_eq!( - sanitized.value.taint, - crate::MemoryTaint::ExternalSync, - "taint must survive sanitization unchanged" - ); - assert!(sanitized.report.text_redactions >= 1); - } -} diff --git a/core/src/store/write_gate_tests.rs b/core/src/store/write_gate_tests.rs deleted file mode 100644 index 9048d6f1..00000000 --- a/core/src/store/write_gate_tests.rs +++ /dev/null @@ -1,176 +0,0 @@ -//! Tests for the host secret/PII write gate. -//! -//! The gate was hoisted out of `namespace_store::documents` (H0, piece 2) so -//! the storage driver never decides what counts as a secret. These tests pin -//! **both** halves of that split, which is what makes the hoist provable rather -//! than cosmetic: -//! -//! * the gated entry points (`upsert_document`, -//! `upsert_document_metadata_only`) still redact — the pre-existing -//! behaviour, unchanged; -//! * the raw driver methods (`*_presanitized`) do **not** — they persist what -//! they are handed, which is only safe because the gate is the sole caller. -//! -//! If someone folds redaction back into the driver, the second half fails. - -use std::sync::Arc; - -use serde_json::json; -use tempfile::TempDir; - -use crate::store::{NamespaceDocumentInput, UnifiedMemory}; -use tinymemory_api::host::NoopEmbedding; - -/// A private key body, split so this source file does not itself contain a -/// scanner-tripping literal in one piece. -const PRIVATE_KEY_BODY: &str = - "-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBgkq\n-----END PRIVATE KEY-----"; - -fn secret_doc(key: &str) -> NamespaceDocumentInput { - NamespaceDocumentInput { - namespace: "safe".to_string(), - key: key.to_string(), - title: "Bearer abcdefghijklmnop".to_string(), - content: PRIVATE_KEY_BODY.to_string(), - source_type: "doc".to_string(), - priority: "medium".to_string(), - tags: vec![], - metadata: json!({}), - category: "core".to_string(), - session_id: None, - document_id: None, - taint: crate::MemoryTaint::Internal, - } -} - -fn fresh() -> (TempDir, UnifiedMemory) { - let tmp = TempDir::new().unwrap(); - let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); - (tmp, memory) -} - -#[tokio::test] -async fn gated_upsert_document_redacts_before_the_driver_persists() { - let (_tmp, memory) = fresh(); - - memory.upsert_document(secret_doc("note")).await.unwrap(); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - assert_eq!(docs.len(), 1); - assert!( - !docs[0].content.contains("BEGIN PRIVATE KEY"), - "the gated entry point must redact private-key material, got {:?}", - docs[0].content - ); - assert!( - !docs[0].title.contains("abcdefghijklmnop"), - "the gated entry point must redact a bearer token in the title, got {:?}", - docs[0].title - ); -} - -#[tokio::test] -async fn gated_metadata_only_upsert_redacts_before_the_driver_persists() { - let (_tmp, memory) = fresh(); - - memory - .upsert_document_metadata_only(secret_doc("note")) - .await - .unwrap(); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - assert_eq!(docs.len(), 1); - assert!( - !docs[0].content.contains("BEGIN PRIVATE KEY"), - "the gated metadata-only entry point must redact, got {:?}", - docs[0].content - ); -} - -#[tokio::test] -async fn raw_driver_upsert_does_not_redact_so_the_gate_is_the_only_thing_doing_it() { - let (_tmp, memory) = fresh(); - - // Bypassing the gate deliberately: this is what proves redaction now lives - // in `write_gate` and not inside the persistence call. - memory - .upsert_document_presanitized(secret_doc("note")) - .await - .unwrap(); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - assert_eq!(docs.len(), 1); - assert!( - docs[0].content.contains("BEGIN PRIVATE KEY"), - "the raw driver method must persist its input verbatim — if this fails, redaction has \ - been folded back into the storage layer, got {:?}", - docs[0].content - ); -} - -#[tokio::test] -async fn raw_metadata_only_driver_upsert_does_not_redact() { - let (_tmp, memory) = fresh(); - - memory - .upsert_document_metadata_only_presanitized(secret_doc("note")) - .await - .unwrap(); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - assert_eq!(docs.len(), 1); - assert!( - docs[0].content.contains("BEGIN PRIVATE KEY"), - "the raw metadata-only driver method must persist its input verbatim, got {:?}", - docs[0].content - ); -} - -#[tokio::test] -async fn gate_rejects_a_secret_like_namespace_or_key_before_touching_the_driver() { - let (_tmp, memory) = fresh(); - - let mut secret_key = secret_doc("sk-1234567890123456789012345"); - secret_key.namespace = "safe".to_string(); - let err = memory.upsert_document(secret_key).await.unwrap_err(); - assert!( - err.contains("cannot contain secrets"), - "secret-like key must be refused, got {err:?}" - ); - - let mut secret_ns = secret_doc("note"); - secret_ns.namespace = "sk-1234567890123456789012345".to_string(); - let err = memory - .upsert_document_metadata_only(secret_ns) - .await - .unwrap_err(); - assert!( - err.contains("cannot contain secrets"), - "secret-like namespace must be refused, got {err:?}" - ); - - // Nothing reached storage. - assert!(memory - .load_documents_for_scope("safe") - .await - .unwrap() - .is_empty()); -} - -#[tokio::test] -async fn gate_canonicalizes_a_pii_like_key_and_keeps_the_row_addressable() { - let (_tmp, memory) = fresh(); - - memory - .upsert_document(secret_doc("ssn-123-45-6789")) - .await - .unwrap(); - - let docs = memory.load_documents_for_scope("safe").await.unwrap(); - assert_eq!(docs.len(), 1); - assert!( - !docs[0].key.contains("123-45-6789"), - "a PII-like key must be canonicalized rather than stored raw, got {:?}", - docs[0].key - ); -} diff --git a/api/Cargo.toml b/crates/tinymemory-api/Cargo.toml similarity index 100% rename from api/Cargo.toml rename to crates/tinymemory-api/Cargo.toml diff --git a/api/src/capabilities.rs b/crates/tinymemory-api/src/capabilities.rs similarity index 100% rename from api/src/capabilities.rs rename to crates/tinymemory-api/src/capabilities.rs diff --git a/api/src/capabilities_tests.rs b/crates/tinymemory-api/src/capabilities_tests.rs similarity index 100% rename from api/src/capabilities_tests.rs rename to crates/tinymemory-api/src/capabilities_tests.rs diff --git a/api/src/chunks.rs b/crates/tinymemory-api/src/chunks.rs similarity index 100% rename from api/src/chunks.rs rename to crates/tinymemory-api/src/chunks.rs diff --git a/api/src/chunks_tests.rs b/crates/tinymemory-api/src/chunks_tests.rs similarity index 100% rename from api/src/chunks_tests.rs rename to crates/tinymemory-api/src/chunks_tests.rs diff --git a/api/src/drivers.rs b/crates/tinymemory-api/src/drivers.rs similarity index 100% rename from api/src/drivers.rs rename to crates/tinymemory-api/src/drivers.rs diff --git a/api/src/error.rs b/crates/tinymemory-api/src/error.rs similarity index 100% rename from api/src/error.rs rename to crates/tinymemory-api/src/error.rs diff --git a/api/src/error_tests.rs b/crates/tinymemory-api/src/error_tests.rs similarity index 100% rename from api/src/error_tests.rs rename to crates/tinymemory-api/src/error_tests.rs diff --git a/api/src/goals.rs b/crates/tinymemory-api/src/goals.rs similarity index 100% rename from api/src/goals.rs rename to crates/tinymemory-api/src/goals.rs diff --git a/api/src/goals_tests.rs b/crates/tinymemory-api/src/goals_tests.rs similarity index 100% rename from api/src/goals_tests.rs rename to crates/tinymemory-api/src/goals_tests.rs diff --git a/api/src/health.rs b/crates/tinymemory-api/src/health.rs similarity index 100% rename from api/src/health.rs rename to crates/tinymemory-api/src/health.rs diff --git a/api/src/health_tests.rs b/crates/tinymemory-api/src/health_tests.rs similarity index 100% rename from api/src/health_tests.rs rename to crates/tinymemory-api/src/health_tests.rs diff --git a/api/src/host/cloud_providers.rs b/crates/tinymemory-api/src/host/cloud_providers.rs similarity index 100% rename from api/src/host/cloud_providers.rs rename to crates/tinymemory-api/src/host/cloud_providers.rs diff --git a/api/src/host/composio.rs b/crates/tinymemory-api/src/host/composio.rs similarity index 100% rename from api/src/host/composio.rs rename to crates/tinymemory-api/src/host/composio.rs diff --git a/api/src/host/config.rs b/crates/tinymemory-api/src/host/config.rs similarity index 100% rename from api/src/host/config.rs rename to crates/tinymemory-api/src/host/config.rs diff --git a/api/src/host/embedding_host.rs b/crates/tinymemory-api/src/host/embedding_host.rs similarity index 100% rename from api/src/host/embedding_host.rs rename to crates/tinymemory-api/src/host/embedding_host.rs diff --git a/api/src/host/embeddings.rs b/crates/tinymemory-api/src/host/embeddings.rs similarity index 100% rename from api/src/host/embeddings.rs rename to crates/tinymemory-api/src/host/embeddings.rs diff --git a/api/src/host/error_reporter.rs b/crates/tinymemory-api/src/host/error_reporter.rs similarity index 100% rename from api/src/host/error_reporter.rs rename to crates/tinymemory-api/src/host/error_reporter.rs diff --git a/api/src/host/events.rs b/crates/tinymemory-api/src/host/events.rs similarity index 100% rename from api/src/host/events.rs rename to crates/tinymemory-api/src/host/events.rs diff --git a/api/src/host/evidence.rs b/crates/tinymemory-api/src/host/evidence.rs similarity index 100% rename from api/src/host/evidence.rs rename to crates/tinymemory-api/src/host/evidence.rs diff --git a/api/src/host/local_ai.rs b/crates/tinymemory-api/src/host/local_ai.rs similarity index 100% rename from api/src/host/local_ai.rs rename to crates/tinymemory-api/src/host/local_ai.rs diff --git a/api/src/host/mod.rs b/crates/tinymemory-api/src/host/mod.rs similarity index 100% rename from api/src/host/mod.rs rename to crates/tinymemory-api/src/host/mod.rs diff --git a/api/src/host/nlp.rs b/crates/tinymemory-api/src/host/nlp.rs similarity index 100% rename from api/src/host/nlp.rs rename to crates/tinymemory-api/src/host/nlp.rs diff --git a/api/src/host/routes.rs b/crates/tinymemory-api/src/host/routes.rs similarity index 100% rename from api/src/host/routes.rs rename to crates/tinymemory-api/src/host/routes.rs diff --git a/api/src/host/scheduler_gate.rs b/crates/tinymemory-api/src/host/scheduler_gate.rs similarity index 100% rename from api/src/host/scheduler_gate.rs rename to crates/tinymemory-api/src/host/scheduler_gate.rs diff --git a/api/src/host/storage_memory.rs b/crates/tinymemory-api/src/host/storage_memory.rs similarity index 100% rename from api/src/host/storage_memory.rs rename to crates/tinymemory-api/src/host/storage_memory.rs diff --git a/api/src/host/subsystems.rs b/crates/tinymemory-api/src/host/subsystems.rs similarity index 100% rename from api/src/host/subsystems.rs rename to crates/tinymemory-api/src/host/subsystems.rs diff --git a/api/src/host/test_support.rs b/crates/tinymemory-api/src/host/test_support.rs similarity index 100% rename from api/src/host/test_support.rs rename to crates/tinymemory-api/src/host/test_support.rs diff --git a/api/src/host/usage.rs b/crates/tinymemory-api/src/host/usage.rs similarity index 100% rename from api/src/host/usage.rs rename to crates/tinymemory-api/src/host/usage.rs diff --git a/api/src/lib.rs b/crates/tinymemory-api/src/lib.rs similarity index 100% rename from api/src/lib.rs rename to crates/tinymemory-api/src/lib.rs diff --git a/api/src/mandatory/mod.rs b/crates/tinymemory-api/src/mandatory/mod.rs similarity index 100% rename from api/src/mandatory/mod.rs rename to crates/tinymemory-api/src/mandatory/mod.rs diff --git a/api/src/mandatory/provider.rs b/crates/tinymemory-api/src/mandatory/provider.rs similarity index 100% rename from api/src/mandatory/provider.rs rename to crates/tinymemory-api/src/mandatory/provider.rs diff --git a/api/src/mandatory/test.rs b/crates/tinymemory-api/src/mandatory/test.rs similarity index 100% rename from api/src/mandatory/test.rs rename to crates/tinymemory-api/src/mandatory/test.rs diff --git a/api/src/null.rs b/crates/tinymemory-api/src/null.rs similarity index 100% rename from api/src/null.rs rename to crates/tinymemory-api/src/null.rs diff --git a/api/src/null_tests.rs b/crates/tinymemory-api/src/null_tests.rs similarity index 100% rename from api/src/null_tests.rs rename to crates/tinymemory-api/src/null_tests.rs diff --git a/api/src/provider/audit.rs b/crates/tinymemory-api/src/provider/audit.rs similarity index 100% rename from api/src/provider/audit.rs rename to crates/tinymemory-api/src/provider/audit.rs diff --git a/api/src/provider/audit_tests.rs b/crates/tinymemory-api/src/provider/audit_tests.rs similarity index 100% rename from api/src/provider/audit_tests.rs rename to crates/tinymemory-api/src/provider/audit_tests.rs diff --git a/api/src/provider/chunks.rs b/crates/tinymemory-api/src/provider/chunks.rs similarity index 100% rename from api/src/provider/chunks.rs rename to crates/tinymemory-api/src/provider/chunks.rs diff --git a/api/src/provider/content.rs b/crates/tinymemory-api/src/provider/content.rs similarity index 100% rename from api/src/provider/content.rs rename to crates/tinymemory-api/src/provider/content.rs diff --git a/api/src/provider/driver.rs b/crates/tinymemory-api/src/provider/driver.rs similarity index 100% rename from api/src/provider/driver.rs rename to crates/tinymemory-api/src/provider/driver.rs diff --git a/api/src/provider/episodic.rs b/crates/tinymemory-api/src/provider/episodic.rs similarity index 100% rename from api/src/provider/episodic.rs rename to crates/tinymemory-api/src/provider/episodic.rs diff --git a/api/src/provider/knowledge.rs b/crates/tinymemory-api/src/provider/knowledge.rs similarity index 100% rename from api/src/provider/knowledge.rs rename to crates/tinymemory-api/src/provider/knowledge.rs diff --git a/api/src/provider/mandatory.rs b/crates/tinymemory-api/src/provider/mandatory.rs similarity index 100% rename from api/src/provider/mandatory.rs rename to crates/tinymemory-api/src/provider/mandatory.rs diff --git a/api/src/provider/mod.rs b/crates/tinymemory-api/src/provider/mod.rs similarity index 100% rename from api/src/provider/mod.rs rename to crates/tinymemory-api/src/provider/mod.rs diff --git a/api/src/provider/people.rs b/crates/tinymemory-api/src/provider/people.rs similarity index 100% rename from api/src/provider/people.rs rename to crates/tinymemory-api/src/provider/people.rs diff --git a/api/src/provider/profile.rs b/crates/tinymemory-api/src/provider/profile.rs similarity index 100% rename from api/src/provider/profile.rs rename to crates/tinymemory-api/src/provider/profile.rs diff --git a/api/src/provider/records.rs b/crates/tinymemory-api/src/provider/records.rs similarity index 100% rename from api/src/provider/records.rs rename to crates/tinymemory-api/src/provider/records.rs diff --git a/api/src/provider/retrieval.rs b/crates/tinymemory-api/src/provider/retrieval.rs similarity index 100% rename from api/src/provider/retrieval.rs rename to crates/tinymemory-api/src/provider/retrieval.rs diff --git a/api/src/provider/types.rs b/crates/tinymemory-api/src/provider/types.rs similarity index 100% rename from api/src/provider/types.rs rename to crates/tinymemory-api/src/provider/types.rs diff --git a/api/src/provider/types_tests.rs b/crates/tinymemory-api/src/provider/types_tests.rs similarity index 100% rename from api/src/provider/types_tests.rs rename to crates/tinymemory-api/src/provider/types_tests.rs diff --git a/api/src/recall.rs b/crates/tinymemory-api/src/recall.rs similarity index 100% rename from api/src/recall.rs rename to crates/tinymemory-api/src/recall.rs diff --git a/api/src/recall_tests.rs b/crates/tinymemory-api/src/recall_tests.rs similarity index 100% rename from api/src/recall_tests.rs rename to crates/tinymemory-api/src/recall_tests.rs diff --git a/api/src/tool_memory.rs b/crates/tinymemory-api/src/tool_memory.rs similarity index 100% rename from api/src/tool_memory.rs rename to crates/tinymemory-api/src/tool_memory.rs diff --git a/api/src/tool_memory_tests.rs b/crates/tinymemory-api/src/tool_memory_tests.rs similarity index 100% rename from api/src/tool_memory_tests.rs rename to crates/tinymemory-api/src/tool_memory_tests.rs diff --git a/api/src/traits.rs b/crates/tinymemory-api/src/traits.rs similarity index 100% rename from api/src/traits.rs rename to crates/tinymemory-api/src/traits.rs diff --git a/api/src/tree.rs b/crates/tinymemory-api/src/tree.rs similarity index 100% rename from api/src/tree.rs rename to crates/tinymemory-api/src/tree.rs diff --git a/api/src/tree_tests.rs b/crates/tinymemory-api/src/tree_tests.rs similarity index 100% rename from api/src/tree_tests.rs rename to crates/tinymemory-api/src/tree_tests.rs diff --git a/api/src/types.rs b/crates/tinymemory-api/src/types.rs similarity index 100% rename from api/src/types.rs rename to crates/tinymemory-api/src/types.rs diff --git a/api/src/types_tests.rs b/crates/tinymemory-api/src/types_tests.rs similarity index 100% rename from api/src/types_tests.rs rename to crates/tinymemory-api/src/types_tests.rs diff --git a/api/src/version.rs b/crates/tinymemory-api/src/version.rs similarity index 100% rename from api/src/version.rs rename to crates/tinymemory-api/src/version.rs diff --git a/api/src/version_tests.rs b/crates/tinymemory-api/src/version_tests.rs similarity index 100% rename from api/src/version_tests.rs rename to crates/tinymemory-api/src/version_tests.rs diff --git a/api/src/wire.rs b/crates/tinymemory-api/src/wire.rs similarity index 100% rename from api/src/wire.rs rename to crates/tinymemory-api/src/wire.rs diff --git a/api/src/wire_tests.rs b/crates/tinymemory-api/src/wire_tests.rs similarity index 100% rename from api/src/wire_tests.rs rename to crates/tinymemory-api/src/wire_tests.rs diff --git a/conformance/Cargo.toml b/crates/tinymemory-conformance/Cargo.toml similarity index 100% rename from conformance/Cargo.toml rename to crates/tinymemory-conformance/Cargo.toml diff --git a/conformance/src/lib.rs b/crates/tinymemory-conformance/src/lib.rs similarity index 100% rename from conformance/src/lib.rs rename to crates/tinymemory-conformance/src/lib.rs diff --git a/conformance/src/reference/mod.rs b/crates/tinymemory-conformance/src/reference/mod.rs similarity index 100% rename from conformance/src/reference/mod.rs rename to crates/tinymemory-conformance/src/reference/mod.rs diff --git a/conformance/src/suite/mod.rs b/crates/tinymemory-conformance/src/suite/mod.rs similarity index 100% rename from conformance/src/suite/mod.rs rename to crates/tinymemory-conformance/src/suite/mod.rs diff --git a/conformance/tests/reference_drivers.rs b/crates/tinymemory-conformance/tests/reference_drivers.rs similarity index 100% rename from conformance/tests/reference_drivers.rs rename to crates/tinymemory-conformance/tests/reference_drivers.rs diff --git a/core/Cargo.toml b/crates/tinymemory-core/Cargo.toml similarity index 100% rename from core/Cargo.toml rename to crates/tinymemory-core/Cargo.toml diff --git a/core/src/chat.rs b/crates/tinymemory-core/src/chat.rs similarity index 100% rename from core/src/chat.rs rename to crates/tinymemory-core/src/chat.rs diff --git a/core/src/chat_host.rs b/crates/tinymemory-core/src/chat_host.rs similarity index 100% rename from core/src/chat_host.rs rename to crates/tinymemory-core/src/chat_host.rs diff --git a/core/src/composio_host.rs b/crates/tinymemory-core/src/composio_host.rs similarity index 100% rename from core/src/composio_host.rs rename to crates/tinymemory-core/src/composio_host.rs diff --git a/core/src/config_loader.rs b/crates/tinymemory-core/src/config_loader.rs similarity index 100% rename from core/src/config_loader.rs rename to crates/tinymemory-core/src/config_loader.rs diff --git a/core/src/conversations/blocking.rs b/crates/tinymemory-core/src/conversations/blocking.rs similarity index 100% rename from core/src/conversations/blocking.rs rename to crates/tinymemory-core/src/conversations/blocking.rs diff --git a/core/src/conversations/blocking_tests.rs b/crates/tinymemory-core/src/conversations/blocking_tests.rs similarity index 100% rename from core/src/conversations/blocking_tests.rs rename to crates/tinymemory-core/src/conversations/blocking_tests.rs diff --git a/core/src/conversations/mod.rs b/crates/tinymemory-core/src/conversations/mod.rs similarity index 100% rename from core/src/conversations/mod.rs rename to crates/tinymemory-core/src/conversations/mod.rs diff --git a/core/src/diff/mod.rs b/crates/tinymemory-core/src/diff/mod.rs similarity index 100% rename from core/src/diff/mod.rs rename to crates/tinymemory-core/src/diff/mod.rs diff --git a/core/src/diff/ops.rs b/crates/tinymemory-core/src/diff/ops.rs similarity index 100% rename from core/src/diff/ops.rs rename to crates/tinymemory-core/src/diff/ops.rs diff --git a/core/src/diff/source.rs b/crates/tinymemory-core/src/diff/source.rs similarity index 100% rename from core/src/diff/source.rs rename to crates/tinymemory-core/src/diff/source.rs diff --git a/core/src/diff/stub.rs b/crates/tinymemory-core/src/diff/stub.rs similarity index 100% rename from core/src/diff/stub.rs rename to crates/tinymemory-core/src/diff/stub.rs diff --git a/core/src/embedding_adapter.rs b/crates/tinymemory-core/src/embedding_adapter.rs similarity index 100% rename from core/src/embedding_adapter.rs rename to crates/tinymemory-core/src/embedding_adapter.rs diff --git a/core/src/embedding_host.rs b/crates/tinymemory-core/src/embedding_host.rs similarity index 100% rename from core/src/embedding_host.rs rename to crates/tinymemory-core/src/embedding_host.rs diff --git a/core/src/engine/backend.rs b/crates/tinymemory-core/src/engine/backend.rs similarity index 100% rename from core/src/engine/backend.rs rename to crates/tinymemory-core/src/engine/backend.rs diff --git a/core/src/engine/chat.rs b/crates/tinymemory-core/src/engine/chat.rs similarity index 100% rename from core/src/engine/chat.rs rename to crates/tinymemory-core/src/engine/chat.rs diff --git a/core/src/engine/config.rs b/crates/tinymemory-core/src/engine/config.rs similarity index 100% rename from core/src/engine/config.rs rename to crates/tinymemory-core/src/engine/config.rs diff --git a/core/src/engine/embeddings.rs b/crates/tinymemory-core/src/engine/embeddings.rs similarity index 100% rename from core/src/engine/embeddings.rs rename to crates/tinymemory-core/src/engine/embeddings.rs diff --git a/core/src/engine/ingest.rs b/crates/tinymemory-core/src/engine/ingest.rs similarity index 100% rename from core/src/engine/ingest.rs rename to crates/tinymemory-core/src/engine/ingest.rs diff --git a/core/src/engine/mod.rs b/crates/tinymemory-core/src/engine/mod.rs similarity index 100% rename from core/src/engine/mod.rs rename to crates/tinymemory-core/src/engine/mod.rs diff --git a/core/src/engine/parity.rs b/crates/tinymemory-core/src/engine/parity.rs similarity index 100% rename from core/src/engine/parity.rs rename to crates/tinymemory-core/src/engine/parity.rs diff --git a/core/src/engine/persona.rs b/crates/tinymemory-core/src/engine/persona.rs similarity index 100% rename from core/src/engine/persona.rs rename to crates/tinymemory-core/src/engine/persona.rs diff --git a/core/src/engine/queue_driver.rs b/crates/tinymemory-core/src/engine/queue_driver.rs similarity index 100% rename from core/src/engine/queue_driver.rs rename to crates/tinymemory-core/src/engine/queue_driver.rs diff --git a/core/src/engine/seal.rs b/crates/tinymemory-core/src/engine/seal.rs similarity index 100% rename from core/src/engine/seal.rs rename to crates/tinymemory-core/src/engine/seal.rs diff --git a/core/src/engine/summariser.rs b/crates/tinymemory-core/src/engine/summariser.rs similarity index 100% rename from core/src/engine/summariser.rs rename to crates/tinymemory-core/src/engine/summariser.rs diff --git a/core/src/engine/sync.rs b/crates/tinymemory-core/src/engine/sync.rs similarity index 100% rename from core/src/engine/sync.rs rename to crates/tinymemory-core/src/engine/sync.rs diff --git a/core/src/events.rs b/crates/tinymemory-core/src/events.rs similarity index 100% rename from core/src/events.rs rename to crates/tinymemory-core/src/events.rs diff --git a/core/src/global.rs b/crates/tinymemory-core/src/global.rs similarity index 100% rename from core/src/global.rs rename to crates/tinymemory-core/src/global.rs diff --git a/core/src/ingest_pipeline.rs b/crates/tinymemory-core/src/ingest_pipeline.rs similarity index 100% rename from core/src/ingest_pipeline.rs rename to crates/tinymemory-core/src/ingest_pipeline.rs diff --git a/core/src/ingestion/README.md b/crates/tinymemory-core/src/ingestion/README.md similarity index 100% rename from core/src/ingestion/README.md rename to crates/tinymemory-core/src/ingestion/README.md diff --git a/core/src/ingestion/mod.rs b/crates/tinymemory-core/src/ingestion/mod.rs similarity index 100% rename from core/src/ingestion/mod.rs rename to crates/tinymemory-core/src/ingestion/mod.rs diff --git a/core/src/ingestion/queue.rs b/crates/tinymemory-core/src/ingestion/queue.rs similarity index 100% rename from core/src/ingestion/queue.rs rename to crates/tinymemory-core/src/ingestion/queue.rs diff --git a/core/src/ingestion/state.rs b/crates/tinymemory-core/src/ingestion/state.rs similarity index 100% rename from core/src/ingestion/state.rs rename to crates/tinymemory-core/src/ingestion/state.rs diff --git a/core/src/ingestion/tests.rs b/crates/tinymemory-core/src/ingestion/tests.rs similarity index 100% rename from core/src/ingestion/tests.rs rename to crates/tinymemory-core/src/ingestion/tests.rs diff --git a/core/src/learning_candidate.rs b/crates/tinymemory-core/src/learning_candidate.rs similarity index 100% rename from core/src/learning_candidate.rs rename to crates/tinymemory-core/src/learning_candidate.rs diff --git a/core/src/lib.rs b/crates/tinymemory-core/src/lib.rs similarity index 100% rename from core/src/lib.rs rename to crates/tinymemory-core/src/lib.rs diff --git a/core/src/nlp_host.rs b/crates/tinymemory-core/src/nlp_host.rs similarity index 100% rename from core/src/nlp_host.rs rename to crates/tinymemory-core/src/nlp_host.rs diff --git a/core/src/observability.rs b/crates/tinymemory-core/src/observability.rs similarity index 100% rename from core/src/observability.rs rename to crates/tinymemory-core/src/observability.rs diff --git a/core/src/people/README.md b/crates/tinymemory-core/src/people/README.md similarity index 100% rename from core/src/people/README.md rename to crates/tinymemory-core/src/people/README.md diff --git a/core/src/people/mod.rs b/crates/tinymemory-core/src/people/mod.rs similarity index 100% rename from core/src/people/mod.rs rename to crates/tinymemory-core/src/people/mod.rs diff --git a/core/src/preferences.rs b/crates/tinymemory-core/src/preferences.rs similarity index 100% rename from core/src/preferences.rs rename to crates/tinymemory-core/src/preferences.rs diff --git a/core/src/queue/README.md b/crates/tinymemory-core/src/queue/README.md similarity index 100% rename from core/src/queue/README.md rename to crates/tinymemory-core/src/queue/README.md diff --git a/core/src/queue/mod.rs b/crates/tinymemory-core/src/queue/mod.rs similarity index 100% rename from core/src/queue/mod.rs rename to crates/tinymemory-core/src/queue/mod.rs diff --git a/core/src/queue/ops.rs b/crates/tinymemory-core/src/queue/ops.rs similarity index 100% rename from core/src/queue/ops.rs rename to crates/tinymemory-core/src/queue/ops.rs diff --git a/core/src/queue/scheduler.rs b/crates/tinymemory-core/src/queue/scheduler.rs similarity index 100% rename from core/src/queue/scheduler.rs rename to crates/tinymemory-core/src/queue/scheduler.rs diff --git a/core/src/queue/store.rs b/crates/tinymemory-core/src/queue/store.rs similarity index 100% rename from core/src/queue/store.rs rename to crates/tinymemory-core/src/queue/store.rs diff --git a/core/src/queue/testing.rs b/crates/tinymemory-core/src/queue/testing.rs similarity index 100% rename from core/src/queue/testing.rs rename to crates/tinymemory-core/src/queue/testing.rs diff --git a/core/src/queue/types.rs b/crates/tinymemory-core/src/queue/types.rs similarity index 100% rename from core/src/queue/types.rs rename to crates/tinymemory-core/src/queue/types.rs diff --git a/core/src/queue/worker.rs b/crates/tinymemory-core/src/queue/worker.rs similarity index 100% rename from core/src/queue/worker.rs rename to crates/tinymemory-core/src/queue/worker.rs diff --git a/core/src/remember.rs b/crates/tinymemory-core/src/remember.rs similarity index 100% rename from core/src/remember.rs rename to crates/tinymemory-core/src/remember.rs diff --git a/core/src/rpc_models.rs b/crates/tinymemory-core/src/rpc_models.rs similarity index 100% rename from core/src/rpc_models.rs rename to crates/tinymemory-core/src/rpc_models.rs diff --git a/core/src/rpc_models_tests.rs b/crates/tinymemory-core/src/rpc_models_tests.rs similarity index 100% rename from core/src/rpc_models_tests.rs rename to crates/tinymemory-core/src/rpc_models_tests.rs diff --git a/core/src/scheduler_gate.rs b/crates/tinymemory-core/src/scheduler_gate.rs similarity index 100% rename from core/src/scheduler_gate.rs rename to crates/tinymemory-core/src/scheduler_gate.rs diff --git a/core/src/search/mod.rs b/crates/tinymemory-core/src/search/mod.rs similarity index 100% rename from core/src/search/mod.rs rename to crates/tinymemory-core/src/search/mod.rs diff --git a/core/src/shutdown.rs b/crates/tinymemory-core/src/shutdown.rs similarity index 100% rename from core/src/shutdown.rs rename to crates/tinymemory-core/src/shutdown.rs diff --git a/core/src/source_scope.rs b/crates/tinymemory-core/src/source_scope.rs similarity index 100% rename from core/src/source_scope.rs rename to crates/tinymemory-core/src/source_scope.rs diff --git a/core/src/sources/README.md b/crates/tinymemory-core/src/sources/README.md similarity index 100% rename from core/src/sources/README.md rename to crates/tinymemory-core/src/sources/README.md diff --git a/core/src/sources/mod.rs b/crates/tinymemory-core/src/sources/mod.rs similarity index 100% rename from core/src/sources/mod.rs rename to crates/tinymemory-core/src/sources/mod.rs diff --git a/core/src/sources/readers/composio.rs b/crates/tinymemory-core/src/sources/readers/composio.rs similarity index 100% rename from core/src/sources/readers/composio.rs rename to crates/tinymemory-core/src/sources/readers/composio.rs diff --git a/core/src/sources/readers/conversation.rs b/crates/tinymemory-core/src/sources/readers/conversation.rs similarity index 100% rename from core/src/sources/readers/conversation.rs rename to crates/tinymemory-core/src/sources/readers/conversation.rs diff --git a/core/src/sources/readers/folder.rs b/crates/tinymemory-core/src/sources/readers/folder.rs similarity index 100% rename from core/src/sources/readers/folder.rs rename to crates/tinymemory-core/src/sources/readers/folder.rs diff --git a/core/src/sources/readers/github.rs b/crates/tinymemory-core/src/sources/readers/github.rs similarity index 100% rename from core/src/sources/readers/github.rs rename to crates/tinymemory-core/src/sources/readers/github.rs diff --git a/core/src/sources/readers/mod.rs b/crates/tinymemory-core/src/sources/readers/mod.rs similarity index 100% rename from core/src/sources/readers/mod.rs rename to crates/tinymemory-core/src/sources/readers/mod.rs diff --git a/core/src/sources/readers/rss.rs b/crates/tinymemory-core/src/sources/readers/rss.rs similarity index 100% rename from core/src/sources/readers/rss.rs rename to crates/tinymemory-core/src/sources/readers/rss.rs diff --git a/core/src/sources/readers/twitter.rs b/crates/tinymemory-core/src/sources/readers/twitter.rs similarity index 100% rename from core/src/sources/readers/twitter.rs rename to crates/tinymemory-core/src/sources/readers/twitter.rs diff --git a/core/src/sources/readers/web_page.rs b/crates/tinymemory-core/src/sources/readers/web_page.rs similarity index 100% rename from core/src/sources/readers/web_page.rs rename to crates/tinymemory-core/src/sources/readers/web_page.rs diff --git a/core/src/sources/reconcile.rs b/crates/tinymemory-core/src/sources/reconcile.rs similarity index 100% rename from core/src/sources/reconcile.rs rename to crates/tinymemory-core/src/sources/reconcile.rs diff --git a/core/src/sources/registry.rs b/crates/tinymemory-core/src/sources/registry.rs similarity index 100% rename from core/src/sources/registry.rs rename to crates/tinymemory-core/src/sources/registry.rs diff --git a/core/src/sources/status.rs b/crates/tinymemory-core/src/sources/status.rs similarity index 100% rename from core/src/sources/status.rs rename to crates/tinymemory-core/src/sources/status.rs diff --git a/core/src/sources/sync.rs b/crates/tinymemory-core/src/sources/sync.rs similarity index 100% rename from core/src/sources/sync.rs rename to crates/tinymemory-core/src/sources/sync.rs diff --git a/core/src/sources/types.rs b/crates/tinymemory-core/src/sources/types.rs similarity index 100% rename from core/src/sources/types.rs rename to crates/tinymemory-core/src/sources/types.rs diff --git a/core/src/store/README.md b/crates/tinymemory-core/src/store/README.md similarity index 100% rename from core/src/store/README.md rename to crates/tinymemory-core/src/store/README.md diff --git a/core/src/store/chunks/connection.rs b/crates/tinymemory-core/src/store/chunks/connection.rs similarity index 100% rename from core/src/store/chunks/connection.rs rename to crates/tinymemory-core/src/store/chunks/connection.rs diff --git a/core/src/store/chunks/embeddings.rs b/crates/tinymemory-core/src/store/chunks/embeddings.rs similarity index 100% rename from core/src/store/chunks/embeddings.rs rename to crates/tinymemory-core/src/store/chunks/embeddings.rs diff --git a/core/src/store/chunks/mod.rs b/crates/tinymemory-core/src/store/chunks/mod.rs similarity index 100% rename from core/src/store/chunks/mod.rs rename to crates/tinymemory-core/src/store/chunks/mod.rs diff --git a/core/src/store/chunks/raw_refs.rs b/crates/tinymemory-core/src/store/chunks/raw_refs.rs similarity index 100% rename from core/src/store/chunks/raw_refs.rs rename to crates/tinymemory-core/src/store/chunks/raw_refs.rs diff --git a/core/src/store/chunks/semantic.rs b/crates/tinymemory-core/src/store/chunks/semantic.rs similarity index 100% rename from core/src/store/chunks/semantic.rs rename to crates/tinymemory-core/src/store/chunks/semantic.rs diff --git a/core/src/store/chunks/store.rs b/crates/tinymemory-core/src/store/chunks/store.rs similarity index 100% rename from core/src/store/chunks/store.rs rename to crates/tinymemory-core/src/store/chunks/store.rs diff --git a/core/src/store/chunks/types.rs b/crates/tinymemory-core/src/store/chunks/types.rs similarity index 100% rename from core/src/store/chunks/types.rs rename to crates/tinymemory-core/src/store/chunks/types.rs diff --git a/core/src/store/client.rs b/crates/tinymemory-core/src/store/client.rs similarity index 100% rename from core/src/store/client.rs rename to crates/tinymemory-core/src/store/client.rs diff --git a/core/src/store/client_tests.rs b/crates/tinymemory-core/src/store/client_tests.rs similarity index 100% rename from core/src/store/client_tests.rs rename to crates/tinymemory-core/src/store/client_tests.rs diff --git a/core/src/store/content/README.md b/crates/tinymemory-core/src/store/content/README.md similarity index 100% rename from core/src/store/content/README.md rename to crates/tinymemory-core/src/store/content/README.md diff --git a/core/src/store/content/mod.rs b/crates/tinymemory-core/src/store/content/mod.rs similarity index 100% rename from core/src/store/content/mod.rs rename to crates/tinymemory-core/src/store/content/mod.rs diff --git a/core/src/store/content/read.rs b/crates/tinymemory-core/src/store/content/read.rs similarity index 100% rename from core/src/store/content/read.rs rename to crates/tinymemory-core/src/store/content/read.rs diff --git a/core/src/store/content/tags.rs b/crates/tinymemory-core/src/store/content/tags.rs similarity index 100% rename from core/src/store/content/tags.rs rename to crates/tinymemory-core/src/store/content/tags.rs diff --git a/core/src/store/entities.rs b/crates/tinymemory-core/src/store/entities.rs similarity index 100% rename from core/src/store/entities.rs rename to crates/tinymemory-core/src/store/entities.rs diff --git a/core/src/store/factories.rs b/crates/tinymemory-core/src/store/factories.rs similarity index 100% rename from core/src/store/factories.rs rename to crates/tinymemory-core/src/store/factories.rs diff --git a/core/src/store/factories_provider_test.rs b/crates/tinymemory-core/src/store/factories_provider_test.rs similarity index 100% rename from core/src/store/factories_provider_test.rs rename to crates/tinymemory-core/src/store/factories_provider_test.rs diff --git a/core/src/store/kinds.rs b/crates/tinymemory-core/src/store/kinds.rs similarity index 100% rename from core/src/store/kinds.rs rename to crates/tinymemory-core/src/store/kinds.rs diff --git a/core/src/store/kv.rs b/crates/tinymemory-core/src/store/kv.rs similarity index 100% rename from core/src/store/kv.rs rename to crates/tinymemory-core/src/store/kv.rs diff --git a/core/src/store/memory_trait.rs b/crates/tinymemory-core/src/store/memory_trait.rs similarity index 100% rename from core/src/store/memory_trait.rs rename to crates/tinymemory-core/src/store/memory_trait.rs diff --git a/core/src/store/mod.rs b/crates/tinymemory-core/src/store/mod.rs similarity index 100% rename from core/src/store/mod.rs rename to crates/tinymemory-core/src/store/mod.rs diff --git a/core/src/store/namespace_store/README.md b/crates/tinymemory-core/src/store/namespace_store/README.md similarity index 100% rename from core/src/store/namespace_store/README.md rename to crates/tinymemory-core/src/store/namespace_store/README.md diff --git a/core/src/store/namespace_store/documents.rs b/crates/tinymemory-core/src/store/namespace_store/documents.rs similarity index 100% rename from core/src/store/namespace_store/documents.rs rename to crates/tinymemory-core/src/store/namespace_store/documents.rs diff --git a/core/src/store/namespace_store/events.rs b/crates/tinymemory-core/src/store/namespace_store/events.rs similarity index 100% rename from core/src/store/namespace_store/events.rs rename to crates/tinymemory-core/src/store/namespace_store/events.rs diff --git a/core/src/store/namespace_store/events_tests.rs b/crates/tinymemory-core/src/store/namespace_store/events_tests.rs similarity index 100% rename from core/src/store/namespace_store/events_tests.rs rename to crates/tinymemory-core/src/store/namespace_store/events_tests.rs diff --git a/core/src/store/namespace_store/graph.rs b/crates/tinymemory-core/src/store/namespace_store/graph.rs similarity index 100% rename from core/src/store/namespace_store/graph.rs rename to crates/tinymemory-core/src/store/namespace_store/graph.rs diff --git a/core/src/store/namespace_store/helpers.rs b/crates/tinymemory-core/src/store/namespace_store/helpers.rs similarity index 100% rename from core/src/store/namespace_store/helpers.rs rename to crates/tinymemory-core/src/store/namespace_store/helpers.rs diff --git a/core/src/store/namespace_store/init.rs b/crates/tinymemory-core/src/store/namespace_store/init.rs similarity index 100% rename from core/src/store/namespace_store/init.rs rename to crates/tinymemory-core/src/store/namespace_store/init.rs diff --git a/core/src/store/namespace_store/mod.rs b/crates/tinymemory-core/src/store/namespace_store/mod.rs similarity index 100% rename from core/src/store/namespace_store/mod.rs rename to crates/tinymemory-core/src/store/namespace_store/mod.rs diff --git a/core/src/store/namespace_store/profile.rs b/crates/tinymemory-core/src/store/namespace_store/profile.rs similarity index 100% rename from core/src/store/namespace_store/profile.rs rename to crates/tinymemory-core/src/store/namespace_store/profile.rs diff --git a/core/src/store/namespace_store/profile_tests.rs b/crates/tinymemory-core/src/store/namespace_store/profile_tests.rs similarity index 100% rename from core/src/store/namespace_store/profile_tests.rs rename to crates/tinymemory-core/src/store/namespace_store/profile_tests.rs diff --git a/core/src/store/namespace_store/query.rs b/crates/tinymemory-core/src/store/namespace_store/query.rs similarity index 100% rename from core/src/store/namespace_store/query.rs rename to crates/tinymemory-core/src/store/namespace_store/query.rs diff --git a/core/src/store/namespace_store/query_tests.rs b/crates/tinymemory-core/src/store/namespace_store/query_tests.rs similarity index 100% rename from core/src/store/namespace_store/query_tests.rs rename to crates/tinymemory-core/src/store/namespace_store/query_tests.rs diff --git a/core/src/store/namespace_store/segments.rs b/crates/tinymemory-core/src/store/namespace_store/segments.rs similarity index 100% rename from core/src/store/namespace_store/segments.rs rename to crates/tinymemory-core/src/store/namespace_store/segments.rs diff --git a/core/src/store/namespace_store/segments_tests.rs b/crates/tinymemory-core/src/store/namespace_store/segments_tests.rs similarity index 100% rename from core/src/store/namespace_store/segments_tests.rs rename to crates/tinymemory-core/src/store/namespace_store/segments_tests.rs diff --git a/core/src/store/profile_store.rs b/crates/tinymemory-core/src/store/profile_store.rs similarity index 100% rename from core/src/store/profile_store.rs rename to crates/tinymemory-core/src/store/profile_store.rs diff --git a/core/src/store/profile_store_tests.rs b/crates/tinymemory-core/src/store/profile_store_tests.rs similarity index 100% rename from core/src/store/profile_store_tests.rs rename to crates/tinymemory-core/src/store/profile_store_tests.rs diff --git a/core/src/store/recall_policy.rs b/crates/tinymemory-core/src/store/recall_policy.rs similarity index 100% rename from core/src/store/recall_policy.rs rename to crates/tinymemory-core/src/store/recall_policy.rs diff --git a/core/src/store/retrieval/mod.rs b/crates/tinymemory-core/src/store/retrieval/mod.rs similarity index 100% rename from core/src/store/retrieval/mod.rs rename to crates/tinymemory-core/src/store/retrieval/mod.rs diff --git a/core/src/store/safety/pii.rs b/crates/tinymemory-core/src/store/safety/pii.rs similarity index 100% rename from core/src/store/safety/pii.rs rename to crates/tinymemory-core/src/store/safety/pii.rs diff --git a/core/src/store/traits.rs b/crates/tinymemory-core/src/store/traits.rs similarity index 100% rename from core/src/store/traits.rs rename to crates/tinymemory-core/src/store/traits.rs diff --git a/core/src/store/trees/hotness.rs b/crates/tinymemory-core/src/store/trees/hotness.rs similarity index 100% rename from core/src/store/trees/hotness.rs rename to crates/tinymemory-core/src/store/trees/hotness.rs diff --git a/core/src/store/trees/mod.rs b/crates/tinymemory-core/src/store/trees/mod.rs similarity index 100% rename from core/src/store/trees/mod.rs rename to crates/tinymemory-core/src/store/trees/mod.rs diff --git a/core/src/store/trees/registry.rs b/crates/tinymemory-core/src/store/trees/registry.rs similarity index 100% rename from core/src/store/trees/registry.rs rename to crates/tinymemory-core/src/store/trees/registry.rs diff --git a/core/src/store/trees/store.rs b/crates/tinymemory-core/src/store/trees/store.rs similarity index 100% rename from core/src/store/trees/store.rs rename to crates/tinymemory-core/src/store/trees/store.rs diff --git a/core/src/store/trees/store_tests.rs b/crates/tinymemory-core/src/store/trees/store_tests.rs similarity index 100% rename from core/src/store/trees/store_tests.rs rename to crates/tinymemory-core/src/store/trees/store_tests.rs diff --git a/core/src/store/trees/types.rs b/crates/tinymemory-core/src/store/trees/types.rs similarity index 100% rename from core/src/store/trees/types.rs rename to crates/tinymemory-core/src/store/trees/types.rs diff --git a/core/src/store/types.rs b/crates/tinymemory-core/src/store/types.rs similarity index 100% rename from core/src/store/types.rs rename to crates/tinymemory-core/src/store/types.rs diff --git a/core/src/store/write_gate.rs b/crates/tinymemory-core/src/store/write_gate.rs similarity index 100% rename from core/src/store/write_gate.rs rename to crates/tinymemory-core/src/store/write_gate.rs diff --git a/core/src/sync/README.md b/crates/tinymemory-core/src/sync/README.md similarity index 100% rename from core/src/sync/README.md rename to crates/tinymemory-core/src/sync/README.md diff --git a/core/src/sync/audit.rs b/crates/tinymemory-core/src/sync/audit.rs similarity index 100% rename from core/src/sync/audit.rs rename to crates/tinymemory-core/src/sync/audit.rs diff --git a/core/src/sync/composio/mod.rs b/crates/tinymemory-core/src/sync/composio/mod.rs similarity index 100% rename from core/src/sync/composio/mod.rs rename to crates/tinymemory-core/src/sync/composio/mod.rs diff --git a/core/src/sync/composio/periodic.rs b/crates/tinymemory-core/src/sync/composio/periodic.rs similarity index 100% rename from core/src/sync/composio/periodic.rs rename to crates/tinymemory-core/src/sync/composio/periodic.rs diff --git a/core/src/sync/composio/providers/catalogs.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs.rs diff --git a/core/src/sync/composio/providers/catalogs_business.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_business.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs_business.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs_business.rs diff --git a/core/src/sync/composio/providers/catalogs_google.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_google.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs_google.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs_google.rs diff --git a/core/src/sync/composio/providers/catalogs_messaging.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_messaging.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs_messaging.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs_messaging.rs diff --git a/core/src/sync/composio/providers/catalogs_microsoft.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs_microsoft.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft.rs diff --git a/core/src/sync/composio/providers/catalogs_productivity.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs_productivity.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity.rs diff --git a/core/src/sync/composio/providers/catalogs_social_media.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_social_media.rs similarity index 100% rename from core/src/sync/composio/providers/catalogs_social_media.rs rename to crates/tinymemory-core/src/sync/composio/providers/catalogs_social_media.rs diff --git a/core/src/sync/composio/providers/clickup/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/clickup/mod.rs similarity index 100% rename from core/src/sync/composio/providers/clickup/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/clickup/mod.rs diff --git a/core/src/sync/composio/providers/clickup/provider.rs b/crates/tinymemory-core/src/sync/composio/providers/clickup/provider.rs similarity index 100% rename from core/src/sync/composio/providers/clickup/provider.rs rename to crates/tinymemory-core/src/sync/composio/providers/clickup/provider.rs diff --git a/core/src/sync/composio/providers/clickup/tests.rs b/crates/tinymemory-core/src/sync/composio/providers/clickup/tests.rs similarity index 100% rename from core/src/sync/composio/providers/clickup/tests.rs rename to crates/tinymemory-core/src/sync/composio/providers/clickup/tests.rs diff --git a/core/src/sync/composio/providers/clickup/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/clickup/tools.rs similarity index 100% rename from core/src/sync/composio/providers/clickup/tools.rs rename to crates/tinymemory-core/src/sync/composio/providers/clickup/tools.rs diff --git a/core/src/sync/composio/providers/descriptions.rs b/crates/tinymemory-core/src/sync/composio/providers/descriptions.rs similarity index 100% rename from core/src/sync/composio/providers/descriptions.rs rename to crates/tinymemory-core/src/sync/composio/providers/descriptions.rs diff --git a/core/src/sync/composio/providers/github/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/github/mod.rs similarity index 100% rename from core/src/sync/composio/providers/github/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/github/mod.rs diff --git a/core/src/sync/composio/providers/github/provider.rs b/crates/tinymemory-core/src/sync/composio/providers/github/provider.rs similarity index 100% rename from core/src/sync/composio/providers/github/provider.rs rename to crates/tinymemory-core/src/sync/composio/providers/github/provider.rs diff --git a/core/src/sync/composio/providers/github/tests.rs b/crates/tinymemory-core/src/sync/composio/providers/github/tests.rs similarity index 100% rename from core/src/sync/composio/providers/github/tests.rs rename to crates/tinymemory-core/src/sync/composio/providers/github/tests.rs diff --git a/core/src/sync/composio/providers/github/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/github/tools.rs similarity index 100% rename from core/src/sync/composio/providers/github/tools.rs rename to crates/tinymemory-core/src/sync/composio/providers/github/tools.rs diff --git a/core/src/sync/composio/providers/gmail/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/gmail/mod.rs similarity index 100% rename from core/src/sync/composio/providers/gmail/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/gmail/mod.rs diff --git a/core/src/sync/composio/providers/gmail/provider.rs b/crates/tinymemory-core/src/sync/composio/providers/gmail/provider.rs similarity index 100% rename from core/src/sync/composio/providers/gmail/provider.rs rename to crates/tinymemory-core/src/sync/composio/providers/gmail/provider.rs diff --git a/core/src/sync/composio/providers/gmail/tests.rs b/crates/tinymemory-core/src/sync/composio/providers/gmail/tests.rs similarity index 100% rename from core/src/sync/composio/providers/gmail/tests.rs rename to crates/tinymemory-core/src/sync/composio/providers/gmail/tests.rs diff --git a/core/src/sync/composio/providers/gmail/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/gmail/tools.rs similarity index 100% rename from core/src/sync/composio/providers/gmail/tools.rs rename to crates/tinymemory-core/src/sync/composio/providers/gmail/tools.rs diff --git a/core/src/sync/composio/providers/helpers.rs b/crates/tinymemory-core/src/sync/composio/providers/helpers.rs similarity index 100% rename from core/src/sync/composio/providers/helpers.rs rename to crates/tinymemory-core/src/sync/composio/providers/helpers.rs diff --git a/core/src/sync/composio/providers/linear/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/linear/mod.rs similarity index 100% rename from core/src/sync/composio/providers/linear/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/linear/mod.rs diff --git a/core/src/sync/composio/providers/linear/provider.rs b/crates/tinymemory-core/src/sync/composio/providers/linear/provider.rs similarity index 100% rename from core/src/sync/composio/providers/linear/provider.rs rename to crates/tinymemory-core/src/sync/composio/providers/linear/provider.rs diff --git a/core/src/sync/composio/providers/linear/tests.rs b/crates/tinymemory-core/src/sync/composio/providers/linear/tests.rs similarity index 100% rename from core/src/sync/composio/providers/linear/tests.rs rename to crates/tinymemory-core/src/sync/composio/providers/linear/tests.rs diff --git a/core/src/sync/composio/providers/linear/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/linear/tools.rs similarity index 100% rename from core/src/sync/composio/providers/linear/tools.rs rename to crates/tinymemory-core/src/sync/composio/providers/linear/tools.rs diff --git a/core/src/sync/composio/providers/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/mod.rs similarity index 100% rename from core/src/sync/composio/providers/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/mod.rs diff --git a/core/src/sync/composio/providers/notion/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/notion/mod.rs similarity index 100% rename from core/src/sync/composio/providers/notion/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/notion/mod.rs diff --git a/core/src/sync/composio/providers/notion/provider.rs b/crates/tinymemory-core/src/sync/composio/providers/notion/provider.rs similarity index 100% rename from core/src/sync/composio/providers/notion/provider.rs rename to crates/tinymemory-core/src/sync/composio/providers/notion/provider.rs diff --git a/core/src/sync/composio/providers/notion/tests.rs b/crates/tinymemory-core/src/sync/composio/providers/notion/tests.rs similarity index 100% rename from core/src/sync/composio/providers/notion/tests.rs rename to crates/tinymemory-core/src/sync/composio/providers/notion/tests.rs diff --git a/core/src/sync/composio/providers/notion/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/notion/tools.rs similarity index 100% rename from core/src/sync/composio/providers/notion/tools.rs rename to crates/tinymemory-core/src/sync/composio/providers/notion/tools.rs diff --git a/core/src/sync/composio/providers/profile.rs b/crates/tinymemory-core/src/sync/composio/providers/profile.rs similarity index 100% rename from core/src/sync/composio/providers/profile.rs rename to crates/tinymemory-core/src/sync/composio/providers/profile.rs diff --git a/core/src/sync/composio/providers/profile_md.rs b/crates/tinymemory-core/src/sync/composio/providers/profile_md.rs similarity index 100% rename from core/src/sync/composio/providers/profile_md.rs rename to crates/tinymemory-core/src/sync/composio/providers/profile_md.rs diff --git a/core/src/sync/composio/providers/registry.rs b/crates/tinymemory-core/src/sync/composio/providers/registry.rs similarity index 100% rename from core/src/sync/composio/providers/registry.rs rename to crates/tinymemory-core/src/sync/composio/providers/registry.rs diff --git a/core/src/sync/composio/providers/scope_lookup.rs b/crates/tinymemory-core/src/sync/composio/providers/scope_lookup.rs similarity index 100% rename from core/src/sync/composio/providers/scope_lookup.rs rename to crates/tinymemory-core/src/sync/composio/providers/scope_lookup.rs diff --git a/core/src/sync/composio/providers/slack/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/slack/mod.rs similarity index 100% rename from core/src/sync/composio/providers/slack/mod.rs rename to crates/tinymemory-core/src/sync/composio/providers/slack/mod.rs diff --git a/core/src/sync/composio/providers/slack/provider.rs b/crates/tinymemory-core/src/sync/composio/providers/slack/provider.rs similarity index 100% rename from core/src/sync/composio/providers/slack/provider.rs rename to crates/tinymemory-core/src/sync/composio/providers/slack/provider.rs diff --git a/core/src/sync/composio/providers/slack/types.rs b/crates/tinymemory-core/src/sync/composio/providers/slack/types.rs similarity index 100% rename from core/src/sync/composio/providers/slack/types.rs rename to crates/tinymemory-core/src/sync/composio/providers/slack/types.rs diff --git a/core/src/sync/composio/providers/sync_state.rs b/crates/tinymemory-core/src/sync/composio/providers/sync_state.rs similarity index 100% rename from core/src/sync/composio/providers/sync_state.rs rename to crates/tinymemory-core/src/sync/composio/providers/sync_state.rs diff --git a/core/src/sync/composio/providers/tool_scope.rs b/crates/tinymemory-core/src/sync/composio/providers/tool_scope.rs similarity index 100% rename from core/src/sync/composio/providers/tool_scope.rs rename to crates/tinymemory-core/src/sync/composio/providers/tool_scope.rs diff --git a/core/src/sync/composio/providers/traits.rs b/crates/tinymemory-core/src/sync/composio/providers/traits.rs similarity index 100% rename from core/src/sync/composio/providers/traits.rs rename to crates/tinymemory-core/src/sync/composio/providers/traits.rs diff --git a/core/src/sync/composio/providers/types.rs b/crates/tinymemory-core/src/sync/composio/providers/types.rs similarity index 100% rename from core/src/sync/composio/providers/types.rs rename to crates/tinymemory-core/src/sync/composio/providers/types.rs diff --git a/core/src/sync/composio/providers/user_scopes.rs b/crates/tinymemory-core/src/sync/composio/providers/user_scopes.rs similarity index 100% rename from core/src/sync/composio/providers/user_scopes.rs rename to crates/tinymemory-core/src/sync/composio/providers/user_scopes.rs diff --git a/core/src/sync/composio/providers/user_scopes_tests.rs b/crates/tinymemory-core/src/sync/composio/providers/user_scopes_tests.rs similarity index 100% rename from core/src/sync/composio/providers/user_scopes_tests.rs rename to crates/tinymemory-core/src/sync/composio/providers/user_scopes_tests.rs diff --git a/core/src/sync/mcp/mod.rs b/crates/tinymemory-core/src/sync/mcp/mod.rs similarity index 100% rename from core/src/sync/mcp/mod.rs rename to crates/tinymemory-core/src/sync/mcp/mod.rs diff --git a/core/src/sync/mod.rs b/crates/tinymemory-core/src/sync/mod.rs similarity index 100% rename from core/src/sync/mod.rs rename to crates/tinymemory-core/src/sync/mod.rs diff --git a/core/src/sync/pipelines/composio/client.rs b/crates/tinymemory-core/src/sync/pipelines/composio/client.rs similarity index 100% rename from core/src/sync/pipelines/composio/client.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/client.rs diff --git a/core/src/sync/pipelines/composio/connect.rs b/crates/tinymemory-core/src/sync/pipelines/composio/connect.rs similarity index 100% rename from core/src/sync/pipelines/composio/connect.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/connect.rs diff --git a/core/src/sync/pipelines/composio/connect_tests.rs b/crates/tinymemory-core/src/sync/pipelines/composio/connect_tests.rs similarity index 100% rename from core/src/sync/pipelines/composio/connect_tests.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/connect_tests.rs diff --git a/core/src/sync/pipelines/composio/gmail.rs b/crates/tinymemory-core/src/sync/pipelines/composio/gmail.rs similarity index 100% rename from core/src/sync/pipelines/composio/gmail.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/gmail.rs diff --git a/core/src/sync/pipelines/composio/gmail_tests.rs b/crates/tinymemory-core/src/sync/pipelines/composio/gmail_tests.rs similarity index 100% rename from core/src/sync/pipelines/composio/gmail_tests.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/gmail_tests.rs diff --git a/core/src/sync/pipelines/composio/mod.rs b/crates/tinymemory-core/src/sync/pipelines/composio/mod.rs similarity index 100% rename from core/src/sync/pipelines/composio/mod.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/mod.rs diff --git a/core/src/sync/pipelines/composio/orchestrator.rs b/crates/tinymemory-core/src/sync/pipelines/composio/orchestrator.rs similarity index 100% rename from core/src/sync/pipelines/composio/orchestrator.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/orchestrator.rs diff --git a/core/src/sync/pipelines/composio/orchestrator_tests.rs b/crates/tinymemory-core/src/sync/pipelines/composio/orchestrator_tests.rs similarity index 100% rename from core/src/sync/pipelines/composio/orchestrator_tests.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/orchestrator_tests.rs diff --git a/core/src/sync/pipelines/composio/page_size.rs b/crates/tinymemory-core/src/sync/pipelines/composio/page_size.rs similarity index 100% rename from core/src/sync/pipelines/composio/page_size.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/page_size.rs diff --git a/core/src/sync/pipelines/composio/page_size_tests.rs b/crates/tinymemory-core/src/sync/pipelines/composio/page_size_tests.rs similarity index 100% rename from core/src/sync/pipelines/composio/page_size_tests.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/page_size_tests.rs diff --git a/core/src/sync/pipelines/composio/providers/clickup.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/clickup.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/clickup.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/clickup.rs diff --git a/core/src/sync/pipelines/composio/providers/common.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/common.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/common.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/common.rs diff --git a/core/src/sync/pipelines/composio/providers/github.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/github.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/github.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/github.rs diff --git a/core/src/sync/pipelines/composio/providers/google_calendar.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/google_calendar.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/google_calendar.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/google_calendar.rs diff --git a/core/src/sync/pipelines/composio/providers/google_docs.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/google_docs.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/google_docs.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/google_docs.rs diff --git a/core/src/sync/pipelines/composio/providers/google_drive.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/google_drive.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/google_drive.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/google_drive.rs diff --git a/core/src/sync/pipelines/composio/providers/google_sheets.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/google_sheets.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/google_sheets.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/google_sheets.rs diff --git a/core/src/sync/pipelines/composio/providers/linear.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/linear.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/linear.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/linear.rs diff --git a/core/src/sync/pipelines/composio/providers/mod.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/mod.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/mod.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/mod.rs diff --git a/core/src/sync/pipelines/composio/providers/notion.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/notion.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/notion.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/notion.rs diff --git a/core/src/sync/pipelines/composio/providers/outlook.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/outlook.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/outlook.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/outlook.rs diff --git a/core/src/sync/pipelines/composio/providers/slack.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/slack.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/slack.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/slack.rs diff --git a/core/src/sync/pipelines/composio/providers/slack_parse.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/slack_parse.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/slack_parse.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/slack_parse.rs diff --git a/core/src/sync/pipelines/composio/providers/todoist.rs b/crates/tinymemory-core/src/sync/pipelines/composio/providers/todoist.rs similarity index 100% rename from core/src/sync/pipelines/composio/providers/todoist.rs rename to crates/tinymemory-core/src/sync/pipelines/composio/providers/todoist.rs diff --git a/core/src/sync/pipelines/dispatcher.rs b/crates/tinymemory-core/src/sync/pipelines/dispatcher.rs similarity index 100% rename from core/src/sync/pipelines/dispatcher.rs rename to crates/tinymemory-core/src/sync/pipelines/dispatcher.rs diff --git a/core/src/sync/pipelines/dispatcher_tests.rs b/crates/tinymemory-core/src/sync/pipelines/dispatcher_tests.rs similarity index 100% rename from core/src/sync/pipelines/dispatcher_tests.rs rename to crates/tinymemory-core/src/sync/pipelines/dispatcher_tests.rs diff --git a/core/src/sync/pipelines/host.rs b/crates/tinymemory-core/src/sync/pipelines/host.rs similarity index 100% rename from core/src/sync/pipelines/host.rs rename to crates/tinymemory-core/src/sync/pipelines/host.rs diff --git a/core/src/sync/pipelines/mod.rs b/crates/tinymemory-core/src/sync/pipelines/mod.rs similarity index 100% rename from core/src/sync/pipelines/mod.rs rename to crates/tinymemory-core/src/sync/pipelines/mod.rs diff --git a/core/src/sync/pipelines/traits.rs b/crates/tinymemory-core/src/sync/pipelines/traits.rs similarity index 100% rename from core/src/sync/pipelines/traits.rs rename to crates/tinymemory-core/src/sync/pipelines/traits.rs diff --git a/core/src/sync/sync_status/mod.rs b/crates/tinymemory-core/src/sync/sync_status/mod.rs similarity index 100% rename from core/src/sync/sync_status/mod.rs rename to crates/tinymemory-core/src/sync/sync_status/mod.rs diff --git a/core/src/sync/workspace/mod.rs b/crates/tinymemory-core/src/sync/workspace/mod.rs similarity index 100% rename from core/src/sync/workspace/mod.rs rename to crates/tinymemory-core/src/sync/workspace/mod.rs diff --git a/core/src/sync/workspace/periodic.rs b/crates/tinymemory-core/src/sync/workspace/periodic.rs similarity index 100% rename from core/src/sync/workspace/periodic.rs rename to crates/tinymemory-core/src/sync/workspace/periodic.rs diff --git a/core/src/sync/workspace/watcher.rs b/crates/tinymemory-core/src/sync/workspace/watcher.rs similarity index 100% rename from core/src/sync/workspace/watcher.rs rename to crates/tinymemory-core/src/sync/workspace/watcher.rs diff --git a/core/src/sync_events.rs b/crates/tinymemory-core/src/sync_events.rs similarity index 100% rename from core/src/sync_events.rs rename to crates/tinymemory-core/src/sync_events.rs diff --git a/core/src/test_env_lock.rs b/crates/tinymemory-core/src/test_env_lock.rs similarity index 100% rename from core/src/test_env_lock.rs rename to crates/tinymemory-core/src/test_env_lock.rs diff --git a/core/src/test_seams.rs b/crates/tinymemory-core/src/test_seams.rs similarity index 100% rename from core/src/test_seams.rs rename to crates/tinymemory-core/src/test_seams.rs diff --git a/core/src/thread_context.rs b/crates/tinymemory-core/src/thread_context.rs similarity index 100% rename from core/src/thread_context.rs rename to crates/tinymemory-core/src/thread_context.rs diff --git a/core/src/tool_memory/README.md b/crates/tinymemory-core/src/tool_memory/README.md similarity index 100% rename from core/src/tool_memory/README.md rename to crates/tinymemory-core/src/tool_memory/README.md diff --git a/core/src/tool_memory/mod.rs b/crates/tinymemory-core/src/tool_memory/mod.rs similarity index 100% rename from core/src/tool_memory/mod.rs rename to crates/tinymemory-core/src/tool_memory/mod.rs diff --git a/core/src/tool_memory/store.rs b/crates/tinymemory-core/src/tool_memory/store.rs similarity index 100% rename from core/src/tool_memory/store.rs rename to crates/tinymemory-core/src/tool_memory/store.rs diff --git a/core/src/tool_memory/test_helpers.rs b/crates/tinymemory-core/src/tool_memory/test_helpers.rs similarity index 100% rename from core/src/tool_memory/test_helpers.rs rename to crates/tinymemory-core/src/tool_memory/test_helpers.rs diff --git a/core/src/traits.rs b/crates/tinymemory-core/src/traits.rs similarity index 100% rename from core/src/traits.rs rename to crates/tinymemory-core/src/traits.rs diff --git a/core/src/tree/README.md b/crates/tinymemory-core/src/tree/README.md similarity index 100% rename from core/src/tree/README.md rename to crates/tinymemory-core/src/tree/README.md diff --git a/core/src/tree/graph/bfs.rs b/crates/tinymemory-core/src/tree/graph/bfs.rs similarity index 100% rename from core/src/tree/graph/bfs.rs rename to crates/tinymemory-core/src/tree/graph/bfs.rs diff --git a/core/src/tree/graph/mod.rs b/crates/tinymemory-core/src/tree/graph/mod.rs similarity index 100% rename from core/src/tree/graph/mod.rs rename to crates/tinymemory-core/src/tree/graph/mod.rs diff --git a/core/src/tree/graph/store.rs b/crates/tinymemory-core/src/tree/graph/store.rs similarity index 100% rename from core/src/tree/graph/store.rs rename to crates/tinymemory-core/src/tree/graph/store.rs diff --git a/core/src/tree/health/doctor.rs b/crates/tinymemory-core/src/tree/health/doctor.rs similarity index 100% rename from core/src/tree/health/doctor.rs rename to crates/tinymemory-core/src/tree/health/doctor.rs diff --git a/core/src/tree/health/mod.rs b/crates/tinymemory-core/src/tree/health/mod.rs similarity index 100% rename from core/src/tree/health/mod.rs rename to crates/tinymemory-core/src/tree/health/mod.rs diff --git a/core/src/tree/health/user_error.rs b/crates/tinymemory-core/src/tree/health/user_error.rs similarity index 100% rename from core/src/tree/health/user_error.rs rename to crates/tinymemory-core/src/tree/health/user_error.rs diff --git a/core/src/tree/ingest.rs b/crates/tinymemory-core/src/tree/ingest.rs similarity index 100% rename from core/src/tree/ingest.rs rename to crates/tinymemory-core/src/tree/ingest.rs diff --git a/core/src/tree/mod.rs b/crates/tinymemory-core/src/tree/mod.rs similarity index 100% rename from core/src/tree/mod.rs rename to crates/tinymemory-core/src/tree/mod.rs diff --git a/core/src/tree/nlp/mod.rs b/crates/tinymemory-core/src/tree/nlp/mod.rs similarity index 100% rename from core/src/tree/nlp/mod.rs rename to crates/tinymemory-core/src/tree/nlp/mod.rs diff --git a/core/src/tree/retrieval/README.md b/crates/tinymemory-core/src/tree/retrieval/README.md similarity index 100% rename from core/src/tree/retrieval/README.md rename to crates/tinymemory-core/src/tree/retrieval/README.md diff --git a/core/src/tree/retrieval/benchmarks.rs b/crates/tinymemory-core/src/tree/retrieval/benchmarks.rs similarity index 100% rename from core/src/tree/retrieval/benchmarks.rs rename to crates/tinymemory-core/src/tree/retrieval/benchmarks.rs diff --git a/core/src/tree/retrieval/cover.rs b/crates/tinymemory-core/src/tree/retrieval/cover.rs similarity index 100% rename from core/src/tree/retrieval/cover.rs rename to crates/tinymemory-core/src/tree/retrieval/cover.rs diff --git a/core/src/tree/retrieval/drill_down.rs b/crates/tinymemory-core/src/tree/retrieval/drill_down.rs similarity index 100% rename from core/src/tree/retrieval/drill_down.rs rename to crates/tinymemory-core/src/tree/retrieval/drill_down.rs diff --git a/core/src/tree/retrieval/engine.rs b/crates/tinymemory-core/src/tree/retrieval/engine.rs similarity index 100% rename from core/src/tree/retrieval/engine.rs rename to crates/tinymemory-core/src/tree/retrieval/engine.rs diff --git a/core/src/tree/retrieval/fast.rs b/crates/tinymemory-core/src/tree/retrieval/fast.rs similarity index 100% rename from core/src/tree/retrieval/fast.rs rename to crates/tinymemory-core/src/tree/retrieval/fast.rs diff --git a/core/src/tree/retrieval/fetch.rs b/crates/tinymemory-core/src/tree/retrieval/fetch.rs similarity index 100% rename from core/src/tree/retrieval/fetch.rs rename to crates/tinymemory-core/src/tree/retrieval/fetch.rs diff --git a/core/src/tree/retrieval/integration_tests.rs b/crates/tinymemory-core/src/tree/retrieval/integration_tests.rs similarity index 100% rename from core/src/tree/retrieval/integration_tests.rs rename to crates/tinymemory-core/src/tree/retrieval/integration_tests.rs diff --git a/core/src/tree/retrieval/mod.rs b/crates/tinymemory-core/src/tree/retrieval/mod.rs similarity index 100% rename from core/src/tree/retrieval/mod.rs rename to crates/tinymemory-core/src/tree/retrieval/mod.rs diff --git a/core/src/tree/retrieval/search.rs b/crates/tinymemory-core/src/tree/retrieval/search.rs similarity index 100% rename from core/src/tree/retrieval/search.rs rename to crates/tinymemory-core/src/tree/retrieval/search.rs diff --git a/core/src/tree/retrieval/source.rs b/crates/tinymemory-core/src/tree/retrieval/source.rs similarity index 100% rename from core/src/tree/retrieval/source.rs rename to crates/tinymemory-core/src/tree/retrieval/source.rs diff --git a/core/src/tree/retrieval/source_scope_tests.rs b/crates/tinymemory-core/src/tree/retrieval/source_scope_tests.rs similarity index 100% rename from core/src/tree/retrieval/source_scope_tests.rs rename to crates/tinymemory-core/src/tree/retrieval/source_scope_tests.rs diff --git a/core/src/tree/retrieval/types.rs b/crates/tinymemory-core/src/tree/retrieval/types.rs similarity index 100% rename from core/src/tree/retrieval/types.rs rename to crates/tinymemory-core/src/tree/retrieval/types.rs diff --git a/core/src/tree/score/README.md b/crates/tinymemory-core/src/tree/score/README.md similarity index 100% rename from core/src/tree/score/README.md rename to crates/tinymemory-core/src/tree/score/README.md diff --git a/core/src/tree/score/embed/README.md b/crates/tinymemory-core/src/tree/score/embed/README.md similarity index 100% rename from core/src/tree/score/embed/README.md rename to crates/tinymemory-core/src/tree/score/embed/README.md diff --git a/core/src/tree/score/embed/factory.rs b/crates/tinymemory-core/src/tree/score/embed/factory.rs similarity index 100% rename from core/src/tree/score/embed/factory.rs rename to crates/tinymemory-core/src/tree/score/embed/factory.rs diff --git a/core/src/tree/score/embed/inert.rs b/crates/tinymemory-core/src/tree/score/embed/inert.rs similarity index 100% rename from core/src/tree/score/embed/inert.rs rename to crates/tinymemory-core/src/tree/score/embed/inert.rs diff --git a/core/src/tree/score/embed/mod.rs b/crates/tinymemory-core/src/tree/score/embed/mod.rs similarity index 100% rename from core/src/tree/score/embed/mod.rs rename to crates/tinymemory-core/src/tree/score/embed/mod.rs diff --git a/core/src/tree/score/embed/openai_compat.rs b/crates/tinymemory-core/src/tree/score/embed/openai_compat.rs similarity index 100% rename from core/src/tree/score/embed/openai_compat.rs rename to crates/tinymemory-core/src/tree/score/embed/openai_compat.rs diff --git a/core/src/tree/score/extract/README.md b/crates/tinymemory-core/src/tree/score/extract/README.md similarity index 100% rename from core/src/tree/score/extract/README.md rename to crates/tinymemory-core/src/tree/score/extract/README.md diff --git a/core/src/tree/score/extract/mod.rs b/crates/tinymemory-core/src/tree/score/extract/mod.rs similarity index 100% rename from core/src/tree/score/extract/mod.rs rename to crates/tinymemory-core/src/tree/score/extract/mod.rs diff --git a/core/src/tree/score/mod.rs b/crates/tinymemory-core/src/tree/score/mod.rs similarity index 100% rename from core/src/tree/score/mod.rs rename to crates/tinymemory-core/src/tree/score/mod.rs diff --git a/core/src/tree/score/signals/README.md b/crates/tinymemory-core/src/tree/score/signals/README.md similarity index 100% rename from core/src/tree/score/signals/README.md rename to crates/tinymemory-core/src/tree/score/signals/README.md diff --git a/core/src/tree/score/store.rs b/crates/tinymemory-core/src/tree/score/store.rs similarity index 100% rename from core/src/tree/score/store.rs rename to crates/tinymemory-core/src/tree/score/store.rs diff --git a/core/src/tree/summarise.rs b/crates/tinymemory-core/src/tree/summarise.rs similarity index 100% rename from core/src/tree/summarise.rs rename to crates/tinymemory-core/src/tree/summarise.rs diff --git a/core/src/tree/tree/bucket_seal.rs b/crates/tinymemory-core/src/tree/tree/bucket_seal.rs similarity index 100% rename from core/src/tree/tree/bucket_seal.rs rename to crates/tinymemory-core/src/tree/tree/bucket_seal.rs diff --git a/core/src/tree/tree/factory.rs b/crates/tinymemory-core/src/tree/tree/factory.rs similarity index 100% rename from core/src/tree/tree/factory.rs rename to crates/tinymemory-core/src/tree/tree/factory.rs diff --git a/core/src/tree/tree/flush.rs b/crates/tinymemory-core/src/tree/tree/flush.rs similarity index 100% rename from core/src/tree/tree/flush.rs rename to crates/tinymemory-core/src/tree/tree/flush.rs diff --git a/core/src/tree/tree/mod.rs b/crates/tinymemory-core/src/tree/tree/mod.rs similarity index 100% rename from core/src/tree/tree/mod.rs rename to crates/tinymemory-core/src/tree/tree/mod.rs diff --git a/core/src/tree/tree/registry.rs b/crates/tinymemory-core/src/tree/tree/registry.rs similarity index 100% rename from core/src/tree/tree/registry.rs rename to crates/tinymemory-core/src/tree/tree/registry.rs diff --git a/core/src/tree/tree_runtime/engine.rs b/crates/tinymemory-core/src/tree/tree_runtime/engine.rs similarity index 100% rename from core/src/tree/tree_runtime/engine.rs rename to crates/tinymemory-core/src/tree/tree_runtime/engine.rs diff --git a/core/src/tree/tree_runtime/mod.rs b/crates/tinymemory-core/src/tree/tree_runtime/mod.rs similarity index 100% rename from core/src/tree/tree_runtime/mod.rs rename to crates/tinymemory-core/src/tree/tree_runtime/mod.rs diff --git a/core/src/tree/tree_runtime/store.rs b/crates/tinymemory-core/src/tree/tree_runtime/store.rs similarity index 100% rename from core/src/tree/tree_runtime/store.rs rename to crates/tinymemory-core/src/tree/tree_runtime/store.rs diff --git a/core/src/tree_policy.rs b/crates/tinymemory-core/src/tree_policy.rs similarity index 100% rename from core/src/tree_policy.rs rename to crates/tinymemory-core/src/tree_policy.rs diff --git a/core/src/tree_source/file.rs b/crates/tinymemory-core/src/tree_source/file.rs similarity index 100% rename from core/src/tree_source/file.rs rename to crates/tinymemory-core/src/tree_source/file.rs diff --git a/core/src/tree_source/mod.rs b/crates/tinymemory-core/src/tree_source/mod.rs similarity index 100% rename from core/src/tree_source/mod.rs rename to crates/tinymemory-core/src/tree_source/mod.rs diff --git a/core/src/tree_source/registry.rs b/crates/tinymemory-core/src/tree_source/registry.rs similarity index 100% rename from core/src/tree_source/registry.rs rename to crates/tinymemory-core/src/tree_source/registry.rs diff --git a/core/src/util/README.md b/crates/tinymemory-core/src/util/README.md similarity index 100% rename from core/src/util/README.md rename to crates/tinymemory-core/src/util/README.md diff --git a/core/src/util/mod.rs b/crates/tinymemory-core/src/util/mod.rs similarity index 100% rename from core/src/util/mod.rs rename to crates/tinymemory-core/src/util/mod.rs diff --git a/core/src/util/redact.rs b/crates/tinymemory-core/src/util/redact.rs similarity index 100% rename from core/src/util/redact.rs rename to crates/tinymemory-core/src/util/redact.rs diff --git a/core/tests/composio_gmail_non_tinycortex_e2e.rs b/crates/tinymemory-core/tests/composio_gmail_non_tinycortex_e2e.rs similarity index 100% rename from core/tests/composio_gmail_non_tinycortex_e2e.rs rename to crates/tinymemory-core/tests/composio_gmail_non_tinycortex_e2e.rs diff --git a/core/tests/fixtures/ingestion/README.md b/crates/tinymemory-core/tests/fixtures/ingestion/README.md similarity index 100% rename from core/tests/fixtures/ingestion/README.md rename to crates/tinymemory-core/tests/fixtures/ingestion/README.md diff --git a/core/tests/fixtures/ingestion/gmail_thread_example.txt b/crates/tinymemory-core/tests/fixtures/ingestion/gmail_thread_example.txt similarity index 100% rename from core/tests/fixtures/ingestion/gmail_thread_example.txt rename to crates/tinymemory-core/tests/fixtures/ingestion/gmail_thread_example.txt diff --git a/core/tests/fixtures/ingestion/notion_page_example.txt b/crates/tinymemory-core/tests/fixtures/ingestion/notion_page_example.txt similarity index 100% rename from core/tests/fixtures/ingestion/notion_page_example.txt rename to crates/tinymemory-core/tests/fixtures/ingestion/notion_page_example.txt diff --git a/adapters/remote/Cargo.toml b/crates/tinymemory-remote/Cargo.toml similarity index 100% rename from adapters/remote/Cargo.toml rename to crates/tinymemory-remote/Cargo.toml diff --git a/adapters/remote/examples/conformance.rs b/crates/tinymemory-remote/examples/conformance.rs similarity index 100% rename from adapters/remote/examples/conformance.rs rename to crates/tinymemory-remote/examples/conformance.rs diff --git a/adapters/remote/src/cognee.rs b/crates/tinymemory-remote/src/cognee.rs similarity index 100% rename from adapters/remote/src/cognee.rs rename to crates/tinymemory-remote/src/cognee.rs diff --git a/adapters/remote/src/cognee_test.rs b/crates/tinymemory-remote/src/cognee_test.rs similarity index 100% rename from adapters/remote/src/cognee_test.rs rename to crates/tinymemory-remote/src/cognee_test.rs diff --git a/adapters/remote/src/common.rs b/crates/tinymemory-remote/src/common.rs similarity index 100% rename from adapters/remote/src/common.rs rename to crates/tinymemory-remote/src/common.rs diff --git a/adapters/remote/src/conformance_test.rs b/crates/tinymemory-remote/src/conformance_test.rs similarity index 100% rename from adapters/remote/src/conformance_test.rs rename to crates/tinymemory-remote/src/conformance_test.rs diff --git a/adapters/remote/src/failure_test.rs b/crates/tinymemory-remote/src/failure_test.rs similarity index 100% rename from adapters/remote/src/failure_test.rs rename to crates/tinymemory-remote/src/failure_test.rs diff --git a/adapters/remote/src/lib.rs b/crates/tinymemory-remote/src/lib.rs similarity index 100% rename from adapters/remote/src/lib.rs rename to crates/tinymemory-remote/src/lib.rs diff --git a/adapters/remote/src/mem0.rs b/crates/tinymemory-remote/src/mem0.rs similarity index 100% rename from adapters/remote/src/mem0.rs rename to crates/tinymemory-remote/src/mem0.rs diff --git a/adapters/remote/src/mem0_test.rs b/crates/tinymemory-remote/src/mem0_test.rs similarity index 100% rename from adapters/remote/src/mem0_test.rs rename to crates/tinymemory-remote/src/mem0_test.rs diff --git a/adapters/remote/src/supermemory.rs b/crates/tinymemory-remote/src/supermemory.rs similarity index 100% rename from adapters/remote/src/supermemory.rs rename to crates/tinymemory-remote/src/supermemory.rs diff --git a/adapters/remote/src/supermemory_test.rs b/crates/tinymemory-remote/src/supermemory_test.rs similarity index 100% rename from adapters/remote/src/supermemory_test.rs rename to crates/tinymemory-remote/src/supermemory_test.rs diff --git a/sources/Cargo.toml b/crates/tinymemory-sources/Cargo.toml similarity index 100% rename from sources/Cargo.toml rename to crates/tinymemory-sources/Cargo.toml diff --git a/sources/src/lib.rs b/crates/tinymemory-sources/src/lib.rs similarity index 100% rename from sources/src/lib.rs rename to crates/tinymemory-sources/src/lib.rs diff --git a/sources/src/raw_kind.rs b/crates/tinymemory-sources/src/raw_kind.rs similarity index 100% rename from sources/src/raw_kind.rs rename to crates/tinymemory-sources/src/raw_kind.rs diff --git a/sources/src/readers/conversation.rs b/crates/tinymemory-sources/src/readers/conversation.rs similarity index 100% rename from sources/src/readers/conversation.rs rename to crates/tinymemory-sources/src/readers/conversation.rs diff --git a/sources/src/readers/conversation_tests.rs b/crates/tinymemory-sources/src/readers/conversation_tests.rs similarity index 100% rename from sources/src/readers/conversation_tests.rs rename to crates/tinymemory-sources/src/readers/conversation_tests.rs diff --git a/sources/src/readers/folder.rs b/crates/tinymemory-sources/src/readers/folder.rs similarity index 100% rename from sources/src/readers/folder.rs rename to crates/tinymemory-sources/src/readers/folder.rs diff --git a/sources/src/readers/folder_tests.rs b/crates/tinymemory-sources/src/readers/folder_tests.rs similarity index 100% rename from sources/src/readers/folder_tests.rs rename to crates/tinymemory-sources/src/readers/folder_tests.rs diff --git a/sources/src/readers/github.rs b/crates/tinymemory-sources/src/readers/github.rs similarity index 100% rename from sources/src/readers/github.rs rename to crates/tinymemory-sources/src/readers/github.rs diff --git a/sources/src/readers/github/api.rs b/crates/tinymemory-sources/src/readers/github/api.rs similarity index 100% rename from sources/src/readers/github/api.rs rename to crates/tinymemory-sources/src/readers/github/api.rs diff --git a/sources/src/readers/github/git.rs b/crates/tinymemory-sources/src/readers/github/git.rs similarity index 100% rename from sources/src/readers/github/git.rs rename to crates/tinymemory-sources/src/readers/github/git.rs diff --git a/sources/src/readers/github/git_tests.rs b/crates/tinymemory-sources/src/readers/github/git_tests.rs similarity index 100% rename from sources/src/readers/github/git_tests.rs rename to crates/tinymemory-sources/src/readers/github/git_tests.rs diff --git a/sources/src/readers/github/issues.rs b/crates/tinymemory-sources/src/readers/github/issues.rs similarity index 100% rename from sources/src/readers/github/issues.rs rename to crates/tinymemory-sources/src/readers/github/issues.rs diff --git a/sources/src/readers/github/types.rs b/crates/tinymemory-sources/src/readers/github/types.rs similarity index 100% rename from sources/src/readers/github/types.rs rename to crates/tinymemory-sources/src/readers/github/types.rs diff --git a/sources/src/readers/github_tests.rs b/crates/tinymemory-sources/src/readers/github_tests.rs similarity index 100% rename from sources/src/readers/github_tests.rs rename to crates/tinymemory-sources/src/readers/github_tests.rs diff --git a/sources/src/readers/mod.rs b/crates/tinymemory-sources/src/readers/mod.rs similarity index 100% rename from sources/src/readers/mod.rs rename to crates/tinymemory-sources/src/readers/mod.rs diff --git a/sources/src/readers/rss.rs b/crates/tinymemory-sources/src/readers/rss.rs similarity index 100% rename from sources/src/readers/rss.rs rename to crates/tinymemory-sources/src/readers/rss.rs diff --git a/sources/src/readers/rss/types.rs b/crates/tinymemory-sources/src/readers/rss/types.rs similarity index 100% rename from sources/src/readers/rss/types.rs rename to crates/tinymemory-sources/src/readers/rss/types.rs diff --git a/sources/src/readers/rss_tests.rs b/crates/tinymemory-sources/src/readers/rss_tests.rs similarity index 100% rename from sources/src/readers/rss_tests.rs rename to crates/tinymemory-sources/src/readers/rss_tests.rs diff --git a/sources/src/readers/ssrf.rs b/crates/tinymemory-sources/src/readers/ssrf.rs similarity index 100% rename from sources/src/readers/ssrf.rs rename to crates/tinymemory-sources/src/readers/ssrf.rs diff --git a/sources/src/readers/ssrf_tests.rs b/crates/tinymemory-sources/src/readers/ssrf_tests.rs similarity index 100% rename from sources/src/readers/ssrf_tests.rs rename to crates/tinymemory-sources/src/readers/ssrf_tests.rs diff --git a/sources/src/readers/web_page.rs b/crates/tinymemory-sources/src/readers/web_page.rs similarity index 100% rename from sources/src/readers/web_page.rs rename to crates/tinymemory-sources/src/readers/web_page.rs diff --git a/sources/src/readers/web_page/types.rs b/crates/tinymemory-sources/src/readers/web_page/types.rs similarity index 100% rename from sources/src/readers/web_page/types.rs rename to crates/tinymemory-sources/src/readers/web_page/types.rs diff --git a/sources/src/readers/web_page_tests.rs b/crates/tinymemory-sources/src/readers/web_page_tests.rs similarity index 100% rename from sources/src/readers/web_page_tests.rs rename to crates/tinymemory-sources/src/readers/web_page_tests.rs diff --git a/sources/src/registry.rs b/crates/tinymemory-sources/src/registry.rs similarity index 100% rename from sources/src/registry.rs rename to crates/tinymemory-sources/src/registry.rs diff --git a/sources/src/registry_tests.rs b/crates/tinymemory-sources/src/registry_tests.rs similarity index 100% rename from sources/src/registry_tests.rs rename to crates/tinymemory-sources/src/registry_tests.rs diff --git a/sources/src/types.rs b/crates/tinymemory-sources/src/types.rs similarity index 100% rename from sources/src/types.rs rename to crates/tinymemory-sources/src/types.rs diff --git a/sources/src/types_tests.rs b/crates/tinymemory-sources/src/types_tests.rs similarity index 100% rename from sources/src/types_tests.rs rename to crates/tinymemory-sources/src/types_tests.rs diff --git a/sources/src/validation.rs b/crates/tinymemory-sources/src/validation.rs similarity index 100% rename from sources/src/validation.rs rename to crates/tinymemory-sources/src/validation.rs diff --git a/sources/src/validation_tests.rs b/crates/tinymemory-sources/src/validation_tests.rs similarity index 100% rename from sources/src/validation_tests.rs rename to crates/tinymemory-sources/src/validation_tests.rs diff --git a/sync/Cargo.toml b/crates/tinymemory-sync/Cargo.toml similarity index 100% rename from sync/Cargo.toml rename to crates/tinymemory-sync/Cargo.toml diff --git a/sync/src/clickup.rs b/crates/tinymemory-sync/src/clickup.rs similarity index 100% rename from sync/src/clickup.rs rename to crates/tinymemory-sync/src/clickup.rs diff --git a/sync/src/clickup_tests.rs b/crates/tinymemory-sync/src/clickup_tests.rs similarity index 100% rename from sync/src/clickup_tests.rs rename to crates/tinymemory-sync/src/clickup_tests.rs diff --git a/sync/src/email_clean.rs b/crates/tinymemory-sync/src/email_clean.rs similarity index 100% rename from sync/src/email_clean.rs rename to crates/tinymemory-sync/src/email_clean.rs diff --git a/sync/src/email_clean_tests.rs b/crates/tinymemory-sync/src/email_clean_tests.rs similarity index 100% rename from sync/src/email_clean_tests.rs rename to crates/tinymemory-sync/src/email_clean_tests.rs diff --git a/sync/src/email_markdown.rs b/crates/tinymemory-sync/src/email_markdown.rs similarity index 100% rename from sync/src/email_markdown.rs rename to crates/tinymemory-sync/src/email_markdown.rs diff --git a/sync/src/github.rs b/crates/tinymemory-sync/src/github.rs similarity index 100% rename from sync/src/github.rs rename to crates/tinymemory-sync/src/github.rs diff --git a/sync/src/github_tests.rs b/crates/tinymemory-sync/src/github_tests.rs similarity index 100% rename from sync/src/github_tests.rs rename to crates/tinymemory-sync/src/github_tests.rs diff --git a/sync/src/gmail_post_process.rs b/crates/tinymemory-sync/src/gmail_post_process.rs similarity index 100% rename from sync/src/gmail_post_process.rs rename to crates/tinymemory-sync/src/gmail_post_process.rs diff --git a/sync/src/gmail_post_process_tests.rs b/crates/tinymemory-sync/src/gmail_post_process_tests.rs similarity index 100% rename from sync/src/gmail_post_process_tests.rs rename to crates/tinymemory-sync/src/gmail_post_process_tests.rs diff --git a/sync/src/helpers.rs b/crates/tinymemory-sync/src/helpers.rs similarity index 100% rename from sync/src/helpers.rs rename to crates/tinymemory-sync/src/helpers.rs diff --git a/sync/src/helpers_tests.rs b/crates/tinymemory-sync/src/helpers_tests.rs similarity index 100% rename from sync/src/helpers_tests.rs rename to crates/tinymemory-sync/src/helpers_tests.rs diff --git a/sync/src/lib.rs b/crates/tinymemory-sync/src/lib.rs similarity index 100% rename from sync/src/lib.rs rename to crates/tinymemory-sync/src/lib.rs diff --git a/sync/src/linear.rs b/crates/tinymemory-sync/src/linear.rs similarity index 100% rename from sync/src/linear.rs rename to crates/tinymemory-sync/src/linear.rs diff --git a/sync/src/linear_tests.rs b/crates/tinymemory-sync/src/linear_tests.rs similarity index 100% rename from sync/src/linear_tests.rs rename to crates/tinymemory-sync/src/linear_tests.rs diff --git a/sync/src/notion.rs b/crates/tinymemory-sync/src/notion.rs similarity index 100% rename from sync/src/notion.rs rename to crates/tinymemory-sync/src/notion.rs diff --git a/sync/src/notion_tests.rs b/crates/tinymemory-sync/src/notion_tests.rs similarity index 100% rename from sync/src/notion_tests.rs rename to crates/tinymemory-sync/src/notion_tests.rs diff --git a/sync/src/slack_post_process.rs b/crates/tinymemory-sync/src/slack_post_process.rs similarity index 100% rename from sync/src/slack_post_process.rs rename to crates/tinymemory-sync/src/slack_post_process.rs diff --git a/sync/src/slack_post_process_tests.rs b/crates/tinymemory-sync/src/slack_post_process_tests.rs similarity index 100% rename from sync/src/slack_post_process_tests.rs rename to crates/tinymemory-sync/src/slack_post_process_tests.rs diff --git a/adapters/tinycortex/Cargo.toml b/crates/tinymemory-tinycortex/Cargo.toml similarity index 100% rename from adapters/tinycortex/Cargo.toml rename to crates/tinymemory-tinycortex/Cargo.toml diff --git a/adapters/tinycortex/src/conformance_test.rs b/crates/tinymemory-tinycortex/src/conformance_test.rs similarity index 100% rename from adapters/tinycortex/src/conformance_test.rs rename to crates/tinymemory-tinycortex/src/conformance_test.rs diff --git a/adapters/tinycortex/src/engine/mod.rs b/crates/tinymemory-tinycortex/src/engine/mod.rs similarity index 100% rename from adapters/tinycortex/src/engine/mod.rs rename to crates/tinymemory-tinycortex/src/engine/mod.rs diff --git a/adapters/tinycortex/src/engine/test.rs b/crates/tinymemory-tinycortex/src/engine/test.rs similarity index 100% rename from adapters/tinycortex/src/engine/test.rs rename to crates/tinymemory-tinycortex/src/engine/test.rs diff --git a/adapters/tinycortex/src/lib.rs b/crates/tinymemory-tinycortex/src/lib.rs similarity index 100% rename from adapters/tinycortex/src/lib.rs rename to crates/tinymemory-tinycortex/src/lib.rs diff --git a/adapters/tinycortex/src/memory.rs b/crates/tinymemory-tinycortex/src/memory.rs similarity index 100% rename from adapters/tinycortex/src/memory.rs rename to crates/tinymemory-tinycortex/src/memory.rs diff --git a/adapters/tinycortex/src/memory_test.rs b/crates/tinymemory-tinycortex/src/memory_test.rs similarity index 100% rename from adapters/tinycortex/src/memory_test.rs rename to crates/tinymemory-tinycortex/src/memory_test.rs diff --git a/adapters/tinycortex/tests/full_provider_conformance.rs b/crates/tinymemory-tinycortex/tests/full_provider_conformance.rs similarity index 100% rename from adapters/tinycortex/tests/full_provider_conformance.rs rename to crates/tinymemory-tinycortex/tests/full_provider_conformance.rs diff --git a/examples/basic.rs b/crates/tinymemory/examples/basic.rs similarity index 100% rename from examples/basic.rs rename to crates/tinymemory/examples/basic.rs diff --git a/examples/tinycortex.rs b/crates/tinymemory/examples/tinycortex.rs similarity index 100% rename from examples/tinycortex.rs rename to crates/tinymemory/examples/tinycortex.rs diff --git a/src/lib.rs b/crates/tinymemory/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/tinymemory/src/lib.rs diff --git a/src/registry/class.rs b/crates/tinymemory/src/registry/class.rs similarity index 100% rename from src/registry/class.rs rename to crates/tinymemory/src/registry/class.rs diff --git a/src/registry/mod.rs b/crates/tinymemory/src/registry/mod.rs similarity index 100% rename from src/registry/mod.rs rename to crates/tinymemory/src/registry/mod.rs diff --git a/src/registry/test.rs b/crates/tinymemory/src/registry/test.rs similarity index 100% rename from src/registry/test.rs rename to crates/tinymemory/src/registry/test.rs diff --git a/tests/capability_negotiation.rs b/crates/tinymemory/tests/capability_negotiation.rs similarity index 100% rename from tests/capability_negotiation.rs rename to crates/tinymemory/tests/capability_negotiation.rs diff --git a/tests/driver_selection.rs b/crates/tinymemory/tests/driver_selection.rs similarity index 100% rename from tests/driver_selection.rs rename to crates/tinymemory/tests/driver_selection.rs diff --git a/tests/null_provider.rs b/crates/tinymemory/tests/null_provider.rs similarity index 100% rename from tests/null_provider.rs rename to crates/tinymemory/tests/null_provider.rs diff --git a/tests/sync_on_a_foreign_driver.rs b/crates/tinymemory/tests/sync_on_a_foreign_driver.rs similarity index 100% rename from tests/sync_on_a_foreign_driver.rs rename to crates/tinymemory/tests/sync_on_a_foreign_driver.rs diff --git a/tests/taint_end_to_end.rs b/crates/tinymemory/tests/taint_end_to_end.rs similarity index 100% rename from tests/taint_end_to_end.rs rename to crates/tinymemory/tests/taint_end_to_end.rs From a11fd6964b2f064b39b312a1a1220cbbdb86c719 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:45:21 +0300 Subject: [PATCH 02/18] chore(workspace): flatten crate layout under crates/ Move every package into a `crates/` directory, one directory per package, and replace the explicit member list with a glob. Remove the root package entirely so the workspace stays virtual and every crate is structurally uniform. Update the patch path for `tinymemory-api` to match the new layout and add `worktrees` to the exclude list so cargo does not walk into git worktree checkouts. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.toml | 141 +++++------------------------------------------------ 1 file changed, 13 insertions(+), 128 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a47b0d41..1cd42a6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,12 @@ [workspace] -# `sync` is the engine-neutral Composio normalisers (issue #18 §B3). -members = [".", "api", "core", "sync", "sources", "adapters/tinycortex", "adapters/remote", "conformance"] -default-members = [".", "api", "core", "sync", "sources", "adapters/tinycortex", "adapters/remote", "conformance"] +resolver = "2" +# Every crate in this repository lives under `crates/`, one directory per +# package, each directory named for the package it holds. There is no +# root package: the facade a host depends on is `crates/tinymemory`, the same +# as any other member. Keeping the root virtual is what makes that uniform — +# a root package would make one crate structurally different from the rest for +# no reason other than history. +members = ["crates/*"] # `vendor/` holds engine submodules (tinycortex, tinybus, tinyagents), each of # which is its own workspace with its own lockfile. Same exclusion # `vendor/tinycortex` uses for its own nested vendor directory. @@ -23,127 +28,11 @@ default-members = [".", "api", "core", "sync", "sources", "adapters/tinycortex", # remain separate repositories and are never workspace members") and what a # separately released artifact wants anyway. Build it with # `--manifest-path crates/tinymemory-module/Cargo.toml`. -exclude = ["vendor", "crates/tinymemory-module"] - -[package] -name = "tinymemory" -# Not published: `tinymemory-core` depends on `tinycortex-api`, which is -# consumed by path and is not on crates.io, so `cargo package` cannot resolve -# the graph. Every consumer takes this repo by path or git. -publish = false -version = "1.0.1" -edition = "2021" -rust-version = "1.96" -license = "MIT" -description = "Engine-neutral memory contract, driver registry, and engine adapters" -repository = "https://github.com/tinyhumansai/tinymemory" -documentation = "https://docs.rs/tinymemory" -readme = "README.md" -keywords = ["memory", "agent", "llm", "retrieval"] -categories = ["database"] -# Keep the published package to what a consumer actually needs. -exclude = [ - ".github/", - ".gitmodules", - "docs/", - "vendor/", - "worktrees/", - ".env.example", - "deny.toml", -] - -[dependencies] -# The contract itself. Re-exported wholesale from `src/lib.rs` so a host takes -# one dependency rather than two, and so `tinymemory::MemoryProvider` and -# `tinymemory_api::provider::MemoryProvider` are the same type. -tinymemory-api = { path = "api" } -# The engines, each behind its own feature (#18 §D1). Optional, so the default -# build is still the contract, the registry and the mandatory composition and -# nothing that links C. # -# This direction only became legal once `mandatory` and the reserved driver ids -# moved into `tinymemory-api`: the adapters used to depend on this crate for -# them, and a facade depending back on the adapters is a package cycle cargo -# forbids. -tinymemory-tinycortex = { path = "adapters/tinycortex", optional = true } -tinymemory-remote = { path = "adapters/remote", optional = true } -# The mandatory capability families are `async fn`s on object-safe traits. -async-trait = "0.1" -# `Memory` is anyhow-typed; `mandatory::engine_error` maps it onto `MemoryError`. -anyhow = "1" -# Diagnostics on the shared store/recall/export paths. -log = "0.4" -# `ExportRecord::payload` is a `serde_json::Value`. -serde_json = "1" - -# `DriverClass` is read out of a host's config file, so its serde form is the -# config form and is pinned by a test. -serde = { version = "1", features = ["derive"] } - -[dev-dependencies] -# The mandatory-family tests are async. -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -# `TestHostConfig`, for the driver-selection tests. A dev-dependency: the -# facade must not carry a test double into a consumer's graph. -tinymemory-api = { path = "api", features = ["test-support"] } -# The reference driver and the behavioural suite, for the workspace-level -# integration tests. A dev-dependency only: the facade must not carry a test -# harness into a consumer's dependency graph. -tinymemory-conformance = { path = "conformance" } -# The extracted Composio normalisers, for the integration test that runs them -# against a driver that is not TinyCortex (issue #18 §B3). -tinymemory-sync = { path = "sync" } - -[features] -# Nothing by default. A host that names no engine links no engine. -default = [] - -# --- Engines --------------------------------------------------------------- -# Each pulls exactly the adapter that serves it. `supermemory`, `mem0` and -# `cognee` share one adapter crate, so enabling several costs one dependency -# rather than three. -tinycortex = ["dep:tinymemory-tinycortex"] -supermemory = ["dep:tinymemory-remote"] -mem0 = ["dep:tinymemory-remote"] -cognee = ["dep:tinymemory-remote"] - -# --- Capability add-ons ---------------------------------------------------- -# Each requires the engine that serves it, so asking for a capability cannot -# produce a build where nothing implements it. -memory-git = ["tinycortex", "tinymemory-tinycortex/memory-git"] - -# Lints apply to this package only, deliberately. `api/` is contract code moved -# verbatim from `tinycortex-api` and is held byte-identical; subjecting it to a -# stricter lint set than it was written under would force cosmetic edits through -# a surface whose serde representations and enum wire strings are persisted on -# disk. Adapter crates opt in individually. -[lints.rust] -unsafe_code = "forbid" -missing_docs = "warn" -missing_debug_implementations = "warn" -unreachable_pub = "warn" -rust_2018_idioms = { level = "warn", priority = -1 } - -[lints.clippy] -all = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } -# Library code must not panic on its own; tests and examples may. -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" -todo = "warn" -unimplemented = "warn" -# Public fallible/panicking APIs must document their failure modes. -missing_errors_doc = "warn" -missing_panics_doc = "warn" -# Documentation hygiene. -doc_markdown = "warn" -# `#[must_use]` on pure public functions. -must_use_candidate = "warn" - -[lints.rustdoc] -broken_intra_doc_links = "warn" -private_intra_doc_links = "warn" +# `worktrees/` holds `git worktree` checkouts of this same repository. Each one +# contains a full copy of this manifest and every crate under it, so without +# this entry cargo walks into them and reports duplicate packages. +exclude = ["vendor", "crates/tinymemory-module", "worktrees"] # The engine adapters name their engines by version requirement, not by path, # so a host that already pins its own engine checkout unifies onto one copy @@ -160,7 +49,7 @@ private_intra_doc_links = "warn" # and tests here. A host that embeds this workspace needs the same entry, the # same way it already patches tinycortex. [patch."https://github.com/tinyhumansai/tinymemory"] -tinymemory-api = { path = "api" } +tinymemory-api = { path = "crates/tinymemory-api" } [patch.crates-io] tinycortex = { path = "vendor/tinycortex" } @@ -186,7 +75,3 @@ tinyagents = { path = "vendor/tinyagents" } lto = "thin" codegen-units = 1 strip = "debuginfo" - -[[example]] -name = "tinycortex" -required-features = ["tinycortex"] From e14e329b3177af755ee4a271210257ddbc764778 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:46:12 +0300 Subject: [PATCH 03/18] fix(store): remove unused dependencies and clean up test imports Remove several unused crate dependencies from tinymemory's Cargo.toml and clean up unused imports in test files across the store module. This reduces compilation overhead and clarifies which dependencies are actually needed for the store's functionality. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .../store/namespace_store/documents_tests.rs | 1508 +++++++++++++++++ .../src/store/namespace_store/fts5.rs | 610 +++++++ .../tinymemory-core/src/store/safety/mod.rs | 340 ++++ .../src/store/write_gate_tests.rs | 176 ++ crates/tinymemory/Cargo.toml | 182 ++ 5 files changed, 2816 insertions(+) create mode 100644 crates/tinymemory-core/src/store/namespace_store/documents_tests.rs create mode 100644 crates/tinymemory-core/src/store/namespace_store/fts5.rs create mode 100644 crates/tinymemory-core/src/store/safety/mod.rs create mode 100644 crates/tinymemory-core/src/store/write_gate_tests.rs create mode 100644 crates/tinymemory/Cargo.toml diff --git a/crates/tinymemory-core/src/store/namespace_store/documents_tests.rs b/crates/tinymemory-core/src/store/namespace_store/documents_tests.rs new file mode 100644 index 00000000..7b2f7ea5 --- /dev/null +++ b/crates/tinymemory-core/src/store/namespace_store/documents_tests.rs @@ -0,0 +1,1508 @@ +//! Tests for the `documents` module — upsert / list / delete / clear-namespace. + +use std::sync::Arc; + +use serde_json::json; +use tempfile::TempDir; + +use crate::store::{NamespaceDocumentInput, UnifiedMemory}; +use tinymemory_api::host::NoopEmbedding; + +fn make_doc_input( + namespace: &str, + key: &str, + title: &str, + content: &str, +) -> NamespaceDocumentInput { + NamespaceDocumentInput { + namespace: namespace.to_string(), + key: key.to_string(), + title: title.to_string(), + content: content.to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + } +} + +fn count_vector_chunks(memory: &UnifiedMemory, namespace: &str, document_id: &str) -> i64 { + let conn = memory.conn.lock(); + conn.query_row( + "SELECT COUNT(*) FROM vector_chunks WHERE namespace = ?1 AND document_id = ?2", + rusqlite::params![UnifiedMemory::sanitize_namespace(namespace), document_id], + |row| row.get(0), + ) + .unwrap() +} + +#[tokio::test] +async fn list_documents_without_namespace_returns_all_docs_across_namespaces() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input("test:one", "doc-a", "Doc A", "A body")) + .await + .unwrap(); + memory + .upsert_document(make_doc_input("test:two", "doc-b", "Doc B", "B body")) + .await + .unwrap(); + + let docs = memory.list_documents(None).await.unwrap(); + assert_eq!(docs["count"].as_u64().unwrap(), 2); + let namespaces: std::collections::BTreeSet<_> = docs["documents"] + .as_array() + .unwrap() + .iter() + .filter_map(|doc| doc["namespace"].as_str()) + .collect(); + assert!(namespaces.contains("test_one")); + assert!(namespaces.contains("test_two")); +} + +#[tokio::test] +async fn list_namespaces_returns_distinct_sorted_sanitized_namespaces() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input("team alpha/#1", "doc-a", "Doc A", "A body")) + .await + .unwrap(); + memory + .upsert_document(make_doc_input("team alpha/#1", "doc-b", "Doc B", "B body")) + .await + .unwrap(); + memory + .upsert_document(make_doc_input("zeta", "doc-c", "Doc C", "C body")) + .await + .unwrap(); + + let namespaces = memory.list_namespaces().await.unwrap(); + assert_eq!( + namespaces, + vec!["team_alpha/_1".to_string(), "zeta".to_string()] + ); +} + +#[tokio::test] +async fn list_documents_with_namespace_filters_by_sanitized_namespace_and_orders_newest_first() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input( + "team alpha/#1", + "doc-a", + "Older Doc", + "A body", + )) + .await + .unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + memory + .upsert_document(make_doc_input( + "team alpha/#1", + "doc-b", + "Newer Doc", + "B body", + )) + .await + .unwrap(); + memory + .upsert_document(make_doc_input("other", "doc-c", "Other Doc", "C body")) + .await + .unwrap(); + + let docs = memory.list_documents(Some("team alpha/#1")).await.unwrap(); + let documents = docs["documents"].as_array().unwrap(); + + assert_eq!(docs["count"].as_u64().unwrap(), 2); + assert_eq!(documents[0]["namespace"], json!("team_alpha/_1")); + assert_eq!(documents[0]["key"], json!("doc-b")); + assert_eq!(documents[1]["key"], json!("doc-a")); +} + +#[tokio::test] +async fn load_documents_for_scope_defaults_invalid_json_fields_from_persisted_rows() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + let namespace = UnifiedMemory::sanitize_namespace("broken/json"); + + { + let conn = memory.conn.lock(); + conn.execute( + "INSERT INTO memory_docs + (document_id, namespace, key, title, content, source_type, priority, tags_json, metadata_json, category, session_id, created_at, updated_at, markdown_rel_path) + VALUES + (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + rusqlite::params![ + "doc-invalid-json", + namespace, + "doc-a", + "Doc A", + "Body", + "doc", + "medium", + "{not json", + "also not json", + "core", + Option::::None, + 10.0_f64, + 20.0_f64, + "memory/namespaces/broken_json/docs/doc-invalid-json.md" + ], + ) + .unwrap(); + } + + let docs = memory + .load_documents_for_scope("broken/json") + .await + .unwrap(); + assert_eq!(docs.len(), 1); + assert!( + docs[0].tags.is_empty(), + "invalid tags_json should fall back to []" + ); + assert_eq!( + docs[0].metadata, + json!({}), + "invalid metadata_json should fall back to an empty object" + ); +} + +#[tokio::test] +async fn upsert_document_metadata_only_reuses_document_id_for_same_namespace_and_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let first_id = memory + .upsert_document_metadata_only(make_doc_input( + "test:meta", + "doc-a", + "Doc A", + "Initial body", + )) + .await + .unwrap(); + let second_id = memory + .upsert_document_metadata_only(make_doc_input( + "test:meta", + "doc-a", + "Doc A v2", + "Updated body", + )) + .await + .unwrap(); + + assert_eq!( + first_id, second_id, + "metadata-only upsert should reuse the document id" + ); + let docs = memory.load_documents_for_scope("test:meta").await.unwrap(); + assert_eq!(docs.len(), 1); + assert_eq!(docs[0].document_id, first_id); + assert_eq!(docs[0].title, "Doc A v2"); + assert_eq!(docs[0].content, "Updated body"); + assert_eq!( + count_vector_chunks(&memory, "test:meta", &first_id), + 0, + "metadata-only writes must not enqueue vector chunks" + ); +} + +#[tokio::test] +async fn upsert_document_metadata_only_preserves_created_at_and_rewrites_sidecar() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let first_id = memory + .upsert_document_metadata_only(make_doc_input( + "test:meta-sidecar", + "doc-a", + "Doc A", + "Initial body", + )) + .await + .unwrap(); + let first_doc = memory + .load_documents_for_scope("test:meta-sidecar") + .await + .unwrap()[0] + .clone(); + let sidecar = tmp.path().join(&first_doc.markdown_rel_path); + let first_markdown = std::fs::read_to_string(&sidecar).unwrap(); + assert!(first_markdown.contains("Initial body")); + + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + + let second_id = memory + .upsert_document_metadata_only(make_doc_input( + "test:meta-sidecar", + "doc-a", + "Doc A v2", + "Updated body", + )) + .await + .unwrap(); + + assert_eq!(first_id, second_id); + let updated_doc = memory + .load_documents_for_scope("test:meta-sidecar") + .await + .unwrap()[0] + .clone(); + assert_eq!(updated_doc.created_at, first_doc.created_at); + assert!(updated_doc.updated_at >= first_doc.updated_at); + let updated_markdown = std::fs::read_to_string(sidecar).unwrap(); + assert!(updated_markdown.contains("Updated body")); + assert!(updated_markdown.contains("Doc A v2")); +} + +#[tokio::test] +async fn upsert_document_metadata_only_over_existing_document_preserves_vector_chunks() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let document_id = memory + .upsert_document(make_doc_input( + "test:meta-preserve-chunks", + "doc-a", + "Doc A", + &"alpha ".repeat(400), + )) + .await + .unwrap(); + let original_chunk_count = + count_vector_chunks(&memory, "test:meta-preserve-chunks", &document_id); + assert!(original_chunk_count > 0); + + let updated_id = memory + .upsert_document_metadata_only(make_doc_input( + "test:meta-preserve-chunks", + "doc-a", + "Doc A v2", + "Updated body without re-embedding", + )) + .await + .unwrap(); + + assert_eq!(updated_id, document_id); + let docs = memory + .load_documents_for_scope("test:meta-preserve-chunks") + .await + .unwrap(); + assert_eq!(docs.len(), 1); + assert_eq!(docs[0].content, "Updated body without re-embedding"); + assert_eq!( + count_vector_chunks(&memory, "test:meta-preserve-chunks", &document_id), + original_chunk_count, + "metadata-only writes should not delete existing semantic chunks" + ); +} + +#[tokio::test] +async fn upsert_document_after_metadata_only_reuses_document_id_and_adds_vector_chunks() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let metadata_only_id = memory + .upsert_document_metadata_only(make_doc_input( + "test:meta-then-full", + "doc-a", + "Doc A", + "Short body", + )) + .await + .unwrap(); + assert_eq!( + count_vector_chunks(&memory, "test:meta-then-full", &metadata_only_id), + 0 + ); + + let full_id = memory + .upsert_document(make_doc_input( + "test:meta-then-full", + "doc-a", + "Doc A Embedded", + &"beta ".repeat(400), + )) + .await + .unwrap(); + + assert_eq!(full_id, metadata_only_id); + let docs = memory + .load_documents_for_scope("test:meta-then-full") + .await + .unwrap(); + assert_eq!(docs.len(), 1); + assert_eq!(docs[0].title, "Doc A Embedded"); + assert!( + count_vector_chunks(&memory, "test:meta-then-full", &full_id) > 0, + "full upsert should backfill chunks for a metadata-only document" + ); +} + +#[tokio::test] +async fn upsert_document_writes_vector_chunks_for_chunked_content() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let long_body = "alpha ".repeat(400); + let document_id = memory + .upsert_document(make_doc_input("test:vector", "doc-a", "Doc A", &long_body)) + .await + .unwrap(); + + assert!( + count_vector_chunks(&memory, "test:vector", &document_id) > 0, + "full document upsert should replace vector chunks for semantic retrieval" + ); +} + +/// Embedder that records how many times `embed` is invoked and returns one +/// fixed-dimension vector per input text. Used to prove `upsert_document` +/// embeds all chunks in a SINGLE batch call rather than one call per chunk. +struct CountingEmbedder { + calls: std::sync::atomic::AtomicUsize, +} + +#[async_trait::async_trait] +impl tinymemory_api::host::EmbeddingProvider for CountingEmbedder { + fn name(&self) -> &str { + "counting" + } + + fn model_id(&self) -> &str { + "counting-test" + } + + fn dimensions(&self) -> usize { + 3 + } + + async fn embed(&self, texts: &[&str]) -> anyhow::Result>> { + self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Ok(texts.iter().map(|_| vec![0.1, 0.2, 0.3]).collect()) + } +} + +#[tokio::test] +async fn upsert_document_batch_embeds_all_chunks_in_one_call() { + let tmp = TempDir::new().unwrap(); + let embedder = Arc::new(CountingEmbedder { + calls: std::sync::atomic::AtomicUsize::new(0), + }); + let memory = UnifiedMemory::new(tmp.path(), embedder.clone(), None).unwrap(); + + // Long enough to chunk into several pieces (chunk size is 225 chars). + let long_body = "alpha ".repeat(400); + let document_id = memory + .upsert_document(make_doc_input("test:batch", "doc-a", "Doc A", &long_body)) + .await + .unwrap(); + + let chunk_count = count_vector_chunks(&memory, "test:batch", &document_id); + assert!( + chunk_count >= 3, + "test body should chunk into >=3 pieces, got {chunk_count}" + ); + assert_eq!( + embedder.calls.load(std::sync::atomic::Ordering::SeqCst), + 1, + "all chunks must be embedded in a single batch call, not one call per chunk" + ); +} + +#[tokio::test] +async fn upsert_document_reuses_document_id_preserves_created_at_and_replaces_vector_chunks() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let first_id = memory + .upsert_document(make_doc_input( + "test:update", + "doc-a", + "Doc A", + &"alpha ".repeat(400), + )) + .await + .unwrap(); + let first_doc = memory + .load_documents_for_scope("test:update") + .await + .unwrap()[0] + .clone(); + let first_chunk_count = count_vector_chunks(&memory, "test:update", &first_id); + assert!(first_chunk_count > 0); + + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + + let second_id = memory + .upsert_document(make_doc_input( + "test:update", + "doc-a", + "Doc A v2", + &"beta ".repeat(40), + )) + .await + .unwrap(); + + assert_eq!( + first_id, second_id, + "upsert should reuse the existing document id" + ); + let updated_doc = memory + .load_documents_for_scope("test:update") + .await + .unwrap()[0] + .clone(); + assert_eq!(updated_doc.document_id, first_id); + assert_eq!(updated_doc.created_at, first_doc.created_at); + assert!(updated_doc.updated_at >= first_doc.updated_at); + assert_eq!(updated_doc.title, "Doc A v2"); + assert_eq!(updated_doc.content, "beta ".repeat(40)); + let second_chunk_count = count_vector_chunks(&memory, "test:update", &second_id); + assert!(second_chunk_count > 0); + assert!( + second_chunk_count <= first_chunk_count, + "replacing with shorter content should not leave stale vector chunks behind" + ); +} + +#[tokio::test] +async fn delete_document_removes_doc_sidecar_and_is_idempotent() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let document_id = memory + .upsert_document(make_doc_input("test:delete", "doc-a", "Doc A", "Delete me")) + .await + .unwrap(); + + let docs = memory + .load_documents_for_scope("test:delete") + .await + .unwrap(); + assert_eq!(docs.len(), 1); + let sidecar = tmp.path().join(&docs[0].markdown_rel_path); + assert!(sidecar.exists(), "sidecar should exist before delete"); + + memory + .graph_upsert_namespace( + "test:delete", + "Alice", + "OWNS", + "Phoenix", + &json!({ + "document_id": document_id.clone(), + "chunk_id": format!("{document_id}:0") + }), + ) + .await + .unwrap(); + + let deleted = memory + .delete_document("test:delete", &document_id) + .await + .unwrap(); + assert_eq!(deleted["deleted"], json!(true)); + assert_eq!(deleted["documentId"], json!(document_id.clone())); + assert!(!sidecar.exists(), "sidecar should be removed on delete"); + assert!(memory + .load_documents_for_scope("test:delete") + .await + .unwrap() + .is_empty()); + assert!( + memory + .graph_relations_namespace("test:delete", None, None) + .await + .unwrap() + .is_empty(), + "document-linked graph relations should be pruned" + ); + + let second = memory + .delete_document("test:delete", &document_id) + .await + .unwrap(); + assert_eq!(second["deleted"], json!(false)); + assert_eq!(second["documentId"], json!(document_id)); +} + +#[tokio::test] +async fn delete_document_succeeds_when_sidecar_is_already_missing() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let document_id = memory + .upsert_document(make_doc_input( + "test:delete-missing-sidecar", + "doc-a", + "Doc A", + "Delete me", + )) + .await + .unwrap(); + + let docs = memory + .load_documents_for_scope("test:delete-missing-sidecar") + .await + .unwrap(); + assert_eq!(docs.len(), 1); + let sidecar = tmp.path().join(&docs[0].markdown_rel_path); + assert!(sidecar.exists()); + std::fs::remove_file(&sidecar).unwrap(); + assert!(!sidecar.exists()); + + let deleted = memory + .delete_document("test:delete-missing-sidecar", &document_id) + .await + .unwrap(); + assert_eq!(deleted["deleted"], json!(true)); + assert!(memory + .load_documents_for_scope("test:delete-missing-sidecar") + .await + .unwrap() + .is_empty()); +} + +#[tokio::test] +async fn delete_document_accepts_unsanitized_namespace_and_removes_chunks() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let document_id = memory + .upsert_document(make_doc_input( + "Team Alpha/#1", + "doc-a", + "Doc A", + &"delete ".repeat(300), + )) + .await + .unwrap(); + assert!(count_vector_chunks(&memory, "Team Alpha/#1", &document_id) > 0); + + let deleted = memory + .delete_document("Team Alpha/#1", &document_id) + .await + .unwrap(); + assert_eq!(deleted["deleted"], json!(true)); + assert_eq!(deleted["namespace"], json!("Team_Alpha/_1")); + assert_eq!( + count_vector_chunks(&memory, "Team Alpha/#1", &document_id), + 0 + ); + assert!(memory + .load_documents_for_scope("Team Alpha/#1") + .await + .unwrap() + .is_empty()); +} + +#[tokio::test] +async fn clear_namespace_removes_all_data_and_preserves_other_namespaces() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + // --- Populate "test:cleanup" namespace --- + + // 3 documents + memory + .upsert_document(make_doc_input( + "test:cleanup", + "doc-a", + "Document A", + "Content of document A for cleanup.", + )) + .await + .unwrap(); + memory + .upsert_document(make_doc_input( + "test:cleanup", + "doc-b", + "Document B", + "Content of document B for cleanup.", + )) + .await + .unwrap(); + memory + .upsert_document(make_doc_input( + "test:cleanup", + "doc-c", + "Document C", + "Content of document C for cleanup.", + )) + .await + .unwrap(); + + // 2 KV entries + memory + .kv_set_namespace("test:cleanup", "pref-1", &json!({"theme": "dark"})) + .await + .unwrap(); + memory + .kv_set_namespace("test:cleanup", "pref-2", &json!({"lang": "en"})) + .await + .unwrap(); + + // 2 graph relations + memory + .graph_upsert_namespace( + "test:cleanup", + "Alice", + "knows", + "Bob", + &json!({"source": "test"}), + ) + .await + .unwrap(); + memory + .graph_upsert_namespace( + "test:cleanup", + "Bob", + "works_at", + "Acme", + &json!({"source": "test"}), + ) + .await + .unwrap(); + + // --- Populate "test:other" namespace (control) --- + + memory + .upsert_document(make_doc_input( + "test:other", + "other-doc", + "Other Document", + "Content of document in the other namespace.", + )) + .await + .unwrap(); + memory + .kv_set_namespace("test:other", "other-key", &json!({"value": true})) + .await + .unwrap(); + memory + .graph_upsert_namespace( + "test:other", + "X", + "relates_to", + "Y", + &json!({"source": "other"}), + ) + .await + .unwrap(); + + // --- Verify pre-conditions --- + + let cleanup_docs = memory.list_documents(Some("test:cleanup")).await.unwrap(); + assert_eq!( + cleanup_docs["count"].as_u64().unwrap(), + 3, + "test:cleanup should have 3 documents before clear" + ); + + let cleanup_kv = memory.kv_list_namespace("test:cleanup").await.unwrap(); + assert_eq!( + cleanup_kv.len(), + 2, + "test:cleanup should have 2 KV entries before clear" + ); + + let cleanup_graph = memory + .graph_relations_namespace("test:cleanup", None, None) + .await + .unwrap(); + assert_eq!( + cleanup_graph.len(), + 2, + "test:cleanup should have 2 graph relations before clear" + ); + + let other_docs = memory.list_documents(Some("test:other")).await.unwrap(); + assert_eq!( + other_docs["count"].as_u64().unwrap(), + 1, + "test:other should have 1 document before clear" + ); + + // --- Execute clear_namespace --- + + memory.clear_namespace("test:cleanup").await.unwrap(); + + // --- Assert: "test:cleanup" is empty --- + + let cleanup_docs_after = memory.list_documents(Some("test:cleanup")).await.unwrap(); + assert_eq!( + cleanup_docs_after["count"].as_u64().unwrap(), + 0, + "test:cleanup documents should be empty after clear" + ); + + let cleanup_kv_after = memory.kv_list_namespace("test:cleanup").await.unwrap(); + assert!( + cleanup_kv_after.is_empty(), + "test:cleanup KV entries should be empty after clear" + ); + + let cleanup_graph_after = memory + .graph_relations_namespace("test:cleanup", None, None) + .await + .unwrap(); + assert!( + cleanup_graph_after.is_empty(), + "test:cleanup graph relations should be empty after clear" + ); + + // --- Assert: "test:other" is untouched (critical) --- + + let other_docs_after = memory.list_documents(Some("test:other")).await.unwrap(); + assert_eq!( + other_docs_after["count"].as_u64().unwrap(), + 1, + "test:other document must still exist after clearing test:cleanup" + ); + + let other_kv_after = memory.kv_list_namespace("test:other").await.unwrap(); + assert_eq!( + other_kv_after.len(), + 1, + "test:other KV entry must still exist after clearing test:cleanup" + ); + + let other_graph_after = memory + .graph_relations_namespace("test:other", None, None) + .await + .unwrap(); + assert_eq!( + other_graph_after.len(), + 1, + "test:other graph relation must still exist after clearing test:cleanup" + ); +} + +#[tokio::test] +async fn clear_namespace_on_empty_namespace_is_noop() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + // Clearing a namespace that has never been used should succeed without error. + memory.clear_namespace("nonexistent").await.unwrap(); + + let docs = memory.list_documents(Some("nonexistent")).await.unwrap(); + assert_eq!(docs["count"].as_u64().unwrap(), 0); +} + +#[tokio::test] +async fn clear_namespace_removes_on_disk_markdown_files() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input( + "test:diskcheck", + "disk-doc", + "Disk Doc", + "This doc has a markdown file on disk.", + )) + .await + .unwrap(); + + let docs_dir = tmp + .path() + .join("memory") + .join("namespaces") + .join("test_diskcheck") + .join("docs"); + assert!( + docs_dir.exists(), + "docs directory should exist after upsert" + ); + + memory.clear_namespace("test:diskcheck").await.unwrap(); + + assert!( + !docs_dir.exists(), + "docs directory should be removed after clear_namespace" + ); +} + +#[tokio::test] +async fn clear_namespace_accepts_unsanitized_namespace_and_removes_sanitized_docs_dir() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input( + "Team Alpha/#1", + "doc-a", + "Doc A", + "Namespace cleanup body", + )) + .await + .unwrap(); + memory + .kv_set_namespace("Team Alpha/#1", "pref-1", &json!({"theme": "dark"})) + .await + .unwrap(); + + let docs_dir = tmp + .path() + .join("memory") + .join("namespaces") + .join("Team_Alpha/_1") + .join("docs"); + assert!(docs_dir.exists()); + + memory.clear_namespace("Team Alpha/#1").await.unwrap(); + + assert!(memory + .load_documents_for_scope("Team Alpha/#1") + .await + .unwrap() + .is_empty()); + assert!(memory + .kv_list_namespace("Team Alpha/#1") + .await + .unwrap() + .is_empty()); + assert!(!docs_dir.exists()); +} + +#[tokio::test] +async fn list_namespaces_skips_blank_rows_inserted_outside_normal_writes() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + { + let conn = memory.conn.lock(); + conn.execute( + "INSERT INTO memory_docs + (document_id, namespace, key, title, content, source_type, priority, tags_json, metadata_json, category, session_id, created_at, updated_at, markdown_rel_path) + VALUES + (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + rusqlite::params![ + "doc-blank-ns", + " ", + "doc-a", + "Doc A", + "Body", + "doc", + "medium", + "[]", + "{}", + "core", + Option::::None, + 10.0_f64, + 20.0_f64, + "memory/namespaces/blank/docs/doc-blank-ns.md" + ], + ) + .unwrap(); + } + memory + .upsert_document(make_doc_input("valid/ns", "doc-b", "Doc B", "Body")) + .await + .unwrap(); + + let namespaces = memory.list_namespaces().await.unwrap(); + assert_eq!(namespaces, vec!["valid/ns".to_string()]); +} + +#[tokio::test] +async fn upsert_document_redacts_secret_like_content_before_persisting() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(NamespaceDocumentInput { + namespace: "safe".to_string(), + key: "secret-note".to_string(), + title: "Bearer abcdefghijklmnop".to_string(), + content: "token=abc123\n-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----" + .to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec!["sk-1234567890123456789012345".to_string()], + metadata: json!({ + "token": "raw", + "notes": "api_key=really-secret" + }), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .unwrap(); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + assert_eq!(docs.len(), 1); + let doc = &docs[0]; + assert!(!doc.title.contains("abcdefghijklmnop")); + assert!(doc.title.contains("[REDACTED]")); + assert!(!doc.content.contains("BEGIN PRIVATE KEY")); + assert!(doc.content.contains("[REDACTED_PRIVATE_KEY]")); + assert_eq!(doc.metadata["token"], json!("[REDACTED_SECRET]")); + assert_eq!(doc.metadata["notes"], json!("api_key=[REDACTED]")); + assert_eq!(doc.tags[0], "[REDACTED]"); + + let markdown = std::fs::read_to_string(tmp.path().join(&doc.markdown_rel_path)).unwrap(); + assert!(!markdown.contains("BEGIN PRIVATE KEY")); + assert!(markdown.contains("[REDACTED_PRIVATE_KEY]")); +} + +#[tokio::test] +async fn kv_set_namespace_redacts_secret_like_payloads() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .kv_set_namespace( + "safe", + "key-1", + &json!({ + "token": "super-secret", + "note": "Bearer abcdefghijklmnop" + }), + ) + .await + .unwrap(); + + let rows = memory.kv_list_namespace("safe").await.unwrap(); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0]["key"], json!("key-1")); + assert_eq!(rows[0]["value"]["token"], json!("[REDACTED_SECRET]")); + assert_eq!(rows[0]["value"]["note"], json!("Bearer [REDACTED]")); +} + +#[tokio::test] +async fn kv_set_namespace_rejects_secret_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let err = memory + .kv_set_namespace( + "safe", + "api_key=sk-1234567890123456789012345", + &json!({"value": "ok"}), + ) + .await + .expect_err("secret-like key should be rejected"); + assert!(err.contains("cannot contain secrets")); +} + +#[tokio::test] +async fn kv_set_namespace_rejects_secret_like_namespace() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let err = memory + .kv_set_namespace( + "Bearer abcdefghijklmnop", + "safe-key", + &json!({"value": "ok"}), + ) + .await + .expect_err("secret-like namespace should be rejected"); + assert!(err.contains("cannot contain secrets")); +} + +#[tokio::test] +async fn kv_set_global_rejects_secret_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let err = memory + .kv_set_global( + "authorization=Bearer abcdefghijklmnop", + &json!({"value": "ok"}), + ) + .await + .expect_err("secret-like global key should be rejected"); + assert!(err.contains("cannot contain secrets")); +} + +#[tokio::test] +async fn upsert_document_rejects_secret_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let err = memory + .upsert_document(NamespaceDocumentInput { + namespace: "safe".to_string(), + key: "api_key=sk-1234567890123456789012345".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect_err("secret-like key should be rejected"); + assert!(err.contains("cannot contain secrets")); +} + +#[tokio::test] +async fn upsert_document_rejects_secret_like_namespace() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let err = memory + .upsert_document(NamespaceDocumentInput { + namespace: "Bearer abcdefghijklmnop".to_string(), + key: "k1".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect_err("secret-like namespace should be rejected"); + assert!(err.contains("cannot contain secrets")); +} + +#[tokio::test] +async fn upsert_document_metadata_only_rejects_secret_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let err = memory + .upsert_document_metadata_only(NamespaceDocumentInput { + namespace: "safe".to_string(), + key: "refresh_token=abcdef".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect_err("secret-like key should be rejected"); + assert!(err.contains("cannot contain secrets")); +} + +// --------------------------------------------------------------------------- +// Personal-identifier (PII) at the namespace/key boundary — auto-sanitize. +// +// Rather than rejecting writes with PII-like keys/namespaces (which caused +// unthrottled retry loops, see #5164), the store now auto-sanitizes the +// namespace and key using `redact_pii` before persisting. These tests verify +// the upsert succeeds and the stored key/namespace contains redacted tokens. +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn kv_set_global_auto_sanitizes_pii_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + // SSN-like key should be auto-sanitized, not rejected. + memory + .kv_set_global("ssn-123-45-6789", &json!({"value": "ok"})) + .await + .expect("PII-like global key should be auto-sanitized, not rejected"); + + // ... and the caller must be able to read it back with the identifier it + // wrote. The canonicalization is a storage-address transform, so the read + // path applies the same one (#5164). A miss here is what made the caller + // write again, which is the loop the issue was reported for. + let stored = memory.kv_get_global("ssn-123-45-6789").await.unwrap(); + assert_eq!( + stored, + Some(json!({"value": "ok"})), + "a canonicalized KV key must stay readable by its original identifier" + ); + assert!( + memory.kv_delete_global("ssn-123-45-6789").await.unwrap(), + "delete must address the same canonicalized row the write created" + ); +} + +#[tokio::test] +async fn kv_set_namespace_auto_sanitizes_pii_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .kv_set_namespace("safe", "ssn-123-45-6789", &json!({"value": "ok"})) + .await + .expect("PII-like key should be auto-sanitized, not rejected"); + + let records = memory.kv_records_namespace("safe").await.unwrap(); + // The record should still exist; the key gets redacted internally. + assert_eq!(records.len(), 1); + assert_eq!( + memory + .kv_get_namespace("safe", "ssn-123-45-6789") + .await + .unwrap(), + Some(json!({"value": "ok"})), + "a canonicalized KV key must stay readable by its original identifier" + ); +} + +#[tokio::test] +async fn kv_set_namespace_auto_sanitizes_pii_like_namespace() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .kv_set_namespace("user/111.444.777-35", "safe-key", &json!({"value": "ok"})) + .await + .expect("PII-like namespace should be auto-sanitized, not rejected"); + + assert_eq!( + memory + .kv_get_namespace("user/111.444.777-35", "safe-key") + .await + .unwrap(), + Some(json!({"value": "ok"})), + "a canonicalized KV namespace must stay readable by its original value" + ); +} + +#[tokio::test] +async fn upsert_document_auto_sanitizes_pii_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let doc_id = memory + .upsert_document(NamespaceDocumentInput { + namespace: "safe".to_string(), + key: "cuit-20-11111111-2".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect("PII-like key should be auto-sanitized, not rejected"); + + // The document was stored with a redacted key. + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); + assert!(!doc.key.contains("20-11111111-2"), "key should be redacted"); + assert!( + doc.key.contains("REDACTED"), // matches [REDACTED_PII_CUIT] + "key should contain a redaction token, got: {}", + doc.key + ); +} + +#[tokio::test] +async fn upsert_document_auto_sanitizes_pii_like_namespace() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let doc_id = memory + .upsert_document(NamespaceDocumentInput { + namespace: "cliente-RFC-VECJ880326XK4".to_string(), + key: "k1".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect("PII-like namespace should be auto-sanitized, not rejected"); + + // Look up the document by sanitized namespace (note sanitize_namespace + // normalises special chars, so `[` becomes `_`). + let docs = memory + .load_documents_for_scope("cliente-RFC-VECJ880326XK4") + .await + .unwrap(); + let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); + assert!( + doc.namespace.contains("REDACTED"), // [REDACTED_PII_RFC] after sanitize_namespace + "namespace should contain a redaction token, got: {}", + doc.namespace + ); +} + +#[tokio::test] +async fn upsert_document_metadata_only_auto_sanitizes_pii_like_key() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let doc_id = memory + .upsert_document_metadata_only(NamespaceDocumentInput { + namespace: "safe".to_string(), + key: "ssn-123-45-6789".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect("PII-like key should be auto-sanitized, not rejected"); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); + assert!( + doc.key.contains("REDACTED"), // [REDACTED_PII_SSN] + "key should contain a redaction token, got: {}", + doc.key + ); +} + +#[tokio::test] +async fn upsert_document_metadata_only_auto_sanitizes_pii_like_namespace() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + let doc_id = memory + .upsert_document_metadata_only(NamespaceDocumentInput { + namespace: "user/111.444.777-35".to_string(), + key: "safe-key".to_string(), + title: "Title".to_string(), + content: "Body".to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + }) + .await + .expect("PII-like namespace should be auto-sanitized, not rejected"); + + let docs = memory + .load_documents_for_scope("user/111.444.777-35") + .await + .unwrap(); + let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); + assert!( + doc.namespace.contains("REDACTED"), // [REDACTED_PII_CPF] after sanitize_namespace + "namespace should contain a redaction token, got: {}", + doc.namespace + ); +} + +// --------------------------------------------------------------------------- +// #5164 — the identifier canonicalization has to be symmetric, and it has to +// leave scanner-built identifiers alone. +// +// Canonicalizing a namespace/key rewrites the row's *address*. Two failure +// modes follow, and both re-create the unthrottled write loop the issue was +// filed for (silently, this time): +// +// 1. a read path that addresses the raw caller identifier never finds the +// canonicalized row, so the caller writes it again; +// 2. canonicalizing with the lenient *content* scrubber maps every +// phone-shaped identifier onto one placeholder, so distinct chats collapse +// onto one `(namespace, key)` and the upsert's `ON CONFLICT … DO UPDATE` +// has one contact's document overwrite another's. +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn pii_like_document_key_round_trips_through_get_and_forget() { + use crate::traits::Memory; + + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input( + "clients", + "ssn-123-45-6789", + "Title", + "Body", + )) + .await + .expect("PII-like key should be canonicalized, not rejected"); + + let entry = memory + .get("clients", "ssn-123-45-6789") + .await + .unwrap() + .expect("a canonicalized key must stay readable by its original identifier"); + assert_eq!(entry.content, "Body"); + assert!( + !entry.key.contains("123-45-6789"), + "the SSN must not be persisted as the storage address, got: {}", + entry.key + ); + + assert!( + memory.forget("clients", "ssn-123-45-6789").await.unwrap(), + "forget must address the same canonicalized row the write created" + ); + assert!(memory + .get("clients", "ssn-123-45-6789") + .await + .unwrap() + .is_none()); +} + +#[tokio::test] +async fn pii_like_namespace_round_trips_through_get_and_list() { + use crate::traits::Memory; + + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document(make_doc_input( + "cliente-RFC-VECJ880326XK4", + "notes", + "Title", + "Body", + )) + .await + .expect("PII-like namespace should be canonicalized, not rejected"); + + assert!( + memory + .get("cliente-RFC-VECJ880326XK4", "notes") + .await + .unwrap() + .is_some(), + "a canonicalized namespace must stay readable by its original value" + ); + let listed = memory + .list(Some("cliente-RFC-VECJ880326XK4"), None, None) + .await + .unwrap(); + assert_eq!( + listed.len(), + 1, + "list() must canonicalize its namespace the same way the write did" + ); +} + +#[tokio::test] +async fn scanner_built_phone_shaped_keys_stay_distinct_documents() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + // Two different WhatsApp contacts, same day. The digit runs differ only in + // the phone number — the lenient content scrubber replaces both with one + // `[REDACTED_PII_PHONE]` token, which would collapse them onto a single row. + for (key, content) in [ + ("12025551234@c.us:2026-05-30", "alice thread"), + ("12025559999@c.us:2026-05-30", "bob thread"), + ] { + memory + .upsert_document(make_doc_input("whatsapp-web", key, "Chat", content)) + .await + .unwrap(); + } + + let docs = memory + .load_documents_for_scope("whatsapp-web") + .await + .unwrap(); + assert_eq!( + docs.len(), + 2, + "scanner-built phone-shaped keys must stay distinct documents, got: {:?}", + docs.iter().map(|d| d.key.clone()).collect::>() + ); + for key in ["12025551234@c.us:2026-05-30", "12025559999@c.us:2026-05-30"] { + assert!( + docs.iter().any(|d| d.key == key), + "key {key} must be stored verbatim, got: {:?}", + docs.iter().map(|d| d.key.clone()).collect::>() + ); + } +} + +#[tokio::test] +async fn scanner_built_identifiers_are_preserved_verbatim() { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + // The strict boundary predicate deliberately tolerates these shapes + // (WhatsApp group JID, iMessage E.164 chat id, padded ms timestamp); the + // content scrubber does not. Canonicalization must follow the strict set, + // or every scanner rewrites its own storage addresses. + for key in [ + "12025551234-1543890267@g.us:2026-05-30", + "imessage:+12025551234:2026-05-30", + "accepted:000001747729035001", + ] { + let doc_id = memory + .upsert_document(make_doc_input("scanner", key, "Title", "Body")) + .await + .unwrap(); + let docs = memory.load_documents_for_scope("scanner").await.unwrap(); + let doc = docs.iter().find(|d| d.document_id == doc_id).unwrap(); + assert_eq!( + doc.key, key, + "scanner-built identifier must not be rewritten" + ); + } +} + +#[tokio::test] +async fn metadata_only_write_round_trips_through_pii_like_key() { + use crate::traits::Memory; + + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + + memory + .upsert_document_metadata_only(make_doc_input( + "clients", + "cuit-20-11111111-2", + "Title", + "Body", + )) + .await + .expect("PII-like key should be canonicalized, not rejected"); + + assert!( + memory + .get("clients", "cuit-20-11111111-2") + .await + .unwrap() + .is_some(), + "the metadata-only path must canonicalize keys the same way the full upsert does" + ); +} diff --git a/crates/tinymemory-core/src/store/namespace_store/fts5.rs b/crates/tinymemory-core/src/store/namespace_store/fts5.rs new file mode 100644 index 00000000..2bb495a7 --- /dev/null +++ b/crates/tinymemory-core/src/store/namespace_store/fts5.rs @@ -0,0 +1,610 @@ +//! FTS5 episodic memory — full-text search over past sessions. +//! +//! Adds an FTS5 virtual table backed by an `episodic_log` table for storing +//! turn-level records with optional extracted lessons. The Archivist uses +//! this for post-session knowledge extraction and the `search_memory` tool +//! uses it for episodic recall. + +use parking_lot::Mutex; +use rusqlite::Connection; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::store::safety; + +/// A single episodic record (one turn or event). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EpisodicEntry { + pub id: Option, + pub session_id: String, + pub timestamp: f64, + pub role: String, + pub content: String, + pub lesson: Option, + pub tool_calls_json: Option, + pub cost_microdollars: u64, +} + +/// SQL to create the episodic tables. Called during `UnifiedMemory` init. +pub const EPISODIC_INIT_SQL: &str = r#" +CREATE TABLE IF NOT EXISTS episodic_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + timestamp REAL NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + lesson TEXT, + tool_calls_json TEXT, + cost_microdollars INTEGER DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_episodic_session + ON episodic_log(session_id, timestamp); + +CREATE VIRTUAL TABLE IF NOT EXISTS episodic_fts USING fts5( + session_id, + role, + content, + lesson, + content=episodic_log, + content_rowid=id, + tokenize='porter unicode61' +); + +-- Triggers to keep FTS5 in sync with the backing table. +CREATE TRIGGER IF NOT EXISTS episodic_ai AFTER INSERT ON episodic_log BEGIN + INSERT INTO episodic_fts(rowid, session_id, role, content, lesson) + VALUES (new.id, new.session_id, new.role, new.content, new.lesson); +END; + +CREATE TRIGGER IF NOT EXISTS episodic_ad AFTER DELETE ON episodic_log BEGIN + INSERT INTO episodic_fts(episodic_fts, rowid, session_id, role, content, lesson) + VALUES ('delete', old.id, old.session_id, old.role, old.content, old.lesson); +END; + +CREATE TRIGGER IF NOT EXISTS episodic_au AFTER UPDATE ON episodic_log BEGIN + INSERT INTO episodic_fts(episodic_fts, rowid, session_id, role, content, lesson) + VALUES ('delete', old.id, old.session_id, old.role, old.content, old.lesson); + INSERT INTO episodic_fts(rowid, session_id, role, content, lesson) + VALUES (new.id, new.session_id, new.role, new.content, new.lesson); +END; +"#; + +/// Insert an episodic entry. +/// Insert one episodic turn, returning the row id it was assigned. +/// +/// # Why the id comes back from here +/// +/// Callers used to insert and then issue `SELECT last_insert_rowid()`. That is +/// **connection-local** state: this store hands the same `Arc>` +/// to several writers, so an interleaved insert between the two statements +/// returns the wrong id — and the caller files the turn under the wrong +/// conversation segment. Reading it here, still under the lock taken for the +/// insert, is the only place it can be read correctly. +pub fn episodic_insert( + conn: &Arc>, + entry: &EpisodicEntry, +) -> anyhow::Result { + if safety::has_likely_secret(&entry.session_id) || safety::has_likely_secret(&entry.role) { + tracing::warn!( + "[memory:safety] episodic insert rejected secret-like session/role session_chars={} role_chars={}", + entry.session_id.chars().count(), + entry.role.chars().count() + ); + anyhow::bail!("episodic session_id/role cannot contain secrets"); + } + + let content = safety::sanitize_text(&entry.content); + let lesson = entry + .lesson + .as_ref() + .map(|value| safety::sanitize_text(value)); + let tool_calls_json = entry.tool_calls_json.as_ref().map(|value| { + if let Ok(parsed) = serde_json::from_str::(value) { + let sanitized = safety::sanitize_json(&parsed); + safety::Sanitized { + value: sanitized.value.to_string(), + report: sanitized.report, + } + } else { + safety::sanitize_text(value) + } + }); + + let report = content + .report + .merge( + lesson + .as_ref() + .map(|value| value.report) + .unwrap_or_default(), + ) + .merge( + tool_calls_json + .as_ref() + .map(|value| value.report) + .unwrap_or_default(), + ); + if report.changed() { + tracing::warn!( + "[memory:safety] episodic insert sanitized session_chars={} role_chars={} text_redactions={} key_redactions={} blocked_secret_hits={} depth_redactions={} pii_redactions={}", + entry.session_id.chars().count(), + entry.role.chars().count(), + report.text_redactions, + report.key_redactions, + report.blocked_secret_hits, + report.depth_redactions, + report.pii_redactions + ); + } + + let conn = conn.lock(); + conn.execute( + "INSERT INTO episodic_log (session_id, timestamp, role, content, lesson, tool_calls_json, cost_microdollars) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + rusqlite::params![ + &entry.session_id, + entry.timestamp, + &entry.role, + content.value, + lesson.map(|value| value.value), + tool_calls_json.map(|value| value.value), + entry.cost_microdollars as i64, + ], + )?; + // Still holding the lock taken above — see the doc comment. + let id = conn.last_insert_rowid(); + tracing::debug!( + "[fts5] inserted episodic entry: session={}, role={}, id={id}", + entry.session_id, + entry.role + ); + Ok(id) +} + +/// Full-text search over episodic entries. +pub fn episodic_search( + conn: &Arc>, + query: &str, + limit: usize, +) -> anyhow::Result> { + let conn = conn.lock(); + let trimmed = query.trim(); + if trimmed.is_empty() { + tracing::debug!("[fts5] search skipped — empty query"); + return Ok(Vec::new()); + } + let phrase_query = sanitize_fts_query(trimmed); + if phrase_query.is_empty() { + tracing::debug!("[fts5] search skipped — sanitised query is empty"); + return Ok(Vec::new()); + } + + let mut stmt = conn.prepare( + "SELECT el.id, el.session_id, el.timestamp, el.role, el.content, el.lesson, + el.tool_calls_json, el.cost_microdollars + FROM episodic_fts AS ef + JOIN episodic_log AS el ON ef.rowid = el.id + WHERE episodic_fts MATCH ?1 + ORDER BY rank + LIMIT ?2", + )?; + + let rows = stmt + .query_map(rusqlite::params![phrase_query, limit as i64], |row| { + Ok(EpisodicEntry { + id: row.get(0)?, + session_id: row.get(1)?, + timestamp: row.get(2)?, + role: row.get(3)?, + content: row.get(4)?, + lesson: row.get(5)?, + tool_calls_json: row.get(6)?, + cost_microdollars: row.get::<_, i64>(7)? as u64, + }) + })? + .collect::, _>>()?; + + tracing::debug!("[fts5] search returned {} results", rows.len()); + Ok(rows) +} + +/// FTS5 search across **all** sessions, optionally excluding one session +/// from the result set. Used by [`crate`] to surface +/// cross-chat conversational context for the same user/workspace (issue +/// #1505) without leaking the current chat's own history into the +/// "other chats" block. +/// +/// `exclude_session` should be the active session_id when the caller is +/// also pulling same-session entries via [`episodic_session_entries`] — +/// passing `None` returns hits from every session indexed in this DB. +/// +/// Workspace/user scope is enforced at the connection level: the SQLite +/// database lives at `/memory/...` so one DB == one workspace. +/// This helper cannot cross that boundary. +pub fn episodic_cross_session_search( + conn: &Arc>, + query: &str, + limit: usize, + exclude_session: Option<&str>, +) -> anyhow::Result> { + let conn = conn.lock(); + let trimmed = query.trim(); + if trimmed.is_empty() { + tracing::debug!("[fts5] cross-session search skipped — empty query"); + return Ok(Vec::new()); + } + + // FTS5 MATCH expressions are picky about syntax — bare phrases with + // punctuation can fail to parse. Wrap the query in double quotes so + // it's treated as a phrase (FTS5 will still tokenize it). This mirrors + // how the unified store sanitises queries before MATCH. + let phrase_query = sanitize_fts_query(trimmed); + if phrase_query.is_empty() { + tracing::debug!("[fts5] cross-session search skipped — sanitised query is empty"); + return Ok(Vec::new()); + } + + let mut stmt = match exclude_session { + Some(_) => conn.prepare( + "SELECT el.id, el.session_id, el.timestamp, el.role, el.content, el.lesson, + el.tool_calls_json, el.cost_microdollars + FROM episodic_fts AS ef + JOIN episodic_log AS el ON ef.rowid = el.id + WHERE episodic_fts MATCH ?1 AND el.session_id != ?2 + ORDER BY rank + LIMIT ?3", + )?, + None => conn.prepare( + "SELECT el.id, el.session_id, el.timestamp, el.role, el.content, el.lesson, + el.tool_calls_json, el.cost_microdollars + FROM episodic_fts AS ef + JOIN episodic_log AS el ON ef.rowid = el.id + WHERE episodic_fts MATCH ?1 + ORDER BY rank + LIMIT ?2", + )?, + }; + + let map_row = |row: &rusqlite::Row<'_>| -> rusqlite::Result { + Ok(EpisodicEntry { + id: row.get(0)?, + session_id: row.get(1)?, + timestamp: row.get(2)?, + role: row.get(3)?, + content: row.get(4)?, + lesson: row.get(5)?, + tool_calls_json: row.get(6)?, + cost_microdollars: row.get::<_, i64>(7)? as u64, + }) + }; + + let rows: Vec = match exclude_session { + Some(sid) => stmt + .query_map(rusqlite::params![phrase_query, sid, limit as i64], map_row)? + .collect::, _>>()?, + None => stmt + .query_map(rusqlite::params![phrase_query, limit as i64], map_row)? + .collect::, _>>()?, + }; + + // Never log the raw query string — may contain secrets / PII. Emit a + // stable non-reversible hash + length instead so cross-session + // diagnostics stay grep-friendly without leaking user content. + let query_hash = { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + trimmed.hash(&mut hasher); + hasher.finish() + }; + tracing::debug!( + "[fts5] cross-session search query_hash={:016x} query_len={} (exclude={:?}) returned {} results", + query_hash, + trimmed.chars().count(), + exclude_session, + rows.len() + ); + Ok(rows) +} + +/// Best-effort FTS5 query sanitiser: split user text on punctuation and +/// symbols that break the MATCH grammar, then quote each surviving token +/// so FTS5 treats it as literal text. Returns an empty string when +/// nothing usable survives — callers short-circuit to "no hits". +pub(super) fn sanitize_fts_query(query: &str) -> String { + let cleaned: String = query + .chars() + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + ' ' + } + }) + .collect(); + let tokens: Vec = cleaned + .split_whitespace() + .filter(|tok| !tok.is_empty()) + .take(8) + .map(|tok| format!("\"{tok}\"")) + .collect(); + tokens.join(" ") +} + +/// Get all entries for a session (for post-session summary). +pub fn episodic_session_entries( + conn: &Arc>, + session_id: &str, +) -> anyhow::Result> { + let conn = conn.lock(); + let mut stmt = conn.prepare( + "SELECT id, session_id, timestamp, role, content, lesson, tool_calls_json, cost_microdollars + FROM episodic_log + WHERE session_id = ?1 + ORDER BY timestamp ASC", + )?; + + let rows = stmt + .query_map(rusqlite::params![session_id], |row| { + Ok(EpisodicEntry { + id: row.get(0)?, + session_id: row.get(1)?, + timestamp: row.get(2)?, + role: row.get(3)?, + content: row.get(4)?, + lesson: row.get(5)?, + tool_calls_json: row.get(6)?, + cost_microdollars: row.get::<_, i64>(7)? as u64, + }) + })? + .collect::, _>>()?; + + Ok(rows) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn setup_db() -> Arc> { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch(EPISODIC_INIT_SQL).unwrap(); + Arc::new(Mutex::new(conn)) + } + + #[test] + fn insert_and_search() { + let conn = setup_db(); + let entry = EpisodicEntry { + id: None, + session_id: "s1".into(), + timestamp: 1000.0, + role: "user".into(), + content: "How do I deploy to production?".into(), + lesson: Some("User frequently asks about deployment".into()), + tool_calls_json: None, + cost_microdollars: 100, + }; + episodic_insert(&conn, &entry).unwrap(); + + let results = episodic_search(&conn, "deploy production", 10).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].session_id, "s1"); + assert!(results[0].content.contains("deploy")); + } + + #[test] + fn session_entries() { + let conn = setup_db(); + for i in 0..3 { + episodic_insert( + &conn, + &EpisodicEntry { + id: None, + session_id: "s2".into(), + timestamp: 1000.0 + i as f64, + role: if i % 2 == 0 { "user" } else { "assistant" }.into(), + content: format!("Turn {i} content"), + lesson: None, + tool_calls_json: None, + cost_microdollars: 0, + }, + ) + .unwrap(); + } + + let entries = episodic_session_entries(&conn, "s2").unwrap(); + assert_eq!(entries.len(), 3); + assert!(entries[0].timestamp < entries[2].timestamp); + } + + #[test] + fn empty_search_returns_empty() { + let conn = setup_db(); + let results = episodic_search(&conn, "nonexistent query", 10).unwrap(); + assert!(results.is_empty()); + } + + #[test] + fn insert_redacts_secret_like_content() { + let conn = setup_db(); + episodic_insert( + &conn, + &EpisodicEntry { + id: None, + session_id: "s1".into(), + timestamp: 1000.0, + role: "user".into(), + content: "Bearer abcdefghijklmnop".into(), + lesson: Some("token=abc123".into()), + tool_calls_json: Some("{\"api_key\":\"sk-1234567890123456789012345\"}".into()), + cost_microdollars: 0, + }, + ) + .unwrap(); + + let rows = episodic_session_entries(&conn, "s1").unwrap(); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].content, "Bearer [REDACTED]"); + assert_eq!(rows[0].lesson.as_deref(), Some("[REDACTED]")); + assert_eq!( + rows[0].tool_calls_json.as_deref(), + Some("{\"api_key\":\"[REDACTED_SECRET]\"}") + ); + } + + #[test] + fn insert_rejects_secret_like_session_id() { + let conn = setup_db(); + let err = episodic_insert( + &conn, + &EpisodicEntry { + id: None, + session_id: "Bearer abcdefghijklmnop".into(), + timestamp: 1000.0, + role: "user".into(), + content: "hello".into(), + lesson: None, + tool_calls_json: None, + cost_microdollars: 0, + }, + ) + .expect_err("secret-like session_id should be rejected"); + assert!(err.to_string().contains("cannot contain secrets")); + } + + // ── Cross-session search (#1505) ───────────────────────────────────── + + fn insert_turn(conn: &Arc>, session_id: &str, ts: f64, content: &str) { + episodic_insert( + conn, + &EpisodicEntry { + id: None, + session_id: session_id.into(), + timestamp: ts, + role: "user".into(), + content: content.into(), + lesson: None, + tool_calls_json: None, + cost_microdollars: 0, + }, + ) + .unwrap(); + } + + #[test] + fn cross_session_search_surfaces_other_sessions_excluding_current() { + let conn = setup_db(); + // Chat A — user shared the durable fact + insert_turn( + &conn, + "session-a", + 1000.0, + "I prefer Postgres for new services", + ); + // Chat B — current chat, where the question is being asked + insert_turn( + &conn, + "session-b", + 2000.0, + "What database should I use today?", + ); + // Chat C — yet another chat with a related fact + insert_turn(&conn, "session-c", 1500.0, "Postgres timezone is UTC"); + + // Asking from chat B: should see session-a + session-c (not session-b) + let hits = episodic_cross_session_search(&conn, "Postgres", 10, Some("session-b")).unwrap(); + assert!( + !hits.is_empty(), + "cross-session search must surface hits from other sessions" + ); + for hit in &hits { + assert_ne!( + hit.session_id, "session-b", + "current session must be excluded from cross-session sweep, got {}", + hit.session_id + ); + } + let session_ids: std::collections::HashSet<&str> = + hits.iter().map(|h| h.session_id.as_str()).collect(); + assert!(session_ids.contains("session-a")); + assert!(session_ids.contains("session-c")); + } + + #[test] + fn cross_session_search_returns_empty_for_unknown_query() { + let conn = setup_db(); + insert_turn(&conn, "session-a", 1000.0, "I prefer Postgres"); + let hits = episodic_cross_session_search(&conn, "kubernetes", 10, None).unwrap(); + assert!( + hits.is_empty(), + "no FTS match should produce zero hits, not all rows" + ); + } + + #[test] + fn cross_session_search_handles_empty_query() { + let conn = setup_db(); + insert_turn(&conn, "session-a", 1000.0, "anything"); + let hits = episodic_cross_session_search(&conn, " ", 10, None).unwrap(); + assert!(hits.is_empty(), "empty query short-circuits to zero hits"); + } + + #[test] + fn cross_session_search_sanitises_punctuation_safely() { + let conn = setup_db(); + insert_turn(&conn, "session-a", 1000.0, "Postgres deployment notes"); + // Query with FTS5-hostile punctuation — should not panic. Tokens + // shared with the indexed row should still match (FTS5 phrase + // ANDs every quoted token, so we use words that all appear in + // the row to avoid AND-mismatch false negatives). + let hits = + episodic_cross_session_search(&conn, "\"Postgres\" (deployment)?", 10, None).unwrap(); + assert!( + !hits.is_empty(), + "punctuated query whose surviving tokens match the indexed row must still surface it" + ); + assert!(hits[0].content.contains("Postgres")); + } + + #[test] + fn episodic_search_sanitises_punctuation_safely() { + let conn = setup_db(); + insert_turn(&conn, "session-a", 1000.0, "Postgres deployment notes"); + + let hits = episodic_search(&conn, "\"Postgres\",(deployment)?", 10) + .expect("punctuated user query should not trip FTS5 syntax errors"); + + assert!( + !hits.is_empty(), + "punctuated query whose surviving tokens match the indexed row must still surface it" + ); + assert!(hits[0].content.contains("Postgres")); + } + + #[test] + fn cross_session_search_does_not_panic_on_pure_punctuation() { + let conn = setup_db(); + insert_turn(&conn, "session-a", 1000.0, "Postgres deployment notes"); + // All-punctuation query should normalise to empty and produce + // zero hits without panicking. + let hits = episodic_cross_session_search(&conn, "()*\":", 10, None).unwrap(); + assert!( + hits.is_empty(), + "punctuation-only query must produce zero hits" + ); + } + + #[test] + fn cross_session_search_no_exclusion_includes_all_matches() { + let conn = setup_db(); + insert_turn(&conn, "session-a", 1000.0, "Postgres preference"); + insert_turn(&conn, "session-b", 2000.0, "Postgres setup"); + + let hits = episodic_cross_session_search(&conn, "Postgres", 10, None).unwrap(); + let session_ids: std::collections::HashSet<&str> = + hits.iter().map(|h| h.session_id.as_str()).collect(); + assert!(session_ids.contains("session-a")); + assert!(session_ids.contains("session-b")); + } +} diff --git a/crates/tinymemory-core/src/store/safety/mod.rs b/crates/tinymemory-core/src/store/safety/mod.rs new file mode 100644 index 00000000..fe021a09 --- /dev/null +++ b/crates/tinymemory-core/src/store/safety/mod.rs @@ -0,0 +1,340 @@ +//! Secret-detection and redaction for memory writes — thin host shim over +//! `crate::engine::backend::store::safety` (W3). +//! +//! The conservative secret + PII scrubbers (`has_likely_secret`, +//! `has_likely_pii`, `sanitize_text`, `sanitize_json`) + the +//! `SanitizationReport`/`Sanitized` types are the crate's — now including the +//! full multilingual national-ID PII module (ported into the crate so the crate +//! `sanitize_text` matches this host's byte-for-byte). The host keeps only +//! [`sanitize_document_input`], which scrubs the host-specific +//! [`NamespaceDocumentInput`] shape by delegating each field to the crate +//! scrubbers. The retained test suite doubles as a byte-parity guard: it asserts +//! the crate scrubber still redacts every secret/PII pattern the host relied on. + +pub mod pii; + +use crate::store::types::NamespaceDocumentInput; + +pub use crate::engine::backend::store::safety::{ + has_likely_pii, has_likely_secret, sanitize_json, sanitize_text, SanitizationReport, Sanitized, +}; + +/// Canonical storage form of a caller-supplied memory **identifier** — a +/// namespace, a document key, or a KV key. +/// +/// An identifier is an address, not content: whatever this returns is what the +/// row is stored under, so every read / update / delete that addresses a row by +/// identifier has to canonicalize through this same function, or it looks up a +/// row the write never created (#5164). +/// +/// Two properties make that safe, and both follow the split the crate's PII +/// module documents between its **strict boundary predicate** and its **lenient +/// content scrubber**: +/// +/// * **Strict gating.** Only identifiers that trip [`has_likely_pii`] — +/// formatted / keyword-gated national IDs (`ssn-123-45-6789`, +/// `cliente-RFC-VECJ880326XK4`, `cuit-20-11111111-2`) — are rewritten. +/// `redact_pii` on its own also rewrites bare digit-run shapes, and the +/// scanners legitimately build identifiers out of those: WhatsApp JIDs +/// (`12025551234-1543890267@g.us`), iMessage `+1…` chat ids, millisecond +/// timestamps, padded counters. Rewriting those maps two distinct contacts +/// onto one `(namespace, key)`, where the upsert's `ON CONFLICT … DO UPDATE` +/// has one contact's document silently overwrite the other's. +/// * **Idempotence.** The `[REDACTED_PII_*]` placeholders carry no PII pattern +/// of their own, so canonicalizing an already-canonical identifier is a +/// no-op — which is what lets read paths canonicalize unconditionally. +pub fn canonical_identifier(value: &str) -> String { + if !has_likely_pii(value) { + return value.to_string(); + } + pii::redact_pii(value).value +} + +/// Canonical storage form of a document key: the exact transform +/// `upsert_document` / `upsert_document_metadata_only` apply before writing the +/// `memory_docs.key` column (trim, then [`canonical_identifier`]). +/// +/// Single-sourced so the by-key read paths (`Memory::get`, `Memory::forget`) +/// cannot drift from the write path. Drift there is invisible — the lookup +/// simply misses, the caller treats the row as absent and writes again, which +/// is the unthrottled loop #5164 was reported for. +pub fn canonical_document_key(key: &str) -> String { + canonical_identifier(key.trim()) +} + +/// Scrub a namespace-document input, field by field, via the crate scrubbers. +/// +/// Sanitization is content-cleaning only; provenance `taint` survives untouched +/// so the write gate's taint check still sees the real source signal. +pub fn sanitize_document_input(input: NamespaceDocumentInput) -> Sanitized { + let mut report = SanitizationReport::default(); + + let title = sanitize_text(&input.title); + report = report.merge(title.report); + let content = sanitize_text(&input.content); + report = report.merge(content.report); + + let mut tags = Vec::with_capacity(input.tags.len()); + for tag in input.tags { + let sanitized = sanitize_text(&tag); + report = report.merge(sanitized.report); + tags.push(sanitized.value); + } + + let metadata = sanitize_json(&input.metadata); + report = report.merge(metadata.report); + + Sanitized { + value: NamespaceDocumentInput { + namespace: input.namespace, + key: input.key, + title: title.value, + content: content.value, + source_type: input.source_type, + priority: input.priority, + tags, + metadata: metadata.value, + category: input.category, + session_id: input.session_id, + document_id: input.document_id, + taint: input.taint, + }, + report, + } +} + +#[cfg(test)] +mod tests { + //! Byte-parity guard over the crate scrubber: every secret/PII pattern the + //! host used to redact must still be redacted after the port. + use super::*; + use serde_json::json; + + const REDACTED_SECRET: &str = "[REDACTED_SECRET]"; + const REDACTED_PRIVATE_KEY: &str = "[REDACTED_PRIVATE_KEY]"; + const MAX_JSON_SANITIZE_DEPTH: usize = 128; + + #[test] + fn sanitize_text_redacts_bearer_and_openai_key() { + let input = "Authorization: Bearer abcdefghijklmnop and sk-1234567890123456789012345"; + let sanitized = sanitize_text(input); + assert!(sanitized.value.contains("Bearer [REDACTED]")); + assert!(!sanitized.value.contains("sk-1234567890123456789012345")); + assert!(sanitized.report.text_redactions >= 2); + } + + #[test] + fn sanitize_text_blocks_private_key_blocks() { + let input = "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----"; + let sanitized = sanitize_text(input); + assert!(sanitized.value.contains(REDACTED_PRIVATE_KEY)); + assert!(sanitized.report.blocked_secret_hits >= 1); + } + + #[test] + fn sanitize_json_redacts_sensitive_keys_and_nested_strings() { + let input = json!({ + "token": "abc123", + "nested": { "notes": "Bearer supersecretvalue", "ok": "hello" }, + "arr": ["sk-1234567890123456789012345", "safe"] + }); + let sanitized = sanitize_json(&input); + assert_eq!(sanitized.value["token"], json!(REDACTED_SECRET)); + assert_eq!(sanitized.value["nested"]["ok"], json!("hello")); + assert!(sanitized.value["nested"]["notes"] + .as_str() + .unwrap_or_default() + .contains("[REDACTED]")); + assert!(sanitized.report.key_redactions >= 1); + assert!(sanitized.report.text_redactions >= 2); + } + + #[test] + fn sanitize_json_redacts_common_sensitive_key_variants() { + let input = json!({ + "db_password": "p@ss", "secret_key": "abc123", + "api_secret": "def456", "monkey": "banana" + }); + let sanitized = sanitize_json(&input); + assert_eq!(sanitized.value["db_password"], json!(REDACTED_SECRET)); + assert_eq!(sanitized.value["secret_key"], json!(REDACTED_SECRET)); + assert_eq!(sanitized.value["api_secret"], json!(REDACTED_SECRET)); + assert_eq!(sanitized.value["monkey"], json!(REDACTED_SECRET)); + assert!(sanitized.report.key_redactions >= 4); + } + + #[test] + fn has_likely_secret_detects_common_patterns() { + assert!(has_likely_secret("api_key=abc123")); + assert!(has_likely_secret("Bearer abcdefghijklmnopqrstuvwxyz")); + assert!(has_likely_secret("xoxb-1234567890-abcdef-ghijklmnop")); + assert!(has_likely_secret("glpat-aaaaaaaaaaaaaaaaaaaa")); + assert!(has_likely_secret("SG.aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbb")); + assert!(!has_likely_secret("I prefer rust")); + } + + #[test] + fn sanitize_text_redacts_more_provider_secrets() { + let input = "auth=Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== stripe=sk_live_12345678901234567890 npm=npm_abcdefghijklmnopqrstuvwxyz"; + let sanitized = sanitize_text(input); + assert!(!sanitized.value.contains("sk_live_12345678901234567890")); + assert!(!sanitized.value.contains("npm_abcdefghijklmnopqrstuvwxyz")); + assert!(sanitized.value.contains("[REDACTED]")); + assert!(sanitized.report.text_redactions >= 2); + } + + #[test] + fn sanitize_text_redacts_oauth_url_style_params() { + let input = "https://example.com/callback?access_token=abcd1234&refresh_token=efgh5678&id_token=jwt"; + let sanitized = sanitize_text(input); + assert!(!sanitized.value.contains("abcd1234")); + assert!(!sanitized.value.contains("efgh5678")); + assert!(!sanitized.value.contains("id_token=jwt")); + assert!(sanitized.report.text_redactions >= 3); + } + + #[test] + fn sanitize_text_redacts_multiline_private_key_blocks() { + let input = "BEGIN\n-----BEGIN OPENSSH PRIVATE KEY-----\nline1\nline2\n-----END OPENSSH PRIVATE KEY-----\nEND"; + let sanitized = sanitize_text(input); + assert!(!sanitized.value.contains("OPENSSH PRIVATE KEY")); + assert!(sanitized.value.contains(REDACTED_PRIVATE_KEY)); + assert!(sanitized.report.blocked_secret_hits >= 1); + } + + #[test] + fn sanitize_text_also_redacts_pii_after_secrets() { + let input = "Token sk-abcdefghijklmnopqrstuvwxyz; CPF 111.444.777-35; phone +15551234567"; + let sanitized = sanitize_text(input); + assert!(!sanitized.value.contains("sk-abcdefghijklmnopqrstuvwxyz")); + assert!(!sanitized.value.contains("111.444.777-35")); + assert!(!sanitized.value.contains("+15551234567")); + assert!(sanitized.value.contains("[REDACTED_PII_CPF]")); + assert!(sanitized.value.contains("[REDACTED_PII_PHONE]")); + assert!(sanitized.report.text_redactions >= 1); + assert_eq!(sanitized.report.pii_redactions, 2); + } + + #[test] + fn sanitize_json_propagates_pii_redaction_into_nested_strings() { + let input = json!({ + "note": "Cliente RFC VECJ880326XK4 confirmado", + "meta": { "cuit": "20-11111111-2" } + }); + let sanitized = sanitize_json(&input); + assert!(sanitized.value["note"] + .as_str() + .unwrap_or_default() + .contains("[REDACTED_PII_RFC]")); + assert!(sanitized.value["meta"]["cuit"] + .as_str() + .unwrap_or_default() + .contains("[REDACTED_PII_CUIT]")); + assert!(sanitized.report.pii_redactions >= 2); + } + + #[test] + fn sanitize_json_redacts_values_beyond_max_depth() { + let mut nested = json!("leaf"); + for _ in 0..(MAX_JSON_SANITIZE_DEPTH + 2) { + nested = json!({ "nested": nested }); + } + let sanitized = sanitize_json(&nested); + assert!(sanitized.report.depth_redactions >= 1); + assert!(sanitized + .value + .to_string() + .contains(&format!("\"{REDACTED_SECRET}\""))); + } + + /// #5164: identifiers are storage addresses, so canonicalization follows + /// the **strict** boundary predicate. Formatted / keyword-gated national IDs + /// are rewritten; the bare digit-run shapes the scanners build identifiers + /// out of are left alone (rewriting those maps distinct contacts onto one + /// `(namespace, key)` and the upsert silently overwrites). + #[test] + fn canonical_identifier_rewrites_only_strict_pii() { + for identifier in [ + "ssn-123-45-6789", + "cliente-RFC-VECJ880326XK4", + "cuit-20-11111111-2", + "user/111.444.777-35", + ] { + let canonical = canonical_identifier(identifier); + assert_ne!( + canonical, identifier, + "strict PII identifier must be canonicalized: {identifier}" + ); + assert!( + canonical.contains("[REDACTED_PII_"), + "expected a redaction placeholder, got: {canonical}" + ); + } + + for identifier in [ + // WhatsApp group JID / 1:1 JID / broadcast, iMessage E.164 chat id, + // telegram numeric peer id, padded ms timestamp, plain namespaces. + "12025551234-1543890267@g.us:2026-05-30", + "12025551234@c.us:2026-05-30", + "imessage:+12025551234:2026-05-30", + "4123456789:2026-05-30", + "accepted:000001747729035001", + "memory/global/preferences", + "skill-gmail", + ] { + assert_eq!( + canonical_identifier(identifier), + identifier, + "scanner-built identifier must keep its identity: {identifier}" + ); + } + } + + /// Read paths canonicalize unconditionally, so the transform has to be a + /// fixed point on its own output. + #[test] + fn canonical_identifier_is_idempotent() { + for identifier in ["ssn-123-45-6789", "cliente-RFC-VECJ880326XK4", "safe-key"] { + let once = canonical_identifier(identifier); + assert_eq!(canonical_identifier(&once), once, "not idempotent: {once}"); + } + } + + /// `canonical_document_key` single-sources the write-path transform, trim + /// included — otherwise `Memory::get` would address an untrimmed key that + /// `upsert_document` never wrote. + #[test] + fn canonical_document_key_trims_before_canonicalizing() { + assert_eq!(canonical_document_key(" doc-a "), "doc-a"); + assert_eq!( + canonical_document_key(" ssn-123-45-6789 "), + canonical_identifier("ssn-123-45-6789") + ); + assert_eq!(canonical_document_key(" "), ""); + } + + #[test] + fn sanitize_document_input_preserves_taint() { + let input = NamespaceDocumentInput { + namespace: "ns".into(), + key: "k".into(), + title: "Bearer secret123456789 visible title".into(), + content: "content with sk-abcdefghijklmnopqrstuvwxyz".into(), + source_type: "sync".into(), + priority: "normal".into(), + tags: vec!["tag1".into()], + metadata: json!({"safe": "value"}), + category: "core".into(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::ExternalSync, + }; + let sanitized = sanitize_document_input(input); + assert_eq!( + sanitized.value.taint, + crate::MemoryTaint::ExternalSync, + "taint must survive sanitization unchanged" + ); + assert!(sanitized.report.text_redactions >= 1); + } +} diff --git a/crates/tinymemory-core/src/store/write_gate_tests.rs b/crates/tinymemory-core/src/store/write_gate_tests.rs new file mode 100644 index 00000000..9048d6f1 --- /dev/null +++ b/crates/tinymemory-core/src/store/write_gate_tests.rs @@ -0,0 +1,176 @@ +//! Tests for the host secret/PII write gate. +//! +//! The gate was hoisted out of `namespace_store::documents` (H0, piece 2) so +//! the storage driver never decides what counts as a secret. These tests pin +//! **both** halves of that split, which is what makes the hoist provable rather +//! than cosmetic: +//! +//! * the gated entry points (`upsert_document`, +//! `upsert_document_metadata_only`) still redact — the pre-existing +//! behaviour, unchanged; +//! * the raw driver methods (`*_presanitized`) do **not** — they persist what +//! they are handed, which is only safe because the gate is the sole caller. +//! +//! If someone folds redaction back into the driver, the second half fails. + +use std::sync::Arc; + +use serde_json::json; +use tempfile::TempDir; + +use crate::store::{NamespaceDocumentInput, UnifiedMemory}; +use tinymemory_api::host::NoopEmbedding; + +/// A private key body, split so this source file does not itself contain a +/// scanner-tripping literal in one piece. +const PRIVATE_KEY_BODY: &str = + "-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBgkq\n-----END PRIVATE KEY-----"; + +fn secret_doc(key: &str) -> NamespaceDocumentInput { + NamespaceDocumentInput { + namespace: "safe".to_string(), + key: key.to_string(), + title: "Bearer abcdefghijklmnop".to_string(), + content: PRIVATE_KEY_BODY.to_string(), + source_type: "doc".to_string(), + priority: "medium".to_string(), + tags: vec![], + metadata: json!({}), + category: "core".to_string(), + session_id: None, + document_id: None, + taint: crate::MemoryTaint::Internal, + } +} + +fn fresh() -> (TempDir, UnifiedMemory) { + let tmp = TempDir::new().unwrap(); + let memory = UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap(); + (tmp, memory) +} + +#[tokio::test] +async fn gated_upsert_document_redacts_before_the_driver_persists() { + let (_tmp, memory) = fresh(); + + memory.upsert_document(secret_doc("note")).await.unwrap(); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + assert_eq!(docs.len(), 1); + assert!( + !docs[0].content.contains("BEGIN PRIVATE KEY"), + "the gated entry point must redact private-key material, got {:?}", + docs[0].content + ); + assert!( + !docs[0].title.contains("abcdefghijklmnop"), + "the gated entry point must redact a bearer token in the title, got {:?}", + docs[0].title + ); +} + +#[tokio::test] +async fn gated_metadata_only_upsert_redacts_before_the_driver_persists() { + let (_tmp, memory) = fresh(); + + memory + .upsert_document_metadata_only(secret_doc("note")) + .await + .unwrap(); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + assert_eq!(docs.len(), 1); + assert!( + !docs[0].content.contains("BEGIN PRIVATE KEY"), + "the gated metadata-only entry point must redact, got {:?}", + docs[0].content + ); +} + +#[tokio::test] +async fn raw_driver_upsert_does_not_redact_so_the_gate_is_the_only_thing_doing_it() { + let (_tmp, memory) = fresh(); + + // Bypassing the gate deliberately: this is what proves redaction now lives + // in `write_gate` and not inside the persistence call. + memory + .upsert_document_presanitized(secret_doc("note")) + .await + .unwrap(); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + assert_eq!(docs.len(), 1); + assert!( + docs[0].content.contains("BEGIN PRIVATE KEY"), + "the raw driver method must persist its input verbatim — if this fails, redaction has \ + been folded back into the storage layer, got {:?}", + docs[0].content + ); +} + +#[tokio::test] +async fn raw_metadata_only_driver_upsert_does_not_redact() { + let (_tmp, memory) = fresh(); + + memory + .upsert_document_metadata_only_presanitized(secret_doc("note")) + .await + .unwrap(); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + assert_eq!(docs.len(), 1); + assert!( + docs[0].content.contains("BEGIN PRIVATE KEY"), + "the raw metadata-only driver method must persist its input verbatim, got {:?}", + docs[0].content + ); +} + +#[tokio::test] +async fn gate_rejects_a_secret_like_namespace_or_key_before_touching_the_driver() { + let (_tmp, memory) = fresh(); + + let mut secret_key = secret_doc("sk-1234567890123456789012345"); + secret_key.namespace = "safe".to_string(); + let err = memory.upsert_document(secret_key).await.unwrap_err(); + assert!( + err.contains("cannot contain secrets"), + "secret-like key must be refused, got {err:?}" + ); + + let mut secret_ns = secret_doc("note"); + secret_ns.namespace = "sk-1234567890123456789012345".to_string(); + let err = memory + .upsert_document_metadata_only(secret_ns) + .await + .unwrap_err(); + assert!( + err.contains("cannot contain secrets"), + "secret-like namespace must be refused, got {err:?}" + ); + + // Nothing reached storage. + assert!(memory + .load_documents_for_scope("safe") + .await + .unwrap() + .is_empty()); +} + +#[tokio::test] +async fn gate_canonicalizes_a_pii_like_key_and_keeps_the_row_addressable() { + let (_tmp, memory) = fresh(); + + memory + .upsert_document(secret_doc("ssn-123-45-6789")) + .await + .unwrap(); + + let docs = memory.load_documents_for_scope("safe").await.unwrap(); + assert_eq!(docs.len(), 1); + assert!( + !docs[0].key.contains("123-45-6789"), + "a PII-like key must be canonicalized rather than stored raw, got {:?}", + docs[0].key + ); +} diff --git a/crates/tinymemory/Cargo.toml b/crates/tinymemory/Cargo.toml new file mode 100644 index 00000000..2cbff5c1 --- /dev/null +++ b/crates/tinymemory/Cargo.toml @@ -0,0 +1,182 @@ +[package] +name = "tinymemory" +# Not published: `tinymemory-core` depends on `tinycortex-api`, which is +# consumed by path and is not on crates.io, so `cargo package` cannot resolve +# the graph. Every consumer takes this repo by path or git. +publish = false +version = "1.0.1" +edition = "2021" +rust-version = "1.96" +license = "MIT" +description = "Engine-neutral memory contract, driver registry, and engine adapters" +repository = "https://github.com/tinyhumansai/tinymemory" +documentation = "https://docs.rs/tinymemory" +readme = "../../README.md" +keywords = ["memory", "agent", "llm", "retrieval"] +categories = ["database"] + +[dependencies] +# The contract itself. Re-exported wholesale from `src/lib.rs` so a host takes +# one dependency rather than two, and so `tinymemory::MemoryProvider` and +# `tinymemory_api::provider::MemoryProvider` are the same type. +tinymemory-api = { path = "../tinymemory-api" } +# The engines, each behind its own feature (#18 §D1). Optional, so the default +# build is still the contract, the registry and the mandatory composition and +# nothing that links C. +# +# This direction only became legal once `mandatory` and the reserved driver ids +# moved into `tinymemory-api`: the adapters used to depend on this crate for +# them, and a facade depending back on the adapters is a package cycle cargo +# forbids. +tinymemory-tinycortex = { path = "../tinymemory-tinycortex", optional = true } +tinymemory-remote = { path = "../tinymemory-remote", optional = true } +# The rest of the workspace, each behind the feature named after it. Optional +# for the same reason the adapters are: a host that wants the ports and nothing +# else must not link a storage engine, an HTTP stack, or a native library. +# +# `tinymemory-core` is a normal dependency here and this crate is a *dev* +# dependency of it. Cargo permits that, because a cycle closed by a +# dev-dependency edge is not a build cycle — but the core side must stay +# dev-only or the graph becomes unbuildable. +tinymemory-core = { path = "../tinymemory-core", optional = true } +tinymemory-sync = { path = "../tinymemory-sync", optional = true } +tinymemory-sources = { path = "../tinymemory-sources", optional = true } +tinymemory-conformance = { path = "../tinymemory-conformance", optional = true } +# The mandatory capability families are `async fn`s on object-safe traits. +async-trait = "0.1" +# `Memory` is anyhow-typed; `mandatory::engine_error` maps it onto `MemoryError`. +anyhow = "1" +# Diagnostics on the shared store/recall/export paths. +log = "0.4" +# `ExportRecord::payload` is a `serde_json::Value`. +serde_json = "1" + +# `DriverClass` is read out of a host's config file, so its serde form is the +# config form and is pinned by a test. +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +# The mandatory-family tests are async. +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +# `TestHostConfig`, for the driver-selection tests. A dev-dependency: the +# facade must not carry a test double into a consumer's graph. +tinymemory-api = { path = "../tinymemory-api", features = ["test-support"] } +# The reference driver and the behavioural suite, for the workspace-level +# integration tests. A dev-dependency as well as an optional normal one: the +# facade's own suite must have it regardless of which features are selected. +tinymemory-conformance = { path = "../tinymemory-conformance" } +# The extracted Composio normalisers, for the integration test that runs them +# against a driver that is not TinyCortex (issue #18 §B3). +tinymemory-sync = { path = "../tinymemory-sync" } + +# Every crate in this workspace is reachable from here by a feature named after +# it, so a host takes one dependency and states what it wants rather than +# assembling the graph itself. Three groups, all additive: +# +# engines — which backend implements `MemoryProvider` +# subsystems — which workspace crates are re-exported as modules +# capabilities — optional behaviour that needs a specific engine +# +# Nothing is on by default. Naming no feature gets the contract, the registry +# and the mandatory composition: no storage engine, no HTTP stack, no native +# library. `scripts/ci/dependency-budget.sh` holds that to a ceiling. +[features] +default = [] + +# --- Engines --------------------------------------------------------------- +# Each pulls exactly the adapter that serves it. `supermemory`, `mem0` and +# `cognee` share one adapter crate, so enabling several costs one dependency +# rather than three. +tinycortex = ["dep:tinymemory-tinycortex"] +supermemory = ["dep:tinymemory-remote"] +mem0 = ["dep:tinymemory-remote"] +cognee = ["dep:tinymemory-remote"] +# Every engine at once. A host that binds its driver from configuration rather +# than at compile time wants this: `DriverRegistry` admission is a static policy +# table, so which adapters are compiled in decides what it can actually bind. +engines = ["tinycortex", "supermemory", "mem0", "cognee"] + +# --- Subsystems ------------------------------------------------------------ +# The workspace crates that are not adapters, each re-exported as a module of +# the same name. Off by default because each is a real weight: `core` links a +# bundled SQLite and the embedded engine, `sources` an HTTP stack behind its own +# `network` gate. +core = ["dep:tinymemory-core"] +sync = ["dep:tinymemory-sync"] +sources = ["dep:tinymemory-sources"] +# The behavioural suite every driver must pass, for a host that wants to hold +# its *own* `MemoryProvider` to the contract. Contract-only in its dependencies, +# so it is cheap; still opt-in, because a test harness has no business in a +# production graph unless it was asked for. +conformance = ["dep:tinymemory-conformance"] +# The network readers in `tinymemory-sources` — GitHub, RSS, web pages. Implies +# the crate that holds them, so asking for the readers cannot produce a build +# where they are absent. +sources-network = ["sources", "tinymemory-sources/network"] + +# --- Capability add-ons ---------------------------------------------------- +# Each requires the engine that serves it, so asking for a capability cannot +# produce a build where nothing implements it. +memory-git = ["tinycortex", "tinymemory-tinycortex/memory-git"] +# The macOS CNContactStore address-book seeding path, served by the embedded +# engine through `tinymemory-core`. No-op off macOS. +contacts = ["core", "tinymemory-core/contacts"] + +# --- Test support ---------------------------------------------------------- +# The workspace's test doubles and helpers, for a downstream harness that needs +# them: `TestHostConfig` from the contract, and `tinymemory-core`'s chat and +# tool-memory helpers when `core` is on. A production build must not carry them, +# which is why this is not implied by anything above. +test-support = [ + "tinymemory-api/test-support", + "tinymemory-core?/test-support", +] + +# Everything, for the sake of one name. Deliberately excludes `test-support`: +# "give me the whole workspace" is not the same request as "give me the test +# doubles", and rolling them together is how a harness reaches a release build. +full = [ + "engines", + "core", + "sync", + "sources-network", + "conformance", + "memory-git", +] + +# Lints apply to this package only, deliberately. `crates/tinymemory-api` is +# contract code moved verbatim from `tinycortex-api` and is held +# byte-identical; subjecting it to a stricter lint set than it was written under +# would force cosmetic edits through a surface whose serde representations and +# enum wire strings are persisted on disk. Adapter crates opt in individually. +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" +missing_debug_implementations = "warn" +unreachable_pub = "warn" +rust_2018_idioms = { level = "warn", priority = -1 } + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +# Library code must not panic on its own; tests and examples may. +unwrap_used = "warn" +expect_used = "warn" +panic = "warn" +todo = "warn" +unimplemented = "warn" +# Public fallible/panicking APIs must document their failure modes. +missing_errors_doc = "warn" +missing_panics_doc = "warn" +# Documentation hygiene. +doc_markdown = "warn" +# `#[must_use]` on pure public functions. +must_use_candidate = "warn" + +[lints.rustdoc] +broken_intra_doc_links = "warn" +private_intra_doc_links = "warn" + +[[example]] +name = "tinycortex" +required-features = ["tinycortex"] From ab8eac26485cfc71d2acbe701131d14349b30053 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:46:22 +0300 Subject: [PATCH 04/18] chore(deps): bump tinymemory crate versions to 0.2.0 Update all six tinymemory crate versions from 0.1.0 to 0.2.0 to prepare for the next release cycle, ensuring consistent versioning across the workspace. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-conformance/Cargo.toml | 2 +- crates/tinymemory-core/Cargo.toml | 12 ++++++------ crates/tinymemory-module/Cargo.toml | 6 +++--- crates/tinymemory-remote/Cargo.toml | 4 ++-- crates/tinymemory-sources/Cargo.toml | 2 +- crates/tinymemory-tinycortex/Cargo.toml | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/tinymemory-conformance/Cargo.toml b/crates/tinymemory-conformance/Cargo.toml index 809620e7..286585f5 100644 --- a/crates/tinymemory-conformance/Cargo.toml +++ b/crates/tinymemory-conformance/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/tinyhumansai/tinymemory" # prove that a driver is interchangeable, because it would already have chosen # one. In particular it must not reach `tinymemory-core`, which links a bundled # SQLite and the embedded engine unconditionally (issue #18 §D). -tinymemory-api = { path = "../api" } +tinymemory-api = { path = "../tinymemory-api" } # `MemoryProvider` and its families are object-safe async traits. async-trait = "0.1" # `ExportRecord::payload` is a `serde_json::Value`, so the portability diff --git a/crates/tinymemory-core/Cargo.toml b/crates/tinymemory-core/Cargo.toml index c0037888..d9eeef6a 100644 --- a/crates/tinymemory-core/Cargo.toml +++ b/crates/tinymemory-core/Cargo.toml @@ -15,16 +15,16 @@ readme = "../README.md" [dependencies] # The contract. `tinymemory-core` implements and consumes it; the host seam # traits (config, event sink, embeddings, chat) live in `tinymemory_api::host`. -tinymemory-api = { path = "../api" } +tinymemory-api = { path = "../tinymemory-api" } # Composio payload normalisers, extracted out of the engine (issue #18 §B3). # They were reached through `tinycortex` until now, which meant a host binding a # different engine could not have Composio sync despite none of this code # caring which engine is bound. -tinymemory-sync = { path = "../sync" } +tinymemory-sync = { path = "../tinymemory-sync" } # The memory-source contracts and readers (#18 §B4). `network` because this # crate's sync path drives the GitHub/RSS/web-page readers; a host that only # reads local folders can take the crate without them. -tinymemory-sources = { path = "../sources", features = ["network"] } +tinymemory-sources = { path = "../tinymemory-sources", features = ["network"] } # Slack mention tokens (`<@U…>`) in synced messages are rewritten to display # names before ingestion. Returns to the normal graph for this one user — # #18 §D2 removed it when nothing used it, which is no argument against a @@ -78,11 +78,11 @@ walkdir = "2" wiremock = "0.6" # `TestHostConfig` — the concrete `MemoryHostConfig` the extracted test suites # build, since `Config` is a trait object and cannot be `Default`ed. -tinymemory-api = { path = "../api", features = ["test-support"] } +tinymemory-api = { path = "../tinymemory-api", features = ["test-support"] } # The driver conformance suite (#18 §E1). A dev-dependency only: it exists to # hold this crate's own store to the same contract the adapters are held to. # No cycle — `tinymemory-conformance` depends on `tinymemory-api` alone. -tinymemory-conformance = { path = "../conformance" } +tinymemory-conformance = { path = "../tinymemory-conformance" } # Test-only. `store::factories`' tests stand up a throwaway HTTP server to # exercise the embedder's failure paths — the four `axum` references in this # crate are all inside `mod tests`. It was declared as a normal dependency, @@ -95,7 +95,7 @@ axum = { version = "0.8", default-features = false, features = ["http1", "json", # through dev-deps, and this must not become a normal one, which is exactly what # #18 §D1 removed (a facade that depends on adapters cannot have adapters that # depend, through core, back on it). -tinymemory = { path = ".." } +tinymemory = { path = "../tinymemory" } tempfile = "3" tokio = { version = "1", features = ["test-util"] } diff --git a/crates/tinymemory-module/Cargo.toml b/crates/tinymemory-module/Cargo.toml index ced3f1ee..f7e1bde1 100644 --- a/crates/tinymemory-module/Cargo.toml +++ b/crates/tinymemory-module/Cargo.toml @@ -25,13 +25,13 @@ crate-type = ["rlib", "cdylib"] # The contract. Every type crossing the bus is one of these, and all of them # already carry serde impls — which is why this module needs no `wire` module of # its own, unlike the tinywallet one. -tinymemory-api = { path = "../../api" } +tinymemory-api = { path = "../tinymemory-api" } # `MemoryTraitProvider`, which pairs a `Memory` backend with a driver id. tinymemory = { path = "../.." } # The engine and the seam that adapts it. Carrying these is the entire point of # the module: they are 14.7s of the host's critical build path, and a host that # loads this binary compiles neither. -tinymemory-core = { path = "../../core", features = ["memory-git"] } +tinymemory-core = { path = "../tinymemory-core", features = ["memory-git"] } tinymemory-tinycortex = { path = "../../adapters/tinycortex", features = ["memory-git"] } # `people` is enabled here rather than inherited: the module serves the # `MemoryPeople` family directly off the engine's people store, so it needs the @@ -85,7 +85,7 @@ tempfile = "3" # does not reach it. Without this, cargo resolves the git copy alongside the # path copy and `MemoryTaint` from one is not the same type as from the other. [patch."https://github.com/tinyhumansai/tinymemory"] -tinymemory-api = { path = "../../api" } +tinymemory-api = { path = "../tinymemory-api" } [patch.crates-io] tinycortex = { path = "../../vendor/tinycortex" } diff --git a/crates/tinymemory-remote/Cargo.toml b/crates/tinymemory-remote/Cargo.toml index ea30747d..f824c597 100644 --- a/crates/tinymemory-remote/Cargo.toml +++ b/crates/tinymemory-remote/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/tinyhumansai/tinymemory" [dependencies] # The engine-neutral contract and mandatory-family composition. -tinymemory-api = { path = "../../api" } +tinymemory-api = { path = "../tinymemory-api" } # Memory is an object-safe async trait and each native HTTP dialect is async. async-trait = "0.1" # The storage trait deliberately uses opaque backend errors. @@ -34,7 +34,7 @@ sha2 = "0.11" [dev-dependencies] # The behavioural contract suite, run against these adapters over their own # native doubles (issue #18 §E1, acceptance criterion 5). -tinymemory-conformance = { path = "../../conformance" } +tinymemory-conformance = { path = "../tinymemory-conformance" } # Adapter tests run lightweight native-API doubles over a real TCP transport. axum = { version = "0.8", features = ["multipart"] } tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] } diff --git a/crates/tinymemory-sources/Cargo.toml b/crates/tinymemory-sources/Cargo.toml index 8c04a23d..1e6c8345 100644 --- a/crates/tinymemory-sources/Cargo.toml +++ b/crates/tinymemory-sources/Cargo.toml @@ -24,7 +24,7 @@ anyhow = "1" # The contract, for `MemoryError` — `ensure_within_base` reports `PathEscape`, # which the contract already models, so readers speak the same error language # as the drivers they feed. -tinymemory-api = { path = "../api" } +tinymemory-api = { path = "../tinymemory-api" } # The readers are the I/O half of this crate: they walk folders, fetch feeds, # scrape pages and call the GitHub API. These are theirs, and the reason the # readers live here rather than in the contract crate — `tinymemory-api` diff --git a/crates/tinymemory-tinycortex/Cargo.toml b/crates/tinymemory-tinycortex/Cargo.toml index df663d86..efa35e2a 100644 --- a/crates/tinymemory-tinycortex/Cargo.toml +++ b/crates/tinymemory-tinycortex/Cargo.toml @@ -13,7 +13,7 @@ repository = "https://github.com/tinyhumansai/tinymemory" [dependencies] # The contract this adapter targets. -tinymemory-api = { path = "../../api" } +tinymemory-api = { path = "../tinymemory-api" } # The engine being adapted. A version requirement rather than a path, so a host # that already pins its own TinyCortex checkout unifies both onto one copy # through its `[patch.crates-io]`; the workspace root patches it to the nested @@ -27,7 +27,7 @@ tinycortex = { version = "0.1", default-features = false } # makes this adapter heavier than the mandatory-only version it replaces; §D # feature-gates that weight once §A3 has removed the direct engine call sites # core still has. -tinymemory-core = { path = "../../core" } +tinymemory-core = { path = "../tinymemory-core" } # `spawn_blocking`: every family method runs synchronous engine work off the # async executor rather than blocking it. tokio = { version = "1", features = ["rt"] } @@ -52,7 +52,7 @@ anyhow = "1" [dev-dependencies] # The behavioural contract suite, run against this crate's drivers # (issue #18 §E1, acceptance criterion 5). -tinymemory-conformance = { path = "../../conformance" } +tinymemory-conformance = { path = "../tinymemory-conformance" } # The full-provider conformance target opens a real workspace store. tempfile = "3" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } From 24f8170172999191fa2e123e8267eecd425d4d19 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:46:31 +0300 Subject: [PATCH 05/18] chore(deps): bump tinymemory-core version to 0.2.0 Updated the tinymemory-core crate version from 0.1.0 to 0.2.0 in its Cargo.toml to reflect new changes, and adjusted the tinymemory-module dependency accordingly to maintain compatibility. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-core/Cargo.toml | 2 +- crates/tinymemory-module/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tinymemory-core/Cargo.toml b/crates/tinymemory-core/Cargo.toml index d9eeef6a..07eb474b 100644 --- a/crates/tinymemory-core/Cargo.toml +++ b/crates/tinymemory-core/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.96" license = "MIT" description = "The engine-neutral memory subsystem: store, summary tree, sync pipelines, ingestion and recall" repository = "https://github.com/tinyhumansai/tinymemory" -readme = "../README.md" +readme = "../../README.md" [dependencies] # The contract. `tinymemory-core` implements and consumes it; the host seam diff --git a/crates/tinymemory-module/Cargo.toml b/crates/tinymemory-module/Cargo.toml index f7e1bde1..45845ba4 100644 --- a/crates/tinymemory-module/Cargo.toml +++ b/crates/tinymemory-module/Cargo.toml @@ -27,12 +27,12 @@ crate-type = ["rlib", "cdylib"] # its own, unlike the tinywallet one. tinymemory-api = { path = "../tinymemory-api" } # `MemoryTraitProvider`, which pairs a `Memory` backend with a driver id. -tinymemory = { path = "../.." } +tinymemory = { path = "../tinymemory" } # The engine and the seam that adapts it. Carrying these is the entire point of # the module: they are 14.7s of the host's critical build path, and a host that # loads this binary compiles neither. tinymemory-core = { path = "../tinymemory-core", features = ["memory-git"] } -tinymemory-tinycortex = { path = "../../adapters/tinycortex", features = ["memory-git"] } +tinymemory-tinycortex = { path = "../tinymemory-tinycortex", features = ["memory-git"] } # `people` is enabled here rather than inherited: the module serves the # `MemoryPeople` family directly off the engine's people store, so it needs the # gate on even though `tinymemory-core` only re-exports the domain. From f2109e21529f379257bf88ad76f851bd871dbf0f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:46:52 +0300 Subject: [PATCH 06/18] feat(tinymemory): add feature-gated re-exports for core subsystems Add conditional re-exports for the core, sync, sources, and conformance crates behind their respective feature flags, so that hosts can select exactly the subsystems they need through a single dependency on tinymemory rather than pulling in each crate individually. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory/src/lib.rs | 42 ++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/crates/tinymemory/src/lib.rs b/crates/tinymemory/src/lib.rs index 1b829b02..355f8f65 100644 --- a/crates/tinymemory/src/lib.rs +++ b/crates/tinymemory/src/lib.rs @@ -19,8 +19,12 @@ //! rather than re-deriving the same four subtleties. //! - **[`registry`]** — driver admission. Which driver ids exist, what class //! each binds as, and the fail-closed rule for out-of-process drivers. -//! - **Engine adapters** — one crate per engine under `adapters/`, each -//! implementing [`provider::MemoryProvider`] over a concrete engine. +//! - **Engine adapters** — one crate per engine under `crates/`, each +//! implementing [`provider::MemoryProvider`] over a concrete engine, and +//! each selected by the feature named after it. +//! - **Subsystems** — [`core`], [`sync`], [`sources`] and [`conformance`], +//! re-exported behind features of the same name so a host takes one +//! dependency on this crate and states what it wants. //! //! ## What is deliberately *not* here //! @@ -88,6 +92,40 @@ pub use tinymemory_tinycortex as tinycortex; /// happen without changing how hosts ask for them. #[cfg(any(feature = "supermemory", feature = "mem0", feature = "cognee"))] pub use tinymemory_remote as remote; + +/// The engine-neutral memory subsystem, when the `core` feature is on. +/// +/// The store, summary tree, sync pipelines, ingestion and recall. This is the +/// heaviest thing the workspace offers — it links a bundled SQLite and the +/// embedded engine — which is why it is a feature rather than a dependency +/// every consumer of the contract pays for. +#[cfg(feature = "core")] +pub use tinymemory_core as core; + +/// The Composio payload normalisers, when the `sync` feature is on. +/// +/// Pure `Value -> Value` transforms with no engine behind them, which is the +/// point of the crate: a host binding a driver that is not TinyCortex can still +/// run them. +#[cfg(feature = "sync")] +pub use tinymemory_sync as sync; + +/// The memory-source contracts and readers, when the `sources` feature is on. +/// +/// The readers that fetch over the network — GitHub, RSS, web pages — sit +/// behind `sources-network` on top of this, so a host that only reads local +/// folders links no HTTP stack. +#[cfg(feature = "sources")] +pub use tinymemory_sources as sources; + +/// The behavioural conformance suite, when the `conformance` feature is on. +/// +/// Every driver admitted by [`registry`] must pass it. Exposed here so a host +/// can hold a `MemoryProvider` of its own to the same contract the bundled +/// adapters are held to, without taking a second dependency. +#[cfg(feature = "conformance")] +pub use tinymemory_conformance as conformance; + pub mod registry; // The contract, re-exported wholesale. Listed module by module rather than as a From c0f2311b09e10820e6c8670d11a82e7ed4ea8cbb Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 23:59:27 +0300 Subject: [PATCH 07/18] chore(deps): update Cargo.lock for new dependencies The lockfile is updated to include the newly added `tinymemory-core` and `tinymemory-sources` dependencies, ensuring the resolved versions are recorded for reproducible builds. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b8a40a68..14d1addc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1883,7 +1883,9 @@ dependencies = [ "serde_json", "tinymemory-api", "tinymemory-conformance", + "tinymemory-core", "tinymemory-remote", + "tinymemory-sources", "tinymemory-sync", "tinymemory-tinycortex", "tokio", From e1641e160c573d697b66b049cca91f94f75bd824 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:00:41 +0300 Subject: [PATCH 08/18] fix(ci): update engine containment paths after crate rename The engine containment script now references the renamed `crates/tinymemory-core/src/` directory instead of the old `core/src/` path, matching the project's new crate layout. Auto-committed-on: dragonfly Co-authored-by: Medulla --- scripts/ci/engine-containment.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/ci/engine-containment.sh b/scripts/ci/engine-containment.sh index 1290a0d9..89b87c69 100755 --- a/scripts/ci/engine-containment.sh +++ b/scripts/ci/engine-containment.sh @@ -2,10 +2,10 @@ # Issue #18 §C1 acceptance: nothing outside core's engine module names the # tinycortex crate in code. # -# The issue's literal check -- `grep -rl tinycortex core/src` -- counts prose: +# The issue's literal check -- `grep -rl tinycortex crates/tinymemory-core/src` -- counts prose: # doc comments, string literals, and log tags like "[tinycortex:sync]" match it # and always will. What the criterion *means* is that no file outside -# `core/src/engine/` reaches the engine through a code path. This script tests +# `crates/tinymemory-core/src/engine/` reaches the engine through a code path. This script tests # that: a `use tinycortex...` item or a `tinycortex::` path segment, in a # non-comment position, outside the engine module. set -euo pipefail @@ -22,8 +22,8 @@ cd "$(dirname "$0")/../.." # yield false POSITIVES (prose inside `/* */` is not stripped), which fails # safe: a human looks, nothing slips through. offenders="$( - grep -rln --include='*.rs' 'tinycortex' core/src \ - | grep -v '^core/src/engine/' \ + grep -rln --include='*.rs' 'tinycortex' crates/tinymemory-core/src \ + | grep -v '^crates/tinymemory-core/src/engine/' \ | while read -r f; do # Strip string literals first (so a `//` inside one cannot hide the # rest of the line), then line comments; then match path positions. @@ -36,10 +36,10 @@ offenders="$( )" if [ -n "$offenders" ]; then - echo "core/src files outside the engine module reach tinycortex in code:" >&2 + echo "tinymemory-core files outside the engine module reach tinycortex in code:" >&2 echo "$offenders" | sed 's/^/ /' >&2 echo >&2 - echo "Route through core/src/engine/ (the seam) or the memory contract." >&2 + echo "Route through crates/tinymemory-core/src/engine/ (the seam) or the memory contract." >&2 exit 1 fi -echo "engine containment holds: no code path names tinycortex outside core/src/engine/" +echo "engine containment holds: no code path names tinycortex outside crates/tinymemory-core/src/engine/" From 0018b090299ddd32692334c7af767d869aad4e0e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:00:55 +0300 Subject: [PATCH 09/18] fix(ci): update path references in workflow comments and example command Updated the workflow file to reflect the renamed crate path from `api/Cargo.toml` to `crates/tinymemory-api/Cargo.toml` in a comment, and changed the example run command to use the explicit package flag `-p tinymemory` instead of the default workspace example. These changes keep the CI configuration consistent with the current project structure. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 346fdcaa..06132800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: # # This is also the configuration that keeps the promise the feature # exists for: no `git2` / `libgit2-sys` in the graph. - # `api/Cargo.toml` spells out this exact command in a comment and asks + # `crates/tinymemory-api/Cargo.toml` spells out this command in a comment and asks # that the contract crate never link a storage engine, a native library, # an HTTP client, or an async runtime. It was left as a comment, so # nothing checked it — and a forbidden dependency arrives transitively, @@ -84,7 +84,7 @@ jobs: run: ./scripts/ci/dependency-budget.sh - name: Run the bundled example - run: cargo run --example basic + run: cargo run -p tinymemory --example basic - name: Assert engine containment (#18 §C1) run: ./scripts/ci/engine-containment.sh From 54afe7cf26ed6ccbb1fead8daea5f83b4a94c96e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:01:11 +0300 Subject: [PATCH 10/18] fix(ci): pin release workflow to the tinymemory facade crate The release workflow previously used `cargo metadata`'s first package, which is unreliable in a virtual workspace where the root manifest has no package. The workflow now explicitly selects the `tinymemory` facade crate by name, updates its `Cargo.toml` instead of the root, and adjusts comments and `git add` paths accordingly. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/release.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11e86f08..6c352ca8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,9 +83,17 @@ jobs: run: | set -euo pipefail + # The root manifest is a virtual workspace — every crate lives under + # `crates/`, and there is no root package. So name the facade rather + # than taking `.packages[0]`, which is whichever member cargo happened + # to list first and would silently start releasing a different crate's + # version the day that order changes. metadata="$(cargo metadata --format-version 1 --no-deps)" - crate_name="$(jq -r '.packages[0].name' <<< "$metadata")" - current_version="$(jq -r '.packages[0].version' <<< "$metadata")" + crate_name="tinymemory" + current_version="$( + jq -r --arg name "$crate_name" \ + '.packages[] | select(.name == $name) | .version' <<< "$metadata" + )" if [[ -z "$current_version" || "$current_version" == "null" ]]; then echo "Could not resolve the current crate version" >&2 exit 1 @@ -127,7 +135,7 @@ jobs: echo "tag=${tag}" } >> "$GITHUB_OUTPUT" - # Bumps the root `[package]` version only. That is sufficient *because* no + # Bumps the facade's `[package]` version only. That is sufficient *because* no # intra-workspace path dependency carries a `version = "…"` requirement — # a `minor` bump to 0.2.0 against a sibling asking for `^0.1.0` fails # resolution here with "failed to select a version", which is exactly how @@ -152,19 +160,20 @@ jobs: echo "An intra-workspace path dependency carries a version requirement:" >&2 echo "$offenders" >&2 echo >&2 - echo "Bumping the root package will fail to resolve against it. Nothing here" >&2 + echo "Bumping the facade will fail to resolve against it. Nothing here" >&2 echo "is published to crates.io, so drop the 'version' key and keep 'path'." >&2 exit 1 fi - perl -0pi -e 's/(\[package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' Cargo.toml + perl -0pi -e 's/(\[package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' \ + crates/tinymemory/Cargo.toml cargo update -p "$CRATE_NAME" --precise "$NEXT_VERSION" # There are TWO Cargo worlds here, and the module's is the one the # release actually builds. `crates/tinymemory-module` is its own # workspace root with its own `Cargo.lock` (see the root Cargo.toml - # comment for why), and it depends on the root crate by path — so - # bumping the root version leaves that lockfile recording the old one. + # comment for why), and it depends on the facade by path — so bumping + # the facade's version leaves that lockfile recording the old one. # # `native-bundles` then builds with `--locked` and every one of the # eleven jobs fails with "cannot update the lock file … because @@ -188,7 +197,8 @@ jobs: # Both lockfiles: the module's own workspace lock is what the bundle # jobs build against with `--locked`, so a tag that omits it cannot be # built at all. - git add Cargo.toml Cargo.lock crates/tinymemory-module/Cargo.lock + git add crates/tinymemory/Cargo.toml Cargo.lock \ + crates/tinymemory-module/Cargo.lock git commit -m "Release ${RELEASE_TAG}" git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}" From fc0a67e99bc14b796ee5e83e9902716a95a9f16c Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:07:50 +0300 Subject: [PATCH 11/18] docs(readme): update crate layout and feature documentation The README now reflects the new flat `crates/` directory structure with individual packages, replacing the old top-level `api/`, `src/`, `core/`, and `adapters/` layout. A new Features section documents the `tinymemory` facade's feature flags and their dependency relationships, making it clear how hosts can select only the capabilities they need. File paths throughout the document are updated to match the new crate locations. Auto-committed-on: dragonfly Co-authored-by: Medulla --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 149c7ef7..b4940cb2 100644 --- a/README.md +++ b/README.md @@ -11,27 +11,36 @@ its place without the host learning anything new. ## Layout ```text -api/ tinymemory-api — the contract. Dependency-light on - purpose: depending on it never drags in SQLite, git2, - reqwest, or an async runtime. -src/ -├── lib.rs re-exports the contract wholesale, so a host takes one -│ dependency and the types are the same types -├── registry/ driver admission — which ids exist, what class each -│ binds as, and the fail-closed external-driver gate -└── mandatory/ the three mandatory capability families, composed once - over the `Memory` storage trait -core/ tinymemory-core — the substance: ingestion, the summary - tree, chunk storage, entities, the graph, the diff - ledger, goals, tool-memory, and the Composio sync layer. - The largest crate here by a wide margin, and the one a - real host actually depends on. Unlike `api/` it is not - dependency-light: today it links the TinyCortex engine, - a bundled SQLite, and an HTTP stack unconditionally. -adapters/ -├── tinycortex/ the TinyCortex engine seen through the contract -└── remote/ native HTTP dialects for Supermemory, Mem0, and Cognee crates/ +├── tinymemory/ the facade a host depends on. Re-exports the contract +│ wholesale, so the types are the same types, and reaches +│ every other crate here through a feature named after it +│ ├── src/lib.rs the entire public re-export surface +│ ├── src/registry/ driver admission — which ids exist, what class each +│ │ binds as, and the fail-closed external-driver gate +│ ├── tests/ integration tests against the public API only +│ └── examples/ runnable, compiled-in-CI usage examples +├── tinymemory-api/ the contract. Dependency-light on purpose: depending on +│ it never drags in SQLite, git2, reqwest, or an async +│ runtime +├── tinymemory-core/ the substance: ingestion, the summary tree, chunk +│ storage, entities, the graph, the diff ledger, goals, +│ tool-memory, and the Composio sync layer. The largest +│ crate here by a wide margin. Unlike the contract it is +│ not dependency-light: today it links the TinyCortex +│ engine, a bundled SQLite, and an HTTP stack +│ unconditionally +├── tinymemory-sync/ the engine-neutral Composio payload normalisers, so a +│ host binding a driver that is not TinyCortex can run +│ them +├── tinymemory-sources/ memory-source contracts and readers — local folders +│ always, GitHub/RSS/web pages behind `network` +├── tinymemory-tinycortex/ the TinyCortex engine seen through the contract +├── tinymemory-remote/ native HTTP dialects for Supermemory, Mem0, and Cognee +├── tinymemory-conformance/ the behavioural suite every driver must pass +├── tinymemory-testing-ui/ a local HTTP + web harness for driving engines by +│ hand. A workspace member, but held out of +│ `default-members` so the contract commands skip it └── tinymemory-module/ the TinyBus loadable-module driver. Excluded from the workspace on purpose — see the note in `Cargo.toml`. vendor/ @@ -40,8 +49,43 @@ vendor/ └── tinybus/ pinned TinyBus submodule ``` +Every crate lives under `crates/`, one directory per package, each directory +named for the package it holds. The workspace root is virtual — there is no +root package, so the facade is a member like any other and `members` is the +glob `crates/*`: a new crate joins the workspace by existing. + +## Features + +`tinymemory` is the one dependency a host takes, and every other crate in the +workspace is reachable from it by a feature named after it. Nothing is on by +default: naming no feature gets the contract, the registry and the mandatory +composition — no storage engine, no HTTP stack, no native library. +`scripts/ci/dependency-budget.sh` holds that to a ceiling on every run. + +| Feature | Brings in | +| --- | --- | +| `tinycortex` | the embedded TinyCortex engine, as `tinymemory::tinycortex` | +| `supermemory`, `mem0`, `cognee` | the matching HTTP adapter, as `tinymemory::remote` | +| `engines` | all four of the above | +| `core` | `tinymemory::core` — the memory subsystem | +| `sync` | `tinymemory::sync` — the Composio normalisers | +| `sources` | `tinymemory::sources` — source contracts and local readers | +| `sources-network` | `sources`, plus the GitHub/RSS/web-page readers | +| `conformance` | `tinymemory::conformance` — the driver contract suite | +| `memory-git` | git-backed diff snapshots (implies `tinycortex`; links libgit2) | +| `contacts` | the macOS address-book seeding path (implies `core`) | +| `test-support` | the workspace's test doubles and helpers | +| `full` | every feature above except `test-support` | + +Capability features imply the engine that serves them, so asking for a +capability cannot produce a build where nothing implements it. `test-support` +is deliberately outside `full`: "give me the whole workspace" is not the same +request as "give me the test doubles". + + Run `git submodule update --init --recursive` after cloning. Nothing in the -workspace builds without it — `core` names `tinyagents` and `tinycortex` by +workspace builds without it — `tinymemory-core` names `tinyagents` and +`tinycortex` by path through `vendor/`, so an uninitialized checkout fails at manifest resolution rather than at compile time, which reads as a confusing error. @@ -98,7 +142,7 @@ tinyagents = { path = "vendor/tinymemory/vendor/tinyagents" } tinymemory-api = { path = "vendor/tinymemory/api" } ``` -This exact patch set is what the reference consumer in `examples/` and the +This exact patch set is what the reference consumer in `crates/tinymemory/examples/` and the repository's own root manifest use; a build missing any of the four fails at resolution, before compiling a line. @@ -112,7 +156,7 @@ let provider = Arc::new(provider(Arc::new(InMemoryMemoryStore::new()))); That is a complete embedded setup for the mandatory three families. The full eighteen-family engine (`TinycortexProvider`) additionally needs the host seams (`EmbeddingHost` et al.) installed — see -`adapters/tinycortex/tests/full_provider_conformance.rs` for the minimal +`crates/tinymemory-tinycortex/tests/full_provider_conformance.rs` for the minimal working wiring. | Feature | Engine | Class | Families served | From 23bbda1004550f294a5493230f1f608a2e38a85e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:07:59 +0300 Subject: [PATCH 12/18] docs(readme): reflow a long paragraph to avoid line breaks in the middle of a path The description of the submodule requirement was wrapped at an awkward point, splitting a path reference across two lines. The paragraph is now reflowed so the path stays on one line and the line breaks occur at natural sentence boundaries instead. Auto-committed-on: dragonfly Co-authored-by: Medulla --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b4940cb2..35dd8d0b 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,9 @@ request as "give me the test doubles". Run `git submodule update --init --recursive` after cloning. Nothing in the workspace builds without it — `tinymemory-core` names `tinyagents` and -`tinycortex` by -path through `vendor/`, so an uninitialized checkout fails at manifest -resolution rather than at compile time, which reads as a confusing error. +`tinycortex` by path through `vendor/`, so an uninitialized checkout fails at +manifest resolution rather than at compile time, which reads as a confusing +error. ## Using from your project From 197bb8ba42b7793f4fd1bfbdac9e0e9c6c20337f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:09:18 +0300 Subject: [PATCH 13/18] docs(AGENTS.md): update project structure to virtual-root workspace Update the project structure documentation to reflect the migration from a root-package workspace to a virtual-root workspace where every crate lives under `crates/`. The change clarifies that there is no root package, that `members` is the glob `crates/*`, and that the facade, contract, subsystems, and adapters each live in their own crate directory. It also updates the directory tree, the build commands, the lint configuration, and the testing and documentation sections to match the new layout, so that contributors have accurate guidance for working with the restructured workspace. Auto-committed-on: dragonfly Co-authored-by: Medulla --- AGENTS.md | 81 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8bcb325a..bce53cea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,25 +9,31 @@ no longer applies rather than leaving it to rot. ## Project Structure -This is a Cargo **workspace**: the `tinymemory` facade at the root, the -`tinymemory-api` contract in `api/`, and one engine adapter per directory under -`adapters/`. Engines themselves are submodules under `vendor/`, excluded from -the workspace. - -See [`README.md`](README.md) for the layout and the rules that govern it — in -particular, why policy stays in the host and why adapters name their engines by -version requirement rather than by path. +This is a Cargo **workspace** with a virtual root: there is no root package, +and every crate lives in its own directory under `crates/`, named for the +package it holds. `members` is the glob `crates/*`, so a new crate joins the +workspace by existing. `crates/tinymemory` is the facade a host depends on; +`crates/tinymemory-api` is the contract; the rest are the subsystems and the +engine adapters, each reachable from the facade by a feature named after it. +Engines themselves are submodules under `vendor/`, excluded from the workspace. + +See [`README.md`](README.md) for the full layout, the feature table, and the +rules that govern them — in particular, why policy stays in the host and why +adapters name their engines by version requirement rather than by path. ```text -src/ -├── lib.rs # crate docs + the entire public re-export surface -├── error/mod.rs # crate-wide `Error` and `Result` -└── / # one directory per feature area - ├── mod.rs # module docs, wiring, smallest useful public API - ├── types.rs # substantial type definitions - └── test.rs # module-local unit tests -tests/ # integration tests against the public API only -examples/ # runnable, compiled-in-CI usage examples +crates// +├── Cargo.toml # one package; `[lints]` opted into per crate +├── README.md # required of complex crates: design, surface, caveats +└── src/ + ├── lib.rs # crate docs + the entire public re-export surface + ├── error/mod.rs # crate-wide `Error` and `Result` + └── / # one directory per feature area + ├── mod.rs # module docs, wiring, smallest useful public API + ├── types.rs # substantial type definitions + └── test.rs # module-local unit tests +crates//tests/ # integration tests against the public API only +crates//examples/ # runnable, compiled-in-CI usage examples vendor/tinybus/ # pinned TinyBus source; optional until wired by a project docs/ ├── specs/ # behavior and architecture specifications @@ -35,9 +41,14 @@ docs/ └── adr/ # immutable architecture decision records ``` -Each feature area belongs in a focused module directory under `src/`. A module -root explains the module, wires its pieces together, and exposes the smallest -useful API. Move substantial type definitions into `types.rs` and put +A new crate goes in `crates//`, and a package that is not an adapter +or a subsystem of the memory layer probably does not belong here at all. Reach +it from the facade by adding an optional dependency and a feature of the same +name, so a host keeps taking one dependency and stating what it wants. + +Each feature area belongs in a focused module directory under the crate's +`src/`. A module root explains the module, wires its pieces together, and +exposes the smallest useful API. Move substantial type definitions into `types.rs` and put module-local unit tests in a dedicated `test.rs`, wired from the bottom of the module root with: @@ -51,9 +62,10 @@ let a general-purpose `utils.rs` or `helpers.rs` grow — those are a symptom of missing module. Prefer many small modules that each do one thing well over few broad ones. -Keep public exports centralized in `src/lib.rs` so downstream users have one -predictable surface. Put shared error variants in `src/error/mod.rs` and return -the crate-wide `Result` from fallible public APIs. +Keep public exports centralized in each crate's `src/lib.rs` so downstream +users have one predictable surface. Put shared error variants in +`src/error/mod.rs` and return the crate-wide `Result` from fallible public +APIs. ## Build And Test @@ -71,7 +83,9 @@ Supporting commands: - `cargo fmt --all` — format before committing. - `cargo test ` — run a focused subset while iterating. -- `cargo run --example basic` — run the bundled example. +- `cargo run -p tinymemory --example basic` — run the bundled example. The + `-p` is required: the workspace root is virtual, so cargo cannot infer which + package an example belongs to. - `cargo doc --no-deps --all-features` — build the rustdoc CI also builds with `RUSTDOCFLAGS="-D warnings"`. - `cargo test --doc` — run doctests alone when editing documentation examples. @@ -93,13 +107,15 @@ Use standard `rustfmt` output and Rust 2024 idioms. Do not hand-format around `impl Into` at boundaries; return owned, concrete types. - Keep the public surface minimal: default to private, and export deliberately from `src/lib.rs`. -- `unsafe` is forbidden crate-wide by the lint configuration in `Cargo.toml`. +- `unsafe` is forbidden crate-wide by the `[lints]` table in each crate's own + `Cargo.toml` — the root is virtual and carries no lint configuration. If a project genuinely needs it, relax the lint in its own commit and document every invariant with a `// SAFETY:` comment. ### Errors -- One crate-wide `Error` enum in `src/error/mod.rs`, built with `thiserror`. +- One crate-wide `Error` enum in the crate's `src/error/mod.rs`, built with + `thiserror`. - Fallible public functions return `Result`, the crate alias. - Add a specific variant instead of stuffing context into a string; error messages are lowercase, without trailing punctuation. @@ -143,9 +159,10 @@ on every generated crate. ## Testing -- Module-local unit tests live in `src//test.rs` and may touch private - items. -- Integration tests live in `tests/` and exercise only the public API — they are +- Module-local unit tests live in `crates//src//test.rs` and + may touch private items. +- Integration tests live in `crates//tests/` and exercise only the + public API — they are the regression suite for the crate's contract. - Use descriptive, behavioral test names: `rejects_an_empty_name`, not `test_greet_2`. @@ -171,7 +188,7 @@ Write documentation for the reader who has never seen the code. treats as an error. - Start every `mod.rs` and `test.rs` with a concise module-level `//!` description. -- `src/lib.rs` carries the crate-level overview: what the crate does, the +- Each crate's `src/lib.rs` carries its crate-level overview: what the crate does, the primary entry points, and a short runnable example. - Prefer concrete examples over vague description. Doc examples are compiled and run by `cargo test`, so they cannot drift. @@ -226,8 +243,8 @@ publishes to crates.io using the `CARGO_REGISTRY_TOKEN` secret. Consequently: -- Do not hand-edit the `version` field in `Cargo.toml`; the release workflow - owns it. +- Do not hand-edit the `version` field in `crates/tinymemory/Cargo.toml`; the + release workflow owns it. - Follow semantic versioning. Any change to the public surface that is not purely additive is a breaking change and needs a major bump (pre-1.0: a minor bump). From a33ffdf9016278f3d5c21ab20edbdc0907b9596c Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:09:48 +0300 Subject: [PATCH 14/18] docs(AGENTS.md): reflow long lines and clarify crate references Rewrap several paragraphs to stay within a consistent line length, and make two references more precise: the public surface is now exported from "the crate's `src/lib.rs`" instead of just "`src/lib.rs`", and the release workflow updates `crates/tinymemory/Cargo.toml` rather than the generic `Cargo.toml`. Auto-committed-on: dragonfly Co-authored-by: Medulla --- AGENTS.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bce53cea..f8c6c37c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,9 +48,9 @@ name, so a host keeps taking one dependency and stating what it wants. Each feature area belongs in a focused module directory under the crate's `src/`. A module root explains the module, wires its pieces together, and -exposes the smallest useful API. Move substantial type definitions into `types.rs` and put -module-local unit tests in a dedicated `test.rs`, wired from the bottom of the -module root with: +exposes the smallest useful API. Move substantial type definitions into +`types.rs` and put module-local unit tests in a dedicated `test.rs`, wired from +the bottom of the module root with: ```rust #[cfg(test)] @@ -106,11 +106,11 @@ Use standard `rustfmt` output and Rust 2024 idioms. Do not hand-format around - Prefer small, typed APIs over stringly-typed ones. Accept `&str` and generic `impl Into` at boundaries; return owned, concrete types. - Keep the public surface minimal: default to private, and export deliberately - from `src/lib.rs`. + from the crate's `src/lib.rs`. - `unsafe` is forbidden crate-wide by the `[lints]` table in each crate's own - `Cargo.toml` — the root is virtual and carries no lint configuration. - If a project genuinely needs it, relax the lint in its own commit and document - every invariant with a `// SAFETY:` comment. + `Cargo.toml` — the root is virtual and carries no lint configuration. If a + crate genuinely needs it, relax the lint in its own commit and document every + invariant with a `// SAFETY:` comment. ### Errors @@ -162,8 +162,7 @@ on every generated crate. - Module-local unit tests live in `crates//src//test.rs` and may touch private items. - Integration tests live in `crates//tests/` and exercise only the - public API — they are - the regression suite for the crate's contract. + public API — they are the regression suite for the crate's contract. - Use descriptive, behavioral test names: `rejects_an_empty_name`, not `test_greet_2`. - Cover the failure paths, not just the happy path. Every new error variant @@ -188,8 +187,8 @@ Write documentation for the reader who has never seen the code. treats as an error. - Start every `mod.rs` and `test.rs` with a concise module-level `//!` description. -- Each crate's `src/lib.rs` carries its crate-level overview: what the crate does, the - primary entry points, and a short runnable example. +- Each crate's `src/lib.rs` carries its crate-level overview: what the crate + does, its primary entry points, and a short runnable example. - Prefer concrete examples over vague description. Doc examples are compiled and run by `cargo test`, so they cannot drift. - Complex modules must include a module-level `README.md` covering their design, @@ -238,8 +237,9 @@ explicitly declined with a reason. Releases run from `.github/workflows/release.yml` via a manual `workflow_dispatch` with a `patch` / `minor` / `major` bump. The workflow re-runs the full validation suite, computes the next version, updates -`Cargo.toml` and `Cargo.lock`, commits and tags `vX.Y.Z`, packages, pushes, and -publishes to crates.io using the `CARGO_REGISTRY_TOKEN` secret. +`crates/tinymemory/Cargo.toml` and `Cargo.lock`, commits and tags `vX.Y.Z`, +packages, pushes, and publishes to crates.io using the `CARGO_REGISTRY_TOKEN` +secret. Consequently: From 850c4454bbb4487148628052a21b8f97f7ab6ac5 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:10:10 +0300 Subject: [PATCH 15/18] docs(readme): add cross-reference from feature table to engine table Add a short paragraph explaining that the feature table shows which crate each feature brings in, and direct readers to the engine table for details on what each engine feature serves, including driver class and capability families. Also reflow two long lines to stay within the 72-character column limit. Auto-committed-on: dragonfly Co-authored-by: Medulla --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 35dd8d0b..ad0473f9 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,11 @@ capability cannot produce a build where nothing implements it. `test-support` is deliberately outside `full`: "give me the whole workspace" is not the same request as "give me the test doubles". +This table says which crate each feature brings in. For what each *engine* +feature actually serves — driver class, and how many of the eighteen capability +families answer — see the engine table under +[Using from your project](#using-from-your-project). + Run `git submodule update --init --recursive` after cloning. Nothing in the workspace builds without it — `tinymemory-core` names `tinyagents` and @@ -142,9 +147,9 @@ tinyagents = { path = "vendor/tinymemory/vendor/tinyagents" } tinymemory-api = { path = "vendor/tinymemory/api" } ``` -This exact patch set is what the reference consumer in `crates/tinymemory/examples/` and the -repository's own root manifest use; a build missing any of the four fails at -resolution, before compiling a line. +This exact patch set is what the reference consumer in +`crates/tinymemory/examples/` and the repository's own root manifest use; a +build missing any of the four fails at resolution, before compiling a line. ```rust,ignore use std::sync::Arc; @@ -156,8 +161,8 @@ let provider = Arc::new(provider(Arc::new(InMemoryMemoryStore::new()))); That is a complete embedded setup for the mandatory three families. The full eighteen-family engine (`TinycortexProvider`) additionally needs the host seams (`EmbeddingHost` et al.) installed — see -`crates/tinymemory-tinycortex/tests/full_provider_conformance.rs` for the minimal -working wiring. +`crates/tinymemory-tinycortex/tests/full_provider_conformance.rs` for the +minimal working wiring. | Feature | Engine | Class | Families served | | --- | --- | --- | --- | From c9d66e38c6ab72966ccbfb04058f8714eced76e1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:10:21 +0300 Subject: [PATCH 16/18] fix(remote-engines): correct path to cognee.rs in README The README referenced an outdated path for the Cognee constructor note, pointing to `adapters/remote/src/cognee.rs` instead of the current `crates/tinymemory-remote/src/cognee.rs`. This update fixes the path to match the actual project structure. Auto-committed-on: dragonfly Co-authored-by: Medulla --- integration/remote-engines/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/remote-engines/README.md b/integration/remote-engines/README.md index 3b8256af..9cb53348 100644 --- a/integration/remote-engines/README.md +++ b/integration/remote-engines/README.md @@ -38,7 +38,7 @@ cargo run -p tinymemory-remote --example conformance -- \ Cognee Cloud has **no shared endpoint** — `api.cognee.ai` resolves in DNS but nothing listens there (see the constructor note in -`adapters/remote/src/cognee.rs`), which is why this crate exports no default +`crates/tinymemory-remote/src/cognee.rs`), which is why this crate exports no default Cognee endpoint constant. The tenant URL printed beside your API key on the Cognee dashboard is the only address that exists; substitute it above. The command writes a unique conformance namespace, verifies Core, Recall, and Portability, and deletes its test record before From 9e477a3a079ec74e4a1033256fb4f2966374323f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:10:29 +0300 Subject: [PATCH 17/18] docs: update stale file paths in documentation comments Update three file path references that were left pointing to old locations after the crate restructuring, replacing `adapters/` prefixes with the correct `crates/tinymemory-` paths in the testing-ui README, the tinycortex example, and the driver selection test. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-testing-ui/README.md | 4 ++-- crates/tinymemory/examples/tinycortex.rs | 2 +- crates/tinymemory/tests/driver_selection.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/tinymemory-testing-ui/README.md b/crates/tinymemory-testing-ui/README.md index 0b7c26e3..d66fbd97 100644 --- a/crates/tinymemory-testing-ui/README.md +++ b/crates/tinymemory-testing-ui/README.md @@ -91,7 +91,7 @@ the self-hosted deployment for Mem0 and Cognee. ### Graph support per engine -- **Cognee** — real. `cognee_graph_provider` (`adapters/remote/src/graph_provider.rs`, +- **Cognee** — real. `cognee_graph_provider` (`crates/tinymemory-remote/src/graph_provider.rs`, `cognee_graph.rs`) wraps Cognee's `GET /api/v1/datasets/{id}/graph` and reshapes its nodes/edges into `(subject, predicate, object)` triples. Only `relations` has a Cognee counterpart — `kv_get`/`kv_put`/`kv_delete`/`kv_list` @@ -117,7 +117,7 @@ the self-hosted deployment for Mem0 and Cognee. `graph_store` makes `/search` and `/memories` responses grow a `relations` key, no server code changes needed — but downgrading two major versions of a shared test harness's core dependency was judged too risky to keep, so it - was reverted. Instead, `Mem0Graph` (`adapters/remote/src/mem0_graph.rs`) + was reverted. Instead, `Mem0Graph` (`crates/tinymemory-remote/src/mem0_graph.rs`) derives a graph client-side: it lists a namespace's stored entries and runs a plain co-occurrence heuristic over each entry's content — no LLM, no NER, just grouping runs of capitalized words per sentence and linking consecutive diff --git a/crates/tinymemory/examples/tinycortex.rs b/crates/tinymemory/examples/tinycortex.rs index fe4b1ea9..68f1e5d2 100644 --- a/crates/tinymemory/examples/tinycortex.rs +++ b/crates/tinymemory/examples/tinycortex.rs @@ -11,7 +11,7 @@ //! The backend is the engine's own in-memory store — a complete embedded //! setup for the mandatory three families: no workspace, no host seams. (The //! full eighteen-family `TinycortexProvider` additionally needs the host -//! seams installed; `adapters/tinycortex/tests/full_provider_conformance.rs` +//! seams installed; `crates/tinymemory-tinycortex/tests/full_provider_conformance.rs` //! is the minimal working wiring for that.) use std::sync::Arc; diff --git a/crates/tinymemory/tests/driver_selection.rs b/crates/tinymemory/tests/driver_selection.rs index ea3a36d3..d851e91f 100644 --- a/crates/tinymemory/tests/driver_selection.rs +++ b/crates/tinymemory/tests/driver_selection.rs @@ -14,7 +14,7 @@ //! //! What still does not exist is the last clause of §A5, that `create_memory_*` //! return a bound `Arc`. It cannot, and the reason is -//! structural rather than unfinished: `adapters/tinycortex` depends on +//! structural rather than unfinished: `crates/tinymemory-tinycortex` depends on //! `tinymemory-core` since §C3, so a core factory returning a constructed //! adapter provider would be a dependency cycle. Selection resolves the //! *decision*; the host constructs — which is what `src/registry`'s own module From 1db03881ede6ee3dfa1d6c129011eb4527adc778 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:10:37 +0300 Subject: [PATCH 18/18] fix(docs): correct stale source paths in code comments Update two inline comments that referenced the old `adapters/remote` path to point to the current `crates/tinymemory-remote` location, ensuring the documentation accurately reflects the project's directory structure. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/mandatory/mod.rs | 2 +- crates/tinymemory-core/src/store/factories.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tinymemory-api/src/mandatory/mod.rs b/crates/tinymemory-api/src/mandatory/mod.rs index f427268c..efbf2a50 100644 --- a/crates/tinymemory-api/src/mandatory/mod.rs +++ b/crates/tinymemory-api/src/mandatory/mod.rs @@ -78,7 +78,7 @@ pub const SCOPE_UNAPPLIED: &str = #[must_use] pub fn engine_error(error: anyhow::Error) -> MemoryError { // §A4: an adapter that already knows the failure's class attaches a typed - // MemoryError as the anyhow payload (adapters/remote/src/common.rs does + // MemoryError as the anyhow payload (crates/tinymemory-remote/src/common.rs does // for transport and HTTP-status failures). Recover it here instead of // flattening everything to Other — this one line is what makes // "unauthorized" distinguishable from "unreachable" across every diff --git a/crates/tinymemory-core/src/store/factories.rs b/crates/tinymemory-core/src/store/factories.rs index e44e6584..f2da6be1 100644 --- a/crates/tinymemory-core/src/store/factories.rs +++ b/crates/tinymemory-core/src/store/factories.rs @@ -359,7 +359,7 @@ pub fn create_memory( /// Issue #18 §A3/§A5. Until this existed, `tinymemory-core`'s /// [`UnifiedMemory`] was the one storage /// implementation in the workspace with no route through the contract: the -/// TinyCortex adapter binds the bundled engine, `adapters/remote` binds the +/// TinyCortex adapter binds the bundled engine, `tinymemory-remote` binds the /// three hosted services, and core's own SQLite store was reachable only by /// naming its concrete type. A host could therefore not treat "the store /// tinymemory ships with" as a driver, which is the whole premise of the