Skip to content

feat: implement Mem0 memory backend adapter#338

Closed
Aureliolo wants to merge 11 commits into
mainfrom
feat/mem0-adapter
Closed

feat: implement Mem0 memory backend adapter#338
Aureliolo wants to merge 11 commits into
mainfrom
feat/mem0-adapter

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Implement Mem0MemoryBackend adapter behind the pluggable MemoryBackend protocol — embedded Qdrant + SQLite, all Mem0 calls wrapped in asyncio.to_thread()
  • Full CRUD: store, retrieve, get, delete, count with per-agent isolation via user_id
  • Shared knowledge store: publish, search_shared, retract with publisher ownership tracking
  • Vendor-agnostic Mem0EmbedderConfigprovider and model are required fields (no hardcoded defaults)
  • Defensive parsing throughout: invalid categories, malformed datetimes, missing/blank IDs, non-dict metadata, non-list results
  • MemoryError/RecursionError re-raised before generic except Exception handlers
  • Post-retrieval filtering (category, tags, time range, min relevance) via apply_post_filters
  • Factory integration with embedder validation and dedicated MEMORY_BACKEND_CONFIG_INVALID event
  • Comprehensive test suite: 164 tests (unit + integration) covering all CRUD, shared knowledge, edge cases, and defensive paths

Test plan

  • uv run ruff check src/ tests/ — passes
  • uv run mypy src/ tests/ — no errors
  • uv run pytest tests/ -n auto -m "unit or integration" — 164 mem0 tests pass
  • Full suite: 7213 passed, 94.59% coverage

Closes #206

Add concrete MemoryBackend implementation using Mem0 (embedded Qdrant +
SQLite) as the storage layer, unblocking all downstream memory features.

- Mem0MemoryBackend implements MemoryBackend, MemoryCapabilities, and
  SharedKnowledgeStore protocols with asyncio.to_thread for all sync
  Mem0 calls
- Mapping layer (mappers.py) for bidirectional domain model <-> Mem0
  dict conversion with _synthorg_ metadata prefix
- Mem0BackendConfig + config builder deriving from CompanyMemoryConfig
- Factory wired up with deferred import (no longer raises
  MemoryConfigError for mem0 backend)
- Shared knowledge via reserved __synthorg_shared__ namespace with
  publisher ownership tracking
- 95 unit tests (adapter, mappers, config) + 6 integration tests
  (retrieval pipeline, shared knowledge flow)

Closes #206
Pre-reviewed by 9 agents, 33 findings addressed:
- Remove vendor-specific defaults from Mem0EmbedderConfig (now required)
- Fix metadata dict mutation in publish() (use spread instead)
- Add path traversal validation on Mem0BackendConfig.data_dir
- Extract _validate_add_result helper for store/publish result validation
- Add explicit ImportError handling in connect()
- Make health_check() probe backend with lightweight get_all call
- Add defensive category parsing in _extract_category (handle invalid enums)
- Add missing publisher check in retract() (not a shared memory entry)
- Replace C-style loop in search_shared with generator expression
- Use meaningful variable names (raw_entries/filtered vs triple result)
- Change logger.exception to logger.warning for self-raised errors
- Add structured error/error_type kwargs to all re-raise logs
- Add MemoryConnectionError to Raises docstrings for shared methods
- Update factory to require embedder config (no vendor defaults)
- Remove dead store_request_to_mem0_args function and its tests
- Add tests: path traversal, missing id, empty content, malformed datetime,
  invalid category, no-publisher retract, delete-after-get failure,
  protocol conformance, health_check probe failure
…iewers

- Add MemoryError/RecursionError guards before all except Exception blocks
- Capture exceptions in re-raise blocks for structured logging
- Move helper functions (validate_add_result, extract_category, extract_publisher)
  from adapter.py to mappers.py to keep adapter under 800-line limit
- Add type/tag/confidence validation in mappers (items 9, 11, 23)
- Add logging before all raise statements (items 7, 20, 21)
- Fix health_check exception log level from debug to warning
- Add search_shared query context to error logs
- Strengthen factory.py type annotation (embedder: Mem0EmbedderConfig | None)
- Parametrize config traversal tests, add ImportError/MemoryError/publish tests
- Update CLAUDE.md, README, roadmap, and design spec for Mem0 adapter status
- Fix dependency review CI: add LicenseRef-scancode-protobuf, ZPL-2.1
  to license allow-list and allow-dependencies-licenses for packages
  with null SPDX metadata (mem0ai, numpy, qdrant-client, posthog)
- Fix validate_add_result blank-ID gap: check for None and whitespace
  IDs, not just missing key (Greptile finding)
- Fix factory.py wrong event constant: use MEMORY_BACKEND_CONFIG_INVALID
  instead of MEMORY_BACKEND_UNKNOWN for embedder config errors (Greptile)
- Fix retract() bare except: capture as exc and log error detail
- Document count() limitation: capped at max_memories_per_agent
- Add 37 new tests: validate_add_result (blank/None/whitespace/numeric
  ID, non-list results), extract_category, extract_publisher, MemoryError
  re-raise for all operations, blank-ID through store/retrieve, shared
  namespace fallback, count empty results, retract delete failure
Copilot AI review requested due to automatic review settings March 13, 2026 06:26
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 13, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
pip/backoff 2.2.1 🟢 3.4
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/24 approved changesets -- score normalized to 2
Maintained⚠️ 0project is archived
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST🟢 7SAST tool detected but not run on all commits
pip/greenlet 3.3.2 UnknownUnknown
pip/grpcio 1.78.0 UnknownUnknown
pip/h2 4.3.0 UnknownUnknown
pip/hpack 4.1.0 UnknownUnknown
pip/hyperframe 6.1.0 UnknownUnknown
pip/mem0ai 1.0.5 UnknownUnknown
pip/numpy 2.4.3 UnknownUnknown
pip/portalocker 3.2.0 UnknownUnknown
pip/posthog 7.9.12 UnknownUnknown
pip/protobuf 5.29.6 UnknownUnknown
pip/pytz 2026.1.post1 UnknownUnknown
pip/qdrant-client 1.17.0 UnknownUnknown
pip/sqlalchemy 2.0.48 UnknownUnknown

Scanned Files

  • uv.lock

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 13, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Mem0 memory backend is now available via the memory factory (requires embedder config) and is exposed in the public API.
  • Documentation

    • Memory docs, roadmap, and README updated with Mem0 adapter and embedder configuration guidance.
  • Observability

    • New memory-related event types added for backend config and agent ID validation.
  • License

    • Additional license allowances and handling for deps with missing SPDX metadata.
  • Tests

    • Extensive unit and integration tests for Mem0 backend, config, mappers, and retrieval flows.
  • Style

    • Cleaned up test import lint comments.

Walkthrough

Adds a concrete Mem0-backed MemoryBackend: new mem0 backend package (config, adapter, mappers), factory wiring and exports, dependency and CI updates, observability event constants, docs updates, and extensive unit + integration tests. Also removes many inline test noqa comments (stylistic).

Changes

Cohort / File(s) Summary
Dependency & CI
pyproject.toml, .github/workflows/dependency-review.yml
Add optional mem0ai==1.0.5, mypy/ruff overrides for mem0.*, and expand allowed licenses / allow-dependencies-licenses entries for packages with missing SPDX metadata.
Public exports
src/ai_company/memory/__init__.py, src/ai_company/memory/backends/__init__.py, src/ai_company/memory/backends/mem0/__init__.py
Expose Mem0 public types (Mem0EmbedderConfig, Mem0MemoryBackend) and add backends package initializers.
Mem0 implementation
src/ai_company/memory/backends/mem0/adapter.py, src/ai_company/memory/backends/mem0/config.py, src/ai_company/memory/backends/mem0/mappers.py
Add Mem0MemoryBackend (lifecycle, CRUD, shared ops, capability flags, guarded async calls, error wrapping), Mem0 config models/builders, and stateless mappers for domain↔Mem0 translations and post-filters.
Factory wiring
src/ai_company/memory/factory.py
Update create_memory_backend(...) to accept optional embedder, implement backend == "mem0" path with validation, config building, and instantiation; update events used.
Observability events
src/ai_company/observability/events/memory.py
Add MEMORY_BACKEND_CONFIG_INVALID and MEMORY_BACKEND_AGENT_ID_REJECTED event constants.
Docs & README
CLAUDE.md, README.md, docs/design/memory.md, docs/roadmap/index.md
Document Mem0 adapter availability, pluggable memory backend wording, embedder configuration example, and related status text.
Tests — Mem0
tests/unit/memory/..., tests/integration/memory/test_mem0_backend.py, tests/unit/memory/test_factory.py, tests/unit/memory/test_init.py
Add comprehensive unit & integration tests and fixtures for Mem0 adapter, config, mappers, factory wiring, shared flows, retrieval pipeline, and mocked client helpers.
Tests — lint cleanup
tests/**/* (many test files)
Remove numerous inline # noqa comments across many tests (stylistic cleanup, no behavioral changes).

Sequence Diagram(s)

sequenceDiagram
    participant Agent
    participant Mem0Backend as Mem0MemoryBackend
    participant Mem0Lib as Mem0 Client
    participant VectorStore as Vector Store
    participant SQLite as SQLite History

    Agent->>Mem0Backend: store(agent_id, request)
    activate Mem0Backend
    Mem0Backend->>Mem0Lib: add(name, messages, metadata)
    activate Mem0Lib
    Mem0Lib->>VectorStore: embed & index memory
    Mem0Lib->>SQLite: persist metadata/history
    Mem0Lib-->>Mem0Backend: {id, ...}
    deactivate Mem0Lib
    Mem0Backend->>Mem0Backend: validate_add_result()
    Mem0Backend-->>Agent: memory_id
    deactivate Mem0Backend

    Agent->>Mem0Backend: retrieve(agent_id, query)
    activate Mem0Backend
    Mem0Backend->>Mem0Lib: search(query, agent_id)
    activate Mem0Lib
    Mem0Lib->>VectorStore: vector similarity search
    Mem0Lib->>SQLite: fetch metadata
    Mem0Lib-->>Mem0Backend: [result1, result2, ...]
    deactivate Mem0Lib
    Mem0Backend->>Mem0Backend: mem0_result_to_entry()<br/>apply_post_filters()
    Mem0Backend-->>Agent: tuple[MemoryEntry, ...]
    deactivate Mem0Backend
Loading
sequenceDiagram
    participant Agent as Agent A
    participant Mem0Backend
    participant Mem0Lib as Mem0 Client
    participant SharedNS as Shared Namespace

    Agent->>Mem0Backend: publish(agent_id, request)
    activate Mem0Backend
    Mem0Backend->>Mem0Backend: build_mem0_metadata(add publisher)
    Mem0Backend->>Mem0Lib: add(..., user_id=shared)
    activate Mem0Lib
    Mem0Lib->>SharedNS: store in shared namespace
    Mem0Lib-->>Mem0Backend: memory_id
    deactivate Mem0Lib
    Mem0Backend-->>Agent: shared_memory_id
    deactivate Mem0Backend

    rect rgba(100,150,200,0.5)
    Agent B->>Mem0Backend: search_shared(query, exclude_agent=Agent A)
    activate Mem0Backend
    Mem0Backend->>Mem0Lib: search(query, shared namespace)
    activate Mem0Lib
    Mem0Lib->>SharedNS: vector search across shared
    Mem0Lib-->>Mem0Backend: [result1, result2, ...]
    deactivate Mem0Lib
    Mem0Backend->>Mem0Backend: filter(exclude_agent)<br/>apply_post_filters()
    Mem0Backend-->>Agent B: tuple[MemoryEntry, ...]
    deactivate Mem0Backend
    end

    Agent->>Mem0Backend: retract(agent_id, memory_id)
    activate Mem0Backend
    Mem0Backend->>Mem0Lib: get(memory_id)
    activate Mem0Lib
    Mem0Lib-->>Mem0Backend: result (with publisher)
    deactivate Mem0Lib
    Mem0Backend->>Mem0Backend: extract_publisher() / verify ownership
    Mem0Backend->>Mem0Lib: delete(memory_id)
    Mem0Lib->>SharedNS: remove from shared
    Mem0Backend-->>Agent: success: bool
    deactivate Mem0Backend
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement Mem0 memory backend adapter' clearly and concisely summarizes the main change—implementing a concrete Mem0 backend adapter for the memory system.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the implementation of the Mem0MemoryBackend adapter, CRUD operations, shared knowledge store, configuration, testing, and closure of issue #206.
Linked Issues check ✅ Passed The PR fully addresses #206 by implementing a concrete MemoryBackend adapter (Mem0MemoryBackend), ensuring MemoryCapabilities and SharedKnowledgeStore protocol compatibility, integrating with the retrieval pipeline, maintaining Python 3.14+ compatibility, and delivering comprehensive test coverage.
Out of Scope Changes check ✅ Passed The PR includes removal of noqa comments from multiple test files, which are minor linting/formatting changes unrelated to the core objective of implementing the Mem0 backend but are acceptable as cleanup accompanying the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mem0-adapter
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/mem0-adapter
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the memory system by integrating Mem0 as a pluggable memory backend. This allows the system to leverage an embedded vector database (Qdrant) and SQLite for persistent agent memory, providing robust storage and retrieval capabilities. The changes ensure that memory operations are non-blocking, support both individual agent memories and a shared knowledge store, and are thoroughly tested for reliability and correctness. This implementation moves the project closer to a fully modular and extensible memory architecture.

Highlights

  • Mem0 Memory Backend Implementation: Implemented the Mem0MemoryBackend adapter, which uses embedded Qdrant and SQLite, wrapping all Mem0 calls in asyncio.to_thread() for non-blocking operations.
  • Comprehensive CRUD Operations: Enabled full CRUD functionality (store, retrieve, get, delete, count) with per-agent isolation via user_id.
  • Shared Knowledge Store: Introduced publish, search_shared, and retract operations for a shared knowledge store, including publisher ownership tracking.
  • Robust Configuration and Parsing: Added a vendor-agnostic Mem0EmbedderConfig requiring explicit provider and model fields, and implemented defensive parsing for various data types and formats.
  • Error Handling and Observability: Ensured MemoryError and RecursionError are re-raised before generic exception handlers, and integrated factory validation with a dedicated MEMORY_BACKEND_CONFIG_INVALID event.
  • Post-Retrieval Filtering: Included apply_post_filters for advanced filtering based on category, tags, time range, and minimum relevance after initial retrieval.
  • Extensive Test Coverage: Developed a comprehensive test suite of 164 unit and integration tests covering all CRUD, shared knowledge, edge cases, and defensive paths, achieving 94.59% coverage.
Changelog
  • CLAUDE.md
    • Updated the description of the memory module to reflect the pluggable MemoryBackend protocol and the Mem0 adapter.
    • Added an optional mem0ai dependency group for the Mem0 memory backend.
  • README.md
    • Updated the project status to indicate that the Mem0 backend adapter is now implemented.
  • docs/design/memory.md
    • Updated the memory design document to reflect that Mem0 is now implemented.
  • docs/roadmap/index.md
    • Removed the 'Mem0 adapter' entry from the roadmap as it is now implemented.
  • pyproject.toml
    • Added mem0ai as a new dependency.
    • Added a Mypy override for the mem0.* module to ignore missing imports.
  • src/ai_company/memory/init.py
    • Imported and re-exported Mem0EmbedderConfig and Mem0MemoryBackend.
  • src/ai_company/memory/backends/init.py
    • Added a new file to export Mem0MemoryBackend.
  • src/ai_company/memory/backends/mem0/init.py
    • Added a new file to export Mem0BackendConfig, Mem0EmbedderConfig, and Mem0MemoryBackend.
  • src/ai_company/memory/backends/mem0/adapter.py
    • Added a new file implementing the Mem0MemoryBackend with full CRUD operations and shared knowledge store functionality, wrapping synchronous Mem0 calls in asyncio.to_thread().
  • src/ai_company/memory/backends/mem0/config.py
    • Added a new file defining Mem0EmbedderConfig and Mem0BackendConfig models, including a validator to prevent directory traversal and a function to build Mem0-compatible configuration dictionaries.
  • src/ai_company/memory/backends/mem0/mappers.py
    • Added a new file containing stateless mapping functions for converting between SynthOrg domain models and Mem0 dictionary formats, including metadata serialization/deserialization and post-retrieval filtering.
  • src/ai_company/memory/factory.py
    • Updated the create_memory_backend function to instantiate Mem0MemoryBackend when config.backend is 'mem0', including validation for the required embedder configuration.
    • Removed the 'not yet implemented' error for the Mem0 backend.
  • src/ai_company/observability/events/memory.py
    • Added a new observability event MEMORY_BACKEND_CONFIG_INVALID.
  • tests/integration/memory/test_mem0_backend.py
    • Added a new integration test file for the Mem0 backend, covering store, retrieve, shared knowledge flow, and counting, using a mocked Mem0 client.
  • tests/unit/memory/backends/mem0/test_adapter.py
    • Added a new unit test file for the Mem0MemoryBackend adapter, covering properties, capabilities, lifecycle, connection guards, CRUD operations, shared knowledge, and edge cases.
  • tests/unit/memory/backends/mem0/test_config.py
    • Added a new unit test file for Mem0EmbedderConfig and Mem0BackendConfig, including validation for required fields, immutability, and path traversal prevention.
  • tests/unit/memory/backends/mem0/test_mappers.py
    • Added a new unit test file for Mem0 mapping functions, covering metadata serialization, datetime parsing, relevance score normalization, result-to-entry conversion, query argument building, and post-filtering.
  • tests/unit/memory/test_factory.py
    • Updated unit tests for create_memory_backend to reflect the new Mem0 backend implementation and its embedder requirements.
  • tests/unit/memory/test_init.py
    • Updated unit tests for __all__ exports in ai_company.memory to include Mem0EmbedderConfig and Mem0MemoryBackend.
  • uv.lock
    • Updated locked dependencies to include backoff, greenlet, grpcio, h2, hpack, hyperframe, mem0ai, numpy, portalocker, posthog, protobuf, pytz, qdrant-client, and sqlalchemy.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/dependency-review.yml
Activity
  • All ruff check validations passed for src/ and tests/.
  • No mypy errors were found in src/ and tests/.
  • 164 unit and integration tests specific to Mem0 passed successfully.
  • The full test suite passed with 7213 tests and achieved 94.59% code coverage.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 13, 2026

Greptile Summary

This PR introduces a Mem0MemoryBackend adapter that implements the MemoryBackend, MemoryCapabilities, and SharedKnowledgeStore protocols using Mem0 (embedded Qdrant + SQLite), along with a comprehensive 164-test suite. The core architecture is well-structured — per-agent isolation via user_id, publisher ownership tracking for shared knowledge, and thorough defensive parsing throughout the mapper layer.

Key findings from this review pass:

  • count() vs expiry inconsistency (adapter.py:644–697): count() tallies raw entries from Mem0 without passing them through apply_post_filters, so expired entries inflate the returned total. retrieve() and search_shared() both filter expired entries via apply_post_filters, making count() semantically inconsistent. A caller guarding writes behind count() < limit will allow excess writes as entries age out.
  • search_shared assigns _SHARED_NAMESPACE as agent_id for publisher-less entries (adapter.py:804–814): entries whose publisher metadata was not preserved get a fabricated agent_id of _SHARED_NAMESPACE, which makes the exclude_agent filter behave unexpectedly — either silently dropping or always including a whole class of entries depending on the caller's value.
  • Several issues flagged in earlier review threads (Python 2 except comma syntax in adapter.py and mappers.py, publish/retract missing _validate_agent_id, qdrant-external silent fallback, mem0ai as a hard dependency) remain open in the current HEAD.

Confidence Score: 2/5

  • Not safe to merge — multiple previously-flagged critical issues (Python 2 syntax errors, missing access control guards) remain unresolved alongside a new count/expiry inconsistency.
  • The adapter still contains Python 2 except A, B: syntax (a SyntaxError in Python 3) across 10+ sites in adapter.py and 4 sites in mappers.py; publish and retract both skip _validate_agent_id; qdrant-external is accepted but silently uses embedded Qdrant; and mem0ai is a hard dependency contrary to documentation. The new count() expiry inconsistency adds another logical bug. While several prior concerns (disconnect reset, connect idempotency, get ownership check, delete shared-namespace guard, expiry filtering in retrieve/search_shared) have been addressed well, the volume of outstanding issues keeps confidence low.
  • Pay close attention to src/ai_company/memory/backends/mem0/adapter.py (count expiry, except syntax, missing guards) and src/ai_company/memory/backends/mem0/mappers.py (except syntax).

Important Files Changed

Filename Overview
src/ai_company/memory/backends/mem0/adapter.py Core adapter implementing MemoryBackend + SharedKnowledgeStore. Several previously flagged issues (Python 2 except syntax, publish/retract missing _validate_agent_id, qdrant-external silent fallback) remain. New issue: count() does not filter expired entries, creating an inconsistency with retrieve() and search_shared(). The exclude_agent comparison in search_shared also relies on a fabricated fallback agent_id for publisher-less entries.
src/ai_company/memory/backends/mem0/config.py Pydantic config models and config builder. Path traversal guard is sound. The qdrant-external acceptance with silent embedded fallback was flagged in a prior review thread and remains unaddressed.
src/ai_company/memory/backends/mem0/mappers.py Bidirectional mapping between domain models and Mem0 dicts. apply_post_filters correctly handles expiry, category, tags, time range, and min_relevance. Python 2 except comma syntax (lines 76, 104, 123, 145) was flagged in a prior thread and remains present. Defensive parsing throughout is well-implemented.
src/ai_company/memory/factory.py Factory cleanly defers Mem0 imports and validates embedder config before construction. Error handling and logging are thorough. No new issues found.
pyproject.toml mem0ai==1.0.5 is still declared as a required project dependency rather than an optional extra, despite CLAUDE.md documenting it as optional. This forces all consumers to install Qdrant-client, posthog, numpy, and other heavy transitive dependencies even when no Mem0 backend is configured.
tests/unit/memory/backends/mem0/test_adapter_crud.py Comprehensive unit test coverage for CRUD operations. No test validates count() against entries with expires_at in the past, which would surface the overcount bug described in the adapter review.
tests/integration/memory/test_mem0_backend.py Integration test suite covering real Mem0 CRUD and shared knowledge flows. Well-structured with proper setup/teardown. Does not exercise expired-entry filtering against count().

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Mem0MemoryBackend
    participant asyncio_thread as asyncio.to_thread
    participant Mem0SDK as Mem0 SDK (Memory)
    participant Qdrant as Embedded Qdrant
    participant SQLite

    Note over Caller,SQLite: Lifecycle
    Caller->>Mem0MemoryBackend: connect()
    Mem0MemoryBackend->>asyncio_thread: Memory.from_config(config_dict)
    asyncio_thread->>Qdrant: init collection
    asyncio_thread->>SQLite: init history DB
    asyncio_thread-->>Mem0MemoryBackend: Memory client
    Mem0MemoryBackend-->>Caller: connected

    Note over Caller,SQLite: Private CRUD (per-agent isolation via user_id)
    Caller->>Mem0MemoryBackend: store(agent_id, request)
    Mem0MemoryBackend->>asyncio_thread: client.add(user_id=agent_id, infer=False)
    asyncio_thread->>Qdrant: upsert vector
    asyncio_thread->>SQLite: log history
    asyncio_thread-->>Mem0MemoryBackend: {results: [{id, ...}]}
    Mem0MemoryBackend-->>Caller: memory_id

    Caller->>Mem0MemoryBackend: retrieve(agent_id, query)
    alt query.text set
        Mem0MemoryBackend->>asyncio_thread: client.search(query, user_id)
        asyncio_thread->>Qdrant: ANN vector search
    else no text
        Mem0MemoryBackend->>asyncio_thread: client.get_all(user_id)
        asyncio_thread->>Qdrant: fetch all for user
    end
    asyncio_thread-->>Mem0MemoryBackend: raw results
    Mem0MemoryBackend->>Mem0MemoryBackend: apply_post_filters (expiry, category, tags, time, relevance)
    Mem0MemoryBackend-->>Caller: tuple[MemoryEntry]

    Note over Caller,SQLite: Shared Knowledge Store (reserved __synthorg_shared__ namespace)
    Caller->>Mem0MemoryBackend: publish(agent_id, request)
    Mem0MemoryBackend->>asyncio_thread: client.add(user_id=_SHARED_NAMESPACE, metadata+publisher)
    asyncio_thread->>Qdrant: upsert to shared namespace
    asyncio_thread-->>Mem0MemoryBackend: {results: [{id}]}
    Mem0MemoryBackend-->>Caller: shared memory_id

    Caller->>Mem0MemoryBackend: retract(agent_id, memory_id)
    Mem0MemoryBackend->>asyncio_thread: client.get(memory_id)
    asyncio_thread-->>Mem0MemoryBackend: raw entry
    Mem0MemoryBackend->>Mem0MemoryBackend: verify publisher == agent_id
    Mem0MemoryBackend->>asyncio_thread: client.delete(memory_id)
    asyncio_thread-->>Mem0MemoryBackend: ok
    Mem0MemoryBackend-->>Caller: True
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 644-697

