loop-memory 0.4.21__tar.gz → 0.4.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {loop_memory-0.4.21 → loop_memory-0.4.22}/PKG-INFO +7 -4
- {loop_memory-0.4.21 → loop_memory-0.4.22}/README.md +6 -3
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/read.py +29 -4
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/mcp/__init__.py +39 -2
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/sdk.py +50 -8
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/memories.py +26 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/system.py +25 -1
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/storage/retrieval.py +36 -4
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/storage/sqlite_store.py +149 -12
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory.egg-info/PKG-INFO +7 -4
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory.egg-info/SOURCES.txt +1 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/pyproject.toml +1 -1
- loop_memory-0.4.22/tests/test_namespace_isolation_2026_11_15.py +569 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/LICENSE +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/bench/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/bench/fixtures/default.json +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/bench/runner.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/bench.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/cognitive.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/doctor.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/hooks.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/rules.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/commands/write.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/cli/main.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/decorators.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/doctor.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/dream.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/security/transport.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/handlers.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/cognitive.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/export.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/css/app.css +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/css/layout.css +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/i18n/en.json +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/i18n/zh.json +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/img/logo-light.svg +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/img/logo-mark.svg +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/img/logo.svg +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/index.html +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/App.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/api.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Dashboard.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Diagnostic.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/IngestPopover.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/KnowledgeGraph.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/RingMeter.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/RunStrip.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/SectionTitle.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Settings.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Sidebar.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Tabs.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Timeline.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Toast.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/TopBar.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Wiki.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/WikiEditor.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/composables/useI18n.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/lib/sanitize.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/lib/vue.esm-browser.prod.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/main.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/static/js/store.js +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/storage/lexical.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/storage/snapshot.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/setup.cfg +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_agents_init.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_audit_triggers_2026_09_27.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_bench_2026_11_01.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_busy_timeout_2026_09_24.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cache_cap_2026_09_23.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_audit_supersede.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_export_okf.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_init.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_memory_stats.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_recall_as_of.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_recall_paths.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_recall_verbose.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_rules.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_snapshot.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_cli_version.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_content_hash_dedup_2026_10_18.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_contradictions.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_decorators_2026_10_18.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_doctor_2026_10_12.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_dream_2026_10_25.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_dream_cli_http_2026_10_25.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_evolution_quality.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_export_okf.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_graph.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_healthz_readyz_2026_09_24.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_import_okf_2026_09_27.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_latest_only_2026_10_25.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_lexical_ranking_2026_11_01.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_loop.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_memory_stats.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_package_version.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_packaging_2026_11_01.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_path_safety_2026_09_23.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_recall_as_of.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_recall_budget_2026_10_05.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_recall_paths.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_recall_provenance.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_recall_validity_http_2026_10_18.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_repo_framing.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_retrieval_channels_2026_10_18.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_routes_audit_2026_09_13.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_routes_audit_2026_09_20.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_short_query.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_snapshot.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_store.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_subgraph_hops_2026_09_23.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_subject_key_2026_10_25.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_supersession_chain.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_transport_security_2026_11_08.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_wiki_export_escape.py +0 -0
- {loop_memory-0.4.21 → loop_memory-0.4.22}/tests/test_wiki_prompts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loop-memory
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.22
|
|
4
4
|
Summary: A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI.
|
|
5
5
|
Author: Loop Memory contributors <loop-memory@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,13 +67,14 @@ Dynamic: license-file
|
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
-
> **What's new in 0.4.
|
|
70
|
+
> **What's new in 0.4.22** — **the agent-namespace filter now works, and it is enforced in the query instead of on top of the answer** (audit 2026-11-15, weekly ecosystem sync). The audit for this release (2026-11-15) turned up a real hole rather than a hypothetical one. `GET /api/v1/recall?agent_id=X` was documented as *"Unified recall filtered to a single Agent namespace"* — but the filter was applied to the store's **output**, and it had two separate failure modes. **On the legacy path it was a silent no-op:** `MemoryStore.recall()` never returned the `agent_id` / `user_id` columns, so the route's predicate read `None`/`None` for every row, decided every row was "global", and let all of them through — **an agent that explicitly asked for its own namespace was handed every other agent's memories as well.** **On the hybrid path it ran after the `LIMIT`:** the filter worked, but it ran on the store's already-trimmed result set, so a foreign namespace could spend the caller's *entire* result budget — an agent asking for 5 hits could get **zero**, while its own matching rows sat further down the ranking. Both failures have the same root cause: **the predicate was in the caller instead of in the query** — exactly what the upstream projects in this window converged on. `plastic-labs/honcho` v3.6.0 (2026-11-10, AGPL-3.0) made `tenant_id` lead every primary key, with Postgres row-level security scoping **every read and write**, and refused to hand a tenant an empty namespace; `EverMind-AI/EverOS` v1.6.0 (2026-11-11) made deletion owner-scoped and answered `503 SESSION_BUSY` rather than deleting half of a session; `thedotmack/claude-mem` (v15.16.x) gave each scope its own project by default and made *"an explicitly empty project selection show nothing instead of observations from every project"* (#5866); `vectorize-io/hindsight` v1.2.1 stopped a bank clone from taking over another bank's alias (#6280) — pattern references only, no code copied, and the two AGPL-3.0 projects contributed nothing but an idea. **What shipped:** a single predicate builder (`_scope_clause()`) that every retrieval channel builds its query from, so the namespace lives in the **SQL** of the BM25 channel (both the FTS5 query and the `LIKE` backstop), the semantic channel, the entity channel, `recall()`, `recall_hybrid()` and `recall_as_of()` — the fused ranking, and the `limit` that trims it, now only ever sees the caller's own rows; every hit now reports its `agent_id` / `user_id`, which is what makes the legacy no-op impossible to reintroduce silently; all four public surfaces expose it — `GET /api/v1/recall` and `GET /api/recall` accept `agent_id` / `user_id`, the MCP `recall` tool scopes to `LOOP_MEMORY_AGENT_ID` / `LOOP_MEMORY_USER_ID` (the MCP server is started once per client, so the environment already identifies the caller) and accepts explicit overrides, and `loop-memory recall` gained `--agent-id` / `--user-id`; and a client that **writes** into a namespace now **reads** from it — `MemoryClient.memory(store, agent_id="x")` and `MemoryClient.http(url, agent_id="x")` fall back to that identity on `recall()`, which is precisely the asymmetry that hid the leak (pass `agent_id=""` for the explicit escape hatch). A memory with no `agent_id` stays **global** and visible to everyone, because the ingest pipeline, the consolidator and the dream job all write global rows. 43 new regression cases; suite at **1084 passed / 4 skipped**. See [`docs/namespace-isolation.md`](./docs/namespace-isolation.md) for the full contract. [Full CHANGELOG →](./CHANGELOG.md#0422---2026-11-15)
|
|
71
71
|
|
|
72
72
|
<details>
|
|
73
|
-
<summary><b>Older releases</b> — 0.4.
|
|
73
|
+
<summary><b>Older releases</b> — 0.4.21 and earlier, one line each (the CHANGELOG has the full detail)</summary>
|
|
74
74
|
|
|
75
75
|
| Version | Date | What changed | Full notes |
|
|
76
76
|
| --- | --- | --- | --- |
|
|
77
|
+
| [0.4.21](./CHANGELOG.md#0421---2026-11-08) | 2026-11-08 | The remote SDK can authenticate, and it refuses to send memory data over a plaintext connection |
|
|
77
78
|
| [0.4.20](./CHANGELOG.md#0420---2026-11-01) | 2026-11-01 | The packaging test works on the oldest supported Python (no `tomllib` on 3.10) |
|
|
78
79
|
| [0.4.19](./CHANGELOG.md#0419---2026-11-01) | 2026-11-01 | The wheel ships the whole dashboard and the benchmark fixture (recursive package-data globs) |
|
|
79
80
|
| [0.4.18](./CHANGELOG.md#0418---2026-11-01) | 2026-11-01 | The BM25 channel no longer publishes a score that collapses to zero — plus a retrieval benchmark |
|
|
@@ -268,6 +269,7 @@ Hermes, clawx) can read and write, we built this for you.
|
|
|
268
269
|
| [docs/agent-memory-api.md](docs/agent-memory-api.md) | Stable four-verb SDK / HTTP / MCP contract for any Agent |
|
|
269
270
|
| [docs/universal-agent-memory.md](docs/universal-agent-memory.md) | v7 graph memory, cognitive sleep, portable bundles, namespaces, and MCP/CLI extensions |
|
|
270
271
|
| [docs/transport-security.md](docs/transport-security.md) | How a remote client is allowed to talk to `loop-memory serve` — the transport guard, bearer-token auth, redirect hardening, and the `plaintext_exposure` doctor check (0.4.21) |
|
|
272
|
+
| [docs/namespace-isolation.md](docs/namespace-isolation.md) | Who may read which memory — the agent / user namespace predicate, why it lives in the SQL instead of on top of the answer, and the global-row semantics (0.4.22) |
|
|
271
273
|
| [docs/providers.md](docs/providers.md) | LLM provider reference — built-in providers, defaults, base URLs, and how to add a new one |
|
|
272
274
|
| [docs/auto-capture.md](docs/auto-capture.md) | Hooking Codex / Claude / Hermes / OpenClaw watchers (filesystem, launchd, systemd, cron) |
|
|
273
275
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev loop, pytest, where secrets live, how to add a provider/source |
|
|
@@ -631,12 +633,13 @@ loop_memory/
|
|
|
631
633
|
memory/types.py # MemoryItem + 4 tiers
|
|
632
634
|
examples/demo.py # runnable, zero-API-key demo
|
|
633
635
|
py.typed
|
|
634
|
-
tests/ #
|
|
636
|
+
tests/ # 1084 unit tests across memory / SDK / serve / CLI / scripts
|
|
635
637
|
docs/
|
|
636
638
|
auto-capture.md # launchd / systemd / cron recipes
|
|
637
639
|
architecture.md # layered view + 5-stage evolution pipeline
|
|
638
640
|
api.md # HTTP API reference
|
|
639
641
|
transport-security.md # how a remote client may talk to `serve` (0.4.21)
|
|
642
|
+
namespace-isolation.md # who may read which memory (0.4.22)
|
|
640
643
|
agent-memory-api.md # four-verb SDK / HTTP / MCP contract
|
|
641
644
|
universal-agent-memory.md # v7 graph memory + cognitive sleep + bundles
|
|
642
645
|
providers.md # LLM provider reference
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
> **What's new in 0.4.
|
|
25
|
+
> **What's new in 0.4.22** — **the agent-namespace filter now works, and it is enforced in the query instead of on top of the answer** (audit 2026-11-15, weekly ecosystem sync). The audit for this release (2026-11-15) turned up a real hole rather than a hypothetical one. `GET /api/v1/recall?agent_id=X` was documented as *"Unified recall filtered to a single Agent namespace"* — but the filter was applied to the store's **output**, and it had two separate failure modes. **On the legacy path it was a silent no-op:** `MemoryStore.recall()` never returned the `agent_id` / `user_id` columns, so the route's predicate read `None`/`None` for every row, decided every row was "global", and let all of them through — **an agent that explicitly asked for its own namespace was handed every other agent's memories as well.** **On the hybrid path it ran after the `LIMIT`:** the filter worked, but it ran on the store's already-trimmed result set, so a foreign namespace could spend the caller's *entire* result budget — an agent asking for 5 hits could get **zero**, while its own matching rows sat further down the ranking. Both failures have the same root cause: **the predicate was in the caller instead of in the query** — exactly what the upstream projects in this window converged on. `plastic-labs/honcho` v3.6.0 (2026-11-10, AGPL-3.0) made `tenant_id` lead every primary key, with Postgres row-level security scoping **every read and write**, and refused to hand a tenant an empty namespace; `EverMind-AI/EverOS` v1.6.0 (2026-11-11) made deletion owner-scoped and answered `503 SESSION_BUSY` rather than deleting half of a session; `thedotmack/claude-mem` (v15.16.x) gave each scope its own project by default and made *"an explicitly empty project selection show nothing instead of observations from every project"* (#5866); `vectorize-io/hindsight` v1.2.1 stopped a bank clone from taking over another bank's alias (#6280) — pattern references only, no code copied, and the two AGPL-3.0 projects contributed nothing but an idea. **What shipped:** a single predicate builder (`_scope_clause()`) that every retrieval channel builds its query from, so the namespace lives in the **SQL** of the BM25 channel (both the FTS5 query and the `LIKE` backstop), the semantic channel, the entity channel, `recall()`, `recall_hybrid()` and `recall_as_of()` — the fused ranking, and the `limit` that trims it, now only ever sees the caller's own rows; every hit now reports its `agent_id` / `user_id`, which is what makes the legacy no-op impossible to reintroduce silently; all four public surfaces expose it — `GET /api/v1/recall` and `GET /api/recall` accept `agent_id` / `user_id`, the MCP `recall` tool scopes to `LOOP_MEMORY_AGENT_ID` / `LOOP_MEMORY_USER_ID` (the MCP server is started once per client, so the environment already identifies the caller) and accepts explicit overrides, and `loop-memory recall` gained `--agent-id` / `--user-id`; and a client that **writes** into a namespace now **reads** from it — `MemoryClient.memory(store, agent_id="x")` and `MemoryClient.http(url, agent_id="x")` fall back to that identity on `recall()`, which is precisely the asymmetry that hid the leak (pass `agent_id=""` for the explicit escape hatch). A memory with no `agent_id` stays **global** and visible to everyone, because the ingest pipeline, the consolidator and the dream job all write global rows. 43 new regression cases; suite at **1084 passed / 4 skipped**. See [`docs/namespace-isolation.md`](./docs/namespace-isolation.md) for the full contract. [Full CHANGELOG →](./CHANGELOG.md#0422---2026-11-15)
|
|
26
26
|
|
|
27
27
|
<details>
|
|
28
|
-
<summary><b>Older releases</b> — 0.4.
|
|
28
|
+
<summary><b>Older releases</b> — 0.4.21 and earlier, one line each (the CHANGELOG has the full detail)</summary>
|
|
29
29
|
|
|
30
30
|
| Version | Date | What changed | Full notes |
|
|
31
31
|
| --- | --- | --- | --- |
|
|
32
|
+
| [0.4.21](./CHANGELOG.md#0421---2026-11-08) | 2026-11-08 | The remote SDK can authenticate, and it refuses to send memory data over a plaintext connection |
|
|
32
33
|
| [0.4.20](./CHANGELOG.md#0420---2026-11-01) | 2026-11-01 | The packaging test works on the oldest supported Python (no `tomllib` on 3.10) |
|
|
33
34
|
| [0.4.19](./CHANGELOG.md#0419---2026-11-01) | 2026-11-01 | The wheel ships the whole dashboard and the benchmark fixture (recursive package-data globs) |
|
|
34
35
|
| [0.4.18](./CHANGELOG.md#0418---2026-11-01) | 2026-11-01 | The BM25 channel no longer publishes a score that collapses to zero — plus a retrieval benchmark |
|
|
@@ -223,6 +224,7 @@ Hermes, clawx) can read and write, we built this for you.
|
|
|
223
224
|
| [docs/agent-memory-api.md](docs/agent-memory-api.md) | Stable four-verb SDK / HTTP / MCP contract for any Agent |
|
|
224
225
|
| [docs/universal-agent-memory.md](docs/universal-agent-memory.md) | v7 graph memory, cognitive sleep, portable bundles, namespaces, and MCP/CLI extensions |
|
|
225
226
|
| [docs/transport-security.md](docs/transport-security.md) | How a remote client is allowed to talk to `loop-memory serve` — the transport guard, bearer-token auth, redirect hardening, and the `plaintext_exposure` doctor check (0.4.21) |
|
|
227
|
+
| [docs/namespace-isolation.md](docs/namespace-isolation.md) | Who may read which memory — the agent / user namespace predicate, why it lives in the SQL instead of on top of the answer, and the global-row semantics (0.4.22) |
|
|
226
228
|
| [docs/providers.md](docs/providers.md) | LLM provider reference — built-in providers, defaults, base URLs, and how to add a new one |
|
|
227
229
|
| [docs/auto-capture.md](docs/auto-capture.md) | Hooking Codex / Claude / Hermes / OpenClaw watchers (filesystem, launchd, systemd, cron) |
|
|
228
230
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev loop, pytest, where secrets live, how to add a provider/source |
|
|
@@ -586,12 +588,13 @@ loop_memory/
|
|
|
586
588
|
memory/types.py # MemoryItem + 4 tiers
|
|
587
589
|
examples/demo.py # runnable, zero-API-key demo
|
|
588
590
|
py.typed
|
|
589
|
-
tests/ #
|
|
591
|
+
tests/ # 1084 unit tests across memory / SDK / serve / CLI / scripts
|
|
590
592
|
docs/
|
|
591
593
|
auto-capture.md # launchd / systemd / cron recipes
|
|
592
594
|
architecture.md # layered view + 5-stage evolution pipeline
|
|
593
595
|
api.md # HTTP API reference
|
|
594
596
|
transport-security.md # how a remote client may talk to `serve` (0.4.21)
|
|
597
|
+
namespace-isolation.md # who may read which memory (0.4.22)
|
|
595
598
|
agent-memory-api.md # four-verb SDK / HTTP / MCP contract
|
|
596
599
|
universal-agent-memory.md # v7 graph memory + cognitive sleep + bundles
|
|
597
600
|
providers.md # LLM provider reference
|
|
@@ -62,10 +62,14 @@ def run_recall(args) -> int:
|
|
|
62
62
|
caller can ask "what did we know about this query at that
|
|
63
63
|
moment?". The wiki and entity sections are unchanged — only
|
|
64
64
|
the raw-memory block grows the new line.
|
|
65
|
+
|
|
66
|
+
``--agent-id`` / ``--user-id`` (audit 2026-11-15, loop-memory
|
|
67
|
+
0.4.22) restrict the answer to one namespace. Memories written
|
|
68
|
+
without an agent or user id are global and stay visible.
|
|
65
69
|
"""
|
|
66
70
|
from ...storage.sqlite_store import MemoryStore
|
|
67
71
|
if not args:
|
|
68
|
-
return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
|
|
72
|
+
return die("usage: loop-memory recall <query> [--verbose] [--limit N] [--agent-id <id>] [--user-id <id>]")
|
|
69
73
|
# Parse the small flag subset we expose. Keep it dumb-on-purpose:
|
|
70
74
|
# we don't want argparse to swallow a token that happens to start
|
|
71
75
|
# with "--" inside the user's query (e.g. ``recall --foo bar``).
|
|
@@ -80,6 +84,11 @@ def run_recall(args) -> int:
|
|
|
80
84
|
level = 1
|
|
81
85
|
# Audit 2026-10-25 (loop-memory 0.4.16) -- latest-only mode.
|
|
82
86
|
latest_only = False
|
|
87
|
+
# Audit 2026-11-15 (loop-memory 0.4.22) -- agent / user
|
|
88
|
+
# namespace. ``None`` means "no restriction", which keeps the
|
|
89
|
+
# legacy see-everything behaviour.
|
|
90
|
+
agent_id: str | None = None
|
|
91
|
+
user_id: str | None = None
|
|
83
92
|
qargs: list[str] = []
|
|
84
93
|
i = 0
|
|
85
94
|
while i < len(args):
|
|
@@ -158,6 +167,17 @@ def run_recall(args) -> int:
|
|
|
158
167
|
except ValueError:
|
|
159
168
|
return die(f"--level must be 0, 1, or 2, got {args[i + 1]!r}")
|
|
160
169
|
i += 2
|
|
170
|
+
elif a == "--agent-id" and i + 1 < len(args):
|
|
171
|
+
# Audit 2026-11-15 (loop-memory 0.4.22) -- restrict the
|
|
172
|
+
# answer to one agent namespace. Memories with no agent
|
|
173
|
+
# id are global and stay visible.
|
|
174
|
+
agent_id = args[i + 1].strip() or None
|
|
175
|
+
i += 2
|
|
176
|
+
elif a == "--user-id" and i + 1 < len(args):
|
|
177
|
+
# Audit 2026-11-15 (loop-memory 0.4.22) -- restrict the
|
|
178
|
+
# answer to one user namespace.
|
|
179
|
+
user_id = args[i + 1].strip() or None
|
|
180
|
+
i += 2
|
|
161
181
|
elif a == "--limit" and i + 1 < len(args):
|
|
162
182
|
try:
|
|
163
183
|
limit = int(args[i + 1])
|
|
@@ -170,13 +190,14 @@ def run_recall(args) -> int:
|
|
|
170
190
|
qargs.append(a)
|
|
171
191
|
i += 1
|
|
172
192
|
if not qargs:
|
|
173
|
-
return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--latest-only] [--limit N] [--max-chars N] [--level 0|1|2]")
|
|
193
|
+
return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--latest-only] [--agent-id <id>] [--user-id <id>] [--limit N] [--max-chars N] [--level 0|1|2]")
|
|
174
194
|
store = MemoryStore(default_db_path())
|
|
175
195
|
query = " ".join(qargs)
|
|
176
196
|
if as_of is not None:
|
|
177
197
|
try:
|
|
178
198
|
r = store.recall_as_of(query, as_of, limit=limit,
|
|
179
|
-
max_chars=max_chars, level=level
|
|
199
|
+
max_chars=max_chars, level=level,
|
|
200
|
+
agent_id=agent_id, user_id=user_id)
|
|
180
201
|
except ValueError as e:
|
|
181
202
|
return die(str(e))
|
|
182
203
|
elif outline:
|
|
@@ -190,7 +211,11 @@ def run_recall(args) -> int:
|
|
|
190
211
|
# "everything" semantics.
|
|
191
212
|
as_of_ts=validity_at,
|
|
192
213
|
# Audit 2026-10-25: one answer per fact.
|
|
193
|
-
latest_only=latest_only
|
|
214
|
+
latest_only=latest_only,
|
|
215
|
+
# Audit 2026-11-15: namespace predicate in
|
|
216
|
+
# SQL -- the limit counts only the caller's
|
|
217
|
+
# own rows.
|
|
218
|
+
agent_id=agent_id, user_id=user_id)
|
|
194
219
|
has = False
|
|
195
220
|
if r["wiki"]:
|
|
196
221
|
has = True
|
|
@@ -77,7 +77,27 @@ def tool_recall(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
77
77
|
if not query:
|
|
78
78
|
return [_err("missing 'query' argument")]
|
|
79
79
|
store = _store()
|
|
80
|
-
|
|
80
|
+
ctx_agent, ctx_user = _agent_context()
|
|
81
|
+
source = arguments.get("source") or ctx_agent
|
|
82
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): namespace isolation.
|
|
83
|
+
# The MCP server is started once per client (Codex, Claude Code,
|
|
84
|
+
# Hermes, ...), so ``LOOP_MEMORY_AGENT_ID`` / ``LOOP_MEMORY_USER_ID``
|
|
85
|
+
# already identify the caller. The recall tool now scopes the
|
|
86
|
+
# answer to that namespace -- inside the store's SQL, so a foreign
|
|
87
|
+
# namespace can neither leak a row nor eat the result budget.
|
|
88
|
+
# An explicit ``agent_id`` / ``user_id`` argument overrides the
|
|
89
|
+
# environment; passing an empty string means "no restriction",
|
|
90
|
+
# which keeps the legacy see-everything behaviour reachable.
|
|
91
|
+
agent_id = arguments.get("agent_id")
|
|
92
|
+
if agent_id is None:
|
|
93
|
+
agent_id = ctx_agent
|
|
94
|
+
elif not str(agent_id).strip():
|
|
95
|
+
agent_id = None
|
|
96
|
+
user_id = arguments.get("user_id")
|
|
97
|
+
if user_id is None:
|
|
98
|
+
user_id = ctx_user
|
|
99
|
+
elif not str(user_id).strip():
|
|
100
|
+
user_id = None
|
|
81
101
|
# Audit 2026-10-05: thread ``max_chars`` (token / char budget cap)
|
|
82
102
|
# + ``level`` (tiered-loader) through the MCP tool so an MCP-aware
|
|
83
103
|
# agent loop can ask for a bounded retrieval inline. ``None`` and
|
|
@@ -98,7 +118,8 @@ def tool_recall(arguments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
98
118
|
except (TypeError, ValueError):
|
|
99
119
|
level = 1
|
|
100
120
|
r = store.recall(query, limit=limit, source=source,
|
|
101
|
-
max_chars=max_chars, level=level
|
|
121
|
+
max_chars=max_chars, level=level,
|
|
122
|
+
agent_id=agent_id, user_id=user_id)
|
|
102
123
|
n_mem = len(r["memories"])
|
|
103
124
|
n_wiki = len(r["wiki"])
|
|
104
125
|
n_ent = len(r["entities"])
|
|
@@ -528,6 +549,22 @@ TOOLS = [
|
|
|
528
549
|
"properties": {
|
|
529
550
|
"query": {"type": "string", "description": "Free-text query"},
|
|
530
551
|
"limit": {"type": "integer", "description": "Max results (default 8, max 50)"},
|
|
552
|
+
"agent_id": {
|
|
553
|
+
"type": "string",
|
|
554
|
+
"description": (
|
|
555
|
+
"Agent namespace to scope the answer to "
|
|
556
|
+
"(overrides LOOP_MEMORY_AGENT_ID). Empty string "
|
|
557
|
+
"means no restriction."
|
|
558
|
+
),
|
|
559
|
+
},
|
|
560
|
+
"user_id": {
|
|
561
|
+
"type": "string",
|
|
562
|
+
"description": (
|
|
563
|
+
"User namespace to scope the answer to "
|
|
564
|
+
"(overrides LOOP_MEMORY_USER_ID). Empty string "
|
|
565
|
+
"means no restriction."
|
|
566
|
+
),
|
|
567
|
+
},
|
|
531
568
|
},
|
|
532
569
|
"required": ["query"],
|
|
533
570
|
},
|
|
@@ -298,6 +298,14 @@ class MemoryClient(MemoryClientExt, AbstractContextManager):
|
|
|
298
298
|
validity window are dropped, subject-key slots collapse to
|
|
299
299
|
their newest live row, and near-duplicate texts collapse to
|
|
300
300
|
the newest one.
|
|
301
|
+
|
|
302
|
+
``agent_id`` / ``user_id`` (audit 2026-11-15, loop-memory
|
|
303
|
+
0.4.22) restrict the answer to one namespace. A client built
|
|
304
|
+
with ``MemoryClient.memory(..., agent_id=...)`` /
|
|
305
|
+
``MemoryClient.http(..., agent_id=...)`` falls back to that
|
|
306
|
+
identity, so a client that *writes* into a namespace also
|
|
307
|
+
*reads* from it. Memories written without an agent / user id
|
|
308
|
+
are global and stay visible to everyone.
|
|
301
309
|
"""
|
|
302
310
|
raise NotImplementedError
|
|
303
311
|
|
|
@@ -416,12 +424,28 @@ class _InProcessClient(MemoryClient):
|
|
|
416
424
|
# the legacy LIKE-based ``recall`` if a stale DB doesn't have
|
|
417
425
|
# it (older installs, tests).
|
|
418
426
|
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
427
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): the namespace the
|
|
428
|
+
# client was built with is the namespace it reads from. The
|
|
429
|
+
# predicate is applied inside the store's SQL so the limit
|
|
430
|
+
# counts only the caller's own rows.
|
|
431
|
+
# An explicit empty string means "no restriction"; ``None``
|
|
432
|
+
# falls back to the identity the client was built with.
|
|
433
|
+
if agent_id is None:
|
|
434
|
+
scope_agent = self._default_agent_id
|
|
435
|
+
else:
|
|
436
|
+
scope_agent = (str(agent_id).strip() or None)
|
|
437
|
+
if user_id is None:
|
|
438
|
+
scope_user = self._default_user_id
|
|
439
|
+
else:
|
|
440
|
+
scope_user = (str(user_id).strip() or None)
|
|
419
441
|
if hasattr(self._store, "recall_hybrid"):
|
|
420
442
|
r = self._store.recall_hybrid(
|
|
421
443
|
query, limit=limit, include=wanted,
|
|
422
444
|
bump_signals=True, source=source, level=level,
|
|
423
445
|
max_chars=max_chars,
|
|
424
446
|
latest_only=latest_only,
|
|
447
|
+
agent_id=scope_agent,
|
|
448
|
+
user_id=scope_user,
|
|
425
449
|
)
|
|
426
450
|
else:
|
|
427
451
|
r = self._store.recall(
|
|
@@ -432,9 +456,13 @@ class _InProcessClient(MemoryClient):
|
|
|
432
456
|
max_chars=max_chars,
|
|
433
457
|
level=level,
|
|
434
458
|
latest_only=latest_only,
|
|
459
|
+
agent_id=scope_agent,
|
|
460
|
+
user_id=scope_user,
|
|
435
461
|
)
|
|
436
|
-
#
|
|
437
|
-
#
|
|
462
|
+
# Defence in depth: the store already applied the namespace
|
|
463
|
+
# predicate inside its SQL, so this can only ever remove a
|
|
464
|
+
# row the SQL should not have returned anyway. It is kept so
|
|
465
|
+
# a future regression in the SQL cannot leak a row.
|
|
438
466
|
def _own(h: dict[str, Any]) -> bool:
|
|
439
467
|
ag = h.get("agent_id")
|
|
440
468
|
ur = h.get("user_id")
|
|
@@ -443,9 +471,9 @@ class _InProcessClient(MemoryClient):
|
|
|
443
471
|
# match the caller's filter or be unset on the memory.
|
|
444
472
|
if ag is None and ur is None:
|
|
445
473
|
return True
|
|
446
|
-
if
|
|
474
|
+
if scope_agent is not None and ag not in (None, scope_agent):
|
|
447
475
|
return False
|
|
448
|
-
if
|
|
476
|
+
if scope_user is not None and ur not in (None, scope_user):
|
|
449
477
|
return False
|
|
450
478
|
return True
|
|
451
479
|
return RecallResult(
|
|
@@ -780,10 +808,24 @@ class _HttpClient(MemoryClient):
|
|
|
780
808
|
params["latest_only"] = "1"
|
|
781
809
|
if max_chars is not None:
|
|
782
810
|
params["max_chars"] = int(max_chars)
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
811
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): a client built with
|
|
812
|
+
# ``MemoryClient.http(..., agent_id=...)`` writes into that
|
|
813
|
+
# namespace, so it reads from it too. The server applies the
|
|
814
|
+
# namespace predicate inside its SQL.
|
|
815
|
+
# An explicit empty string means "no restriction"; ``None``
|
|
816
|
+
# falls back to the identity the client was built with.
|
|
817
|
+
if agent_id is None:
|
|
818
|
+
scope_agent = self._default_agent_id
|
|
819
|
+
else:
|
|
820
|
+
scope_agent = (str(agent_id).strip() or None)
|
|
821
|
+
if user_id is None:
|
|
822
|
+
scope_user = self._default_user_id
|
|
823
|
+
else:
|
|
824
|
+
scope_user = (str(user_id).strip() or None)
|
|
825
|
+
if scope_agent is not None:
|
|
826
|
+
params["agent_id"] = scope_agent
|
|
827
|
+
if scope_user is not None:
|
|
828
|
+
params["user_id"] = scope_user
|
|
787
829
|
qs = urllib.parse.urlencode({k: v for k, v in params.items() if v != ""})
|
|
788
830
|
resp = self._request("GET", f"/api/v1/recall?{qs}")
|
|
789
831
|
return RecallResult(
|
|
@@ -307,6 +307,24 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
|
|
|
307
307
|
"global" and visible to every caller (matches the SDK
|
|
308
308
|
semantics).
|
|
309
309
|
|
|
310
|
+
Audit 2026-11-15 (loop-memory 0.4.22) -- the namespace
|
|
311
|
+
predicate is now applied **inside** the store's SQL, not on
|
|
312
|
+
top of its output. Before this release the filter ran after
|
|
313
|
+
the store had already applied its ``LIMIT``, which meant:
|
|
314
|
+
|
|
315
|
+
* in ``hybrid`` mode a foreign namespace could spend the
|
|
316
|
+
caller's whole result budget, so an agent could get zero
|
|
317
|
+
hits while its own matching rows sat further down the
|
|
318
|
+
ranking; and
|
|
319
|
+
* in ``legacy`` mode the filter was a **silent no-op** --
|
|
320
|
+
``store.recall()`` did not return ``agent_id`` /
|
|
321
|
+
``user_id``, so every row looked "global" and *every*
|
|
322
|
+
namespace saw *every* other namespace's memories.
|
|
323
|
+
|
|
324
|
+
The Python filter below is kept as a defence-in-depth net so
|
|
325
|
+
a future regression in the SQL cannot leak a row; the SQL is
|
|
326
|
+
what makes the ``limit`` meaningful.
|
|
327
|
+
|
|
310
328
|
``latest_only=1`` collapses the answer to one row per logical
|
|
311
329
|
fact: rows outside their validity window are dropped, subject
|
|
312
330
|
key slots collapse to their newest live row, and near-duplicate
|
|
@@ -315,6 +333,10 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
|
|
|
315
333
|
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
316
334
|
if not wanted:
|
|
317
335
|
wanted = ("memories", "wiki", "entities")
|
|
336
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): a blank namespace
|
|
337
|
+
# means "not specified" -- never "match nothing".
|
|
338
|
+
agent_id = (agent_id or "").strip() or None
|
|
339
|
+
user_id = (user_id or "").strip() or None
|
|
318
340
|
if mode == "legacy" or not hasattr(store, "recall_hybrid"):
|
|
319
341
|
r = store.recall(
|
|
320
342
|
q,
|
|
@@ -323,12 +345,16 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
|
|
|
323
345
|
bump_signals=True,
|
|
324
346
|
source=source,
|
|
325
347
|
latest_only=bool(latest_only),
|
|
348
|
+
agent_id=agent_id,
|
|
349
|
+
user_id=user_id,
|
|
326
350
|
)
|
|
327
351
|
else:
|
|
328
352
|
r = store.recall_hybrid(
|
|
329
353
|
q, limit=limit, include=wanted,
|
|
330
354
|
bump_signals=True, source=source, level=1,
|
|
331
355
|
latest_only=bool(latest_only),
|
|
356
|
+
agent_id=agent_id,
|
|
357
|
+
user_id=user_id,
|
|
332
358
|
)
|
|
333
359
|
if agent_id is not None or user_id is not None:
|
|
334
360
|
def _own(m: dict) -> bool:
|
|
@@ -403,7 +403,14 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
403
403
|
# their validity window are dropped, subject-key slots
|
|
404
404
|
# collapse to their newest live row, and near-duplicate
|
|
405
405
|
# texts collapse to the newest one.
|
|
406
|
-
latest_only: int = 0
|
|
406
|
+
latest_only: int = 0,
|
|
407
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): optional
|
|
408
|
+
# agent / user namespace. The predicate is applied in
|
|
409
|
+
# the store's SQL, so the ``limit`` counts only the
|
|
410
|
+
# caller's own rows. Unset (the default) keeps the
|
|
411
|
+
# dashboard's "see everything" behaviour.
|
|
412
|
+
agent_id: str | None = None,
|
|
413
|
+
user_id: str | None = None):
|
|
407
414
|
"""Unified recall — wiki + memories + entities in one ranked stream.
|
|
408
415
|
|
|
409
416
|
``mode`` is 'hybrid' (default) for BM25+semantic+entity RRF,
|
|
@@ -437,7 +444,16 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
437
444
|
vectorize-io/hindsight v0.10.2 "concise extraction by
|
|
438
445
|
default" + aiming-lab/SimpleMem ICML'26 semantic-lossless-
|
|
439
446
|
compression.
|
|
447
|
+
|
|
448
|
+
``agent_id`` / ``user_id`` (audit 2026-11-15, loop-memory
|
|
449
|
+
0.4.22) restrict the answer to one agent / user namespace.
|
|
450
|
+
Rows with a NULL ``agent_id`` / ``user_id`` are global and
|
|
451
|
+
stay visible to everyone.
|
|
440
452
|
"""
|
|
453
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): a blank namespace
|
|
454
|
+
# means "not specified" -- never "match nothing".
|
|
455
|
+
agent_id = (agent_id or "").strip() or None
|
|
456
|
+
user_id = (user_id or "").strip() or None
|
|
441
457
|
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
442
458
|
if not wanted:
|
|
443
459
|
wanted = ("memories", "wiki", "entities")
|
|
@@ -461,6 +477,8 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
461
477
|
source=source,
|
|
462
478
|
max_chars=max_chars,
|
|
463
479
|
level=level,
|
|
480
|
+
agent_id=agent_id,
|
|
481
|
+
user_id=user_id,
|
|
464
482
|
)
|
|
465
483
|
recall_mode = "as_of"
|
|
466
484
|
except ValueError as e:
|
|
@@ -481,6 +499,9 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
481
499
|
as_of_ts=(validity_at if (validity_at and str(validity_at).strip()) else None),
|
|
482
500
|
# Audit 2026-10-25: one answer per fact.
|
|
483
501
|
latest_only=bool(latest_only),
|
|
502
|
+
# Audit 2026-11-15: namespace predicate in SQL.
|
|
503
|
+
agent_id=agent_id,
|
|
504
|
+
user_id=user_id,
|
|
484
505
|
)
|
|
485
506
|
except ValueError as e:
|
|
486
507
|
# Invalid ``validity_at`` -> 400 (audit 2026-10-18)
|
|
@@ -496,6 +517,9 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
496
517
|
as_of_ts=(validity_at if (validity_at and str(validity_at).strip()) else None),
|
|
497
518
|
# Audit 2026-10-25: one answer per fact.
|
|
498
519
|
latest_only=bool(latest_only),
|
|
520
|
+
# Audit 2026-11-15: namespace predicate in SQL.
|
|
521
|
+
agent_id=agent_id,
|
|
522
|
+
user_id=user_id,
|
|
499
523
|
)
|
|
500
524
|
except ValueError as e:
|
|
501
525
|
raise HTTPException(400, str(e))
|
|
@@ -90,6 +90,11 @@ def bm25_search(
|
|
|
90
90
|
kind: str = "memories",
|
|
91
91
|
limit: int = 50,
|
|
92
92
|
source_filter: str | None = None,
|
|
93
|
+
# Audit 2026-11-15 (loop-memory 0.4.22): agent / user namespace.
|
|
94
|
+
# Applied inside the SQL so a foreign namespace cannot consume
|
|
95
|
+
# this channel's ``LIMIT`` budget.
|
|
96
|
+
agent_id: str | None = None,
|
|
97
|
+
user_id: str | None = None,
|
|
93
98
|
) -> list[dict]:
|
|
94
99
|
"""Run an FTS5 query and return a list of ``{"id": ..., "_score": bm25}``.
|
|
95
100
|
|
|
@@ -105,6 +110,13 @@ def bm25_search(
|
|
|
105
110
|
AND total length <3, FTS5 returns nothing; we then issue a
|
|
106
111
|
``LIKE '%<q>%'`` over the source table as a backstop. For our
|
|
107
112
|
scale (<10k memories, <1k wiki) LIKE is well under 5 ms.
|
|
113
|
+
|
|
114
|
+
``agent_id`` / ``user_id`` (audit 2026-11-15, loop-memory
|
|
115
|
+
0.4.22) restrict the ``memories`` branch to one namespace. The
|
|
116
|
+
predicate is applied in SQL -- both on the FTS5 query and on the
|
|
117
|
+
``LIKE`` backstop -- so the row budget is spent on the caller's
|
|
118
|
+
own memories. Rows with a NULL ``agent_id`` / ``user_id`` are
|
|
119
|
+
global and stay visible to everyone.
|
|
108
120
|
"""
|
|
109
121
|
import re as _re
|
|
110
122
|
fts_q = _escape_fts(query)
|
|
@@ -128,15 +140,19 @@ def bm25_search(
|
|
|
128
140
|
# answer. Without this filter the BM25 channel
|
|
129
141
|
# happily returned both versions of a fact, which
|
|
130
142
|
# broke the new ``latest_only`` semantics.
|
|
143
|
+
scope_sql, scope_params = store._scope_clause(
|
|
144
|
+
agent_id, user_id, alias="m"
|
|
145
|
+
)
|
|
131
146
|
sql = (
|
|
132
147
|
f"SELECT m.id AS id, {score_col} AS s "
|
|
133
148
|
f"FROM {table} t "
|
|
134
149
|
f"JOIN memories m ON m.rowid = t.rowid "
|
|
135
150
|
f"WHERE {table} MATCH ? "
|
|
136
151
|
f" AND m.superseded_by IS NULL "
|
|
152
|
+
f" {scope_sql} "
|
|
137
153
|
f"ORDER BY s LIMIT ?"
|
|
138
154
|
)
|
|
139
|
-
rows = c.execute(sql, (fts_q, limit)).fetchall()
|
|
155
|
+
rows = c.execute(sql, (fts_q, *scope_params, limit)).fetchall()
|
|
140
156
|
else:
|
|
141
157
|
# Wiki: also join back to wiki_pages so we can apply the
|
|
142
158
|
# per-source scope filter at the SQL layer. We negate
|
|
@@ -158,7 +174,8 @@ def bm25_search(
|
|
|
158
174
|
# lists are merged by id; LIKE rows get a synthetic importance
|
|
159
175
|
# score so the RRF caller can rank them alongside BM25.
|
|
160
176
|
like_rows = _like_fallback(store, query, kind=kind, limit=limit,
|
|
161
|
-
source_filter=source_filter
|
|
177
|
+
source_filter=source_filter,
|
|
178
|
+
agent_id=agent_id, user_id=user_id)
|
|
162
179
|
merged: dict[str, dict] = {}
|
|
163
180
|
for r in (rows or []):
|
|
164
181
|
# ``_fts_score`` keeps the raw SQLite score for diagnostics.
|
|
@@ -250,6 +267,8 @@ def _like_fallback(
|
|
|
250
267
|
kind: str,
|
|
251
268
|
limit: int,
|
|
252
269
|
source_filter: str | None = None,
|
|
270
|
+
agent_id: str | None = None,
|
|
271
|
+
user_id: str | None = None,
|
|
253
272
|
) -> list:
|
|
254
273
|
"""Brute-force LIKE fallback used when FTS5 trigram can't index
|
|
255
274
|
a query -- short strings, all-CJK with <3 chars, or the FTS5
|
|
@@ -273,13 +292,20 @@ def _like_fallback(
|
|
|
273
292
|
pat = f"%{q}%"
|
|
274
293
|
with store._conn() as c:
|
|
275
294
|
if kind == "memories":
|
|
295
|
+
scope_sql, scope_params = store._scope_clause(
|
|
296
|
+
agent_id, user_id, alias="m"
|
|
297
|
+
)
|
|
276
298
|
sql = (
|
|
277
299
|
"SELECT m.id AS id, m.importance AS importance "
|
|
278
300
|
"FROM memories m WHERE LOWER(text) LIKE LOWER(?) "
|
|
279
301
|
" AND m.superseded_by IS NULL "
|
|
302
|
+
f" {scope_sql} "
|
|
280
303
|
"ORDER BY m.importance DESC, m.created_at DESC LIMIT ?"
|
|
281
304
|
)
|
|
282
|
-
whole = [
|
|
305
|
+
whole = [
|
|
306
|
+
dict(r)
|
|
307
|
+
for r in c.execute(sql, (pat, *scope_params, limit)).fetchall()
|
|
308
|
+
]
|
|
283
309
|
if whole:
|
|
284
310
|
return whole
|
|
285
311
|
# Per-token OR pass -- at least one query word must hit.
|
|
@@ -293,9 +319,15 @@ def _like_fallback(
|
|
|
293
319
|
"SELECT m.id AS id, m.importance AS importance "
|
|
294
320
|
f"FROM memories m WHERE ({placeholders}) "
|
|
295
321
|
" AND m.superseded_by IS NULL "
|
|
322
|
+
f" {scope_sql} "
|
|
296
323
|
"ORDER BY m.importance DESC, m.created_at DESC LIMIT ?"
|
|
297
324
|
)
|
|
298
|
-
return [
|
|
325
|
+
return [
|
|
326
|
+
dict(r)
|
|
327
|
+
for r in c.execute(
|
|
328
|
+
sql_t, (*params, *scope_params, limit)
|
|
329
|
+
).fetchall()
|
|
330
|
+
]
|
|
299
331
|
sql = (
|
|
300
332
|
"SELECT w.id AS id, w.importance AS importance, w.scope AS scope "
|
|
301
333
|
"FROM wiki_pages w "
|