Comment:
**`count()` overcounts when entries have `expires_at` set**

`count()` fetches raw entries from Mem0 and tallies them directly, but never calls `apply_post_filters`. This means entries whose `expires_at` has already passed are still included in the total.

`retrieve()` and `search_shared()` both route through `apply_post_filters`, which discards expired entries at line 370 of `mappers.py`:

```python
if entry.expires_at is not None and entry.expires_at <= now:
    continue
```

So after entries expire, `count()` returns a stale inflated total while `retrieve()` on the same agent returns fewer entries — a direct semantic inconsistency. A caller who guards `store()` behind a `count() < limit` check would allow more writes than intended once entries start expiring.

The fix is to deserialise the raw list into `MemoryEntry` objects and pass them through `apply_post_filters` before counting, the same way `retrieve` does, or at minimum inline the expiry check:

```python
now = datetime.now(UTC)
...
if category is None:
    total = sum(
        1 for item in raw_list
        if not (
            parse_mem0_datetime(item.get("metadata", {}).get("_synthorg_expires_at"))
            is not None
            and parse_mem0_datetime(item.get("metadata", {}).get("_synthorg_expires_at")) <= now
        )
    )
```

A cleaner approach: deserialise with `mem0_result_to_entry` and pass through `apply_post_filters(entries, MemoryQuery(limit=..., categories={category} if category else set()))` so all post-filters stay in one place.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 812-814

Comment:
**`exclude_agent` comparison uses fabricated `agent_id` for publisher-less entries**

When a shared entry has no publisher metadata, `extract_publisher(item)` returns `None`, so `agent_id` is set to `_SHARED_NAMESPACE` in `mem0_result_to_entry`:

```python
mem0_result_to_entry(
    item,
    extract_publisher(item) or _SHARED_NAMESPACE,  # ← fabricated fallback
)
```

Then the `exclude_agent` filter compares `e.agent_id != exclude_agent`. If two different agents each published entries whose publisher metadata was stripped (e.g. by a Mem0 SDK update that drops unrecognised metadata keys), both entries land with `agent_id == _SHARED_NAMESPACE`. Passing `exclude_agent=_SHARED_NAMESPACE` would then silently suppress all such entries, while passing any real `agent_id` would include them regardless of who actually wrote them.

Since `retract` already requires the publisher key to be present (it raises when `publisher is None`), publisher-less shared entries represent corrupted state. It would be safer to log a warning and skip them in `search_shared` rather than assigning an ownership guess to `_SHARED_NAMESPACE`.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: ac7d47e

Comment thread src/ai_company/memory/backends/mem0/adapter.py
Comment thread src/ai_company/memory/backends/mem0/adapter.py
Comment thread pyproject.toml
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a well-implemented Mem0 memory backend adapter, complete with thorough configuration, mappers, and an extensive test suite. The code is well-structured and follows good practices for creating a pluggable backend, including defensive programming and detailed logging. My main concern is a recurring Python 2-style except syntax that is invalid in Python 3 and will cause syntax errors. After addressing this critical issue, the pull request will be in excellent condition.

try:
config_dict = build_mem0_config_dict(self._mem0_config)
client = await asyncio.to_thread(Memory.from_config, config_dict)
except MemoryError, RecursionError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The except A, B: syntax for catching multiple exceptions is from Python 2 and results in a SyntaxError in Python 3. The project's pyproject.toml specifies requires-python = ">=3.14".

You should use parentheses to create a tuple of exception types: except (MemoryError, RecursionError):.

This issue is present in multiple places in this file (e.g., lines 173, 283, 345, 403, 465, 537, 604, 679, 766) and should be corrected throughout.

        except (MemoryError, RecursionError):

return None
try:
dt = datetime.fromisoformat(raw)
except ValueError, TypeError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The except A, B: syntax for catching multiple exceptions is from Python 2 and is a SyntaxError in Python 3. Please use except (ValueError, TypeError):. This issue also occurs on line 139.

    except (ValueError, TypeError):

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the Mem0-based MemoryBackend so SynthOrg can persist and retrieve agent memories via Mem0 (embedded Qdrant + SQLite), and wires it into the memory backend factory with supporting config/models/tests/docs.

Changes:

  • Add Mem0 backend implementation (adapter + mappers + config) and export it from ai_company.memory.
  • Update create_memory_backend() to instantiate Mem0 when selected (with required embedder config).
  • Add comprehensive unit/integration tests and update docs/workflows/dependencies to reflect the new backend.

Reviewed changes

Copilot reviewed 20 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
uv.lock Locks new transitive dependencies pulled in by Mem0/Qdrant stack.
pyproject.toml Adds mem0ai dependency; mypy override for mem0.*.
src/ai_company/observability/events/memory.py Adds new event constant for invalid backend config.
src/ai_company/memory/factory.py Implements Mem0 backend construction + embedder validation/logging.
src/ai_company/memory/backends/mem0/adapter.py New Mem0 MemoryBackend + shared knowledge store implementation.
src/ai_company/memory/backends/mem0/mappers.py New Mem0↔domain mapping/helpers used by the adapter.
src/ai_company/memory/backends/mem0/config.py New Mem0 config models + builder for Mem0 SDK config dict.
src/ai_company/memory/backends/mem0/__init__.py Exposes Mem0 backend/config types.
src/ai_company/memory/backends/__init__.py Adds concrete backends package export(s).
src/ai_company/memory/__init__.py Re-exports Mem0 backend/config types from ai_company.memory.
tests/unit/memory/test_init.py Extends public export checks for Mem0 symbols.
tests/unit/memory/test_factory.py Updates factory tests to assert Mem0 backend creation + validation errors.
tests/unit/memory/backends/mem0/test_mappers.py New mapper unit tests.
tests/unit/memory/backends/mem0/test_config.py New config model unit tests.
tests/unit/memory/backends/mem0/test_adapter.py New adapter unit tests.
tests/integration/memory/test_mem0_backend.py New integration-style pipeline test using mocked Mem0 client.
README.md Updates status text to reflect Mem0 adapter being implemented.
docs/roadmap/index.md Removes Mem0 adapter from “remaining” roadmap items.
docs/design/memory.md Updates design doc to mark Mem0 initial backend as implemented.
CLAUDE.md Updates repository overview + dependency notes for Mem0 backend.
.github/workflows/dependency-review.yml Adjusts license allowlists for new dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyproject.toml
Comment on lines 21 to 26
"litellm==1.82.1",
"litestar[standard,structlog,pydantic,brotli,prometheus]==2.21.1",
"mcp==1.26.0",
"mem0ai==1.0.5",
"pydantic==2.12.5",
"pyjwt[crypto]==2.11.0",
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/dependency-review.yml:
- Around line 47-51: The allow-dependencies-licenses list currently contains
unversioned package PURLs which bypass license checks for all future releases;
update each entry under allow-dependencies-licenses to the exact, version-pinned
PURLs from the lockfile: replace pkg:pypi/mem0ai with pkg:pypi/mem0ai@1.0.5,
pkg:pypi/numpy with pkg:pypi/numpy@2.4.3, pkg:pypi/qdrant-client with
pkg:pypi/qdrant-client@1.17.0, and pkg:pypi/posthog with pkg:pypi/posthog@7.9.12
so the exceptions apply only to the locked versions.

In `@CLAUDE.md`:
- Line 208: There is a mismatch between CLAUDE.md (which lists mem0ai as
"Optional") and pyproject.toml (which currently lists mem0ai as a required
dependency); either update the docs or change the pyproject to match: to keep
mem0ai optional, move it out of the main dependencies in pyproject.toml and add
it under an optional/extras section (e.g., [project.optional-dependencies] or
extras like "mem0") so it is only installed when backend: "mem0" is used, or
conversely change CLAUDE.md to mark mem0ai as "Required" if you intend it to
stay in the main dependencies; reference the mem0ai entry, CLAUDE.md line
describing backend: "mem0", and the dependency list in pyproject.toml when
making the edit.

In `@src/ai_company/memory/backends/mem0/adapter.py`:
- Around line 326-336: The Mem0 client call may return None, causing
raw_result.get(...) to raise an AttributeError; after calling
self._client.search(...) or self._client.get_all(...) (the raw_result
assignment), defensively check if raw_result is None and either replace it with
an empty dict or raise a clear error mentioning the agent_id and which call
failed (search vs get_all); update the block that builds kwargs via
query_to_mem0_search_args / query_to_mem0_getall_args and then calls
mem0_result_to_entry and apply_post_filters to use the validated raw_result (or
short-circuit) so subsequent raw_result.get("results", []) is safe and errors
are informative.
- Around line 140-148: The disconnect method currently nulls out self._client
without closing the underlying httpx.Client; change Memory.disconnect to, before
setting self._client = None, check if self._client is truthy and then close the
sync client via await asyncio.to_thread(self._client.close) (or the appropriate
.client.close if the wrapper stores it as .client), then set self._client = None
and self._connected = False; keep the existing logger calls and ensure the close
happens inside asyncio.to_thread to avoid blocking the event loop.

In `@src/ai_company/memory/backends/mem0/config.py`:
- Around line 81-129: The builder currently drops CompanyMemoryConfig.storage
overrides and silently uses embedded Qdrant+SQLite; update
build_config_from_company_config to validate
CompanyMemoryConfig.storage.vector_store and .history_store and raise a clear
exception (e.g., ValueError) for unsupported values such as "qdrant-external" or
"postgresql" so callers fail fast; locate the validation in
build_config_from_company_config (before constructing Mem0BackendConfig) and
ensure build_mem0_config_dict remains consistent with accepted values by only
emitting embedded Qdrant/SQLite when storage fields are missing or explicitly
set to the embedded options.
- Around line 8-15: The module currently raises in _reject_traversal without any
structured logging; add "from ai_company.observability import get_logger" and
initialize "logger = get_logger(__name__)" at module scope, then update
_reject_traversal to log the invalid path and context at WARNING or ERROR
(include the offending path, type information, and any relevant config field)
immediately before raising; also apply the same logging-to-raised-error pattern
for the other failure path around lines 69-78 so every error path in this config
module logs structured context before raising.

In `@src/ai_company/memory/backends/mem0/mappers.py`:
- Around line 148-153: The tags extraction may pass non-string values into
NotBlankStr; change the filtering/mapping so raw_tags elements are explicitly
converted to strings before constructing NotBlankStr: when building tags from
raw_tags (the raw_metadata lookup using f"{_PREFIX}tags"), coerce each retained
element with str(...) and then apply NotBlankStr only to the string (e.g., use a
generator that converts t to str(t) and trims/filters blank strings) so
NotBlankStr always receives a string.
- Around line 136-146: Parsed confidence from raw_metadata (raw_confidence ->
confidence) may be outside [0.0, 1.0], which causes Pydantic ValidationError
when constructing MemoryMetadata; after parsing (or on defaulting to 1.0) clamp
confidence into the valid range with something like confidence = max(0.0,
min(1.0, confidence)), and if clamping changed the value emit a logger.warning
using MEMORY_MODEL_INVALID with field="confidence", raw_value=raw_confidence and
reason indicating it was clamped to the valid range before passing into
MemoryMetadata.

In `@src/ai_company/memory/factory.py`:
- Around line 48-93: The Mem0 branch in create_memory_backend currently lets
backend-specific errors from build_config_from_company_config and
Mem0MemoryBackend escape; extract the Mem0 logic into a small helper (e.g.,
_create_mem0_backend) that is called from create_memory_backend after the
existing embedder checks (Mem0EmbedderConfig validation), and wrap the calls to
build_config_from_company_config and Mem0MemoryBackend in a try/except that
catches Exception, logs MEMORY_BACKEND_CONFIG_INVALID with backend="mem0", a
suitable reason like "backend_config_error", and the error details, then raise
MemoryConfigError with a clear message; ensure the helper returns the created
backend and that create_memory_backend delegates to it to keep function size
under the limit.

In `@tests/unit/memory/backends/mem0/test_adapter.py`:
- Around line 1-1117: The test module is too large and mixes many concerns;
split it into smaller files grouping related test classes (e.g., move
TestLifecycle, TestConnectionGuard to
tests/unit/memory/backends/mem0/test_lifecycle.py; TestStore and TestPublish to
test_store.py; TestRetrieve and TestSearchShared to test_retrieve_shared.py;
TestGet/TestDelete/TestCount/TestRetract to their own files; and
TestAdditionalEdgeCases to test_edgecases.py) and replace repeated similar
assertions with pytest.mark.parametrize (collapse matrix-like tests in
TestConnectionGuard, TestStore empty/missing/id cases, TestRetrieve variants,
TestCount variants, TestPublish variants, TestSearchShared exclude/namespace
cases, and TestRetract ownership/no-publisher/delete-failure cases) to reduce
duplication while preserving unique test names like
TestLifecycle.test_connect_success,
Mem0MemoryBackend.store/retrieve/get/delete/count/publish/search_shared/retract,
and helper fixtures (_mem0_add_result, _mem0_search_result, _mem0_get_result,
_make_store_request) so each new file stays under ~800 lines and functions
remain <50 lines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b6224e3-7a24-4e20-85bc-c80da6a70b08

📥 Commits

Reviewing files that changed from the base of the PR and between 7a68d34 and 28eba7a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • .github/workflows/dependency-review.yml
  • CLAUDE.md
  • README.md
  • docs/design/memory.md
  • docs/roadmap/index.md
  • pyproject.toml
  • src/ai_company/memory/__init__.py
  • src/ai_company/memory/backends/__init__.py
  • src/ai_company/memory/backends/mem0/__init__.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/config.py
  • src/ai_company/memory/backends/mem0/mappers.py
  • src/ai_company/memory/factory.py
  • src/ai_company/observability/events/memory.py
  • tests/integration/memory/test_mem0_backend.py
  • tests/unit/memory/backends/__init__.py
  • tests/unit/memory/backends/mem0/__init__.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/unit/memory/backends/mem0/test_config.py
  • tests/unit/memory/backends/mem0/test_mappers.py
  • tests/unit/memory/test_factory.py
  • tests/unit/memory/test_init.py
💤 Files with no reviewable changes (1)
  • docs/roadmap/index.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (4)
pyproject.toml

📄 CodeRabbit inference engine (CLAUDE.md)

pyproject.toml: Pinned: all versions use == in pyproject.toml
Groups in pyproject.toml: test (pytest + plugins), dev (includes test + ruff, mypy, pre-commit, commitizen). Install: uv sync installs everything (dev group is default)

Files:

  • pyproject.toml
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use except A, B: syntax (no parentheses) — ruff enforces PEP 758 except syntax on Python 3.14
Type hints: all public functions, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Config vs runtime state: use frozen Pydantic models for config/identity; separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Models: Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use @computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr for all identifier/name fields — including optional and tuple variants — instead of manual whitespace validators.
Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task. Existing code is being migrated incrementally.
Line length: 88 characters (ruff)
Functions: < 50 lines, files < 800 lines
Errors: handle explicitly, never silently swallow
Validate: at system boundaries (user input, external APIs, config files)

Files:

  • tests/unit/memory/test_init.py
  • tests/unit/memory/backends/mem0/test_config.py
  • src/ai_company/memory/backends/mem0/config.py
  • src/ai_company/observability/events/memory.py
  • src/ai_company/memory/__init__.py
  • tests/unit/memory/test_factory.py
  • tests/unit/memory/backends/mem0/test_mappers.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • src/ai_company/memory/backends/mem0/__init__.py
  • tests/integration/memory/test_mem0_backend.py
  • src/ai_company/memory/factory.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/__init__.py
  • src/ai_company/memory/backends/mem0/mappers.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Tests: markers @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow
Coverage: 80% minimum (enforced in CI)
Async: asyncio_mode = "auto" — no manual @pytest.mark.asyncio needed
Timeout: 30 seconds per test
Parallelism: pytest-xdist via -n auto — ALWAYS include -n auto when running pytest, never run tests sequentially
Prefer @pytest.mark.parametrize for testing similar cases
Vendor-agnostic everywhere: Tests must use test-provider, test-small-001, etc. (Vendor names may only appear in operations design page, .claude/ skill/agent files, or third-party import paths)

Files:

  • tests/unit/memory/test_init.py
  • tests/unit/memory/backends/mem0/test_config.py
  • tests/unit/memory/test_factory.py
  • tests/unit/memory/backends/mem0/test_mappers.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/integration/memory/test_mem0_backend.py
src/ai_company/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/ai_company/**/*.py: Every module with business logic MUST have: from ai_company.observability import get_logger then logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code
Variable name: always logger (not _logger, not log)
Event names: always use constants from the domain-specific module under ai_company.observability.events (e.g. PROVIDER_CALL_START from events.provider). Import directly: from ai_company.observability.events.<domain> import EVENT_CONSTANT
Structured kwargs in logging: always logger.info(EVENT, key=value) — never logger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO
DEBUG for object creation, internal flow, entry/exit of key functions
Vendor-agnostic everywhere: NEVER use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, large/medium/small as aliases. Tests must use test-provider, test-small-001, etc.

Files:

  • src/ai_company/memory/backends/mem0/config.py
  • src/ai_company/observability/events/memory.py
  • src/ai_company/memory/__init__.py
  • src/ai_company/memory/backends/mem0/__init__.py
  • src/ai_company/memory/factory.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/__init__.py
  • src/ai_company/memory/backends/mem0/mappers.py
🧠 Learnings (4)
📚 Learning: 2026-03-13T06:12:19.113Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:12:19.113Z
Learning: Applies to pyproject.toml : Groups in pyproject.toml: `test` (pytest + plugins), `dev` (includes test + ruff, mypy, pre-commit, commitizen). Install: `uv sync` installs everything (dev group is default)

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:12:19.113Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:12:19.113Z
Learning: Applies to pyproject.toml : Pinned: all versions use `==` in `pyproject.toml`

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:12:19.113Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:12:19.113Z
Learning: Applies to **/*.yml : Config: YAML company config loading and validation — use frozen Pydantic models for config validation

Applied to files:

  • src/ai_company/memory/backends/mem0/config.py
📚 Learning: 2026-03-13T06:12:19.113Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:12:19.113Z
Learning: Applies to src/ai_company/**/*.py : Event names: always use constants from the domain-specific module under `ai_company.observability.events` (e.g. `PROVIDER_CALL_START` from `events.provider`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`

Applied to files:

  • src/ai_company/observability/events/memory.py
🧬 Code graph analysis (8)
tests/unit/memory/backends/mem0/test_config.py (2)
src/ai_company/memory/backends/mem0/config.py (4)
  • Mem0BackendConfig (46-78)
  • Mem0EmbedderConfig (17-43)
  • build_config_from_company_config (111-129)
  • build_mem0_config_dict (81-108)
src/ai_company/memory/config.py (2)
  • CompanyMemoryConfig (139-195)
  • MemoryStorageConfig (25-104)
src/ai_company/memory/backends/mem0/config.py (1)
src/ai_company/memory/config.py (1)
  • CompanyMemoryConfig (139-195)
src/ai_company/memory/__init__.py (2)
src/ai_company/memory/backends/mem0/config.py (1)
  • Mem0EmbedderConfig (17-43)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • Mem0MemoryBackend (77-785)
tests/unit/memory/backends/mem0/test_adapter.py (5)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/backends/mem0/adapter.py (20)
  • Mem0MemoryBackend (77-785)
  • max_memories_per_agent (227-229)
  • backend_name (195-197)
  • is_connected (190-192)
  • supported_categories (202-204)
  • supports_graph (207-209)
  • supports_temporal (212-214)
  • supports_vector_search (217-219)
  • supports_shared_access (222-224)
  • connect (101-138)
  • disconnect (140-148)
  • health_check (150-187)
  • store (245-301)
  • retrieve (303-362)
  • get (364-422)
  • delete (424-484)
  • count (486-555)
  • publish (559-621)
  • search_shared (623-697)
  • retract (699-785)
src/ai_company/memory/backends/mem0/config.py (2)
  • Mem0BackendConfig (46-78)
  • Mem0EmbedderConfig (17-43)
src/ai_company/memory/errors.py (4)
  • MemoryConnectionError (17-18)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
  • MemoryError (13-14)
src/ai_company/memory/models.py (2)
  • MemoryQuery (153-230)
  • MemoryStoreRequest (55-79)
src/ai_company/memory/backends/mem0/__init__.py (2)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • Mem0MemoryBackend (77-785)
src/ai_company/memory/backends/mem0/config.py (2)
  • Mem0BackendConfig (46-78)
  • Mem0EmbedderConfig (17-43)
src/ai_company/memory/factory.py (5)
src/ai_company/memory/config.py (1)
  • CompanyMemoryConfig (139-195)
src/ai_company/memory/backends/mem0/config.py (2)
  • Mem0EmbedderConfig (17-43)
  • build_config_from_company_config (111-129)
src/ai_company/memory/errors.py (1)
  • MemoryConfigError (40-41)
src/ai_company/memory/protocol.py (1)
  • MemoryBackend (20-182)
src/ai_company/memory/backends/mem0/adapter.py (2)
  • Mem0MemoryBackend (77-785)
  • max_memories_per_agent (227-229)
src/ai_company/memory/backends/__init__.py (1)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • Mem0MemoryBackend (77-785)
src/ai_company/memory/backends/mem0/mappers.py (5)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/errors.py (2)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
src/ai_company/memory/models.py (4)
  • MemoryEntry (82-150)
  • MemoryMetadata (20-52)
  • MemoryQuery (153-230)
  • MemoryStoreRequest (55-79)
src/ai_company/observability/_logger.py (1)
  • get_logger (8-28)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • get (364-422)
🔇 Additional comments (24)
.github/workflows/dependency-review.yml (1)

32-35: Helpful license rationale comments for auditability.

The added context makes compliance exceptions easier to review and maintain.

pyproject.toml (1)

24-24: Dependency and mypy updates look correct.

Pinned dependency and scoped mypy override are consistent with the Mem0 backend integration.

Also applies to: 188-190

CLAUDE.md (1)

97-97: Memory package structure update is clear and accurate.

The added backend path details match the implemented architecture.

src/ai_company/memory/backends/mem0/__init__.py (1)

1-6: Public export surface is clean and consistent.

__all__ and imports are aligned with the intended Mem0 backend API.

src/ai_company/memory/backends/__init__.py (1)

1-5: Backend initializer is correct.

The module cleanly exposes the concrete backend as intended.

README.md (1)

130-130: Status update accurately reflects delivered scope.

Including memory with the Mem0 adapter is consistent with this PR.

docs/design/memory.md (1)

33-33: Design diagram status is now aligned with implementation.

Good documentation sync for backend maturity.

src/ai_company/memory/__init__.py (1)

6-15: Top-level memory exports are consistent and well-integrated.

The added imports and __all__ entries correctly expose the Mem0 backend surface.

Also applies to: 82-84

src/ai_company/observability/events/memory.py (1)

22-22: New observability event is well-scoped and correctly named.

This constant fills an important gap for invalid backend config paths.

src/ai_company/memory/backends/mem0/mappers.py (7)

1-36: LGTM!

Module setup follows coding guidelines correctly: proper logger setup with get_logger(__name__), event constants imported from the domain-specific observability module, and no from __future__ import annotations.


38-57: LGTM!

Clean, stateless mapping function that correctly serializes domain metadata to Mem0-compatible format. Creates a new dict and properly handles optional fields.


60-86: LGTM!

Robust datetime parsing with proper defensive handling for malformed input, correct PEP 758 exception syntax, and appropriate assumption that naive datetimes are UTC.


167-232: LGTM!

Comprehensive conversion logic with proper validation of required fields, appropriate error handling via MemoryRetrievalError, and sensible fallback for missing created_at. Good use of helper functions for parsing subcomponents.


235-283: LGTM!

Simple and focused query conversion functions with appropriate validation for the search case where query.text is required.


286-318: LGTM!

Well-implemented post-retrieval filtering with correct filter semantics: AND for tags, exclusive upper bound for until, and appropriate handling of None relevance scores. Creates new tuple without mutating inputs.


324-386: LGTM!

Solid helper functions moved from adapter to keep it under 800 lines. validate_add_result provides clear error context, and extract_category/extract_publisher handle missing or malformed metadata gracefully.

src/ai_company/memory/backends/mem0/adapter.py (8)

1-98: LGTM!

Clean module setup with correct logger configuration, appropriate imports, and well-structured class initialization. Using Any for _client is reasonable given the runtime import of mem0.Memory.


189-241: LGTM!

Capability properties are correctly implemented with accurate documentation. The _require_connected() guard provides consistent connection enforcement across all operations.


245-301: LGTM!

Correct implementation with proper asyncio.to_thread usage, explicit infer=False to disable Mem0's auto-extraction, and comprehensive error handling with structured logging.


364-484: LGTM!

Both methods are well-implemented. The get() method correctly handles None results. The check-then-delete pattern in delete() is appropriately documented as a workaround for Mem0's API limitation, with acceptable TOCTOU semantics for this use case.


486-555: LGTM!

Well-documented count() implementation with clear documentation of the O(n) complexity and capping behavior. The limitation that results may underestimate for agents exceeding max_memories_per_agent is appropriately noted.


559-621: LGTM!

Correct shared memory publication with reserved namespace isolation and publisher ownership tracking via metadata. The pattern mirrors store() with appropriate adjustments for the shared context.


623-697: LGTM!

Well-structured shared knowledge search with appropriate namespace isolation, publisher extraction for agent attribution, and post-filtering support. The exclude_agent filter correctly excludes the requesting agent's own published memories.


699-785: LGTM!

Robust retract() implementation with proper ownership verification. The authorization checks raise MemoryStoreError with clear messages, and the re-raise pattern at line 761-765 correctly avoids duplicate logging since context was already captured in the warning.

Comment thread .github/workflows/dependency-review.yml Outdated
Comment thread CLAUDE.md Outdated
Comment on lines +140 to +148
async def disconnect(self) -> None:
"""Close the Mem0 connection.

Safe to call even if not connected.
"""
logger.info(MEMORY_BACKEND_DISCONNECTING, backend="mem0")
self._client = None
self._connected = False
logger.info(MEMORY_BACKEND_DISCONNECTED, backend="mem0")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does mem0 Memory client have a close or shutdown method for cleanup?

💡 Result:

No. In the official mem0ai Python SDK, MemoryClient / AsyncMemoryClient do not expose a close(), shutdown(), or aclose() method; they just hold an underlying httpx.Client / httpx.AsyncClient instance (client / async_client). [1] The Mem0 docs for AsyncMemoryClient also list its methods (add/search/get_all/delete/…) and do not mention any close/shutdown API. [2]

Cleanup options:

  • Sync: client.client.close()
  • Async: await client.async_client.aclose()
  • Or pass your own httpx.Client / httpx.AsyncClient into the constructor and manage its lifetime (e.g., with a context manager). [1]

Sources
[1] https://github.com/mem0ai/mem0/blob/34c797d2/mem0/client/main.py
[2] https://docs.mem0.ai/platform/features/async-client


🏁 Script executed:

# Check the imports and client instantiation in the adapter file
head -50 src/ai_company/memory/backends/mem0/adapter.py | cat -n

Repository: Aureliolo/synthorg

Length of output: 1766


🏁 Script executed:

# Search for where _client is instantiated (in connect method)
sed -n '101,139p' src/ai_company/memory/backends/mem0/adapter.py | cat -n

Repository: Aureliolo/synthorg

Length of output: 1763


Close the underlying httpx.Client in disconnect().

The Memory client holds an internal httpx.Client for API communication. Setting _client = None without closing it leaves the HTTP connection pool open until garbage collection. Wrap the cleanup in asyncio.to_thread() since the underlying client is synchronous:

Suggested fix
async def disconnect(self) -> None:
    """Close the Mem0 connection.

    Safe to call even if not connected.
    """
    logger.info(MEMORY_BACKEND_DISCONNECTING, backend="mem0")
    if self._client:
        await asyncio.to_thread(self._client.client.close)
    self._client = None
    self._connected = False
    logger.info(MEMORY_BACKEND_DISCONNECTED, backend="mem0")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/backends/mem0/adapter.py` around lines 140 - 148, The
disconnect method currently nulls out self._client without closing the
underlying httpx.Client; change Memory.disconnect to, before setting
self._client = None, check if self._client is truthy and then close the sync
client via await asyncio.to_thread(self._client.close) (or the appropriate
.client.close if the wrapper stores it as .client), then set self._client = None
and self._connected = False; keep the existing logger calls and ensure the close
happens inside asyncio.to_thread to avoid blocking the event loop.

Comment thread src/ai_company/memory/backends/mem0/adapter.py
Comment thread src/ai_company/memory/backends/mem0/config.py
Comment on lines +81 to +129
def build_mem0_config_dict(config: Mem0BackendConfig) -> dict[str, Any]:
"""Build the dict that ``Memory.from_config()`` expects.

Args:
config: Mem0 backend configuration.

Returns:
Configuration dict suitable for ``Memory.from_config()``.
"""
return {
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": config.collection_name,
"embedding_model_dims": config.embedder.dims,
"path": f"{config.data_dir}/qdrant",
},
},
"embedder": {
"provider": config.embedder.provider,
"config": {
"model": config.embedder.model,
},
},
"history_db_path": f"{config.data_dir}/history.db",
# Mem0 config schema version — required by Memory.from_config().
"version": "v1.1",
}


def build_config_from_company_config(
config: CompanyMemoryConfig,
*,
embedder: Mem0EmbedderConfig,
) -> Mem0BackendConfig:
"""Derive a ``Mem0BackendConfig`` from the top-level memory config.

Args:
config: Company-wide memory configuration.
embedder: Embedder settings (provider and model must be
supplied explicitly to avoid vendor names in defaults).

Returns:
Mem0-specific backend configuration.
"""
return Mem0BackendConfig(
data_dir=config.storage.data_dir,
embedder=embedder,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject unsupported storage overrides instead of silently ignoring them.

CompanyMemoryConfig.storage still accepts vector_store="qdrant-external" and history_store="postgresql", but this builder drops both fields and build_mem0_config_dict() always emits embedded Qdrant + SQLite. A caller can therefore request external storage and silently get local files instead. Please fail fast on unsupported values here until the adapter actually wires those settings through. As per coding guidelines "Validate: at system boundaries (user input, external APIs, config files)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/backends/mem0/config.py` around lines 81 - 129, The
builder currently drops CompanyMemoryConfig.storage overrides and silently uses
embedded Qdrant+SQLite; update build_config_from_company_config to validate
CompanyMemoryConfig.storage.vector_store and .history_store and raise a clear
exception (e.g., ValueError) for unsupported values such as "qdrant-external" or
"postgresql" so callers fail fast; locate the validation in
build_config_from_company_config (before constructing Mem0BackendConfig) and
ensure build_mem0_config_dict remains consistent with accepted values by only
emitting embedded Qdrant/SQLite when storage fields are missing or explicitly
set to the embedded options.

Comment thread src/ai_company/memory/backends/mem0/mappers.py Outdated
Comment thread src/ai_company/memory/backends/mem0/mappers.py Outdated
Comment on lines 48 to +93
if config.backend == "mem0":
msg = "mem0 backend not yet implemented"
logger.warning(
MEMORY_BACKEND_NOT_IMPLEMENTED,
from ai_company.memory.backends.mem0 import Mem0MemoryBackend # noqa: PLC0415
from ai_company.memory.backends.mem0.config import ( # noqa: PLC0415
Mem0EmbedderConfig,
build_config_from_company_config,
)

if embedder is None:
msg = (
"Mem0 backend requires an embedder configuration — "
"pass a Mem0EmbedderConfig instance"
)
logger.warning(
MEMORY_BACKEND_CONFIG_INVALID,
backend="mem0",
reason="missing_embedder",
error=msg,
)
raise MemoryConfigError(msg)
if not isinstance(embedder, Mem0EmbedderConfig):
msg = ( # type: ignore[unreachable]
f"embedder must be a Mem0EmbedderConfig, got {type(embedder).__name__}"
)
logger.warning(
MEMORY_BACKEND_CONFIG_INVALID,
backend="mem0",
reason="invalid_embedder_type",
error=msg,
embedder_type=type(embedder).__name__,
)
raise MemoryConfigError(msg)

mem0_config = build_config_from_company_config(
config,
embedder=embedder,
)
backend = Mem0MemoryBackend(
mem0_config=mem0_config,
max_memories_per_agent=config.options.max_memories_per_agent,
)
logger.info(
MEMORY_BACKEND_CREATED,
backend="mem0",
reason=msg,
data_dir=mem0_config.data_dir,
)
raise MemoryConfigError(msg)
return backend
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't leak raw Mem0 config failures out of the factory.

This branch only normalizes the missing/wrong-embedder cases. Any backend-specific validation failure from build_config_from_company_config() or Mem0MemoryBackend(...) will currently bypass MEMORY_BACKEND_CONFIG_INVALID and escape as a raw exception, even though this function is the config boundary. Extract the Mem0 path into a small helper and wrap those failures as MemoryConfigError there; that also brings create_memory_backend() back under the repo's function-size limit. As per coding guidelines "Functions: < 50 lines" and "Validate: at system boundaries (user input, external APIs, config files)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/factory.py` around lines 48 - 93, The Mem0 branch in
create_memory_backend currently lets backend-specific errors from
build_config_from_company_config and Mem0MemoryBackend escape; extract the Mem0
logic into a small helper (e.g., _create_mem0_backend) that is called from
create_memory_backend after the existing embedder checks (Mem0EmbedderConfig
validation), and wrap the calls to build_config_from_company_config and
Mem0MemoryBackend in a try/except that catches Exception, logs
MEMORY_BACKEND_CONFIG_INVALID with backend="mem0", a suitable reason like
"backend_config_error", and the error details, then raise MemoryConfigError with
a clear message; ensure the helper returns the created backend and that
create_memory_backend delegates to it to keep function size under the limit.

Comment on lines +1 to +1117
"""Tests for the Mem0 memory backend adapter."""

import sys
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from ai_company.core.enums import MemoryCategory
from ai_company.memory.backends.mem0.adapter import (
_SHARED_NAMESPACE,
Mem0MemoryBackend,
)
from ai_company.memory.backends.mem0.config import (
Mem0BackendConfig,
Mem0EmbedderConfig,
)
from ai_company.memory.backends.mem0.mappers import _PUBLISHER_KEY
from ai_company.memory.errors import (
MemoryConnectionError,
MemoryRetrievalError,
MemoryStoreError,
)
from ai_company.memory.models import (
MemoryQuery,
MemoryStoreRequest,
)

pytestmark = pytest.mark.timeout(30)


def _test_embedder() -> Mem0EmbedderConfig:
"""Vendor-agnostic embedder config for tests."""
return Mem0EmbedderConfig(
provider="test-provider",
model="test-embedding-001",
)


@pytest.fixture
def mem0_config() -> Mem0BackendConfig:
"""Default Mem0 config for tests."""
return Mem0BackendConfig(
data_dir="/tmp/test-memory", # noqa: S108
embedder=_test_embedder(),
)


@pytest.fixture
def mock_client() -> MagicMock:
"""Mock Mem0 Memory client."""
return MagicMock()


@pytest.fixture
def backend(
mem0_config: Mem0BackendConfig,
mock_client: MagicMock,
) -> Mem0MemoryBackend:
"""Connected backend with mocked Mem0 client."""
b = Mem0MemoryBackend(mem0_config=mem0_config, max_memories_per_agent=100)
b._client = mock_client
b._connected = True
return b


def _mem0_add_result(memory_id: str = "mem-001") -> dict[str, Any]:
"""Build a typical Mem0 add() return value."""
return {
"results": [
{
"id": memory_id,
"memory": "test content",
"event": "ADD",
},
],
}


def _mem0_search_result(
items: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a typical Mem0 search() return value."""
if items is None:
items = [
{
"id": "mem-001",
"memory": "found content",
"score": 0.85,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {
"_synthorg_category": "episodic",
"_synthorg_confidence": 0.9,
},
},
]
return {"results": items}


def _mem0_get_result(memory_id: str = "mem-001") -> dict[str, Any]:
"""Build a typical Mem0 get() return value."""
return {
"id": memory_id,
"memory": "stored content",
"created_at": "2026-03-12T10:00:00+00:00",
"updated_at": None,
"metadata": {
"_synthorg_category": "episodic",
"_synthorg_confidence": 1.0,
},
}


def _make_store_request(
*,
category: MemoryCategory = MemoryCategory.EPISODIC,
content: str = "test content",
) -> MemoryStoreRequest:
"""Helper to build a store request."""
return MemoryStoreRequest(category=category, content=content)


# ── Properties ────────────────────────────────────────────────────


@pytest.mark.unit
class TestProperties:
def test_backend_name(self, backend: Mem0MemoryBackend) -> None:
assert backend.backend_name == "mem0"

def test_is_connected_true(self, backend: Mem0MemoryBackend) -> None:
assert backend.is_connected is True

def test_is_connected_false(self, mem0_config: Mem0BackendConfig) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
assert b.is_connected is False


# ── Capabilities ──────────────────────────────────────────────────


@pytest.mark.unit
class TestCapabilities:
def test_supported_categories(self, backend: Mem0MemoryBackend) -> None:
assert backend.supported_categories == frozenset(MemoryCategory)

def test_supports_graph_false(self, backend: Mem0MemoryBackend) -> None:
assert backend.supports_graph is False

def test_supports_temporal_true(self, backend: Mem0MemoryBackend) -> None:
assert backend.supports_temporal is True

def test_supports_vector_search_true(
self,
backend: Mem0MemoryBackend,
) -> None:
assert backend.supports_vector_search is True

def test_supports_shared_access_true(
self,
backend: Mem0MemoryBackend,
) -> None:
assert backend.supports_shared_access is True

def test_max_memories_per_agent(
self,
backend: Mem0MemoryBackend,
) -> None:
assert backend.max_memories_per_agent == 100


# ── Protocol Conformance ─────────────────────────────────────────


@pytest.mark.unit
class TestProtocolConformance:
"""Verify Mem0MemoryBackend conforms to protocol interfaces."""

def test_has_memory_backend_methods(
self,
backend: Mem0MemoryBackend,
) -> None:
assert hasattr(backend, "connect")
assert hasattr(backend, "disconnect")
assert hasattr(backend, "health_check")
assert hasattr(backend, "store")
assert hasattr(backend, "retrieve")
assert hasattr(backend, "get")
assert hasattr(backend, "delete")
assert hasattr(backend, "count")

def test_has_capabilities_properties(
self,
backend: Mem0MemoryBackend,
) -> None:
assert hasattr(backend, "supported_categories")
assert hasattr(backend, "supports_graph")
assert hasattr(backend, "supports_temporal")
assert hasattr(backend, "supports_vector_search")
assert hasattr(backend, "supports_shared_access")
assert hasattr(backend, "max_memories_per_agent")

def test_has_shared_knowledge_methods(
self,
backend: Mem0MemoryBackend,
) -> None:
assert hasattr(backend, "publish")
assert hasattr(backend, "search_shared")
assert hasattr(backend, "retract")


# ── Lifecycle ─────────────────────────────────────────────────────


@pytest.mark.unit
class TestLifecycle:
async def test_connect_success(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
mock_memory = MagicMock()
with patch(
"ai_company.memory.backends.mem0.adapter.asyncio.to_thread",
return_value=mock_memory,
):
await b.connect()

assert b.is_connected is True

async def test_connect_failure_raises(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with (
patch(
"ai_company.memory.backends.mem0.adapter.asyncio.to_thread",
side_effect=RuntimeError("connection failed"),
),
pytest.raises(MemoryConnectionError, match="Failed to connect"),
):
await b.connect()
assert b.is_connected is False

async def test_disconnect(self, backend: Mem0MemoryBackend) -> None:
await backend.disconnect()
assert backend.is_connected is False
assert backend._client is None

async def test_disconnect_when_not_connected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
await b.disconnect() # Should not raise
assert b.is_connected is False

async def test_health_check_connected(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.return_value = {"results": []}
assert await backend.health_check() is True

async def test_health_check_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
assert await b.health_check() is False

async def test_health_check_probe_failure(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.side_effect = RuntimeError("backend down")
assert await backend.health_check() is False

async def test_connect_import_error_raises(
self,
mem0_config: Mem0BackendConfig,
) -> None:
"""ImportError when mem0 package is not installed."""
b = Mem0MemoryBackend(mem0_config=mem0_config)
with (
patch.dict(sys.modules, {"mem0": None}),
pytest.raises(MemoryConnectionError, match="not installed"),
):
await b.connect()

async def test_health_check_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError propagates through health_check."""
mock_client.get_all.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.health_check()


# ── Connection guard ──────────────────────────────────────────────


@pytest.mark.unit
class TestConnectionGuard:
async def test_store_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.store("test-agent-001", _make_store_request())

async def test_retrieve_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.retrieve("test-agent-001", MemoryQuery(text="test"))

async def test_get_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.get("test-agent-001", "mem-001")

async def test_delete_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.delete("test-agent-001", "mem-001")

async def test_count_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.count("test-agent-001")

async def test_publish_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.publish("test-agent-001", _make_store_request())

async def test_search_shared_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.search_shared(MemoryQuery(text="test"))

async def test_retract_raises_when_disconnected(
self,
mem0_config: Mem0BackendConfig,
) -> None:
b = Mem0MemoryBackend(mem0_config=mem0_config)
with pytest.raises(MemoryConnectionError, match="Not connected"):
await b.retract("test-agent-001", "mem-001")


# ── Store ─────────────────────────────────────────────────────────


@pytest.mark.unit
class TestStore:
async def test_store_success(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.return_value = _mem0_add_result("new-mem-id")

memory_id = await backend.store(
"test-agent-001",
_make_store_request(),
)

assert memory_id == "new-mem-id"
mock_client.add.assert_called_once()
call_kwargs = mock_client.add.call_args[1]
assert call_kwargs["user_id"] == "test-agent-001"
assert call_kwargs["infer"] is False

async def test_store_empty_results_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.return_value = {"results": []}

with pytest.raises(MemoryStoreError, match="no results"):
await backend.store("test-agent-001", _make_store_request())

async def test_store_missing_id_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.return_value = {
"results": [{"memory": "no id", "event": "ADD"}],
}

with pytest.raises(MemoryStoreError, match="missing or blank 'id'"):
await backend.store("test-agent-001", _make_store_request())

async def test_store_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.side_effect = RuntimeError("disk full")

with pytest.raises(MemoryStoreError, match="Failed to store") as exc_info:
await backend.store("test-agent-001", _make_store_request())

assert exc_info.value.__cause__ is not None

async def test_store_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping."""
mock_client.add.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.store("test-agent-001", _make_store_request())


# ── Retrieve ──────────────────────────────────────────────────────


@pytest.mark.unit
class TestRetrieve:
async def test_retrieve_with_text(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.search.return_value = _mem0_search_result()

query = MemoryQuery(text="find relevant", limit=5)
entries = await backend.retrieve("test-agent-001", query)

assert len(entries) == 1
assert entries[0].content == "found content"
assert entries[0].relevance_score == 0.85
mock_client.search.assert_called_once()

async def test_retrieve_without_text(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.return_value = _mem0_search_result(
[
{
"id": "mem-001",
"memory": "all content",
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {},
},
],
)

query = MemoryQuery(text=None, limit=10)
entries = await backend.retrieve("test-agent-001", query)

assert len(entries) == 1
mock_client.get_all.assert_called_once()

async def test_retrieve_applies_post_filters(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.search.return_value = _mem0_search_result(
[
{
"id": "m1",
"memory": "episodic",
"score": 0.9,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {"_synthorg_category": "episodic"},
},
{
"id": "m2",
"memory": "semantic",
"score": 0.8,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {"_synthorg_category": "semantic"},
},
],
)

query = MemoryQuery(
text="test",
categories=frozenset({MemoryCategory.EPISODIC}),
)
entries = await backend.retrieve("test-agent-001", query)

assert len(entries) == 1
assert entries[0].category == MemoryCategory.EPISODIC

async def test_retrieve_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.search.side_effect = RuntimeError("search failed")

with pytest.raises(MemoryRetrievalError, match="Failed to retrieve"):
await backend.retrieve(
"test-agent-001",
MemoryQuery(text="test"),
)

async def test_retrieve_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping."""
mock_client.search.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.retrieve(
"test-agent-001",
MemoryQuery(text="test"),
)


# ── Get ───────────────────────────────────────────────────────────


@pytest.mark.unit
class TestGet:
async def test_get_existing(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = _mem0_get_result("mem-001")

entry = await backend.get("test-agent-001", "mem-001")

assert entry is not None
assert entry.id == "mem-001"
assert entry.agent_id == "test-agent-001"

async def test_get_not_found(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = None

entry = await backend.get("test-agent-001", "nonexistent")

assert entry is None

async def test_get_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.side_effect = RuntimeError("backend error")

with pytest.raises(MemoryRetrievalError, match="Failed to get"):
await backend.get("test-agent-001", "mem-001")


# ── Delete ────────────────────────────────────────────────────────


@pytest.mark.unit
class TestDelete:
async def test_delete_existing(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = _mem0_get_result("mem-001")
mock_client.delete.return_value = None

result = await backend.delete("test-agent-001", "mem-001")

assert result is True
mock_client.delete.assert_called_once_with("mem-001")

async def test_delete_not_found(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = None

result = await backend.delete("test-agent-001", "nonexistent")

assert result is False
mock_client.delete.assert_not_called()

async def test_delete_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.side_effect = RuntimeError("backend error")

with pytest.raises(MemoryStoreError, match="Failed to delete"):
await backend.delete("test-agent-001", "mem-001")

async def test_delete_get_ok_but_delete_fails(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = _mem0_get_result("mem-001")
mock_client.delete.side_effect = RuntimeError("delete failed")

with pytest.raises(MemoryStoreError, match="Failed to delete"):
await backend.delete("test-agent-001", "mem-001")


# ── Count ─────────────────────────────────────────────────────────


@pytest.mark.unit
class TestCount:
async def test_count_all(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.return_value = {
"results": [
{"id": "m1", "memory": "a", "metadata": {}},
{"id": "m2", "memory": "b", "metadata": {}},
],
}

count = await backend.count("test-agent-001")
assert count == 2

async def test_count_by_category(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.return_value = {
"results": [
{
"id": "m1",
"memory": "a",
"metadata": {"_synthorg_category": "episodic"},
},
{
"id": "m2",
"memory": "b",
"metadata": {"_synthorg_category": "semantic"},
},
{
"id": "m3",
"memory": "c",
"metadata": {"_synthorg_category": "episodic"},
},
],
}

count = await backend.count(
"test-agent-001",
category=MemoryCategory.EPISODIC,
)
assert count == 2

async def test_count_with_invalid_category_in_data(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Invalid category in stored data defaults to WORKING."""
mock_client.get_all.return_value = {
"results": [
{
"id": "m1",
"memory": "a",
"metadata": {"_synthorg_category": "bogus_category"},
},
{
"id": "m2",
"memory": "b",
"metadata": {"_synthorg_category": "episodic"},
},
],
}

count = await backend.count(
"test-agent-001",
category=MemoryCategory.WORKING,
)
# "bogus_category" defaults to WORKING
assert count == 1

async def test_count_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.side_effect = RuntimeError("fail")

with pytest.raises(MemoryRetrievalError, match="Failed to count"):
await backend.count("test-agent-001")


# ── Shared Knowledge Store ────────────────────────────────────────


@pytest.mark.unit
class TestPublish:
async def test_publish_success(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.return_value = _mem0_add_result("shared-mem-001")

memory_id = await backend.publish(
"test-agent-001",
_make_store_request(),
)

assert memory_id == "shared-mem-001"
call_kwargs = mock_client.add.call_args[1]
assert call_kwargs["user_id"] == _SHARED_NAMESPACE
assert _PUBLISHER_KEY in call_kwargs["metadata"]
assert call_kwargs["metadata"][_PUBLISHER_KEY] == "test-agent-001"

async def test_publish_empty_results_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.return_value = {"results": []}

with pytest.raises(MemoryStoreError, match="no results"):
await backend.publish("test-agent-001", _make_store_request())

async def test_publish_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.add.side_effect = RuntimeError("network error")

with pytest.raises(MemoryStoreError, match="Failed to publish"):
await backend.publish("test-agent-001", _make_store_request())

async def test_publish_missing_id_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Publish result missing 'id' raises MemoryStoreError."""
mock_client.add.return_value = {
"results": [{"memory": "no id", "event": "ADD"}],
}

with pytest.raises(MemoryStoreError, match="missing or blank 'id'"):
await backend.publish("test-agent-001", _make_store_request())


@pytest.mark.unit
class TestSearchShared:
async def test_search_shared_with_text(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.search.return_value = _mem0_search_result(
[
{
"id": "shared-1",
"memory": "shared fact",
"score": 0.9,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {
"_synthorg_category": "semantic",
_PUBLISHER_KEY: "test-agent-002",
},
},
],
)

query = MemoryQuery(text="find shared", limit=5)
entries = await backend.search_shared(query)

assert len(entries) == 1
assert entries[0].agent_id == "test-agent-002"
mock_client.search.assert_called_once()
call_kwargs = mock_client.search.call_args[1]
assert call_kwargs["user_id"] == _SHARED_NAMESPACE

async def test_search_shared_without_text(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get_all.return_value = _mem0_search_result(
[
{
"id": "shared-1",
"memory": "shared fact",
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {
_PUBLISHER_KEY: "test-agent-002",
},
},
],
)

query = MemoryQuery(text=None)
entries = await backend.search_shared(query)

assert len(entries) == 1
mock_client.get_all.assert_called_once()

async def test_search_shared_exclude_agent(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.search.return_value = _mem0_search_result(
[
{
"id": "s1",
"memory": "from agent 1",
"score": 0.9,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {_PUBLISHER_KEY: "test-agent-001"},
},
{
"id": "s2",
"memory": "from agent 2",
"score": 0.8,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {_PUBLISHER_KEY: "test-agent-002"},
},
],
)

query = MemoryQuery(text="test")
entries = await backend.search_shared(
query,
exclude_agent="test-agent-001",
)

assert len(entries) == 1
assert entries[0].agent_id == "test-agent-002"

async def test_search_shared_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.search.side_effect = RuntimeError("search error")

with pytest.raises(MemoryRetrievalError, match="Failed to search"):
await backend.search_shared(MemoryQuery(text="test"))


@pytest.mark.unit
class TestRetract:
async def test_retract_success(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = {
"id": "shared-001",
"memory": "shared content",
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {_PUBLISHER_KEY: "test-agent-001"},
}
mock_client.delete.return_value = None

result = await backend.retract("test-agent-001", "shared-001")

assert result is True
mock_client.delete.assert_called_once_with("shared-001")

async def test_retract_not_found(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = None

result = await backend.retract("test-agent-001", "nonexistent")

assert result is False

async def test_retract_ownership_mismatch(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = {
"id": "shared-001",
"memory": "content",
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {_PUBLISHER_KEY: "test-agent-002"},
}

with pytest.raises(MemoryStoreError, match="cannot retract"):
await backend.retract("test-agent-001", "shared-001")

async def test_retract_no_publisher_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.return_value = {
"id": "not-shared-001",
"memory": "private content",
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {},
}

with pytest.raises(MemoryStoreError, match="not a shared memory"):
await backend.retract("test-agent-001", "not-shared-001")

async def test_retract_exception_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
mock_client.get.side_effect = RuntimeError("backend error")

with pytest.raises(MemoryStoreError, match="Failed to retract"):
await backend.retract("test-agent-001", "shared-001")

async def test_retract_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping."""
mock_client.get.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.retract("test-agent-001", "shared-001")

async def test_retract_delete_failure_wraps(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Exception during delete phase wraps in MemoryStoreError."""
mock_client.get.return_value = {
"id": "shared-001",
"memory": "content",
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {_PUBLISHER_KEY: "test-agent-001"},
}
mock_client.delete.side_effect = RuntimeError("delete failed")

with pytest.raises(MemoryStoreError, match="Failed to retract"):
await backend.retract("test-agent-001", "shared-001")


@pytest.mark.unit
class TestAdditionalEdgeCases:
"""Edge cases for improved coverage."""

async def test_store_blank_id_from_add_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Store result with blank ID raises MemoryStoreError."""
mock_client.add.return_value = {
"results": [{"id": "", "event": "ADD"}],
}
with pytest.raises(MemoryStoreError, match="missing or blank 'id'"):
await backend.store("test-agent-001", _make_store_request())

async def test_store_whitespace_id_from_add_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Store result with whitespace-only ID raises MemoryStoreError."""
mock_client.add.return_value = {
"results": [{"id": " ", "event": "ADD"}],
}
with pytest.raises(MemoryStoreError, match="missing or blank 'id'"):
await backend.store("test-agent-001", _make_store_request())

async def test_get_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping in get()."""
mock_client.get.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.get("test-agent-001", "mem-001")

async def test_delete_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping in delete()."""
mock_client.get.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.delete("test-agent-001", "mem-001")

async def test_count_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping in count()."""
mock_client.get_all.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.count("test-agent-001")

async def test_publish_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping in publish()."""
mock_client.add.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.publish("test-agent-001", _make_store_request())

async def test_search_shared_reraises_memory_error(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""MemoryError is re-raised without wrapping in search_shared()."""
mock_client.search.side_effect = MemoryError("out of memory")
with pytest.raises(MemoryError):
await backend.search_shared(MemoryQuery(text="test"))

async def test_store_non_list_results_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Store result with non-list 'results' raises MemoryStoreError."""
mock_client.add.return_value = {"results": "not-a-list"}
with pytest.raises(MemoryStoreError, match="no results"):
await backend.store("test-agent-001", _make_store_request())

async def test_retrieve_invalid_entry_raises(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Invalid entry in search results wraps as MemoryRetrievalError."""
mock_client.search.return_value = {
"results": [
{"id": "", "memory": "blank id", "metadata": {}},
],
}
with pytest.raises(MemoryRetrievalError, match="missing or blank"):
await backend.retrieve(
"test-agent-001",
MemoryQuery(text="test"),
)

async def test_search_shared_no_publisher_uses_namespace(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Entries without publisher metadata use the shared namespace."""
mock_client.search.return_value = _mem0_search_result(
[
{
"id": "shared-1",
"memory": "orphan fact",
"score": 0.9,
"created_at": "2026-03-12T10:00:00+00:00",
"metadata": {"_synthorg_category": "semantic"},
},
],
)

entries = await backend.search_shared(MemoryQuery(text="test"))
assert len(entries) == 1
assert entries[0].agent_id == _SHARED_NAMESPACE

async def test_count_empty_results(
self,
backend: Mem0MemoryBackend,
mock_client: MagicMock,
) -> None:
"""Count returns 0 for empty results."""
mock_client.get_all.return_value = {"results": []}
count = await backend.count("test-agent-001")
assert count == 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Split this test module by concern before it gets harder to maintain.

This new file is already over 1,100 lines and mixes lifecycle, CRUD, shared-store, and edge-case coverage in one place. Please split it into smaller modules and collapse the repeated test matrices with @pytest.mark.parametrize so failures stay easy to locate and the file stays under the repo limit. As per coding guidelines "Functions: < 50 lines, files < 800 lines" and "Prefer @pytest.mark.parametrize for testing similar cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/memory/backends/mem0/test_adapter.py` around lines 1 - 1117, The
test module is too large and mixes many concerns; split it into smaller files
grouping related test classes (e.g., move TestLifecycle, TestConnectionGuard to
tests/unit/memory/backends/mem0/test_lifecycle.py; TestStore and TestPublish to
test_store.py; TestRetrieve and TestSearchShared to test_retrieve_shared.py;
TestGet/TestDelete/TestCount/TestRetract to their own files; and
TestAdditionalEdgeCases to test_edgecases.py) and replace repeated similar
assertions with pytest.mark.parametrize (collapse matrix-like tests in
TestConnectionGuard, TestStore empty/missing/id cases, TestRetrieve variants,
TestCount variants, TestPublish variants, TestSearchShared exclude/namespace
cases, and TestRetract ownership/no-publisher/delete-failure cases) to reduce
duplication while preserving unique test names like
TestLifecycle.test_connect_success,
Mem0MemoryBackend.store/retrieve/get/delete/count/publish/search_shared/retract,
and helper fixtures (_mem0_add_result, _mem0_search_result, _mem0_get_result,
_make_store_request) so each new file stays under ~800 lines and functions
remain <50 lines.

…ig, and tests

- mappers.py: add str() conversion before NotBlankStr for tags, clamp
  confidence to [0.0, 1.0], log missing/non-dict metadata and unexpected
  tag types, guard blank publisher strings, document None relevance
  score behavior in apply_post_filters, clean up historical comment
- config.py: add structured logging to _reject_traversal validator,
  validate unsupported storage overrides in build_config_from_company_config
- factory.py: wrap ValueError from config construction as MemoryConfigError
- pyproject.toml: move mem0ai to optional-dependencies, add TC to test
  per-file-ignores
- __init__.py: guard Mem0 imports with contextlib.suppress(ImportError)
- adapter.py: log during disconnect reset failure instead of bare pass
- dependency-review.yml: version-pin allowed PURLs
- docs: add embedder config to memory.md YAML example, update roadmap
  Current Status to mention Mem0 adapter
- tests: split test_adapter.py (1118 lines) into conftest.py +
  test_adapter.py + test_adapter_crud.py + test_adapter_shared.py;
  add protocol isinstance checks, connect MemoryError/RecursionError
  propagation, agent_id validation, ownership mismatch, and shared
  namespace guard tests
- Clean up 78 now-unused noqa: TC* directives across test files
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 96.73913% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.90%. Comparing base (7a68d34) to head (ac7d47e).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/ai_company/memory/backends/mem0/adapter.py 95.00% 9 Missing and 3 partials ⚠️
src/ai_company/memory/backends/mem0/mappers.py 97.97% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #338      +/-   ##
==========================================
+ Coverage   93.76%   93.90%   +0.13%     
==========================================
  Files         434      446      +12     
  Lines       19850    20699     +849     
  Branches     1915     1993      +78     
==========================================
+ Hits        18613    19438     +825     
- Misses        956      975      +19     
- Partials      281      286       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

mem0ai was moved to optional-dependencies — CI needs --extra mem0
in the uv sync command to install it for tests.
try:
config_dict = build_mem0_config_dict(self._mem0_config)
client = await asyncio.to_thread(Memory.from_config, config_dict)
except builtins.MemoryError, RecursionError:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 2 except syntax used throughout — invalid in Python 3

Every except clause that catches multiple exception types uses the old Python 2 comma syntax instead of a parenthesized tuple. In Python 3, except A, B: is a SyntaxError; the correct form is except (A, B):.

This pattern appears on lines 164, 222, 354, 418, 491, 579, 653, 732, 810, and 897 of this file, and also at src/ai_company/memory/backends/mem0/mappers.py:76 and mappers.py:145.

Example fix for line 164:

Suggested change
except builtins.MemoryError, RecursionError:
except (builtins.MemoryError, RecursionError):

The same one-line fix applies to every other occurrence — wrap both exception types in parentheses.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 164

Comment:
**Python 2 `except` syntax used throughout — invalid in Python 3**

Every `except` clause that catches multiple exception types uses the old Python 2 comma syntax instead of a parenthesized tuple. In Python 3, `except A, B:` is a `SyntaxError`; the correct form is `except (A, B):`.

This pattern appears on lines 164, 222, 354, 418, 491, 579, 653, 732, 810, and 897 of this file, and also at `src/ai_company/memory/backends/mem0/mappers.py:76` and `mappers.py:145`.

Example fix for line 164:
```suggestion
        except (builtins.MemoryError, RecursionError):
```

The same one-line fix applies to every other occurrence — wrap both exception types in parentheses.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/ai_company/memory/backends/mem0/adapter.py Outdated
Comment on lines +143 to +155
if config.storage.vector_store not in ("qdrant", "qdrant-external"):
msg = (
f"Mem0 backend only supports qdrant vector stores, "
f"got {config.storage.vector_store!r}"
)
logger.warning(
MEMORY_BACKEND_CONFIG_INVALID,
backend="mem0",
field="vector_store",
value=config.storage.vector_store,
reason=msg,
)
raise ValueError(msg)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qdrant-external is accepted but silently falls back to embedded Qdrant

build_config_from_company_config allows "qdrant-external" as a valid vector_store value (line 143), but build_mem0_config_dict always generates an embedded Qdrant config with a local path key — there is no code path that produces a host/port/url-based external Qdrant configuration.

A user who sets vector_store: "qdrant-external" in their YAML will receive a silently embedded Qdrant instance, which likely contradicts their intent (e.g. a production cluster). Either:

  • Remove "qdrant-external" from the allowlist and raise a ValueError if it is supplied, or
  • Add a separate code path in build_mem0_config_dict that emits a host/port config when the external store is requested.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/config.py
Line: 143-155

Comment:
**`qdrant-external` is accepted but silently falls back to embedded Qdrant**

`build_config_from_company_config` allows `"qdrant-external"` as a valid `vector_store` value (line 143), but `build_mem0_config_dict` always generates an embedded Qdrant config with a local `path` key — there is no code path that produces a host/port/url-based external Qdrant configuration.

A user who sets `vector_store: "qdrant-external"` in their YAML will receive a silently embedded Qdrant instance, which likely contradicts their intent (e.g. a production cluster). Either:
- Remove `"qdrant-external"` from the allowlist and raise a `ValueError` if it is supplied, or
- Add a separate code path in `build_mem0_config_dict` that emits a host/port config when the external store is requested.

How can I resolve this? If you propose a fix, please make it concise.

return None
try:
dt = datetime.fromisoformat(raw)
except ValueError, TypeError:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 2 except comma syntax — same issue as adapter.py

except ValueError, TypeError: is the old Python 2 syntax and is a SyntaxError in Python 3. Both occurrences in this file (line 76 and line 145) need parentheses:

Suggested change
except ValueError, TypeError:
except (ValueError, TypeError):
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/mappers.py
Line: 76

Comment:
**Python 2 `except` comma syntax — same issue as `adapter.py`**

`except ValueError, TypeError:` is the old Python 2 syntax and is a `SyntaxError` in Python 3. Both occurrences in this file (line 76 and line 145) need parentheses:

```suggestion
    except (ValueError, TypeError):
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (2)
src/ai_company/memory/backends/mem0/config.py (1)

143-156: ⚠️ Potential issue | 🟠 Major

Reject qdrant-external until external wiring is actually supported.

At Line 143, "qdrant-external" is accepted, but the emitted Mem0 config always uses local embedded Qdrant paths. This silently ignores caller intent and can misroute storage in production.

Proposed fix
-    if config.storage.vector_store not in ("qdrant", "qdrant-external"):
+    if config.storage.vector_store != "qdrant":
         msg = (
-            f"Mem0 backend only supports qdrant vector stores, "
+            f"Mem0 backend currently supports only embedded qdrant, "
             f"got {config.storage.vector_store!r}"
         )
         logger.warning(
             MEMORY_BACKEND_CONFIG_INVALID,
             backend="mem0",
             field="vector_store",
             value=config.storage.vector_store,
             reason=msg,
         )
         raise ValueError(msg)

As per coding guidelines "Validate at system boundaries (user input, external APIs, config files)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/backends/mem0/config.py` around lines 143 - 156, The
check in mem0 config currently accepts "qdrant-external" even though external
wiring isn't implemented; update the validation around
config.storage.vector_store in src/ai_company/memory/backends/mem0/config.py to
only allow "qdrant" (remove "qdrant-external" from the allowed tuple), and
adjust the warning/ValueError message emitted via MEMORY_BACKEND_CONFIG_INVALID
to clearly state that only "qdrant" is supported for backend "mem0" (include the
offending value via config.storage.vector_store in the log and error text).
tests/unit/memory/backends/mem0/test_adapter.py (1)

258-322: 🧹 Nitpick | 🔵 Trivial

Parametrize the disconnected-guard matrix to reduce repetition.

These tests all follow the same setup and assertion pattern; collapsing them into one parametrized test will make failures easier to maintain.

Refactor sketch
+@pytest.mark.parametrize(
+    ("method_name", "args"),
+    [
+        ("store", ("test-agent-001", make_store_request())),
+        ("retrieve", ("test-agent-001", MemoryQuery(text="test"))),
+        ("get", ("test-agent-001", "mem-001")),
+        ("delete", ("test-agent-001", "mem-001")),
+        ("count", ("test-agent-001",)),
+        ("publish", ("test-agent-001", make_store_request())),
+        ("search_shared", (MemoryQuery(text="test"),)),
+        ("retract", ("test-agent-001", "mem-001")),
+    ],
+)
+async def test_operation_raises_when_disconnected(
+    self,
+    mem0_config: Mem0BackendConfig,
+    method_name: str,
+    args: tuple[object, ...],
+) -> None:
+    backend = Mem0MemoryBackend(mem0_config=mem0_config)
+    with pytest.raises(MemoryConnectionError, match="Not connected"):
+        await getattr(backend, method_name)(*args)

As per coding guidelines "Prefer @pytest.mark.parametrize for testing similar cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/memory/backends/mem0/test_adapter.py` around lines 258 - 322, The
tests in TestConnectionGuard duplicate the same disconnected-guard pattern for
methods like Mem0MemoryBackend.store, .retrieve, .get, .delete, .count,
.publish, .search_shared and .retract; replace them with a single parametrized
test using pytest.mark.parametrize that iterates tuples of (method_name, args,
kwargs, expected_msg) and invokes the backend method via getattr(b,
method_name)(*args, **kwargs) inside pytest.raises(MemoryConnectionError,
match="Not connected"); ensure you include proper call payloads (e.g.
make_store_request() for store/publish, MemoryQuery(text="test") for
retrieve/search_shared) and keep TestConnectionGuard and mem0_config fixture
usage the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ai_company/memory/__init__.py`:
- Around line 14-18: The __all__ list currently names Mem0EmbedderConfig and
Mem0MemoryBackend even when mem0ai isn't installed, causing
NameError/autocomplete issues; fix by only adding those symbols to __all__
inside the same contextlib.suppress(ImportError) block (or by using a try/except
ImportError around the from import and appending the names to __all__ on
success), so ensure __all__ is conditionally extended when Mem0EmbedderConfig
and Mem0MemoryBackend are actually defined.

In `@src/ai_company/memory/backends/mem0/adapter.py`:
- Around line 1-916: The file triggers the project's 800-line limit but is
intentionally cohesive; keep the implementation as-is and add a short documented
exemption at the top of the module explaining why this module remains >800 lines
(mentioning the Mem0MemoryBackend class and that it implements
MemoryBackend/MemoryCapabilities/SharedKnowledgeStore together), so reviewers/CI
know this is accepted technical debt rather than an accidental omission.

In `@src/ai_company/memory/backends/mem0/mappers.py`:
- Around line 103-177: parse_mem0_metadata (and mem0_result_to_entry) are too
large and do mixed responsibilities (validation, coercion, mapping, fallback);
split them into small helpers so each function stays <50 lines. Extract: a
_validate_raw_metadata(raw_metadata) that handles the initial dict check and
logging, a _coerce_confidence(raw_confidence) that converts/clamps and logs
errors, a _normalize_tags(raw_tags) that normalizes string/list/tuple and
returns tuple[NotBlankStr], and a _map_metadata_fields(raw_metadata) or
_build_memory_metadata(...) that assembles MemoryMetadata (using source,
confidence, tags) and calls parse_mem0_datetime for expires_at; then simplify
parse_mem0_metadata to call those helpers and keep only orchestration and final
return. Update mem0_result_to_entry similarly to delegate validation, mapping
and fallback logic to small helpers (reuse
_validate_raw_metadata/_normalize_tags/_coerce_confidence where applicable) to
meet the per-function size limit.
- Around line 89-100: The normalize_relevance_score function currently assumes
score is numeric and will raise TypeError if given non-numeric values (e.g.,
"0.82"); update normalize_relevance_score to safe-handle non-numeric inputs by
coercing string numeric values to float and returning None (or a clamped value)
when conversion fails, and ensure the caller that uses raw.get("score") passes
its result through this updated normalize_relevance_score; reference the
function normalize_relevance_score and the usage site where raw.get("score") is
mapped so you convert/validate the value before clamping.
- Around line 405-408: The extract_publisher implementation should normalize the
metadata value read via metadata.get(_PUBLISHER_KEY): coerce non-None values to
str, strip surrounding whitespace, and return None if the resulting string is
empty; otherwise return the stripped string. Update the code in
extract_publisher to treat the retrieved value (currently named value) as
potentially non-string, convert it with str(value) when not None, call .strip(),
and only return the cleaned string or None to satisfy the declared return type
(str | None).
- Around line 354-361: The code in validate_add_result assumes result and
results_list[0] are dicts and dereferences .get(), which can raise
AttributeError for malformed Mem0 payloads; update validate_add_result to first
verify that result is a dict and results_list is a non-empty list of dicts
(check isinstance(result, dict) and isinstance(first, dict)) before calling
.get(), and if those checks fail log MEMORY_ENTRY_STORE_FAILED via
logger.warning with context and a clear message and raise MemoryStoreError;
reference the variables/functions validate_add_result, results_list, first,
raw_id, MemoryStoreError, MEMORY_ENTRY_STORE_FAILED, and logger.warning when
making the changes.
- Around line 154-176: The code passes raw `source` directly into MemoryMetadata
which can raise if blank/invalid; retrieve the value from
`raw_metadata.get(f"{_PREFIX}source")`, sanitize it by converting to string,
trimming, and using NotBlankStr only when non-empty, and catch any exception
from NotBlankStr to set `source = None`; when falling back to None, emit a debug
log using the existing MEMORY_MODEL_INVALID constant with field="source" and the
raw value info; finally pass the sanitized `source` into `MemoryMetadata` (same
as current usage) so malformed source values degrade safely to None.

In `@tests/unit/memory/backends/mem0/conftest.py`:
- Around line 85-96: The helper mem0_get_result should accept an optional
user_id so tests don't need to mutate the result; update the signature of
mem0_get_result to add a parameter like user_id: Optional[str] = None and
include the "user_id" key in the returned dict (set to the passed user_id,
possibly None by default) so callers can produce ownership-specific results
directly; keep the existing memory_id parameter and return structure otherwise
unchanged and ensure typing imports remain valid for Any/Optional.

---

Duplicate comments:
In `@src/ai_company/memory/backends/mem0/config.py`:
- Around line 143-156: The check in mem0 config currently accepts
"qdrant-external" even though external wiring isn't implemented; update the
validation around config.storage.vector_store in
src/ai_company/memory/backends/mem0/config.py to only allow "qdrant" (remove
"qdrant-external" from the allowed tuple), and adjust the warning/ValueError
message emitted via MEMORY_BACKEND_CONFIG_INVALID to clearly state that only
"qdrant" is supported for backend "mem0" (include the offending value via
config.storage.vector_store in the log and error text).

In `@tests/unit/memory/backends/mem0/test_adapter.py`:
- Around line 258-322: The tests in TestConnectionGuard duplicate the same
disconnected-guard pattern for methods like Mem0MemoryBackend.store, .retrieve,
.get, .delete, .count, .publish, .search_shared and .retract; replace them with
a single parametrized test using pytest.mark.parametrize that iterates tuples of
(method_name, args, kwargs, expected_msg) and invokes the backend method via
getattr(b, method_name)(*args, **kwargs) inside
pytest.raises(MemoryConnectionError, match="Not connected"); ensure you include
proper call payloads (e.g. make_store_request() for store/publish,
MemoryQuery(text="test") for retrieve/search_shared) and keep
TestConnectionGuard and mem0_config fixture usage the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 33200657-9220-4122-893d-6bb7875069a4

📥 Commits

Reviewing files that changed from the base of the PR and between 28eba7a and e8c7def.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (70)
  • .github/workflows/dependency-review.yml
  • docs/design/memory.md
  • docs/roadmap/index.md
  • pyproject.toml
  • src/ai_company/memory/__init__.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/config.py
  • src/ai_company/memory/backends/mem0/mappers.py
  • src/ai_company/memory/factory.py
  • src/ai_company/observability/events/memory.py
  • tests/integration/communication/test_meeting_integration.py
  • tests/integration/tools/conftest.py
  • tests/integration/tools/test_sandbox_integration.py
  • tests/unit/api/auth/test_controller.py
  • tests/unit/api/conftest.py
  • tests/unit/api/controllers/test_agents.py
  • tests/unit/api/controllers/test_analytics.py
  • tests/unit/api/controllers/test_approvals.py
  • tests/unit/api/controllers/test_artifacts.py
  • tests/unit/api/controllers/test_autonomy.py
  • tests/unit/api/controllers/test_budget.py
  • tests/unit/api/controllers/test_company.py
  • tests/unit/api/controllers/test_departments.py
  • tests/unit/api/controllers/test_meetings.py
  • tests/unit/api/controllers/test_messages.py
  • tests/unit/api/controllers/test_projects.py
  • tests/unit/api/controllers/test_providers.py
  • tests/unit/api/controllers/test_tasks.py
  • tests/unit/api/test_app.py
  • tests/unit/api/test_guards.py
  • tests/unit/api/test_health.py
  • tests/unit/api/test_middleware.py
  • tests/unit/budget/test_category_analytics.py
  • tests/unit/communication/conflict_resolution/test_authority_strategy.py
  • tests/unit/communication/conflict_resolution/test_debate_strategy.py
  • tests/unit/communication/conflict_resolution/test_helpers.py
  • tests/unit/communication/conflict_resolution/test_hybrid_strategy.py
  • tests/unit/communication/conflict_resolution/test_service.py
  • tests/unit/communication/meeting/conftest.py
  • tests/unit/communication/meeting/test_orchestrator.py
  • tests/unit/communication/meeting/test_position_papers.py
  • tests/unit/communication/meeting/test_protocol.py
  • tests/unit/communication/meeting/test_round_robin.py
  • tests/unit/communication/meeting/test_structured_phases.py
  • tests/unit/engine/task_engine_helpers.py
  • tests/unit/engine/test_agent_engine.py
  • tests/unit/engine/test_agent_engine_errors.py
  • tests/unit/engine/test_agent_engine_lifecycle.py
  • tests/unit/engine/test_context.py
  • tests/unit/engine/test_loop_protocol.py
  • tests/unit/engine/test_metrics.py
  • tests/unit/engine/test_plan_execute_loop.py
  • tests/unit/engine/test_react_loop.py
  • tests/unit/engine/test_routing_models.py
  • tests/unit/engine/test_task_engine_mutations.py
  • tests/unit/hr/test_full_snapshot_strategy.py
  • tests/unit/hr/test_hiring_service.py
  • tests/unit/hr/test_offboarding_service.py
  • tests/unit/hr/test_onboarding_service.py
  • tests/unit/hr/test_registry.py
  • tests/unit/memory/backends/mem0/conftest.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/unit/memory/backends/mem0/test_adapter_crud.py
  • tests/unit/memory/backends/mem0/test_adapter_shared.py
  • tests/unit/providers/conftest.py
  • tests/unit/providers/test_protocol.py
  • tests/unit/tools/git/conftest.py
  • tests/unit/tools/git/test_git_sandbox_integration.py
  • tests/unit/tools/sandbox/conftest.py
  • tests/unit/tools/sandbox/test_protocol.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Test (Python 3.14)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Do NOT use from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use except A, B: (no parentheses) for exception handling syntax per PEP 758 — ruff enforces this on Python 3.14

Files:

  • tests/unit/api/controllers/test_tasks.py
  • tests/unit/tools/sandbox/conftest.py
  • tests/unit/communication/conflict_resolution/test_helpers.py
  • tests/unit/api/controllers/test_artifacts.py
  • tests/unit/engine/test_agent_engine.py
  • tests/unit/hr/test_hiring_service.py
  • tests/unit/communication/meeting/conftest.py
  • tests/unit/engine/task_engine_helpers.py
  • tests/unit/api/controllers/test_approvals.py
  • tests/unit/hr/test_full_snapshot_strategy.py
  • tests/unit/api/controllers/test_providers.py
  • tests/unit/tools/sandbox/test_protocol.py
  • tests/unit/communication/meeting/test_protocol.py
  • tests/unit/hr/test_registry.py
  • tests/unit/engine/test_agent_engine_lifecycle.py
  • tests/integration/tools/conftest.py
  • tests/unit/tools/git/conftest.py
  • tests/unit/api/conftest.py
  • tests/unit/api/test_middleware.py
  • tests/unit/budget/test_category_analytics.py
  • tests/unit/engine/test_loop_protocol.py
  • tests/unit/api/controllers/test_company.py
  • tests/integration/tools/test_sandbox_integration.py
  • tests/unit/engine/test_task_engine_mutations.py
  • tests/unit/communication/meeting/test_orchestrator.py
  • tests/unit/hr/test_onboarding_service.py
  • tests/unit/memory/backends/mem0/test_adapter_crud.py
  • tests/unit/api/controllers/test_projects.py
  • tests/unit/api/controllers/test_messages.py
  • src/ai_company/memory/backends/mem0/config.py
  • tests/unit/api/test_app.py
  • tests/unit/engine/test_plan_execute_loop.py
  • tests/unit/communication/conflict_resolution/test_debate_strategy.py
  • tests/unit/tools/git/test_git_sandbox_integration.py
  • tests/unit/api/test_health.py
  • tests/unit/api/auth/test_controller.py
  • tests/unit/communication/conflict_resolution/test_authority_strategy.py
  • tests/unit/api/controllers/test_analytics.py
  • tests/unit/engine/test_agent_engine_errors.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/observability/events/memory.py
  • src/ai_company/memory/backends/mem0/mappers.py
  • tests/unit/api/controllers/test_agents.py
  • tests/unit/api/controllers/test_departments.py
  • tests/unit/memory/backends/mem0/conftest.py
  • tests/unit/engine/test_react_loop.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/unit/providers/conftest.py
  • tests/unit/engine/test_context.py
  • tests/unit/api/controllers/test_budget.py
  • tests/integration/communication/test_meeting_integration.py
  • tests/unit/communication/meeting/test_round_robin.py
  • tests/unit/communication/meeting/test_structured_phases.py
  • tests/unit/communication/conflict_resolution/test_hybrid_strategy.py
  • tests/unit/api/controllers/test_meetings.py
  • src/ai_company/memory/factory.py
  • tests/unit/hr/test_offboarding_service.py
  • tests/unit/communication/conflict_resolution/test_service.py
  • tests/unit/api/test_guards.py
  • tests/unit/engine/test_metrics.py
  • tests/unit/providers/test_protocol.py
  • tests/unit/engine/test_routing_models.py
  • tests/unit/memory/backends/mem0/test_adapter_shared.py
  • src/ai_company/memory/__init__.py
  • tests/unit/api/controllers/test_autonomy.py
  • tests/unit/communication/meeting/test_position_papers.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow for test markers
Prefer @pytest.mark.parametrize for testing similar cases

Files:

  • tests/unit/api/controllers/test_tasks.py
  • tests/unit/tools/sandbox/conftest.py
  • tests/unit/communication/conflict_resolution/test_helpers.py
  • tests/unit/api/controllers/test_artifacts.py
  • tests/unit/engine/test_agent_engine.py
  • tests/unit/hr/test_hiring_service.py
  • tests/unit/communication/meeting/conftest.py
  • tests/unit/engine/task_engine_helpers.py
  • tests/unit/api/controllers/test_approvals.py
  • tests/unit/hr/test_full_snapshot_strategy.py
  • tests/unit/api/controllers/test_providers.py
  • tests/unit/tools/sandbox/test_protocol.py
  • tests/unit/communication/meeting/test_protocol.py
  • tests/unit/hr/test_registry.py
  • tests/unit/engine/test_agent_engine_lifecycle.py
  • tests/integration/tools/conftest.py
  • tests/unit/tools/git/conftest.py
  • tests/unit/api/conftest.py
  • tests/unit/api/test_middleware.py
  • tests/unit/budget/test_category_analytics.py
  • tests/unit/engine/test_loop_protocol.py
  • tests/unit/api/controllers/test_company.py
  • tests/integration/tools/test_sandbox_integration.py
  • tests/unit/engine/test_task_engine_mutations.py
  • tests/unit/communication/meeting/test_orchestrator.py
  • tests/unit/hr/test_onboarding_service.py
  • tests/unit/memory/backends/mem0/test_adapter_crud.py
  • tests/unit/api/controllers/test_projects.py
  • tests/unit/api/controllers/test_messages.py
  • tests/unit/api/test_app.py
  • tests/unit/engine/test_plan_execute_loop.py
  • tests/unit/communication/conflict_resolution/test_debate_strategy.py
  • tests/unit/tools/git/test_git_sandbox_integration.py
  • tests/unit/api/test_health.py
  • tests/unit/api/auth/test_controller.py
  • tests/unit/communication/conflict_resolution/test_authority_strategy.py
  • tests/unit/api/controllers/test_analytics.py
  • tests/unit/engine/test_agent_engine_errors.py
  • tests/unit/api/controllers/test_agents.py
  • tests/unit/api/controllers/test_departments.py
  • tests/unit/memory/backends/mem0/conftest.py
  • tests/unit/engine/test_react_loop.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/unit/providers/conftest.py
  • tests/unit/engine/test_context.py
  • tests/unit/api/controllers/test_budget.py
  • tests/integration/communication/test_meeting_integration.py
  • tests/unit/communication/meeting/test_round_robin.py
  • tests/unit/communication/meeting/test_structured_phases.py
  • tests/unit/communication/conflict_resolution/test_hybrid_strategy.py
  • tests/unit/api/controllers/test_meetings.py
  • tests/unit/hr/test_offboarding_service.py
  • tests/unit/communication/conflict_resolution/test_service.py
  • tests/unit/api/test_guards.py
  • tests/unit/engine/test_metrics.py
  • tests/unit/providers/test_protocol.py
  • tests/unit/engine/test_routing_models.py
  • tests/unit/memory/backends/mem0/test_adapter_shared.py
  • tests/unit/api/controllers/test_autonomy.py
  • tests/unit/communication/meeting/test_position_papers.py
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Docs source: docs/ (Markdown, built with Zensical). Design spec: docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations).

Files:

  • docs/roadmap/index.md
  • docs/design/memory.md
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Type hints: all public functions must have type hints, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones—never mutate existing objects. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Config vs runtime state: use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use @computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr (from core.types) for all identifier/name fields—including optional and tuple variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (enforced by ruff)
Functions: < 50 lines, files < 800 lines
Handle errors explicitly, never silently swallow them
Validate at system boundaries (user input, external APIs, config files)
Every module with business logic MUST have: from ai_company.observability import get_logger then logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code
Always use logger as the variable name (not _logger, not log)
Use event name constants from ai_company.observability.events domain-spe...

Files:

  • src/ai_company/memory/backends/mem0/config.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/observability/events/memory.py
  • src/ai_company/memory/backends/mem0/mappers.py
  • src/ai_company/memory/factory.py
  • src/ai_company/memory/__init__.py
pyproject.toml

📄 CodeRabbit inference engine (CLAUDE.md)

pyproject.toml: Use asyncio_mode = "auto" in pytest configuration—no manual @pytest.mark.asyncio needed on individual tests
Test timeout: 30 seconds per test
Dependencies: all versions pinned with == in pyproject.toml. Groups: test and dev (includes test). Install via uv sync.

Files:

  • pyproject.toml
.github/workflows/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

CI: Jobs (lint + type-check + test in parallel) → ci-pass gate. Pages workflow exports OpenAPI schema, builds Astro landing + Zensical docs, merges, deploys to GitHub Pages on push to main.

Files:

  • .github/workflows/dependency-review.yml
🧠 Learnings (8)
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to docs/**/*.md : Docs source: `docs/` (Markdown, built with Zensical). Design spec: `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations).

Applied to files:

  • docs/roadmap/index.md
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to src/**/*.py : Every module with business logic MUST have: `from ai_company.observability import get_logger` then `logger = get_logger(__name__)`

Applied to files:

  • src/ai_company/memory/backends/mem0/config.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to src/**/*.py : All error paths must log at WARNING or ERROR with context before raising

Applied to files:

  • src/ai_company/memory/backends/mem0/config.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) for exception handling syntax per PEP 758 — ruff enforces this on Python 3.14

Applied to files:

  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/mappers.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to src/**/*.py : Use event name constants from `ai_company.observability.events` domain-specific modules (e.g. `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, etc.) instead of hardcoded string literals

Applied to files:

  • src/ai_company/observability/events/memory.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to scripts/**/*.py : Scripts: `scripts/` directory with relaxed ruff rules allowing `print` and deferred imports

Applied to files:

  • pyproject.toml
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Fix everything valid when review agents find issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes)—never skip or defer

Applied to files:

  • .github/workflows/dependency-review.yml
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to src/**/*.py : All provider calls go through `BaseCompletionProvider` which applies retry + rate limiting automatically. Never implement retry logic in driver subclasses or calling code—it's handled by the base class.

Applied to files:

  • tests/unit/providers/test_protocol.py
🧬 Code graph analysis (35)
tests/unit/communication/conflict_resolution/test_helpers.py (1)
src/ai_company/communication/delegation/hierarchy.py (1)
  • HierarchyResolver (16-277)
tests/unit/engine/test_agent_engine.py (1)
src/ai_company/core/agent.py (1)
  • AgentIdentity (266-342)
tests/unit/engine/task_engine_helpers.py (1)
src/ai_company/core/task.py (1)
  • Task (45-261)
tests/unit/hr/test_full_snapshot_strategy.py (1)
src/ai_company/memory/consolidation/models.py (1)
  • ArchivalEntry (56-91)
tests/unit/tools/sandbox/test_protocol.py (3)
src/ai_company/tools/sandbox/protocol.py (1)
  • SandboxBackend (17-75)
src/ai_company/tools/sandbox/result.py (1)
  • SandboxResult (6-28)
tests/unit/tools/sandbox/conftest.py (1)
  • subprocess_sandbox (24-32)
tests/unit/hr/test_registry.py (3)
tests/unit/hr/promotion/conftest.py (1)
  • registry (98-100)
tests/unit/hr/conftest.py (1)
  • registry (174-176)
src/ai_company/hr/registry.py (1)
  • AgentRegistryService (32-262)
tests/unit/engine/test_agent_engine_lifecycle.py (3)
src/ai_company/core/agent.py (1)
  • AgentIdentity (266-342)
src/ai_company/core/enums.py (1)
  • TaskStatus (198-224)
src/ai_company/core/task.py (1)
  • Task (45-261)
tests/unit/api/conftest.py (7)
src/ai_company/budget/cost_record.py (1)
  • CostRecord (16-57)
src/ai_company/communication/channel.py (1)
  • Channel (14-39)
src/ai_company/communication/message.py (1)
  • Message (88-138)
src/ai_company/hr/enums.py (1)
  • LifecycleEventType (32-41)
src/ai_company/persistence/errors.py (2)
  • DuplicateRecordError (29-30)
  • QueryError (33-34)
src/ai_company/security/models.py (1)
  • AuditEntry (112-149)
src/ai_company/security/timeout/parked_context.py (1)
  • ParkedContext (19-64)
tests/unit/budget/test_category_analytics.py (1)
src/ai_company/budget/tracker.py (1)
  • CostTracker (68-455)
tests/unit/engine/test_loop_protocol.py (1)
src/ai_company/engine/context.py (1)
  • AgentContext (87-307)
tests/unit/communication/meeting/test_orchestrator.py (1)
src/ai_company/communication/meeting/protocol.py (1)
  • MeetingProtocol (61-100)
tests/unit/hr/test_onboarding_service.py (3)
tests/unit/hr/conftest.py (2)
  • onboarding_service (180-182)
  • registry (174-176)
src/ai_company/hr/onboarding_service.py (1)
  • OnboardingService (28-173)
src/ai_company/hr/registry.py (1)
  • AgentRegistryService (32-262)
tests/unit/memory/backends/mem0/test_adapter_crud.py (1)
src/ai_company/memory/backends/mem0/adapter.py (5)
  • store (315-372)
  • retrieve (374-435)
  • get (437-510)
  • delete (512-598)
  • count (600-683)
src/ai_company/memory/backends/mem0/config.py (1)
src/ai_company/memory/config.py (1)
  • CompanyMemoryConfig (139-195)
tests/unit/engine/test_plan_execute_loop.py (1)
src/ai_company/core/agent.py (1)
  • AgentIdentity (266-342)
tests/unit/communication/conflict_resolution/test_debate_strategy.py (1)
src/ai_company/communication/delegation/hierarchy.py (1)
  • HierarchyResolver (16-277)
tests/unit/communication/conflict_resolution/test_authority_strategy.py (1)
src/ai_company/communication/delegation/hierarchy.py (1)
  • HierarchyResolver (16-277)
tests/unit/engine/test_agent_engine_errors.py (3)
src/ai_company/core/agent.py (1)
  • AgentIdentity (266-342)
src/ai_company/core/enums.py (1)
  • TaskStatus (198-224)
src/ai_company/core/task.py (1)
  • Task (45-261)
src/ai_company/memory/backends/mem0/mappers.py (4)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/errors.py (2)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
src/ai_company/memory/models.py (4)
  • MemoryEntry (82-150)
  • MemoryMetadata (20-52)
  • MemoryQuery (153-230)
  • MemoryStoreRequest (55-79)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • get (437-510)
tests/unit/api/controllers/test_agents.py (1)
src/ai_company/api/auth/service.py (1)
  • AuthService (36-245)
tests/unit/memory/backends/mem0/conftest.py (5)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • max_memories_per_agent (278-280)
src/ai_company/memory/backends/mem0/config.py (2)
  • Mem0BackendConfig (52-91)
  • Mem0EmbedderConfig (23-49)
src/ai_company/memory/models.py (1)
  • MemoryStoreRequest (55-79)
src/ai_company/tools/base.py (1)
  • category (128-130)
tests/unit/engine/test_react_loop.py (1)
src/ai_company/core/agent.py (1)
  • AgentIdentity (266-342)
tests/unit/memory/backends/mem0/test_adapter.py (8)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/backends/mem0/config.py (1)
  • Mem0BackendConfig (52-91)
src/ai_company/memory/capabilities.py (1)
  • MemoryCapabilities (13-56)
src/ai_company/memory/errors.py (2)
  • MemoryConnectionError (17-18)
  • MemoryError (13-14)
src/ai_company/memory/models.py (1)
  • MemoryQuery (153-230)
src/ai_company/memory/protocol.py (1)
  • MemoryBackend (20-182)
src/ai_company/memory/shared.py (1)
  • SharedKnowledgeStore (18-82)
tests/unit/memory/backends/mem0/conftest.py (2)
  • make_store_request (99-105)
  • backend (41-49)
tests/unit/engine/test_context.py (3)
src/ai_company/core/agent.py (1)
  • AgentIdentity (266-342)
src/ai_company/core/enums.py (1)
  • TaskStatus (198-224)
src/ai_company/core/task.py (1)
  • Task (45-261)
tests/integration/communication/test_meeting_integration.py (1)
src/ai_company/communication/meeting/protocol.py (1)
  • MeetingProtocol (61-100)
tests/unit/communication/meeting/test_round_robin.py (1)
src/ai_company/communication/meeting/models.py (1)
  • MeetingAgenda (77-96)
tests/unit/communication/meeting/test_structured_phases.py (1)
src/ai_company/communication/meeting/models.py (1)
  • MeetingAgenda (77-96)
tests/unit/communication/conflict_resolution/test_hybrid_strategy.py (1)
src/ai_company/communication/delegation/hierarchy.py (1)
  • HierarchyResolver (16-277)
src/ai_company/memory/factory.py (5)
src/ai_company/memory/config.py (1)
  • CompanyMemoryConfig (139-195)
src/ai_company/memory/backends/mem0/config.py (1)
  • Mem0EmbedderConfig (23-49)
src/ai_company/memory/errors.py (1)
  • MemoryConfigError (40-41)
src/ai_company/memory/protocol.py (1)
  • MemoryBackend (20-182)
src/ai_company/memory/backends/mem0/adapter.py (2)
  • Mem0MemoryBackend (116-916)
  • max_memories_per_agent (278-280)
tests/unit/hr/test_offboarding_service.py (4)
src/ai_company/communication/channel.py (1)
  • Channel (14-39)
src/ai_company/communication/message.py (1)
  • Message (88-138)
src/ai_company/core/enums.py (2)
  • AgentStatus (67-73)
  • TaskStatus (198-224)
src/ai_company/hr/registry.py (1)
  • AgentRegistryService (32-262)
tests/unit/communication/conflict_resolution/test_service.py (1)
src/ai_company/communication/delegation/hierarchy.py (1)
  • HierarchyResolver (16-277)
tests/unit/engine/test_metrics.py (1)
src/ai_company/engine/context.py (1)
  • AgentContext (87-307)
tests/unit/memory/backends/mem0/test_adapter_shared.py (3)
src/ai_company/memory/errors.py (3)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
  • MemoryError (13-14)
src/ai_company/memory/models.py (1)
  • MemoryQuery (153-230)
tests/unit/memory/backends/mem0/conftest.py (4)
  • make_store_request (99-105)
  • mem0_add_result (52-62)
  • mem0_search_result (65-82)
  • backend (41-49)
src/ai_company/memory/__init__.py (2)
src/ai_company/memory/backends/mem0/config.py (1)
  • Mem0EmbedderConfig (23-49)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • Mem0MemoryBackend (116-916)
tests/unit/communication/meeting/test_position_papers.py (1)
src/ai_company/communication/meeting/models.py (1)
  • MeetingAgenda (77-96)
🔇 Additional comments (63)
tests/unit/api/controllers/test_projects.py (1)

6-6: Import cleanup looks good.

No functional impact here; the import remains correct and consistent with the type annotations used in tests.

tests/unit/api/controllers/test_artifacts.py (1)

6-6: Import cleanup looks good.

Removing the inline suppression on TestClient keeps the test import clean without changing behavior.

tests/unit/engine/task_engine_helpers.py (1)

6-6: Import cleanup looks good.

Line 6 keeps the concrete Task import while removing the inline suppression, with no behavioral change.

tests/unit/engine/test_agent_engine.py (1)

12-12: Safe lint-cleanup import change.

Line 12 is correct as updated; removing the inline suppression does not affect runtime behavior and keeps the test module clean.

tests/unit/tools/git/test_git_sandbox_integration.py (1)

3-3: Import cleanup looks good.

Removing the inline suppression here is safe and does not change behavior.

tests/unit/budget/test_category_analytics.py (1)

15-15: Import cleanup looks good.

This change is safe and does not alter test behavior.

tests/unit/hr/test_onboarding_service.py (1)

8-9: Import cleanup looks good.

Removing the inline suppression comments on Line 8 and Line 9 is a no-op behaviorally and keeps the test module cleaner.

tests/unit/api/controllers/test_messages.py (1)

6-6: Import cleanup looks good.

Removing the inline suppression here keeps the test module cleaner without changing behavior.

tests/unit/hr/test_offboarding_service.py (1)

8-11: Import cleanup looks good.

These import updates are non-functional and keep the test module clear without introducing regressions.

Also applies to: 22-22

tests/unit/engine/test_agent_engine_lifecycle.py (1)

9-11: Import cleanup looks good.

Removing the unnecessary # noqa: TC001 suppressions here is a safe, non-functional cleanup and improves test-file hygiene.

tests/unit/providers/test_protocol.py (1)

3-3: Looks good — safe lint cleanup.

Removing the unnecessary inline # noqa on the AsyncIterator import is a clean, non-behavioral change.

tests/unit/engine/test_plan_execute_loop.py (1)

9-9: Good cleanup: dropping unnecessary noqa suppression.

AgentIdentity is used in this test module’s type annotations, so keeping the direct import without inline suppression is appropriate.

tests/unit/providers/conftest.py (1)

3-3: Import cleanup looks good.

Line 3 keeps a valid and actively used AsyncIterator import; dropping the inline suppression is safe and improves clarity.

tests/unit/engine/test_agent_engine_errors.py (1)

8-10: Import cleanup looks good.

No functional risk here; this is a clean non-behavioral change.

tests/unit/communication/conflict_resolution/test_debate_strategy.py (1)

14-16: Good cleanup: safe removal of inline lint suppression.

This import change is non-functional and keeps type intent explicit without affecting test behavior.

tests/unit/api/controllers/test_autonomy.py (1)

6-6: LGTM!

Clean removal of the # noqa: TC002 suppression comment. The import is valid and the type-checking issue that previously required suppression appears to be resolved.

tests/unit/api/controllers/test_providers.py (1)

6-6: LGTM: Lint cleanup correctly applied.

The removal of the # noqa: TC002 suppression comment indicates the type-checking lint issue has been resolved or is no longer applicable.

tests/unit/hr/test_registry.py (1)

7-7: LGTM!

Removing the inline noqa comment is a clean lint cleanup. The import and its usage remain correct.

tests/unit/api/test_middleware.py (1)

6-6: LGTM!

Clean removal of the noqa comment. The import is straightforward and the tests properly use @pytest.mark.unit markers and @pytest.mark.parametrize as expected.

tests/unit/hr/test_hiring_service.py (1)

15-16: LGTM! Lint suppression removal is correct.

Both OnboardingService and AgentRegistryService are used at runtime in the tests, so removing the # noqa: TC001 suppressions is appropriate—these are not type-checking-only imports.

tests/integration/tools/test_sandbox_integration.py (1)

4-4: Good cleanup: removing the inline noqa is safe here.

This change keeps behavior unchanged and improves lint hygiene.

tests/unit/tools/sandbox/conftest.py (1)

3-3: LGTM! Correctly removed unnecessary lint suppression.

The # noqa: TC003 comment was unnecessary since Path is used at runtime in type annotations (lines 12, 25), not just for static type checking. Removing this suppression improves code hygiene.

tests/unit/tools/git/conftest.py (1)

5-5: LGTM! Correct removal of unnecessary linter suppression.

The # noqa: TC003 suppression was unnecessary because Path is used at runtime (e.g., path operations on lines 67, 84, 85, 101, 105), not just in type annotations. This cleanup improves code clarity.

tests/unit/tools/sandbox/test_protocol.py (1)

3-4: LGTM! Clean import formatting.

The removal of inline noqa comments from these import statements improves code readability without affecting functionality. All imports are properly used throughout the test file.

Also applies to: 12-12

tests/unit/api/controllers/test_company.py (1)

6-6: LGTM: Lint cleanup aligns with project standards.

Removing the # noqa: TC002 suppression is appropriate since TestClient is used at runtime in the test methods (not just for type checking), making the suppression unnecessary.

tests/unit/engine/test_context.py (1)

9-11: LGTM!

The removal of # noqa: TC001 comments is appropriate cleanup. These imports (AgentIdentity and Task) are used at runtime for fixture type annotations and test assertions, so they correctly belong outside a TYPE_CHECKING block. Test markers follow coding guidelines.

tests/unit/api/controllers/test_tasks.py (1)

6-6: LGTM: Lint directive cleanup

The removal of the # noqa: TC002 directive is appropriate. The TestClient import is used in runtime type annotations for test fixtures throughout the file and appears to be correctly imported without suppression.

tests/unit/engine/test_react_loop.py (1)

8-8: LGTM! Valid cleanup for Python 3.14.

Removing the noqa: TC001 suppression is appropriate. With Python 3.14's PEP 649 native lazy annotations, imports used only in type annotations no longer require special handling or suppressions. As per coding guidelines, the project relies on native lazy annotations rather than from __future__ import annotations, making this suppression unnecessary.

tests/unit/engine/test_routing_models.py (1)

5-5: LGTM.

AgentIdentity is still referenced by the test annotations in this file, so this import cleanup does not introduce any local issue.

tests/unit/communication/conflict_resolution/test_helpers.py (1)

12-12: Import cleanup is safe.

Line 12 keeps the same dependency and removes an unnecessary suppression without changing test behavior.

tests/unit/engine/test_metrics.py (1)

6-6: No concerns with this import cleanup.

Line 6 is a non-functional lint/style adjustment and is safe.

tests/unit/api/test_app.py (1)

7-7: Looks good.

Line 7 is a safe import-comment cleanup with no behavior impact.

tests/unit/communication/meeting/test_orchestrator.py (1)

31-31: Import change is clean and non-functional.

Line 31 keeps typing clarity and does not alter orchestrator test behavior.

tests/unit/engine/test_task_engine_mutations.py (1)

8-8: Safe import cleanup.

Line 8 is a stylistic adjustment only and does not change test execution.

tests/unit/hr/test_full_snapshot_strategy.py (1)

12-12: No issues here.

Line 12 is a non-functional import tidy-up and is safe.

docs/roadmap/index.md (1)

11-11: Roadmap update is aligned with the PR scope.

Line 11 accurately reflects the implemented pluggable memory backend and Mem0 adapter status.

tests/integration/tools/conftest.py (1)

5-5: Looks good.

Line 5 is a harmless import cleanup, and Path remains correctly used by the fixture helpers.

tests/unit/communication/conflict_resolution/test_authority_strategy.py (1)

11-12: LGTM!

Lint directive cleanup - removing the noqa: TC001 comment while preserving the import. The HierarchyResolver type is correctly used for fixture type hints throughout the test class.

tests/unit/communication/meeting/conftest.py (1)

16-16: LGTM!

Lint directive removal. The AgentCaller protocol type is correctly used as a return type annotation for make_mock_agent_caller.

tests/unit/communication/meeting/test_structured_phases.py (1)

13-13: LGTM!

Lint directive cleanup. MeetingAgenda is used as a fixture type hint throughout the test methods.

tests/unit/communication/meeting/test_position_papers.py (1)

13-13: LGTM!

Lint directive cleanup. MeetingAgenda is used as a fixture type hint throughout the test methods.

tests/unit/api/test_health.py (1)

6-6: LGTM!

Lint directive cleanup for the TestClient import from litestar.

tests/unit/api/controllers/test_analytics.py (1)

6-6: LGTM!

Lint directive cleanup for the TestClient import.

tests/unit/api/controllers/test_approvals.py (1)

7-9: LGTM!

Lint directive cleanup for TestClient and ApprovalStore imports. Both types are used for fixture type hints throughout the test file.

tests/unit/api/conftest.py (1)

5-39: LGTM!

Comprehensive lint directive cleanup across multiple imports. All types (Generator, CostRecord, Channel, Message, LifecycleEventType, AgentLifecycleEvent, TaskMetricRecord, CollaborationMetricRecord, AuditEntry, AuditVerdictStr, ParkedContext) are used at runtime in the fake repository implementations and fixtures.

tests/unit/communication/meeting/test_round_robin.py (1)

13-13: Import cleanup is safe and keeps test behavior unchanged.

tests/unit/api/auth/test_controller.py (1)

6-7: Non-functional import cleanup looks good.

Also applies to: 47-47

.github/workflows/dependency-review.yml (1)

44-51: Version-pinned dependency license exceptions are a solid compliance hardening step.

src/ai_company/observability/events/memory.py (1)

22-24: New memory backend event constants are consistent with the event namespace and usage pattern.

tests/integration/communication/test_meeting_integration.py (1)

29-30: Import cleanup is safe; no behavioral impact on integration tests.

src/ai_company/memory/backends/mem0/config.py (1)

75-91: Traversal validation and structured warning logging are implemented correctly.

tests/unit/memory/backends/mem0/test_adapter.py (1)

135-253: Lifecycle and exception-propagation coverage here is strong and directly valuable.

tests/unit/api/test_guards.py (1)

6-6: Import-only cleanup is good and behavior-preserving.

pyproject.toml (2)

30-31: LGTM — optional Mem0 dependency correctly configured.

The mem0ai==1.0.5 version is properly pinned per project conventions, and placing it in [project.optional-dependencies] keeps it separate from core dependencies.


191-193: LGTM — mypy override for untyped optional dependency.

Standard practice for optional dependencies without type stubs.

docs/design/memory.md (1)

286-292: LGTM — clear documentation of embedder configuration pattern.

The example correctly demonstrates programmatic embedder configuration via the factory, with vendor-agnostic placeholders.

tests/unit/memory/backends/mem0/test_adapter_crud.py (1)

1-515: LGTM — comprehensive CRUD test coverage.

Excellent test coverage including:

  • Success paths for all operations
  • Edge cases (empty results, blank/whitespace IDs, non-list results)
  • Error wrapping verification
  • builtins.MemoryError re-raise behavior
  • Ownership and shared namespace validation

The tests properly use fixtures and follow project conventions.

tests/unit/memory/backends/mem0/conftest.py (1)

40-49: LGTM — test fixture correctly wires mock client.

Direct assignment to _client and _connected is the appropriate pattern for unit testing with mocked dependencies.

src/ai_company/memory/factory.py (1)

94-103: Exceptions from Mem0MemoryBackend(...) instantiation bypass config validation logging.

The ValueError from build_config_from_company_config is properly wrapped, but if Mem0MemoryBackend.__init__ raises (e.g., validation in a future version), it escapes without MEMORY_BACKEND_CONFIG_INVALID logging. Consider wrapping lines 94-97 similarly:

🛡️ Suggested defensive wrapper
+        try:
             backend = Mem0MemoryBackend(
                 mem0_config=mem0_config,
                 max_memories_per_agent=config.options.max_memories_per_agent,
             )
+        except Exception as exc:
+            msg = f"Failed to create Mem0 backend: {exc}"
+            logger.warning(
+                MEMORY_BACKEND_CONFIG_INVALID,
+                backend="mem0",
+                reason="backend_instantiation_failed",
+                error=msg,
+            )
+            raise MemoryConfigError(msg) from exc
         logger.info(
             MEMORY_BACKEND_CREATED,
tests/unit/memory/backends/mem0/test_adapter_shared.py (1)

1-327: LGTM — thorough shared knowledge store test coverage.

Comprehensive tests covering:

  • Publisher metadata injection and extraction
  • Shared namespace routing
  • Agent exclusion filtering
  • Ownership verification for retract
  • Error wrapping and builtins.MemoryError passthrough
src/ai_company/memory/backends/mem0/adapter.py (3)

179-197: disconnect() may not fully release resources.

The past review flagged that the Mem0 Memory client holds an internal httpx.Client. Calling reset() clears stored memories but doesn't close the HTTP connection pool. If reset() fails, the exception is silently logged at DEBUG.

Consider explicitly closing the underlying client:

🔧 Suggested fix
     async def disconnect(self) -> None:
         """Close the Mem0 connection."""
         logger.info(MEMORY_BACKEND_DISCONNECTING, backend="mem0")
         if self._client is not None:
             try:
-                await asyncio.to_thread(self._client.reset)
-            except Exception:
-                logger.debug(
+                # Close the underlying httpx client to release connections
+                if hasattr(self._client, "client") and self._client.client is not None:
+                    await asyncio.to_thread(self._client.client.close)
+            except Exception as exc:
+                logger.warning(
                     MEMORY_BACKEND_DISCONNECTING,
                     backend="mem0",
-                    note="reset failed during disconnect, ignoring",
+                    note="client close failed during disconnect",
+                    error=str(exc),
                 )
         self._client = None
         self._connected = False
         logger.info(MEMORY_BACKEND_DISCONNECTED, backend="mem0")
Does mem0ai Memory client have a close method or expose httpx client for cleanup?

164-165: LGTM — consistent exception handling pattern.

The except builtins.MemoryError, RecursionError: syntax correctly follows PEP 758 and ensures system-level failures propagate without masking. Based on learnings: "Use except A, B: (no parentheses) for exception handling syntax per PEP 758 — ruff enforces this on Python 3.14".

Also applies to: 222-223, 354-355, 418-419, 491-492, 579-580, 653-654, 732-733, 810-811, 897-898


116-136: LGTM — well-designed class initialization.

Clear separation of configuration (_mem0_config), limits (_max_memories_per_agent), and runtime state (_client, _connected). The class properly implements all three protocols.

Comment thread src/ai_company/memory/__init__.py Outdated
Comment on lines +14 to +18
with contextlib.suppress(ImportError): # mem0ai is optional
from ai_company.memory.backends.mem0 import (
Mem0EmbedderConfig,
Mem0MemoryBackend,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Conditionally-defined exports in __all__ may cause runtime NameError.

When mem0ai is not installed, Mem0EmbedderConfig and Mem0MemoryBackend are not defined, but they're still listed in __all__. This means:

  • from ai_company.memory import Mem0EmbedderConfig raises ImportError (expected)
  • from ai_company.memory import * may behave unexpectedly
  • IDE autocompletion will suggest these names even when unavailable

Consider conditionally extending __all__ or documenting this behavior explicitly in the module docstring.

💡 Optional: Conditionally extend __all__
+_MEM0_EXPORTS: list[str] = []
 with contextlib.suppress(ImportError):  # mem0ai is optional
     from ai_company.memory.backends.mem0 import (
         Mem0EmbedderConfig,
         Mem0MemoryBackend,
     )
+    _MEM0_EXPORTS = ["Mem0EmbedderConfig", "Mem0MemoryBackend"]
 
 __all__ = [
     "ArchivalStore",
     # ... other exports ...
-    "Mem0EmbedderConfig",
-    "Mem0MemoryBackend",
     # ... other exports ...
-]
+] + _MEM0_EXPORTS

Also applies to: 85-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/__init__.py` around lines 14 - 18, The __all__ list
currently names Mem0EmbedderConfig and Mem0MemoryBackend even when mem0ai isn't
installed, causing NameError/autocomplete issues; fix by only adding those
symbols to __all__ inside the same contextlib.suppress(ImportError) block (or by
using a try/except ImportError around the from import and appending the names to
__all__ on success), so ensure __all__ is conditionally extended when
Mem0EmbedderConfig and Mem0MemoryBackend are actually defined.

Comment thread src/ai_company/memory/backends/mem0/adapter.py
Comment thread src/ai_company/memory/backends/mem0/mappers.py Outdated
Comment thread src/ai_company/memory/backends/mem0/mappers.py
Comment thread src/ai_company/memory/backends/mem0/mappers.py Outdated
Comment thread src/ai_company/memory/backends/mem0/mappers.py
Comment thread src/ai_company/memory/backends/mem0/mappers.py Outdated
Comment on lines +85 to +96
def mem0_get_result(memory_id: str = "mem-001") -> dict[str, Any]:
"""Build a typical Mem0 get() return value."""
return {
"id": memory_id,
"memory": "stored content",
"created_at": "2026-03-12T10:00:00+00:00",
"updated_at": None,
"metadata": {
"_synthorg_category": "episodic",
"_synthorg_confidence": 1.0,
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding optional user_id parameter to mem0_get_result.

Several tests (e.g., test_get_ownership_mismatch_returns_none, test_delete_shared_namespace_entry_raises) need to modify the user_id field after calling this helper. Adding an optional parameter would reduce test boilerplate.

💡 Optional improvement
-def mem0_get_result(memory_id: str = "mem-001") -> dict[str, Any]:
+def mem0_get_result(
+    memory_id: str = "mem-001",
+    *,
+    user_id: str | None = None,
+) -> dict[str, Any]:
     """Build a typical Mem0 get() return value."""
     return {
         "id": memory_id,
         "memory": "stored content",
         "created_at": "2026-03-12T10:00:00+00:00",
         "updated_at": None,
+        "user_id": user_id,
         "metadata": {
             "_synthorg_category": "episodic",
             "_synthorg_confidence": 1.0,
         },
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/memory/backends/mem0/conftest.py` around lines 85 - 96, The helper
mem0_get_result should accept an optional user_id so tests don't need to mutate
the result; update the signature of mem0_get_result to add a parameter like
user_id: Optional[str] = None and include the "user_id" key in the returned dict
(set to the passed user_id, possibly None by default) so callers can produce
ownership-specific results directly; keep the existing memory_id parameter and
return structure otherwise unchanged and ensure typing imports remain valid for
Any/Optional.

mem0ai is the only memory backend — no reason to make it optional.
Reverts the optional-dependencies split and CI extra flag.
Copilot AI review requested due to automatic review settings March 13, 2026 07:10
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 13, 2026 07:12 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ai_company/memory/__init__.py`:
- Around line 6-9: The module docstring in __init__.py lists only
Mem0MemoryBackend but the module also re-exports Mem0EmbedderConfig (and
possibly other public symbols); update the top-level summary to include
Mem0EmbedderConfig (and ensure any other names exported in the file are
reflected) so the public-surface description matches the actual re-exports
(search for Mem0MemoryBackend, Mem0EmbedderConfig and the module
__all__/re-export lines to verify and update the docstring).
- Around line 12-15: The top-level module is re-exporting vendor-specific
symbols Mem0EmbedderConfig and Mem0MemoryBackend; change the top-level API to
expose generic aliases (e.g., ExampleEmbedderConfig or EmbedderConfig, and
ExampleMemoryBackend or MemoryBackend) instead of vendor names, and leave the
original Mem0EmbedderConfig and Mem0MemoryBackend defined and exported only from
ai_company.memory.backends.mem0; update the __all__/exports in __init__.py to
use the generic alias names and adjust any import sites/docs/tests to import the
generic names (or import vendor-specific types directly from
ai_company.memory.backends.mem0 when needed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6200036-48c7-4c01-a277-4131798e1c61

📥 Commits

Reviewing files that changed from the base of the PR and between e8c7def and 89e670d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • pyproject.toml
  • src/ai_company/memory/__init__.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: Test (Python 3.14)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Do NOT use from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use except A, B: (no parentheses) for exception handling syntax per PEP 758 — ruff enforces this on Python 3.14

Files:

  • src/ai_company/memory/__init__.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Type hints: all public functions must have type hints, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones—never mutate existing objects. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Config vs runtime state: use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use @computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr (from core.types) for all identifier/name fields—including optional and tuple variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (enforced by ruff)
Functions: < 50 lines, files < 800 lines
Handle errors explicitly, never silently swallow them
Validate at system boundaries (user input, external APIs, config files)
Every module with business logic MUST have: from ai_company.observability import get_logger then logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code
Always use logger as the variable name (not _logger, not log)
Use event name constants from ai_company.observability.events domain-spe...

Files:

  • src/ai_company/memory/__init__.py
pyproject.toml

📄 CodeRabbit inference engine (CLAUDE.md)

pyproject.toml: Use asyncio_mode = "auto" in pytest configuration—no manual @pytest.mark.asyncio needed on individual tests
Test timeout: 30 seconds per test
Dependencies: all versions pinned with == in pyproject.toml. Groups: test and dev (includes test). Install via uv sync.

Files:

  • pyproject.toml
🧠 Learnings (1)
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to scripts/**/*.py : Scripts: `scripts/` directory with relaxed ruff rules allowing `print` and deferred imports

Applied to files:

  • pyproject.toml
🧬 Code graph analysis (1)
src/ai_company/memory/__init__.py (2)
src/ai_company/memory/backends/mem0/config.py (1)
  • Mem0EmbedderConfig (23-49)
src/ai_company/memory/backends/mem0/adapter.py (1)
  • Mem0MemoryBackend (116-916)
🔇 Additional comments (4)
pyproject.toml (3)

138-138: LGTM!

Adding "TC" (flake8-type-checking) to test ignores is appropriate—test files commonly import types at runtime for fixture annotations and parametrized tests, making the type-checking-only import rule overly strict in this context.


189-192: LGTM!

The mypy override for mem0.* follows the established pattern for third-party libraries without type stubs (litellm, jsonschema, aiodocker, etc.). This is the standard approach to suppress import errors for untyped dependencies.


24-24: mem0ai==1.0.5 is valid and properly pinned.

Version 1.0.5 exists on PyPI and is the latest stable release in the 1.0.x series. The dependency is correctly pinned with == per project guidelines. Security advisories could not be verified via automated channels, but the version specification itself is sound.

src/ai_company/memory/__init__.py (1)

12-15: Confirm mem0ai is mandatory before keeping unconditional import.

Line 12 imports Mem0 symbols at module import time. If mem0ai is still optional in packaging, importing ai_company.memory will fail at runtime.

#!/bin/bash
set -euo pipefail

echo "== Dependency manifests =="
fd -HI 'pyproject\.toml|setup\.cfg|setup\.py|requirements.*\.txt|uv\.lock|poetry\.lock'

echo
echo "== mem0 declarations across manifests =="
rg -n -C2 --glob 'pyproject.toml' --glob 'setup.cfg' --glob 'setup.py' \
  --glob 'requirements*.txt' --glob 'uv.lock' --glob 'poetry.lock' \
  'mem0ai|mem0[^a-zA-Z0-9_]'

echo
echo "== Import-guard patterns in memory init =="
rg -n -C2 'from ai_company.memory.backends.mem0|contextlib\.suppress\(ImportError\)|except ImportError' src/ai_company/memory/__init__.py

Comment on lines +6 to +9
concrete backends (``Mem0MemoryBackend``), domain models, config
models, factory, retrieval pipeline, consolidation, org memory, and
error hierarchy so consumers can import from ``ai_company.memory``
directly.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring export list is incomplete.

Line 6 mentions only Mem0MemoryBackend, but Line 13 also re-exports Mem0EmbedderConfig; the public-surface summary is now out of sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/__init__.py` around lines 6 - 9, The module docstring
in __init__.py lists only Mem0MemoryBackend but the module also re-exports
Mem0EmbedderConfig (and possibly other public symbols); update the top-level
summary to include Mem0EmbedderConfig (and ensure any other names exported in
the file are reflected) so the public-surface description matches the actual
re-exports (search for Mem0MemoryBackend, Mem0EmbedderConfig and the module
__all__/re-export lines to verify and update the docstring).

Comment on lines +12 to +15
from ai_company.memory.backends.mem0 import (
Mem0EmbedderConfig,
Mem0MemoryBackend,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Vendor-specific names in top-level API violate project naming policy.

Re-exporting Mem0EmbedderConfig and Mem0MemoryBackend from ai_company.memory introduces vendor-specific naming into project-owned API on Line 12 and Line 82. Consider exposing generic aliases at the top-level and keeping vendor-specific types under backend-specific modules.

As per coding guidelines, "Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, large/medium/small as aliases."

Also applies to: 82-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/__init__.py` around lines 12 - 15, The top-level module
is re-exporting vendor-specific symbols Mem0EmbedderConfig and
Mem0MemoryBackend; change the top-level API to expose generic aliases (e.g.,
ExampleEmbedderConfig or EmbedderConfig, and ExampleMemoryBackend or
MemoryBackend) instead of vendor names, and leave the original
Mem0EmbedderConfig and Mem0MemoryBackend defined and exported only from
ai_company.memory.backends.mem0; update the __all__/exports in __init__.py to
use the generic alias names and adjust any import sites/docs/tests to import the
generic names (or import vendor-specific types directly from
ai_company.memory.backends.mem0 when needed).

Comment on lines +862 to +889

publisher = extract_publisher(raw)
if publisher is None:
logger.warning(
MEMORY_SHARED_RETRACT_FAILED,
agent_id=agent_id,
memory_id=memory_id,
reason="not a shared memory entry (no publisher)",
)
msg = (
f"Memory {memory_id} is not a shared memory entry "
f"(no publisher metadata)"
)
raise MemoryStoreError(msg) # noqa: TRY301

if publisher != str(agent_id):
logger.warning(
MEMORY_SHARED_RETRACT_FAILED,
agent_id=agent_id,
memory_id=memory_id,
reason="ownership mismatch",
publisher=publisher,
)
msg = (
f"Agent {agent_id} cannot retract memory "
f"{memory_id} published by {publisher}"
)
raise MemoryStoreError(msg) # noqa: TRY301
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retract doesn't verify the memory is in the shared namespace

retract relies solely on the presence of _synthorg_publisher in the metadata to identify shared entries, but never checks raw.get("user_id") == _SHARED_NAMESPACE. This means any memory — including private agent memories — that happens to carry the publisher key can be deleted through retract, bypassing the ownership check enforced by delete.

In practice, Mem0's add response strips unrecognized metadata keys in some SDK versions and the publisher key is only written by publish, so the risk is low today. However, as the system evolves (e.g. a future code path serialises full metadata into a raw Mem0 record, or a Mem0 SDK update preserves metadata differently), a private entry could surface with this key and be silently deleted via retract.

Adding a namespace check before the publisher check closes this gap and makes the invariant explicit:

# Verify the entry is actually in the shared namespace
if raw.get("user_id") != _SHARED_NAMESPACE:
    msg = f"Memory {memory_id} is not in the shared namespace"
    logger.warning(
        MEMORY_SHARED_RETRACT_FAILED,
        agent_id=agent_id,
        memory_id=memory_id,
        reason="not a shared-namespace entry",
    )
    raise MemoryStoreError(msg)

publisher = extract_publisher(raw)
...
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 862-889

Comment:
**`retract` doesn't verify the memory is in the shared namespace**

`retract` relies solely on the presence of `_synthorg_publisher` in the metadata to identify shared entries, but never checks `raw.get("user_id") == _SHARED_NAMESPACE`. This means any memory — including private agent memories — that happens to carry the publisher key can be deleted through `retract`, bypassing the ownership check enforced by `delete`.

In practice, Mem0's `add` response strips unrecognized metadata keys in some SDK versions and the publisher key is only written by `publish`, so the risk is low today. However, as the system evolves (e.g. a future code path serialises full metadata into a raw Mem0 record, or a Mem0 SDK update preserves metadata differently), a private entry could surface with this key and be silently deleted via `retract`.

Adding a namespace check before the publisher check closes this gap and makes the invariant explicit:

```python
# Verify the entry is actually in the shared namespace
if raw.get("user_id") != _SHARED_NAMESPACE:
    msg = f"Memory {memory_id} is not in the shared namespace"
    logger.warning(
        MEMORY_SHARED_RETRACT_FAILED,
        agent_id=agent_id,
        memory_id=memory_id,
        reason="not a shared-namespace entry",
    )
    raise MemoryStoreError(msg)

publisher = extract_publisher(raw)
...
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +707 to +722
"""
self._require_connected()
try:
metadata = {
**build_mem0_metadata(request),
_PUBLISHER_KEY: str(agent_id),
}
kwargs = {
"messages": [
{"role": "user", "content": request.content},
],
"user_id": _SHARED_NAMESPACE,
"metadata": metadata,
"infer": False,
}
result = await asyncio.to_thread(self._client.add, **kwargs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish skips _validate_agent_id guard

Every other method that accepts agent_id calls _validate_agent_id(agent_id) immediately after _require_connected(), but publish omits this call:

  • store_require_connected + _validate_agent_id
  • retrieve_require_connected + _validate_agent_id
  • get_require_connected + _validate_agent_id
  • delete_require_connected + _validate_agent_id
  • count_require_connected + _validate_agent_id
  • publish_require_connected only ❌

Without this guard, passing _SHARED_NAMESPACE as agent_id to publish succeeds. The entry is stored with _PUBLISHER_KEY set to the reserved namespace string. Since retract also skips _validate_agent_id, any caller using that reserved string as agent_id can then retract those entries — polluting the publisher metadata that the rest of the access-control logic relies on.

Suggested change
"""
self._require_connected()
try:
metadata = {
**build_mem0_metadata(request),
_PUBLISHER_KEY: str(agent_id),
}
kwargs = {
"messages": [
{"role": "user", "content": request.content},
],
"user_id": _SHARED_NAMESPACE,
"metadata": metadata,
"infer": False,
}
result = await asyncio.to_thread(self._client.add, **kwargs)
self._require_connected()
self._validate_agent_id(agent_id)
try:
metadata = {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 707-722

Comment:
**`publish` skips `_validate_agent_id` guard**

Every other method that accepts `agent_id` calls `_validate_agent_id(agent_id)` immediately after `_require_connected()`, but `publish` omits this call:

- `store``_require_connected` + `_validate_agent_id`- `retrieve``_require_connected` + `_validate_agent_id`- `get``_require_connected` + `_validate_agent_id`- `delete``_require_connected` + `_validate_agent_id`- `count``_require_connected` + `_validate_agent_id`- `publish``_require_connected` only ❌

Without this guard, passing `_SHARED_NAMESPACE` as `agent_id` to `publish` succeeds. The entry is stored with `_PUBLISHER_KEY` set to the reserved namespace string. Since `retract` also skips `_validate_agent_id`, any caller using that reserved string as `agent_id` can then retract those entries — polluting the publisher metadata that the rest of the access-control logic relies on.

```suggestion
        self._require_connected()
        self._validate_agent_id(agent_id)
        try:
            metadata = {
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a concrete Mem0-backed implementation of the ai_company.memory backend protocols, wires it into the backend factory, and updates tests/docs/dependency metadata to reflect the new backend.

Changes:

  • Implement Mem0 backend (Mem0MemoryBackend) plus Mem0-specific config + mapping utilities.
  • Update create_memory_backend() to construct the Mem0 backend (requires an explicit Mem0EmbedderConfig).
  • Add extensive unit/integration tests for the Mem0 backend and update dependencies/lockfile + docs/roadmap.

Reviewed changes

Copilot reviewed 79 out of 82 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
uv.lock Locks new runtime deps (mem0ai + transitive deps like qdrant-client/grpcio/numpy/etc.).
pyproject.toml Adds mem0ai==1.0.5, adjusts ruff per-file ignores, adds mypy override for mem0.*.
src/ai_company/observability/events/memory.py Adds new structured log event constants for config/agent-id rejection.
src/ai_company/memory/factory.py Implements Mem0 backend creation path and embedder validation.
src/ai_company/memory/backends/mem0/adapter.py New Mem0 backend adapter implementing lifecycle, CRUD, and shared knowledge operations.
src/ai_company/memory/backends/mem0/config.py New Mem0 backend config models + builder for Mem0 SDK config dict.
src/ai_company/memory/backends/mem0/mappers.py New stateless mapping helpers between domain models and Mem0 dict payloads.
src/ai_company/memory/backends/mem0/init.py Exposes Mem0 backend/config types from the mem0 backend package.
src/ai_company/memory/backends/init.py Introduces concrete backend package export(s).
src/ai_company/memory/init.py Re-exports Mem0 backend types from ai_company.memory.
README.md Updates project status to reflect Mem0 adapter as implemented.
docs/roadmap/index.md Updates roadmap to remove “Mem0 adapter” from remaining work and mention it as implemented.
docs/design/memory.md Updates design doc text and adds example of passing embedder config via factory.
CLAUDE.md Updates repo structure description and adds a note about mem0ai dependency.
.github/workflows/dependency-review.yml Allows additional licenses / missing-license metadata entries for newly introduced deps.
tests/unit/memory/test_init.py Extends public export/importability expectations for new Mem0 types.
tests/unit/memory/test_factory.py Updates factory tests: Mem0 now creates a backend and validates embedder requirements.
tests/unit/memory/backends/mem0/conftest.py New shared fixtures/helpers for Mem0 backend unit tests.
tests/unit/memory/backends/mem0/test_adapter.py New unit tests for adapter properties/capabilities/lifecycle/guards.
tests/unit/memory/backends/mem0/test_adapter_crud.py New unit tests for store/retrieve/get/delete/count behavior.
tests/unit/memory/backends/mem0/test_adapter_shared.py New unit tests for publish/search_shared/retract behavior.
tests/unit/memory/backends/mem0/test_config.py New unit tests for Mem0 config models and config dict builder.
tests/unit/memory/backends/mem0/test_mappers.py New unit tests for mapping utilities and edge cases.
tests/integration/memory/test_mem0_backend.py New integration-style tests for retrieval pipeline using mocked Mem0 client.
tests/unit/tools/sandbox/test_protocol.py Removes TC-related noqa comments from imports.
tests/unit/tools/sandbox/conftest.py Removes TC-related noqa comments from imports.
tests/unit/tools/git/test_git_sandbox_integration.py Removes TC-related noqa comments from imports.
tests/unit/tools/git/conftest.py Removes TC-related noqa comments from imports.
tests/unit/providers/test_protocol.py Removes TC-related noqa comments from imports.
tests/unit/providers/conftest.py Removes TC-related noqa comments from imports.
tests/unit/hr/test_registry.py Removes TC-related noqa comments from imports.
tests/unit/hr/test_onboarding_service.py Removes TC-related noqa comments from imports.
tests/unit/hr/test_offboarding_service.py Removes TC-related noqa comments from imports.
tests/unit/hr/test_hiring_service.py Removes TC-related noqa comments from imports.
tests/unit/hr/test_full_snapshot_strategy.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_task_engine_mutations.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_routing_models.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_react_loop.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_plan_execute_loop.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_metrics.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_loop_protocol.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_context.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_agent_engine.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_agent_engine_lifecycle.py Removes TC-related noqa comments from imports.
tests/unit/engine/test_agent_engine_errors.py Removes TC-related noqa comments from imports.
tests/unit/engine/task_engine_helpers.py Removes TC-related noqa comments from imports.
tests/unit/communication/meeting/test_structured_phases.py Removes TC-related noqa comments from imports.
tests/unit/communication/meeting/test_round_robin.py Removes TC-related noqa comments from imports.
tests/unit/communication/meeting/test_protocol.py Removes TC-related noqa comments from imports.
tests/unit/communication/meeting/test_position_papers.py Removes TC-related noqa comments from imports.
tests/unit/communication/meeting/test_orchestrator.py Removes TC-related noqa comments from imports.
tests/unit/communication/meeting/conftest.py Removes TC-related noqa comments from imports.
tests/unit/communication/conflict_resolution/test_service.py Removes TC-related noqa comments from imports.
tests/unit/communication/conflict_resolution/test_hybrid_strategy.py Removes TC-related noqa comments from imports.
tests/unit/communication/conflict_resolution/test_helpers.py Removes TC-related noqa comments from imports.
tests/unit/communication/conflict_resolution/test_debate_strategy.py Removes TC-related noqa comments from imports.
tests/unit/communication/conflict_resolution/test_authority_strategy.py Removes TC-related noqa comments from imports.
tests/unit/budget/test_category_analytics.py Removes TC-related noqa comments from imports.
tests/unit/api/test_middleware.py Removes TC-related noqa comments from imports.
tests/unit/api/test_health.py Removes TC-related noqa comments from imports.
tests/unit/api/test_guards.py Removes TC-related noqa comments from imports.
tests/unit/api/test_app.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_tasks.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_providers.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_projects.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_messages.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_meetings.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_departments.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_company.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_budget.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_autonomy.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_artifacts.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_approvals.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_analytics.py Removes TC-related noqa comments from imports.
tests/unit/api/controllers/test_agents.py Removes TC-related noqa comments from imports in an inner import.
tests/unit/api/conftest.py Removes TC-related noqa comments from imports.
tests/unit/api/auth/test_controller.py Removes TC-related noqa comments from imports / inner import.
tests/integration/tools/test_sandbox_integration.py Removes TC-related noqa comments from imports.
tests/integration/tools/conftest.py Removes TC-related noqa comments from imports.
tests/integration/communication/test_meeting_integration.py Removes TC-related noqa comments from imports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +294 to +311
def _validate_agent_id(self, agent_id: NotBlankStr) -> None:
"""Reject the reserved shared namespace as an agent ID.

Raises:
MemoryStoreError: If ``agent_id`` collides with the
reserved ``_SHARED_NAMESPACE``.
"""
if str(agent_id) == _SHARED_NAMESPACE:
logger.warning(
MEMORY_BACKEND_AGENT_ID_REJECTED,
agent_id=agent_id,
reason="reserved shared namespace",
)
msg = (
f"agent_id must not be the reserved shared namespace: "
f"{_SHARED_NAMESPACE!r}"
)
raise MemoryStoreError(msg)
Comment on lines +80 to +93
try:
mem0_config = build_config_from_company_config(
config,
embedder=embedder,
)
except ValueError as exc:
msg = f"Invalid Mem0 configuration: {exc}"
logger.warning(
MEMORY_BACKEND_CONFIG_INVALID,
backend="mem0",
reason="config_build_failed",
error=msg,
)
raise MemoryConfigError(msg) from exc
Comment thread CLAUDE.md Outdated

- **Pinned**: all versions use `==` in `pyproject.toml`
- **Groups**: `test` (pytest + plugins), `dev` (includes test + ruff, mypy, pre-commit, commitizen)
- **Optional**: `mem0ai` (Mem0 memory backend — only needed when `backend: "mem0"` is configured)
…d Gemini

- CRITICAL: remove Memory.reset() from disconnect() — it wiped all data
- MAJOR: wrap Mem0MemoryBackend init errors in factory as MemoryConfigError
- MEDIUM: coerce string scores in normalize_relevance_score
- MEDIUM: validate dict structure in validate_add_result
- MEDIUM: sanitize source metadata via _coerce_source helper
- MEDIUM: update CLAUDE.md — mem0ai is a required dependency, not optional
- MEDIUM: extract _coerce_confidence, _coerce_source, _normalize_tags helpers
- MINOR: coerce non-string values in extract_publisher
- MINOR: add user_id param to mem0_get_result fixture
- MINOR: document adapter.py 800-line exemption
- Tests: add coverage for string scores, non-dict results, source
  sanitization, publisher coercion, disconnect-no-reset
Comment thread src/ai_company/memory/backends/mem0/mappers.py
Comment thread src/ai_company/memory/backends/mem0/adapter.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
tests/unit/memory/backends/mem0/test_adapter.py (1)

267-331: 🧹 Nitpick | 🔵 Trivial

Consider using @pytest.mark.parametrize to reduce duplication.

The TestConnectionGuard class has 8 similar tests that each create a backend and verify MemoryConnectionError is raised. These could be collapsed with parametrization.

♻️ Optional: Parametrize connection guard tests
`@pytest.mark.unit`
class TestConnectionGuard:
    `@pytest.mark.parametrize`(
        "method,args",
        [
            ("store", ("test-agent-001", make_store_request())),
            ("retrieve", ("test-agent-001", MemoryQuery(text="test"))),
            ("get", ("test-agent-001", "mem-001")),
            ("delete", ("test-agent-001", "mem-001")),
            ("count", ("test-agent-001",)),
            ("publish", ("test-agent-001", make_store_request())),
            ("search_shared", (MemoryQuery(text="test"),)),
            ("retract", ("test-agent-001", "mem-001")),
        ],
    )
    async def test_raises_when_disconnected(
        self,
        mem0_config: Mem0BackendConfig,
        method: str,
        args: tuple,
    ) -> None:
        b = Mem0MemoryBackend(mem0_config=mem0_config)
        with pytest.raises(MemoryConnectionError, match="Not connected"):
            await getattr(b, method)(*args)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/memory/backends/mem0/test_adapter.py` around lines 267 - 331,
Collapse the eight nearly identical tests in TestConnectionGuard into a single
parametrized test using pytest.mark.parametrize: instantiate Mem0MemoryBackend
once per test and call the backend method by name (use getattr on the
Mem0MemoryBackend instance) with the corresponding args, asserting
MemoryConnectionError with match "Not connected"; replace individual methods
test_store_raises_when_disconnected, test_retrieve_raises_when_disconnected,
test_get_raises_when_disconnected, test_delete_raises_when_disconnected,
test_count_raises_when_disconnected, test_publish_raises_when_disconnected,
test_search_shared_raises_when_disconnected, and
test_retract_raises_when_disconnected with a single async
test_raises_when_disconnected that parametrize tuples like ("store",
("test-agent-001", make_store_request())) and so on.
src/ai_company/memory/factory.py (1)

26-113: 🧹 Nitpick | 🔵 Trivial

Consider extracting the Mem0 path into a helper function.

create_memory_backend is approximately 70 lines, exceeding the 50-line guideline. The Mem0 branch (lines 48-113) could be extracted into a _create_mem0_backend(config, embedder) helper to improve readability and meet the size limit.

The error handling is correct — both build_config_from_company_config and Mem0MemoryBackend failures are properly wrapped as MemoryConfigError with logging, addressing the past review comment.

♻️ Optional: Extract Mem0 path to helper
+def _create_mem0_backend(
+    config: CompanyMemoryConfig,
+    embedder: Mem0EmbedderConfig,
+) -> MemoryBackend:
+    """Create a Mem0 backend instance."""
+    from ai_company.memory.backends.mem0 import Mem0MemoryBackend
+    from ai_company.memory.backends.mem0.config import build_config_from_company_config
+
+    try:
+        mem0_config = build_config_from_company_config(config, embedder=embedder)
+    except ValueError as exc:
+        msg = f"Invalid Mem0 configuration: {exc}"
+        logger.warning(
+            MEMORY_BACKEND_CONFIG_INVALID,
+            backend="mem0",
+            reason="config_build_failed",
+            error=msg,
+        )
+        raise MemoryConfigError(msg) from exc
+    try:
+        backend = Mem0MemoryBackend(
+            mem0_config=mem0_config,
+            max_memories_per_agent=config.options.max_memories_per_agent,
+        )
+    except Exception as exc:
+        msg = f"Failed to create Mem0 backend: {exc}"
+        logger.warning(
+            MEMORY_BACKEND_CONFIG_INVALID,
+            backend="mem0",
+            reason="backend_init_failed",
+            error=msg,
+        )
+        raise MemoryConfigError(msg) from exc
+    logger.info(
+        MEMORY_BACKEND_CREATED,
+        backend="mem0",
+        data_dir=mem0_config.data_dir,
+    )
+    return backend
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/memory/factory.py` around lines 26 - 113, Refactor the long
mem0 branch in create_memory_backend into a small helper named
_create_mem0_backend(config, embedder) that performs the current logic: import
Mem0MemoryBackend, Mem0EmbedderConfig, build_config_from_company_config;
validate embedder presence/type and raise MemoryConfigError with the same
logger.warning calls using MEMORY_BACKEND_CONFIG_INVALID; call
build_config_from_company_config and wrap ValueError into MemoryConfigError with
identical logging; instantiate Mem0MemoryBackend and wrap exceptions into
MemoryConfigError with the same logging; log MEMORY_BACKEND_CREATED and return
the backend. Update create_memory_backend to call and return
_create_mem0_backend when config.backend == "mem0". Ensure all referenced
symbols (Mem0MemoryBackend, Mem0EmbedderConfig,
build_config_from_company_config, MemoryConfigError,
MEMORY_BACKEND_CONFIG_INVALID, MEMORY_BACKEND_CREATED) are used exactly as
before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/ai_company/memory/factory.py`:
- Around line 26-113: Refactor the long mem0 branch in create_memory_backend
into a small helper named _create_mem0_backend(config, embedder) that performs
the current logic: import Mem0MemoryBackend, Mem0EmbedderConfig,
build_config_from_company_config; validate embedder presence/type and raise
MemoryConfigError with the same logger.warning calls using
MEMORY_BACKEND_CONFIG_INVALID; call build_config_from_company_config and wrap
ValueError into MemoryConfigError with identical logging; instantiate
Mem0MemoryBackend and wrap exceptions into MemoryConfigError with the same
logging; log MEMORY_BACKEND_CREATED and return the backend. Update
create_memory_backend to call and return _create_mem0_backend when
config.backend == "mem0". Ensure all referenced symbols (Mem0MemoryBackend,
Mem0EmbedderConfig, build_config_from_company_config, MemoryConfigError,
MEMORY_BACKEND_CONFIG_INVALID, MEMORY_BACKEND_CREATED) are used exactly as
before.

In `@tests/unit/memory/backends/mem0/test_adapter.py`:
- Around line 267-331: Collapse the eight nearly identical tests in
TestConnectionGuard into a single parametrized test using
pytest.mark.parametrize: instantiate Mem0MemoryBackend once per test and call
the backend method by name (use getattr on the Mem0MemoryBackend instance) with
the corresponding args, asserting MemoryConnectionError with match "Not
connected"; replace individual methods test_store_raises_when_disconnected,
test_retrieve_raises_when_disconnected, test_get_raises_when_disconnected,
test_delete_raises_when_disconnected, test_count_raises_when_disconnected,
test_publish_raises_when_disconnected,
test_search_shared_raises_when_disconnected, and
test_retract_raises_when_disconnected with a single async
test_raises_when_disconnected that parametrize tuples like ("store",
("test-agent-001", make_store_request())) and so on.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3ec61326-e4eb-4c6a-bb51-24b4cf8bac53

📥 Commits

Reviewing files that changed from the base of the PR and between 89e670d and 6227b58.

📒 Files selected for processing (8)
  • CLAUDE.md
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/mappers.py
  • src/ai_company/memory/factory.py
  • tests/unit/memory/backends/mem0/conftest.py
  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/unit/memory/backends/mem0/test_adapter_crud.py
  • tests/unit/memory/backends/mem0/test_mappers.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Test (Python 3.14)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Do NOT use from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use except A, B: (no parentheses) for exception handling syntax per PEP 758 — ruff enforces this on Python 3.14

Files:

  • tests/unit/memory/backends/mem0/test_adapter.py
  • src/ai_company/memory/factory.py
  • tests/unit/memory/backends/mem0/test_mappers.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • tests/unit/memory/backends/mem0/conftest.py
  • src/ai_company/memory/backends/mem0/mappers.py
  • tests/unit/memory/backends/mem0/test_adapter_crud.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow for test markers
Prefer @pytest.mark.parametrize for testing similar cases

Files:

  • tests/unit/memory/backends/mem0/test_adapter.py
  • tests/unit/memory/backends/mem0/test_mappers.py
  • tests/unit/memory/backends/mem0/conftest.py
  • tests/unit/memory/backends/mem0/test_adapter_crud.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Type hints: all public functions must have type hints, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones—never mutate existing objects. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Config vs runtime state: use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use @computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr (from core.types) for all identifier/name fields—including optional and tuple variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (enforced by ruff)
Functions: < 50 lines, files < 800 lines
Handle errors explicitly, never silently swallow them
Validate at system boundaries (user input, external APIs, config files)
Every module with business logic MUST have: from ai_company.observability import get_logger then logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code
Always use logger as the variable name (not _logger, not log)
Use event name constants from ai_company.observability.events domain-spe...

Files:

  • src/ai_company/memory/factory.py
  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/mappers.py
🧠 Learnings (5)
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) for exception handling syntax per PEP 758 — ruff enforces this on Python 3.14

Applied to files:

  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/mappers.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to src/**/*.py : Functions: < 50 lines, files < 800 lines

Applied to files:

  • src/ai_company/memory/backends/mem0/adapter.py
  • src/ai_company/memory/backends/mem0/mappers.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to src/**/*.py : Line length: 88 characters (enforced by ruff)

Applied to files:

  • src/ai_company/memory/backends/mem0/adapter.py
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to docs/**/*.md : Docs source: `docs/` (Markdown, built with Zensical). Design spec: `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations).

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.690Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.690Z
Learning: Applies to pyproject.toml : Dependencies: all versions pinned with `==` in `pyproject.toml`. Groups: `test` and `dev` (includes test). Install via `uv sync`.

Applied to files:

  • CLAUDE.md
🧬 Code graph analysis (4)
src/ai_company/memory/backends/mem0/adapter.py (3)
src/ai_company/memory/backends/mem0/config.py (2)
  • Mem0BackendConfig (52-91)
  • build_mem0_config_dict (94-121)
src/ai_company/memory/backends/mem0/mappers.py (5)
  • extract_category (432-454)
  • extract_publisher (457-470)
  • mem0_result_to_entry (229-294)
  • query_to_mem0_search_args (297-326)
  • validate_add_result (389-429)
src/ai_company/memory/errors.py (4)
  • MemoryConnectionError (17-18)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
  • MemoryError (13-14)
tests/unit/memory/backends/mem0/conftest.py (3)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/backends/mem0/config.py (2)
  • Mem0BackendConfig (52-91)
  • Mem0EmbedderConfig (23-49)
src/ai_company/memory/models.py (1)
  • MemoryStoreRequest (55-79)
src/ai_company/memory/backends/mem0/mappers.py (3)
src/ai_company/core/enums.py (1)
  • MemoryCategory (101-108)
src/ai_company/memory/errors.py (2)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
src/ai_company/memory/models.py (4)
  • MemoryEntry (82-150)
  • MemoryMetadata (20-52)
  • MemoryQuery (153-230)
  • MemoryStoreRequest (55-79)
tests/unit/memory/backends/mem0/test_adapter_crud.py (3)
src/ai_company/memory/backends/mem0/adapter.py (2)
  • store (312-369)
  • retrieve (371-432)
src/ai_company/memory/errors.py (3)
  • MemoryRetrievalError (25-26)
  • MemoryStoreError (21-22)
  • MemoryError (13-14)
src/ai_company/memory/models.py (1)
  • MemoryQuery (153-230)
🔇 Additional comments (54)
src/ai_company/memory/backends/mem0/mappers.py (13)

1-29: LGTM — module setup follows conventions.

Proper logger initialization, event constant imports, and clear module docstring. The _PREFIX constant for metadata namespacing is well-documented.


38-57: LGTM — metadata serialization is correct.

build_mem0_metadata properly handles optional fields and uses the prefixed keys consistently.


60-86: LGTM — datetime parsing with proper fallback.

Handles naive datetimes by assuming UTC, logs malformed input appropriately, and uses correct PEP 758 exception syntax.


89-112: LGTM — score normalization handles edge cases.

Correctly coerces string scores, clamps to [0.0, 1.0], and returns None for non-numeric input with appropriate logging.


115-131: LGTM — confidence coercion with clamping.

Properly extracts, parses, and clamps confidence values to valid range [0.0, 1.0].


134-151: LGTM — source sanitization handles malformed input.

Coerces to string, strips whitespace, and returns None for blank values with debug logging.


154-172: LGTM — tag normalization is robust.

Handles string, list, tuple, and unexpected types gracefully. Explicit str(t) conversion before NotBlankStr addresses the past review comment about non-string elements.


175-226: LGTM — metadata parsing delegates to helpers properly.

Function stays focused on orchestration, delegating validation and coercion to the extracted helper functions (_coerce_confidence, _coerce_source, _normalize_tags).


229-294: LGTM — result-to-entry conversion with thorough validation.

Validates ID and content presence, handles missing timestamps, and delegates metadata parsing correctly.


297-345: LGTM — query argument builders are correct.

query_to_mem0_search_args properly validates that query.text is required for search operations.


348-383: LGTM — post-filter logic is correct.

The min_relevance filter correctly skips entries with relevance_score=None, and the until filter is exclusive as documented.


389-429: LGTM — add-result validation is thorough.

Validates result is a dict, results is a non-empty list, first item is a dict, and ID is non-blank. Addresses past review comment about validating object shapes.


432-470: LGTM — category and publisher extraction are defensive.

Both functions handle missing/non-dict metadata gracefully, and extract_publisher properly coerces and strips values. Addresses past review comment about normalizing publisher strings.

src/ai_company/memory/backends/mem0/adapter.py (14)

1-18: LGTM — module docstring properly documents the size exemption.

The docstring explains why this file exceeds the 800-line guideline (cohesive implementation of three protocols) and documents the re-raise behavior for system-level exceptions.


88-119: LGTM — defensive result validation helper.

_validate_mem0_result addresses the past review concern about handling None or non-dict results from Mem0 SDK calls.


146-183: LGTM — connect handles import and initialization errors properly.

Deferred import of mem0.Memory, proper error wrapping to MemoryConnectionError, and re-raises MemoryError/RecursionError without wrapping.


196-235: LGTM — health check probes the backend correctly.

Uses a lightweight get_all with limit=1 to verify connectivity, returns False on any exception (except system-level ones which are re-raised).


281-308: LGTM — guards are well-implemented.

_require_connected and _validate_agent_id provide clear error messages and appropriate logging.


312-369: LGTM — store operation is correct.

Validates connection and agent ID, builds metadata, calls Mem0 add() via asyncio.to_thread(), and validates the result.


371-432: LGTM — retrieve handles search vs get_all correctly.

Dispatches based on query.text presence, validates results, converts to domain entries, and applies post-filters.


434-507: LGTM — get verifies ownership correctly.

Returns None for ownership mismatch rather than raising, which is the expected behavior for a "not found from this agent's perspective" scenario.


509-595: LGTM — delete has proper ownership and namespace checks.

Blocks deletion of shared-namespace entries (must use retract()), verifies ownership before delete, and handles the not-found case correctly.


597-680: LGTM — count with truncation warning.

Logs a warning when the count equals max_memories_per_agent to indicate potential underestimation.


684-746: LGTM — publish adds publisher metadata correctly.

Merges _PUBLISHER_KEY into metadata and uses the reserved _SHARED_NAMESPACE for isolation.


748-825: LGTM — search_shared handles publisher extraction and filtering.

Uses extract_publisher to populate agent_id for shared entries, applies post-filters, and supports exclude_agent.


827-913: LGTM — retract verifies publisher ownership.

Correctly blocks retraction if the memory has no publisher metadata (not a shared entry) or if the publisher doesn't match the requesting agent.


185-194: No action required — mem0.Memory does not provide or require explicit close() for resource cleanup.

The mem0 library's Memory client does not expose a close() method. Per the official mem0 SDK documentation and AutoGen's integration, mem0 clients do not require explicit cleanup; setting _client = None allows Python's garbage collector to properly reclaim the instance. This is by design in the embedded Qdrant + SQLite configuration, where all storage is local and the library manages any internal resource pooling.

			> Likely an incorrect or invalid review comment.
src/ai_company/memory/factory.py (1)

48-78: LGTM — embedder validation is thorough.

Validates both missing embedder and wrong type cases with clear error messages and appropriate logging.

CLAUDE.md (2)

97-97: LGTM — memory module description updated correctly.

Documentation now accurately reflects the pluggable MemoryBackend protocol and Mem0 adapter structure.


208-208: LGTM — dependency classification corrected.

mem0ai is now correctly documented as "Required" rather than "Optional", matching pyproject.toml. This addresses the past review comment about the mismatch.

tests/unit/memory/backends/mem0/test_adapter_crud.py (6)

1-24: LGTM — test module setup is correct.

Proper imports, timeout marker, and use of shared fixtures from conftest.


31-136: LGTM — store tests cover key scenarios.

Tests success path, empty results, missing/blank IDs, exception wrapping, MemoryError propagation, and shared namespace rejection.


141-254: LGTM — retrieve tests validate search and get_all paths.

Tests text-based search, get_all fallback, post-filtering, exception wrapping, and invalid entry handling.


260-318: LGTM — get tests cover existence and ownership checks.

Tests found/not-found cases, exception wrapping, MemoryError propagation, and ownership mismatch returning None.


324-408: LGTM — delete tests verify ownership and namespace restrictions.

Tests successful delete, not-found, exception wrapping, shared namespace blocking, and cross-agent deletion rejection.


414-518: LGTM — count tests cover filtering and edge cases.

Tests all/by-category counting, invalid category defaulting, empty results, and MemoryError propagation.

tests/unit/memory/backends/mem0/test_adapter.py (4)

1-19: LGTM — test module imports and setup are correct.

Proper imports for protocol interfaces and fixtures.


25-71: LGTM — properties and capabilities tests are thorough.

Tests all capability properties and verifies expected values.


77-129: LGTM — protocol conformance tests verify interface compliance.

Tests hasattr for all required methods and isinstance for all three protocols.


135-261: LGTM — lifecycle tests cover success, failure, and error propagation.

Tests connect success/failure, disconnect (including no-reset check), health check states, and MemoryError/RecursionError propagation through connect.

tests/unit/memory/backends/mem0/conftest.py (4)

1-23: LGTM — test embedder uses vendor-agnostic names.

_test_embedder() uses "test-provider" and "test-embedding-001" following the coding guidelines to avoid real vendor names.


25-49: LGTM — fixtures are well-structured.

mem0_config provides a test configuration, mock_client provides a MagicMock, and backend wires them together in a connected state.


52-108: LGTM — mock response helpers are comprehensive.

mem0_add_result, mem0_search_result, and mem0_get_result (with optional user_id) cover the main Mem0 response shapes needed for testing. The user_id parameter addresses the past review comment.


111-117: LGTM — make_store_request helper is clean.

Simple factory with sensible defaults for test scenarios.

tests/unit/memory/backends/mem0/test_mappers.py (10)

1-56: LGTM — test setup and helper are correct.

_make_entry helper provides sensible defaults for building test entries.


58-91: LGTM — build_mem0_metadata tests cover basic and full metadata.

Tests verify correct prefixing, optional field handling, and ISO format for expires_at.


94-122: LGTM — datetime parsing tests are thorough.

Tests None, empty string, aware/naive timestamps, non-UTC timezones, and malformed input.


125-150: LGTM — relevance score tests cover coercion and edge cases.

Tests None, in-range, clamping, boundaries, string coercion, and non-numeric input.


153-206: LGTM — metadata parsing tests cover all fields and defaults.

Tests None, empty, full metadata, missing/invalid category, empty tags filtering, blank source, and non-string source coercion.


209-289: LGTM — mem0_result_to_entry tests validate conversion logic.

Tests basic result, score extraction, missing created_at default, no metadata handling, and error cases (missing ID, empty content, data key fallback).


292-315: LGTM — query argument builder tests verify output shapes.

Tests for both search and getall argument construction.


318-411: LGTM — post-filter tests cover all filter types.

Tests no filters, category, tags, time range (since/until exclusive), min_relevance, and combined filters.


414-468: LGTM — validate_add_result tests cover all validation paths.

Tests valid result, empty results, missing key, non-list, missing/None/blank/whitespace ID, numeric ID coercion, and non-dict inputs.


471-544: LGTM — category and publisher extraction tests are comprehensive.

Tests valid values, missing/empty/None/non-dict metadata, invalid category defaulting, and publisher coercion/blank handling.

- Remove unused noqa A004 directive (ruff auto-fix)
- Fix import sorting in adapter.py (ruff auto-fix)
- Add connect() idempotency test (verifies no-op when already connected)
- Add _validate_agent_id error_cls tests for read ops (retrieve, get, count)
- Add expires_at filtering tests in apply_post_filters
- Add config validation tests using model_construct to bypass Pydantic
- Fix factory init test patch target to match local import
Copilot AI review requested due to automatic review settings March 13, 2026 07:43
@Aureliolo
Copy link
Copy Markdown
Owner Author

Closing to create a fresh PR with clean review history.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a concrete Mem0-backed implementation of the ai_company.memory backend (adapter + config + mapping utilities), wires it into the memory backend factory, and updates dependencies/docs/tests accordingly.

Changes:

  • Add Mem0 backend implementation (Mem0MemoryBackend) with config builder and mapping helpers.
  • Wire create_memory_backend() to construct Mem0 backend instances (with explicit embedder config).
  • Add extensive unit/integration tests and update docs/dependencies to reflect Mem0 support.

Reviewed changes

Copilot reviewed 79 out of 82 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uv.lock Locks new Mem0-related dependencies (mem0ai + transitive deps).
pyproject.toml Adds mem0ai dependency; mypy override; ruff per-file ignores for TC in tests.
src/ai_company/observability/events/memory.py Adds new structured logging event constants for config/agent-id validation.
src/ai_company/memory/factory.py Implements Mem0 backend construction in factory with validation/wrapping.
src/ai_company/memory/backends/mem0/adapter.py New Mem0 backend adapter implementing CRUD + shared knowledge operations.
src/ai_company/memory/backends/mem0/config.py New Mem0 backend config models + builder to Mem0 SDK config dict.
src/ai_company/memory/backends/mem0/mappers.py New Mem0↔domain mapping utilities and post-filtering helpers.
src/ai_company/memory/backends/mem0/init.py Exposes Mem0 backend/config symbols.
src/ai_company/memory/backends/init.py Exposes concrete backends at package level.
src/ai_company/memory/init.py Re-exports Mem0 backend/config from ai_company.memory.
README.md Updates project status to indicate Mem0 adapter is implemented.
docs/roadmap/index.md Updates roadmap to reflect Mem0 adapter completion.
docs/design/memory.md Documents Mem0 as implemented and shows embedder config usage pattern.
CLAUDE.md Updates repo structure notes and dependency notes for Mem0.
.github/workflows/dependency-review.yml Allows additional licenses / missing license metadata for new deps.
tests/unit/memory/test_init.py Adds Mem0 exports to expected ai_company.memory public surface.
tests/unit/memory/test_factory.py Updates factory tests for Mem0 backend creation + error handling.
tests/unit/memory/backends/init.py Package marker for backend unit tests.
tests/unit/memory/backends/mem0/init.py Package marker for Mem0 unit tests.
tests/unit/memory/backends/mem0/conftest.py Shared fixtures for Mem0 backend unit tests.
tests/unit/memory/backends/mem0/test_adapter.py Unit tests for adapter lifecycle/guards/capabilities.
tests/unit/memory/backends/mem0/test_adapter_crud.py Unit tests for store/retrieve/get/delete/count flows.
tests/unit/memory/backends/mem0/test_adapter_shared.py Unit tests for publish/search_shared/retract flows.
tests/unit/memory/backends/mem0/test_config.py Unit tests for Mem0 config models and builders.
tests/unit/memory/backends/mem0/test_mappers.py Unit tests for mapping/parsing/filtering utilities.
tests/integration/memory/test_mem0_backend.py “Integration” tests validating adapter works in retrieval pipeline with mocked client.
tests/unit/tools/sandbox/test_protocol.py Removes TC-related noqa on imports.
tests/unit/tools/sandbox/conftest.py Removes TC-related noqa on imports.
tests/unit/tools/git/test_git_sandbox_integration.py Removes TC-related noqa on imports.
tests/unit/tools/git/conftest.py Removes TC-related noqa on imports.
tests/unit/providers/test_protocol.py Removes TC-related noqa on imports.
tests/unit/providers/conftest.py Removes TC-related noqa on imports.
tests/unit/hr/test_registry.py Removes TC-related noqa on imports.
tests/unit/hr/test_onboarding_service.py Removes TC-related noqa on imports.
tests/unit/hr/test_offboarding_service.py Removes TC-related noqa on imports.
tests/unit/hr/test_hiring_service.py Removes TC-related noqa on imports.
tests/unit/hr/test_full_snapshot_strategy.py Removes TC-related noqa on imports.
tests/unit/engine/test_task_engine_mutations.py Removes TC-related noqa on imports.
tests/unit/engine/test_routing_models.py Removes TC-related noqa on imports.
tests/unit/engine/test_react_loop.py Removes TC-related noqa on imports.
tests/unit/engine/test_plan_execute_loop.py Removes TC-related noqa on imports.
tests/unit/engine/test_metrics.py Removes TC-related noqa on imports.
tests/unit/engine/test_loop_protocol.py Removes TC-related noqa on imports.
tests/unit/engine/test_context.py Removes TC-related noqa on imports.
tests/unit/engine/test_agent_engine.py Removes TC-related noqa on imports.
tests/unit/engine/test_agent_engine_lifecycle.py Removes TC-related noqa on imports.
tests/unit/engine/test_agent_engine_errors.py Removes TC-related noqa on imports.
tests/unit/engine/task_engine_helpers.py Removes TC-related noqa on imports.
tests/unit/communication/meeting/test_structured_phases.py Removes TC-related noqa on imports.
tests/unit/communication/meeting/test_round_robin.py Removes TC-related noqa on imports.
tests/unit/communication/meeting/test_protocol.py Removes TC-related noqa on imports.
tests/unit/communication/meeting/test_position_papers.py Removes TC-related noqa on imports.
tests/unit/communication/meeting/test_orchestrator.py Removes TC-related noqa on imports.
tests/unit/communication/meeting/conftest.py Removes TC-related noqa on imports.
tests/unit/communication/conflict_resolution/test_service.py Removes TC-related noqa on imports.
tests/unit/communication/conflict_resolution/test_hybrid_strategy.py Removes TC-related noqa on imports.
tests/unit/communication/conflict_resolution/test_helpers.py Removes TC-related noqa on imports.
tests/unit/communication/conflict_resolution/test_debate_strategy.py Removes TC-related noqa on imports.
tests/unit/communication/conflict_resolution/test_authority_strategy.py Removes TC-related noqa on imports.
tests/unit/budget/test_category_analytics.py Removes TC-related noqa on imports.
tests/unit/api/test_middleware.py Removes TC-related noqa on imports.
tests/unit/api/test_health.py Removes TC-related noqa on imports.
tests/unit/api/test_guards.py Removes TC-related noqa on imports.
tests/unit/api/test_app.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_tasks.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_providers.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_projects.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_messages.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_meetings.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_departments.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_company.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_budget.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_autonomy.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_artifacts.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_approvals.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_analytics.py Removes TC-related noqa on imports.
tests/unit/api/controllers/test_agents.py Removes TC-related noqa on imports.
tests/unit/api/conftest.py Removes TC-related noqa on imports.
tests/unit/api/auth/test_controller.py Removes TC-related noqa on imports.
tests/integration/tools/test_sandbox_integration.py Removes TC-related noqa on imports.
tests/integration/tools/conftest.py Removes TC-related noqa on imports.
tests/integration/communication/test_meeting_integration.py Removes TC-related noqa on imports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +248 to +252
@property
def backend_name(self) -> NotBlankStr:
"""Human-readable backend identifier."""
return NotBlankStr("mem0")

Comment on lines +154 to +173
def _normalize_tags(
raw_metadata: dict[str, Any],
) -> tuple[NotBlankStr, ...]:
"""Extract and normalize tags from Mem0 metadata.

Handles string, list, tuple, and unexpected types gracefully.
"""
raw_tags = raw_metadata.get(f"{_PREFIX}tags", ())
if isinstance(raw_tags, str):
raw_tags = [raw_tags]
elif not isinstance(raw_tags, (list, tuple)):
logger.debug(
MEMORY_MODEL_INVALID,
field="tags",
raw_value=type(raw_tags).__name__,
reason="unexpected tags type, ignoring",
)
raw_tags = ()
return tuple(NotBlankStr(str(t)) for t in raw_tags if t and str(t).strip())

Comment on lines +243 to +287
raw_id = raw.get("id")
if raw_id is None or not str(raw_id).strip():
msg = f"Mem0 result has missing or blank 'id': keys={list(raw.keys())}"
logger.warning(
MEMORY_MODEL_INVALID,
field="id",
raw_value=raw_id,
reason=msg,
)
raise MemoryRetrievalError(msg)
memory_id = NotBlankStr(str(raw_id))

raw_content = raw.get("memory") or raw.get("data")
if not raw_content or not str(raw_content).strip():
msg = f"Mem0 result {raw.get('id', '?')} has empty content"
logger.warning(
MEMORY_MODEL_INVALID,
field="content",
raw_value=raw_content,
reason=msg,
)
raise MemoryRetrievalError(msg)
content = NotBlankStr(str(raw_content))

created_at = parse_mem0_datetime(raw.get("created_at"))
if created_at is None:
logger.debug(
MEMORY_MODEL_INVALID,
field="created_at",
memory_id=str(raw.get("id", "?")),
reason="missing or unparseable created_at, defaulting to now()",
)
created_at = datetime.now(UTC)
updated_at = parse_mem0_datetime(raw.get("updated_at"))

raw_metadata = raw.get("metadata")
category, metadata, expires_at = parse_mem0_metadata(raw_metadata)

raw_score = raw.get("score")
relevance_score = normalize_relevance_score(raw_score)

return MemoryEntry(
id=memory_id,
agent_id=NotBlankStr(agent_id),
category=category,
Comment on lines +425 to +433
raw_id = first.get("id")
if raw_id is None or not str(raw_id).strip():
msg = (
f"Mem0 add result has missing or blank 'id' for {context}: "
f"keys={list(first.keys())}"
)
logger.warning(MEMORY_ENTRY_STORE_FAILED, context=context, error=msg)
raise MemoryStoreError(msg)
return NotBlankStr(str(raw_id))
Comment on lines +644 to +697
self._require_connected()
self._validate_agent_id(agent_id, error_cls=MemoryRetrievalError)
try:
raw_result = await asyncio.to_thread(
self._client.get_all,
user_id=str(agent_id),
limit=self._max_memories_per_agent,
)
raw_list = _validate_mem0_result(raw_result, context="count")
if category is None:
total = len(raw_list)
else:
total = sum(
1 for item in raw_list if extract_category(item) == category
)
except MemoryRetrievalError as exc:
logger.warning(
MEMORY_ENTRY_COUNT_FAILED,
agent_id=agent_id,
error=str(exc),
error_type="MemoryRetrievalError",
)
raise
except builtins.MemoryError, RecursionError:
raise
except Exception as exc:
logger.warning(
MEMORY_ENTRY_COUNT_FAILED,
agent_id=agent_id,
error=str(exc),
error_type=type(exc).__name__,
)
msg = f"Failed to count memories: {exc}"
raise MemoryRetrievalError(msg) from exc
else:
truncated = total == self._max_memories_per_agent
if truncated:
logger.warning(
MEMORY_ENTRY_COUNTED,
agent_id=agent_id,
count=total,
category=category.value if category else None,
truncated=True,
reason="count equals max_memories_per_agent, "
"actual count may be higher",
)
else:
logger.info(
MEMORY_ENTRY_COUNTED,
agent_id=agent_id,
count=total,
category=category.value if category else None,
)
return total
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

count() overcounts when entries have expires_at set

count() fetches raw entries from Mem0 and tallies them directly, but never calls apply_post_filters. This means entries whose expires_at has already passed are still included in the total.

retrieve() and search_shared() both route through apply_post_filters, which discards expired entries at line 370 of mappers.py:

if entry.expires_at is not None and entry.expires_at <= now:
    continue

So after entries expire, count() returns a stale inflated total while retrieve() on the same agent returns fewer entries — a direct semantic inconsistency. A caller who guards store() behind a count() < limit check would allow more writes than intended once entries start expiring.

The fix is to deserialise the raw list into MemoryEntry objects and pass them through apply_post_filters before counting, the same way retrieve does, or at minimum inline the expiry check:

now = datetime.now(UTC)
...
if category is None:
    total = sum(
        1 for item in raw_list
        if not (
            parse_mem0_datetime(item.get("metadata", {}).get("_synthorg_expires_at"))
            is not None
            and parse_mem0_datetime(item.get("metadata", {}).get("_synthorg_expires_at")) <= now
        )
    )

A cleaner approach: deserialise with mem0_result_to_entry and pass through apply_post_filters(entries, MemoryQuery(limit=..., categories={category} if category else set())) so all post-filters stay in one place.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 644-697

Comment:
**`count()` overcounts when entries have `expires_at` set**

`count()` fetches raw entries from Mem0 and tallies them directly, but never calls `apply_post_filters`. This means entries whose `expires_at` has already passed are still included in the total.

`retrieve()` and `search_shared()` both route through `apply_post_filters`, which discards expired entries at line 370 of `mappers.py`:

```python
if entry.expires_at is not None and entry.expires_at <= now:
    continue
```

So after entries expire, `count()` returns a stale inflated total while `retrieve()` on the same agent returns fewer entries — a direct semantic inconsistency. A caller who guards `store()` behind a `count() < limit` check would allow more writes than intended once entries start expiring.

The fix is to deserialise the raw list into `MemoryEntry` objects and pass them through `apply_post_filters` before counting, the same way `retrieve` does, or at minimum inline the expiry check:

```python
now = datetime.now(UTC)
...
if category is None:
    total = sum(
        1 for item in raw_list
        if not (
            parse_mem0_datetime(item.get("metadata", {}).get("_synthorg_expires_at"))
            is not None
            and parse_mem0_datetime(item.get("metadata", {}).get("_synthorg_expires_at")) <= now
        )
    )
```

A cleaner approach: deserialise with `mem0_result_to_entry` and pass through `apply_post_filters(entries, MemoryQuery(limit=..., categories={category} if category else set()))` so all post-filters stay in one place.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +812 to +814

if exclude_agent is not None:
filtered = tuple(e for e in filtered if e.agent_id != exclude_agent)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exclude_agent comparison uses fabricated agent_id for publisher-less entries

When a shared entry has no publisher metadata, extract_publisher(item) returns None, so agent_id is set to _SHARED_NAMESPACE in mem0_result_to_entry:

mem0_result_to_entry(
    item,
    extract_publisher(item) or _SHARED_NAMESPACE,  # ← fabricated fallback
)

Then the exclude_agent filter compares e.agent_id != exclude_agent. If two different agents each published entries whose publisher metadata was stripped (e.g. by a Mem0 SDK update that drops unrecognised metadata keys), both entries land with agent_id == _SHARED_NAMESPACE. Passing exclude_agent=_SHARED_NAMESPACE would then silently suppress all such entries, while passing any real agent_id would include them regardless of who actually wrote them.

Since retract already requires the publisher key to be present (it raises when publisher is None), publisher-less shared entries represent corrupted state. It would be safer to log a warning and skip them in search_shared rather than assigning an ownership guess to _SHARED_NAMESPACE.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/memory/backends/mem0/adapter.py
Line: 812-814

Comment:
**`exclude_agent` comparison uses fabricated `agent_id` for publisher-less entries**

When a shared entry has no publisher metadata, `extract_publisher(item)` returns `None`, so `agent_id` is set to `_SHARED_NAMESPACE` in `mem0_result_to_entry`:

```python
mem0_result_to_entry(
    item,
    extract_publisher(item) or _SHARED_NAMESPACE,  # ← fabricated fallback
)
```

Then the `exclude_agent` filter compares `e.agent_id != exclude_agent`. If two different agents each published entries whose publisher metadata was stripped (e.g. by a Mem0 SDK update that drops unrecognised metadata keys), both entries land with `agent_id == _SHARED_NAMESPACE`. Passing `exclude_agent=_SHARED_NAMESPACE` would then silently suppress all such entries, while passing any real `agent_id` would include them regardless of who actually wrote them.

Since `retract` already requires the publisher key to be present (it raises when `publisher is None`), publisher-less shared entries represent corrupted state. It would be safer to log a warning and skip them in `search_shared` rather than assigning an ownership guess to `_SHARED_NAMESPACE`.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement concrete memory backend adapter

2 participants