Skip to content

Latest commit

 

History

History
1590 lines (1331 loc) · 74.5 KB

File metadata and controls

1590 lines (1331 loc) · 74.5 KB

Backlog — Known Bugs & Hygiene Items

Short, specific things that came out of diagnostics and operational work but haven't been picked up yet. Strategic direction lives in ROADMAP.md; this doc is for items small enough to land as a single PR each.

Format per entry:

  • PriorityP0 / P1 / P2 / P3
  • Statusopen / in-progress / blocked / mitigated / closed
  • Severitybug (wrong behavior) / security / data-retention / hygiene (right behavior, needs polish)
  • Why — what the fix buys us
  • Where — pointer to code / test location

Current priority index:

  • P1 open: items 36 (dead-man backup ping), 51 (identity for DCR-created clients)
  • P1 closed: item 37 (auth-derived user_id — opt-in enforcement)
  • P1 closed: item 42 (TTL off by default — commit c89cc6f)
  • P2 open: items 38 (memory_type filter), 40 (infer=true content-loss warning), 43 (sanity-guard TTL values when enabled), 44 (sweeper soft delete with recovery window), 46 (read-time stale_warning), 50 (living-summary workflow guide)
  • P3 open: items 39 (dedup threshold parametrization), 41 (memory_reconcile alias), 45 (sweeper deletion alerts), 47 (pre-write similarity check), 48 (pool health endpoint), 49 (duplicate detection report)

Context for the 2026-05-29 additions: see SESSION_REVIEW_2026-05-29.md, which also captures non-repo follow-ups (TRC redeploy.sh commit, telepilothub outage triage, agent-side metadata conventions) that do not fit this backlog's "single PR" scope.

Items 39-41 are design discussions surfaced during the 2026-05-28 live review of the four bugs (now closed in code as items DEFECT-06..09 under "Closed"). They were missed in the initial 2026-05-29 backlog pass and added in a follow-up.

Items 42-45 close the TTL-driven data-loss class identified in SESSION_REVIEW_2026-05-29.md §2 P5. Item 42 alone closes the bulk of the risk at the API boundary; 43-45 are defense in depth for installs that intentionally use TTL.

Items 46-50 come from the data-degradation design exploration captured in DATA_DEGRADATION_DESIGN_2026-05-29.md. That document maps the design space of prevention and remediation for the slow-signal-to-noise-erosion class (distinct from the TTL terminal deletion class). Item 46 is the cheapest highest-return move from that analysis; 47-49 are deferred until proven necessary; 50 is workflow documentation rather than runtime code.


1. mcp.py masks KeyError from tool execution as "Unknown tool"

  • Status: closed

  • Severity: bug

  • Why: When a client passes the wrong argument name to a tool, the KeyError raised from inside OPERATIONS_BY_MCP_NAME[name].execute(source) is caught by the outer except KeyError that was meant to handle "unknown tool name" lookups. The client sees {"code": -32601, "message": "Unknown tool: memory_add"} and reasonably concludes the tool doesn't exist. Bad DX and hides the real schema mismatch.

    Reproduced during the first diagnostic run (v1 report) by sending memory_add with text as messages (wrong field name). Surfaced as "Unknown tool" instead of a clear validation error.

  • Where: agentmemory/mcp.pyhandle_request, the tools/call branch:

    try:
        return success(request_id, handle_call(name, arguments))
    except KeyError:
        return error(request_id, -32601, f"Unknown tool: {name}")
  • Fix: tools/call now resolves the tool before execution and returns -32601 only when the tool name is unknown. Bad arguments are validated against the published schema and returned as structured ProviderValidationError tool errors instead of JSON-RPC unknown-tool errors.


2. No rate limiting on /mcp or /oauth/token

  • Status: closed

  • Severity: bug (abuse risk)

  • Why: Anyone with the bearer token (or a leaked OAuth client secret) can loop memory_add and drain OpenRouter budget. Today only the owner has the credentials, so the blast radius is self-inflicted — but any future sharing of a token (even for read-only use) inherits this risk.

    OpenRouter account limit is currently $2 with most of it remaining (see GET /api/v1/key). Cost per memory_add is fractions of a cent, so sustained abuse would take many minutes to matter — but those minutes are unsupervised.

  • Where: agentmemory/api.py — in the bearer-auth path shared by /mcp, /add, /search, /update, /memories, /admin/*, and /oauth/token.

  • Fix: agentmemory/api.py now enforces an in-process token bucket keyed by the presented bearer token for authenticated API requests and by OAuth client_id for /oauth/token. The default is 60 requests / minute, overridable via AGENTMEMORY_RATE_LIMIT_PER_MINUTE. Overrun returns 429 with a Retry-After header. /health, /.well-known/oauth-*, and /oauth/authorize remain unthrottled. tests/test_agentmemory_api.py covers both bearer-auth and OAuth token-exchange rate limiting.

  • Scope note: /health, /.well-known/oauth-*, and /oauth/authorize stay unthrottled — they're either unauthenticated liveness or part of the discovery handshake that can burst legitimately.


3. Compose v2 network drift: local guard existed but upstream tracking was missing

  • Status: closed

  • Severity: hygiene

  • Why: docker compose up -d --build --force-recreate has been observed dropping agentmemory off the external netbird_netbird network even when --force-recreate is specified. Seen twice on this server (once on first deploy, once while verifying the post-remediation grammar fix). The same bug caused the 20-minute tca-web outage documented in /opt/telegramchatanalyzer/POSTMORTEM_NETWORK_DETACH.md.

    We ship deploy/redeploy.sh which forces the re-attach idempotently — the symptom is gone — but the root cause is still there and could bite any future service that joins netbird_netbird.

  • Where: deploy/docker-compose.yml declares the external network correctly. The behavior is upstream in docker compose v2.

  • Fix: The repository now carries the missing operational bundle around the existing self-heal:

    1. docs/COMPOSE_V2_NETWORK_DRIFT.md contains a minimal reproducer, issue draft, and repo policy for the defect.
    2. deploy/repro-compose-network-drift.sh provides a self-cleaning local reproducer for hosts that need verification before deploy changes.
    3. deploy/docker-compose.yml, deploy/redeploy.sh, and docs/DEPLOY.md now point directly at the drift context so the guard is not removed as "dead workaround". The Docker/Compose root cause remains upstream, but the repo-side follow-up is now complete and explicit instead of tribal knowledge.

4. No provider-neutral memory export/import

  • Status: closed

  • Severity: hygiene

  • Why: Memories currently live in a provider-shaped store (Qdrant for mem0, flat JSON for localjson). Migrating between providers or between embedding models requires manual work. Also means the backup produced by deploy/backup-agentmemory.sh is only restorable onto a compatible Qdrant + embedding-dimension combination — a change to either breaks the restore.

    An export tool that walks the scope inventory and emits a JSONL file of MemoryRecords, plus an import that replays them via memory_add, would buy provider-neutral portability and make backup/restore independent of the storage layer.

  • Where: new agentmemory/runtime/portability.py + MCP tools memory_export / memory_import + CLI wrappers agentmemory export-memories / agentmemory import-memories.

  • Fix: agentmemory/runtime/portability.py now exports canonical MemoryRecord JSONL by walking list_scopes plus per-scope list_memories and deduplicating by memory id. Providers that support scopeless list also get a final scopeless pass. Import streams the JSONL file and replays each row through the current memory_add path with infer=false, preserving scope fields and memory_type while tagging metadata with source: "import" plus import provenance fields. The implementation explicitly fails closed if scope inventory or per-scope list hits the current fixed export limit, so the first version cannot silently truncate data while providers still lack pagination. Coverage lives in tests/test_agentmemory_portability.py, tests/test_agentmemory_mcp_server.py, tests/test_agentmemory_operation_adapters.py, tests/test_provider_contract_v1.py, and tests/test_agentmemory_core.py.

  • Pagination follow-up: The provider contract now includes list_memories_page / search_memory_page page shapes and supports_pagination. localjson implements real cursor paging; mem0 keeps the safe single-page fallback until a backend-safe cursor strategy is available. Export uses list_memories_page for paginated providers, removing the record fixed-limit guard for those providers while keeping the guard for legacy non-paginated providers. Scope inventory now has list_scopes_page, and export walks scope pages.


5. Re-enable infer=true with observable rewrites — or commit to "never"

  • Status: closed

  • Severity: hygiene (decision, not a bug)

  • Why: DEFECT-04 was closed by flipping the default to infer=false. The mechanism for infer=true still works and returns transformed, original_text, stored_text — so the hostile silent rewrite is gone. But the feature is effectively invisible to users: nobody will discover it from the default.

    Two paths:

    1. Remove infer=true entirely. Commit to "the runtime stores what you sent, full stop". Simplest product story.
    2. Add an example in docs/USE_CASES.md or a dedicated doc showing infer=true for fact extraction (e.g. from chat transcripts) with the surfaced transformed metadata.

    Letting it sit in the middle — available but undocumented — is the worst of both worlds.

  • Where: agentmemory/runtime/operations.py (schema + _execute_add) + docs.


6. Conflict detection / memory hygiene

  • Status: closed

  • Severity: hygiene (feature gap)

  • Why: Already have TTL and dedup-on-add. The next step in "real memory runtime, not just an append-only store" is detecting contradictory memories (A says X at t1, B says ¬X at t2) and surfacing them to callers so they can resolve. This is the differentiating feature vs. "mem0 behind HTTP".

    Out of scope for a single PR — proper fact reconciliation is a mini-product. Parked here as the flag for when it becomes a priority.

  • Where: would live in a new agentmemory/runtime/reconcile.py with a dedicated MCP tool (memory_reconcile returning conflict pairs), and optional enforcement policy in memory_add (warn / reject / supersede).

  • Fix: agentmemory/runtime/reconcile.py now provides the first read-only memory hygiene pass. memory_reconcile lists memories in a caller-specified scope through the normal runtime transport, applies conservative deterministic claim heuristics, and returns likely conflict pairs without mutating storage. The first version detects opposite-polarity text claims (likes / does not like, is / is not), different values for the same simple preference/fact claim, and structured metadata claims via claim_key/claim_value or conflict_key/claim_value. CLI access is available through agentmemory reconcile-memories. Enforcement policies (warn / reject / supersede) remain intentionally out of scope until the read-only signal is proven useful.


7. Internal owner-process proxy requests do not propagate API auth

  • Status: closed

  • Severity: bug

  • Why: When AGENTMEMORY_API_TOKEN or OAuth is enabled, the public HTTP API correctly requires a bearer token for /add, /search, /memories, /admin/scopes, and other protected operations. Non-owner processes that use an owner_process_proxy provider call those same endpoints through agentmemory/runtime/http_client.py, but the internal client does not send an Authorization header. /health can still return {"ok": true} without auth, so readiness can look healthy while real memory operations fail with 401.

  • Where: agentmemory/runtime/http_client.py::_request.

  • Fix: agentmemory/runtime/http_client.py::_request now propagates AGENTMEMORY_API_TOKEN as a bearer token for internal proxy requests. tests/test_agentmemory_http_client.py covers every current proxy_* method with auth enabled so future endpoint additions do not silently regress.


8. localjson direct transport is not multi-process safe

  • Status: closed

  • Severity: bug

  • Why: LocalJsonProvider advertises direct transport and protects read/modify/write with only an in-process threading.Lock. If CLI, MCP, and API processes write the same JSON file concurrently, each process has its own lock. Updates can be lost, and readers can observe partially written files. This undermines the shared local runtime story for the default evaluation provider.

  • Where: agentmemory/providers/localjson.py_load, _save, and runtime_policy.

  • Fix: LocalJsonProvider now wraps file reads/writes with a cross-process lock file and writes through a temp file followed by os.replace. The direct transport policy remains valid because the provider no longer relies only on an in-process lock. tests/test_localjson_provider.py covers concurrent writes from separate Python processes.


9. Root Docker Compose can expose an unauthenticated API

  • Status: closed

  • Severity: bug (security footgun)

  • Why: The deployment compose requires AGENTMEMORY_API_TOKEN, but the root docker-compose.yml binds the API to 0.0.0.0, publishes the port, and does not require or set an API token. A user who starts the root compose can expose the memory API on the host without authentication.

  • Where: docker-compose.yml.

  • Fix: root docker-compose.yml now requires a non-empty AGENTMEMORY_API_TOKEN and binds the published host port to 127.0.0.1 by default. External exposure requires an explicit AGENTMEMORY_BIND_ADDR override, with bearer auth still enabled.


10. CLI memory_add still defaults to infer=true

  • Status: closed

  • Severity: bug

  • Why: DEFECT-04 flipped the runtime default to infer=false, but the CLI adapter still maps add requests as infer = not args.no_infer. That means the CLI silently opts into provider-side rewriting unless callers remember to pass --no-infer, which contradicts the "store exactly what was sent unless explicitly requested" direction.

  • Where: agentmemory/runtime/operation_adapters.py::cli_operation_source.

  • Fix: agentmemory ops_cli add now exposes --infer as explicit opt-in and defaults to infer=false. The old --no-infer flag is retained as a hidden no-op for compatibility with scripts that already pass it.


11. MCP schemas are advertised but not enforced server-side

  • Status: closed

  • Severity: bug

  • Why: MCP tools expose inputSchema, but tools/call sends arguments directly into mcp_operation_source without a validation step. Bad payloads therefore become Python exceptions such as KeyError instead of structured validation errors. This compounds the existing "Unknown tool" masking issue.

  • Where: agentmemory/mcp.py and agentmemory/runtime/operation_adapters.py::mcp_operation_source.

  • Fix: agentmemory/mcp.py now validates tool arguments against each operation's published schema before adapter/execution. Required fields, unknown fields, basic type mismatches, enum values, and integer/number minimums return structured ProviderValidationError tool results.


12. mem0 scope inventory depends on private Qdrant pickle internals

  • Status: closed

  • Severity: bug

  • Why: Mem0Provider.list_scopes opens Qdrant's storage.sqlite, reads rows from the private points table, and calls pickle.loads on point blobs to recover payloads. This is version-coupled to Qdrant/mem0 internals and is unsafe if the storage file is ever attacker-controlled.

  • Where: agentmemory/providers/mem0.py::_iter_scope_payloads and list_scopes.

  • Fix: list_scopes now reads from an AgentMemory-owned SQLite scope registry shared across providers. mem0 and localjson update the registry on add/update/delete, and legacy mem0 installs can be migrated with the explicit agentmemory rebuild-scope-registry command. The old Qdrant pickle reader remains only as the one-shot rebuild seed path, not a runtime inventory dependency.


13. API handler tests depend on local auth environment

  • Status: closed

  • Severity: bug

  • Why: API tests instantiate BaseHTTPRequestHandler manually via Handler.__new__ and do not isolate AGENTMEMORY_API_TOKEN or OAuth env loaded from a local .env. With auth enabled, _require_auth calls send_response on a fake handler missing fields such as requestline, producing unrelated test failures.

  • Where: tests/test_agentmemory_api.py.

  • Fix: tests/test_agentmemory_api.py now clears API token and OAuth env around each test, restores the caller's environment in tearDown, and covers both /health modes explicitly: public liveness without auth and registry-backed health when an auth header is present.


14. OPENROUTER_API_KEY leaks into process-wide os.environ

  • Status: closed

  • Severity: bug (secret handling)

  • Why: Mem0Provider._load_memory calls os.environ.setdefault("OPENAI_API_KEY", api_key) and the same for OPENAI_BASE_URL. The key is already threaded into config["llm"] and config["embedder"] a few lines below, so the environment mutation is redundant, but the side effect is real — any child process spawned after _load_memory runs (for example, MCP clients launched by connect-clients or the sweeper thread's downstream helpers) inherits the key, and any library in the same process that reads OPENAI_API_KEY now sees it.

  • Where: agentmemory/providers/mem0.py::_load_memory (≈ L260).

  • Fix: Mem0Provider._load_memory no longer writes OPENAI_API_KEY or OPENAI_BASE_URL into process-wide os.environ. The OpenRouter key is passed explicitly into both llm.config.api_key and embedder.config.api_key before Memory.from_config.


15. CLI onboarding passes OpenRouter key through argv

  • Status: closed

  • Severity: bug (secret handling)

  • Why: run_onboarding calls run_command(['configure', '--openrouter-api-key', key]). The key lands in argv, which is visible in ps aux, Windows Task Manager's command-line column, shell history, CI job logs, and most process auditors.

  • Where: agentmemory/interactive.py::run_onboarding and the configure command in agentmemory/cli.py.

  • Fix: configure now supports --openrouter-api-key-stdin and --openrouter-api-key-env NAME in addition to the legacy argv form. run_onboarding now passes the prompted key through the stdin variant, so the secret no longer appears in the onboarding command argv. Tests cover the stdin/env provider paths and assert onboarding does not include the key in argv.


16. filter_unexpired silently violates the limit contract

  • Status: closed

  • Severity: bug

  • Why: _execute_search and _execute_list apply lifecycle_module.filter_unexpired to the list returned by the provider after limit has already capped it. If the caller asked for limit=10 and half the top-10 are TTL-expired, the caller receives 5 items and cannot distinguish "no more data" from "more data exists but it was filtered". Pagination and top-k semantics quietly break.

  • Where: agentmemory/runtime/operations.py_execute_search (≈ L198) and _execute_list.

  • Fix: runtime search and list now retry with a larger provider limit when TTL filtering removes items from the first batch. This keeps the observable limit contract intact when more live records exist, while bounding retries so the refill path cannot loop forever. Regression tests cover both list and search refill behavior.


17. proxy_add / proxy_search defaults drift from runtime defaults

  • Status: closed

  • Severity: bug (regression risk)

  • Why: http_client.proxy_add has infer=True as its default and proxy_search has rerank=True. The project-wide defaults were flipped to infer=False in DEFECT-04, and rerank is provider-capability-gated. All current call sites pass kwargs explicitly, so the defaults are inert today — but any future direct caller of proxy_* gets the pre-DEFECT-04 behavior silently. This is exactly the footgun DEFECT-04 was meant to remove.

  • Where: agentmemory/runtime/http_client.py::proxy_add, proxy_search.

  • Fix: agentmemory/runtime/http_client.py now requires callers to pass infer to proxy_add and rerank to proxy_search explicitly. This turns future omissions into immediate TypeErrors instead of silently reviving old proxy-layer defaults. Regression tests cover the explicit-failure path, and the current tree has no remaining implicit call sites.


18. HTTP body size is not capped

  • Status: closed

  • Severity: bug (local DoS)

  • Why: AgentMemoryHandler._read_json reads int(self.headers.get("Content-Length", "0")) bytes with no upper bound. A malformed or malicious local request can request tens of GB allocation and OOM the API process. This is on top of BACKLOG #2 (rate limiting) — the cap is per-request, the rate limit is per-minute; both are needed.

  • Where: agentmemory/api.py::AgentMemoryHandler._read_json (≈ L89–92).

  • Fix: the API now caps request bodies at 16 MiB by default (overridable via AGENTMEMORY_MAX_BODY_BYTES). Declared oversized bodies are rejected before reading, and bodies without Content-Length are read with a strict ceiling. HTTP endpoints return 413 Payload Too Large, and the MCP endpoint returns a JSON-RPC error with the same HTTP status. Regression tests cover declared and undeclared oversized bodies.


19. Client-registration paths hardcode Windows AppData/Roaming

  • Status: closed

  • Severity: bug (cross-platform correctness)

  • Why: agentmemory/clients.py defines CLAUDE_DESKTOP_CONFIG, VSCODE_MCP, ROO_MCP, KILO_MCP, CLINE_*_MCP as Path.home() / "AppData" / "Roaming" / .... On macOS or Linux this produces paths like ~/AppData/Roaming/Code/... — neither a real config location nor an empty miss. connect-clients and doctor-clients will either skip them as non-existent or, worse, create garbage directories.

  • Where: agentmemory/clients.py — module-level path constants (L22–27).

  • Fix: agentmemory/clients.py now resolves client config paths through platform-aware helpers instead of hardcoded Windows AppData/Roaming constants. Windows uses %APPDATA% (fallback ~/AppData/Roaming), macOS uses ~/Library/Application Support, and Linux uses $XDG_CONFIG_HOME (fallback ~/.config). connect-clients, status-clients, and doctor-clients now read those helpers at call time, so non-Windows runs no longer inspect or create fake ~/AppData/Roaming/... trees. tests/test_agentmemory_clients.py covers Windows, macOS, Linux, and the Linux lowercase claude fallback for existing setups.


20. ensure_api_running has a cold-start race

  • Status: closed

  • Severity: bug

  • Why: Two non-owner processes that both call ensure_api_running at the same time each see api_is_healthy() == False, each Popen a fresh API, and race on the PID file. The later write wins; the loser becomes an orphan still bound to the port. The owner process that later calls stop-api kills the recorded PID, leaving the orphan holding the port until OS cleanup.

  • Where: agentmemory/runtime/http_client.py::ensure_api_running (L78–102), agentmemory/cli.py::start_api_process (L484–514).

  • Fix: agentmemory/runtime/http_client.py::ensure_api_running now holds a cross-process lock file across the entire cold-start critical section: re-check health, launch the API, clear runtime caches, and wait for readiness. Competing callers now serialize instead of double-starting the owner API. Regression coverage includes a real multiprocessing test that starts two concurrent callers against the same lock file and verifies only one launcher path runs.


21. stop-api on Windows force-kills without graceful shutdown

  • Status: closed

  • Severity: bug

  • Why: cli.py::stop_api_process uses taskkill /F on Windows, which skips the SIGTERM-style handler that would otherwise flush the TTL sweeper and remove the PID/state files. The PID/state files are then removed by stop-api itself, but during the force-kill window another process can observe a dead PID as live.

  • Where: agentmemory/cli.py::stop_api_process (L545–551).

  • Fix: agentmemory/cli.py::stop_api_process now uses a two-phase shutdown path. On Windows it first calls taskkill /PID <pid> without /F, waits for the process to exit, and escalates to /F only if the grace period expires. On POSIX it now mirrors the same contract with SIGTERM followed by a bounded wait and SIGKILL fallback. Regression tests cover the POSIX path, Windows graceful success, and Windows forced escalation.


22. Error payload shape diverges between HTTP / MCP / CLI

  • Status: closed

  • Severity: hygiene (client-integration friction)

  • Why: HTTP returns {"error": "...", "error_type": "...", "message": "..."} (two of the three keys duplicate each other). MCP returns {"error_type": "...", "message": "..."} embedded as JSON in content[0].text, with isError: true. CLI prints str(exc) to stderr with no structure. A client writing a unified wrapper around AgentMemory has to branch per surface to extract the same information.

  • Where: agentmemory/api.py::_send_error_payload (L94–98), agentmemory/mcp.py::error_result (L52), agentmemory/ops_cli.py error path (L102–103).

  • Fix: agentmemory/providers/base.py now exposes the shared provider_error_payload() helper, and transport surfaces route through it. CLI stderr now emits the same structured JSON {error_type, message} shape used by MCP and HTTP. HTTP retains "error" as a compatibility alias while the canonical structured keys remain error_type and message.


23. memory_add metric counts dedup hits as inserts

  • Status: closed

  • Severity: hygiene (observability correctness)

  • Why: OperationSpec.__post_init__ wraps execute with metrics_registry.timed(name). _execute_add returns a pre-existing record when _maybe_dedup_existing fires, without inserting. Both paths increment memory_add.ok, so the counter is the sum of real inserts plus dedup reads. There is no separate counter for dedup hits, and the silent except Exception: return None inside _maybe_dedup_existing hides provider errors during the dedup probe.

  • Where: agentmemory/runtime/operations.py_maybe_dedup_existing (L107–123) and _execute_add (L137–140).

  • Fix: agentmemory/runtime/operations.py now records explicit auxiliary events memory_add.dedup_hit, memory_add.inserted, and memory_add.dedup_probe_failed through the metrics registry. The original memory_add.ok counter is retained for back-compat, but callers can now distinguish true inserts from dedup returns. Dedup probe failures also emit a warning log with traceback instead of failing silently. Summary and Prometheus output now expose the auxiliary event counters, and tests/test_observability_lifecycle.py covers dedup-hit and probe-failure paths.


24. should_proxy_to_api reads cached runtime policy

  • Status: closed

  • Severity: bug (latent)

  • Why: active_provider_runtime_policy in agentmemory/runtime/config.py is @lru_cache'd. If the owner API process reloads a new config (provider switched from mem0 to localjson, or transport mode flipped), other long-lived processes continue to route by the old decision. Today this manifests only during manual reconfiguration, which is uncommon — but future features like POST /admin/reload would expose the divergence immediately.

  • Where: agentmemory/runtime/http_client.py::should_proxy_to_api (L26–33) and agentmemory/runtime/config.py::active_provider_* cached accessors.

  • Fix: agentmemory/runtime/config.py now wraps its cached runtime config and provider accessors with automatic invalidation keyed to the config file marker (path, existence, mtime_ns, and size). Long-lived processes now observe provider/runtime-policy changes after on-disk config updates without requiring manual clear_caches(). Regression tests cover both runtime policy and capability updates after an external config-file rewrite.


25. Page APIs bypass TTL filtering

  • Priority: P1

  • Status: closed

  • Severity: data-retention bug

  • Why: Runtime memory_search and memory_list filter expired records and refill the limit when TTL removes records from the first provider batch. The newer cursor page operations return provider pages directly. As a result, expired records can be returned through memory_search_page, memory_list_page, HTTP /search/page, HTTP /memories/page, MCP page tools, and export paths that call page readers. That violates the documented read-path TTL contract and can expose records that should be logically expired.

  • Where: agentmemory/runtime/operations.py::_execute_search_page and _execute_list_page.

  • Fix outline: Add page-aware TTL filtering while preserving cursor semantics. The page response should never include expired records. If a page loses items to TTL filtering, either fetch forward until the requested page is filled or return fewer items with the next provider cursor clearly preserved. Add regression tests for list/search page operations and HTTP/MCP paths.

  • Fix: memory_search_page and memory_list_page now filter expired records before returning page payloads while preserving the provider next_cursor. Provider-neutral export also filters expired records when it consumes page readers directly. Regression coverage lives in tests/test_observability_lifecycle.py and tests/test_agentmemory_portability.py.


26. TTL sweeper can permanently miss expired records

  • Priority: P1

  • Status: closed

  • Severity: data-retention bug

  • Why: The background sweeper calls list_scopes(limit=500) and then list_memories(..., limit=500) for each scope. If there are more than 500 scopes, later scopes are never swept. If a scope has more than 500 records, expired records outside the first provider window may never be hard-deleted. TTL read filtering hides the symptom from normal reads, but the provider store can retain expired data indefinitely.

  • Where: agentmemory/runtime/lifecycle.py::_collect_expired_ids.

  • Fix outline: Avoid provider fixed-window walks for hard-delete discovery. Scope pagination exists, but mem0 record pagination is intentionally conservative, so the robust path is to use AgentMemory's registry as a TTL index and rebuild it from the primary store when diagnostics report drift.

  • Fix: Scope registry rows now include the normalized metadata.expires_at value, and the TTL sweeper uses a registry-backed expired-id index instead of walking list_scopes(limit=500) plus list_memories(limit=500). The old scope/memory walk remains only as a compatibility fallback for callers that do not inject the registry expired-id helper. This removes the permanent-miss failure mode for registry-backed providers while preserving the existing delete semantics.


27. Scope registry inventory scans all rows before applying limit

  • Priority: P2

  • Status: closed

  • Severity: hygiene (scalability)

  • Why: scope_registry.list_inventory fetches every registry row for the provider, aggregates buckets in Python, sorts all buckets, and only then applies items[:limit]. Small calls such as memory_list_scopes(limit=20) still pay full-table cost. This affects admin/doctor/export/sweeper paths and will become more visible as providers and memory counts grow.

  • Where: agentmemory/runtime/scope_registry.py::list_inventory.

  • Fix outline: Add registry-side inventory pagination and/or SQL aggregation by scope kind/value. Preserve current ordering (kind, -count, value) and totals while avoiding full-table work on the hot path.

  • Fix: Scope inventory grouping, filtering, ordering, and page limiting now run inside SQLite through provider-scoped aggregation queries instead of materializing all registry rows into Python first. The runtime preserves the existing (kind, -count, value) ordering and totals shape, while inventory pages only fetch the requested window plus one lookahead row. Provider-scoped indexes now cover the hot scope and expiry columns.


28. Admin memory views bypass runtime TTL filtering

  • Priority: P2

  • Status: closed

  • Severity: bug (observability correctness)

  • Why: list_admin_memories calls runtime.config.memory_list and memory_search directly instead of going through the operation layer that applies TTL filtering/refill. Admin stats and browser/admin memory views can therefore count or display expired records until the sweeper deletes them. If admin is meant to inspect raw provider state, the behavior should be documented explicitly; otherwise it should match normal read semantics.

  • Where: agentmemory/runtime/admin.py::list_admin_memories.

  • Fix outline: Route admin list/search through shared operations or apply the same lifecycle filtering locally. Add tests proving expired memories are hidden from admin list/stats unless an explicit raw-provider inspection mode is added.

  • Fix: Admin list/search results and admin stats now apply the same lifecycle TTL hiding before overlays/counts are computed. get_admin_memory also treats expired records as absent. Regression tests cover list, stats, and direct admin-get behavior.


29. list_scopes has no cursor pagination

  • Priority: P1

  • Status: closed

  • Severity: hygiene (portability/scalability)

  • Why: Provider-neutral export now uses list_memories_page for paginated record walks, but the first step is still list_scopes(limit=10_000). If the scope inventory reaches that guard, export fails closed. This is correct because silent truncation would be worse, but it leaves a hard ceiling in the portability story and blocks robust sweeper pagination.

  • Where: agentmemory/runtime/portability.py::EXPORT_SCOPE_LIMIT, agentmemory/runtime/config.py::memory_list_scopes, agentmemory/runtime/scope_registry.py::list_inventory.

  • Fix outline: Add list_scopes_page with an opaque cursor and a shared ScopeInventoryPage shape. Update export to walk pages. Keep the existing list_scopes response as the backwards-compatible first-page API.

  • Fix: Added provider-neutral list_scopes_page with opaque cursors across provider base contract, localjson, mem0, runtime config, HTTP proxy, operation registry, MCP tool generation, HTTP /admin/scopes/page, and CLI list-scopes-page. Provider-neutral export now walks scope pages instead of relying on the fixed EXPORT_SCOPE_LIMIT guard. The legacy list_scopes response shape remains unchanged.


30. Invalid numeric HTTP query params return 500

  • Priority: P3

  • Status: closed

  • Severity: bug (API validation)

  • Why: Some HTTP GET handlers parse limit with raw int(...). A non-numeric query value raises ValueError and falls into the generic exception handler, returning 500 instead of a structured 400 validation error.

  • Where: agentmemory/api.py/admin/stats, /admin/memories, and other direct int(params.get(...)) query parsing sites.

  • Fix outline: Move numeric query parsing into a small shared helper that raises ProviderValidationError with a clear field name and minimum-value check. Cover affected endpoints with bad-query tests.

  • Fix: agentmemory/api.py now parses numeric query params through a shared helper that raises typed ProviderValidationError with field-specific messages. /admin/stats and /admin/memories now return structured 400 validation payloads for invalid limit values instead of falling through to generic 500, and regression tests cover both endpoints.


31. Provider contract does not correctly allow unsupported update/delete

  • Priority: P2

  • Status: closed

  • Severity: bug (provider compatibility)

  • Why: ProviderCapabilities includes supports_update and supports_delete, but the provider contract harness still treats update/delete as always-required behavior. Future read-only, append-only, archive, graph, or file-backed providers may legitimately be unable to update or delete individual records. Today those providers would be forced either to fake support or fail certification even when they declare the capability accurately.

  • Where: agentmemory/providers/base.py, tests/provider_contract_harness.py, and runtime update/delete validation paths.

  • Fix outline: Make update/delete capability-gated across the provider harness and runtime validation. If a provider declares support, the existing normalized MemoryRecord / DeleteResult contract still applies. If it declares no support, calls must fail consistently with ProviderCapabilityError, and certification should treat that as valid.

  • Fix: BaseMemoryProvider now provides default ProviderCapabilityError implementations for unsupported update/delete. Runtime operations validate supports_update / supports_delete before dispatch, and the reusable provider harness treats unsupported update/delete as a valid declared capability state while still requiring full normalized behavior from providers that advertise support.


32. Provider registry, certification, and onboarding are split across hardcoded lists

  • Priority: P2

  • Status: closed

  • Severity: hygiene (provider compatibility)

  • Why: Runtime provider loading, certification targets, certification policy, and first-run onboarding are wired through separate hardcoded lists. A new provider can easily be added to one path but missed in another, causing semi-integrated providers that work in tests but not in CLI setup, or work at runtime but are invisible to certification.

  • Where: agentmemory/runtime/config.py::provider_registry, agentmemory/certification/registry.py, agentmemory/certification/policy.py, and agentmemory/interactive.py.

  • Fix outline: Introduce a single provider metadata source or descriptor model used by runtime registry, certification registry, policy, and onboarding. Keep provider-specific setup hooks inside provider modules so shared setup code does not need if provider == "mem0" branches.

  • Fix: Provider metadata now lives on provider classes and is surfaced through agentmemory.providers.registry.ProviderDescriptor. Runtime provider lookup, certification targets, certification policy, and interactive onboarding all read that descriptor source. Provider-specific onboarding prompts are delegated to provider hooks instead of shared-layer provider-name branching.


33. Provider certification docs lag behind the current contract

  • Priority: P2

  • Status: closed

  • Severity: hygiene (documentation correctness)

  • Why: The certification checklist still describes the older provider contract and does not fully document newer obligations: cursor page methods, supports_pagination, scope registry maintenance, rebuild support, degraded registry diagnostics, and capability-gated unsupported operations. This makes future provider integration harder and increases the chance of a provider passing an outdated checklist.

  • Where: docs/PROVIDER_CERTIFICATION.md, docs/PROVIDER_ADAPTER_RULES.md, and docs/future-memory-providers/README.md.

  • Fix outline: Update certification docs to the current contract. Include required page shapes, scope-registry rules for providers that advertise supports_scope_inventory, rebuild expectations, degraded marker behavior, and explicit rules for unsupported update/delete.

  • Fix: docs/PROVIDER_CERTIFICATION.md, docs/PROVIDER_ADAPTER_RULES.md, and docs/future-memory-providers/README.md now document pagination, scope-registry maintenance, degraded registry semantics, and valid unsupported update/delete capability behavior.


34. Stale public docs still describe legacy mem0 Qdrant inventory internals

  • Priority: P3

  • Status: closed

  • Severity: hygiene (documentation correctness)

  • Why: Public positioning docs still say AgentMemory enumerates scopes by inspecting backend storage directly, including Qdrant SQLite internals for mem0. That is no longer the normal runtime architecture. Normal list_scopes uses AgentMemory's SQLite scope registry; the legacy Qdrant reader is only an explicit one-shot rebuild path.

  • Where: docs/WHAT_AGENTMEMORY_ACTUALLY_ADDS.md scope inventory section.

  • Fix outline: Update the wording to describe the scope registry as the normal inventory source and mention legacy backend inspection only as an explicit migration/rebuild mechanism when applicable.

  • Fix: docs/WHAT_AGENTMEMORY_ACTUALLY_ADDS.md now describes the AgentMemory-owned scope registry as the normal inventory path and limits backend inspection to explicit legacy rebuild/migration.


35. Interactive provider setup is mem0/localjson-only

  • Priority: P3

  • Status: closed

  • Severity: hygiene (provider compatibility)

  • Why: Interactive setup prompts only advertise mem0 and localjson, and unknown input falls back to mem0. Future providers would be invisible in first-run setup, and a typo can silently select a semantic provider requiring unrelated credentials.

  • Where: agentmemory/interactive.py.

  • Fix outline: Generate the provider list from the shared provider registry, reject unknown provider names clearly, and delegate provider-specific prompt questions to provider metadata/hooks.

  • Fix: Interactive onboarding now reads providers from agentmemory.providers.registry, rejects unknown provider names without falling back to mem0, and delegates provider-specific prompt behavior to BaseMemoryProvider.onboarding_configuration() implementations.


36. Dead-man ping after successful backup runs

  • Priority: P1
  • Status: open
  • Severity: data-retention
  • Why: The backup chain has two independent legs — 0 4 * * * server cron running deploy/backup-agentmemory.sh, and a Windows Task Scheduler job at 12:30 local running pull-agentmemory-backups.ps1. Each writes a log on success and a non-zero exit on failure, but nothing alerts when a scheduled job does not run at all — a stopped cron, a Task Scheduler task that was disabled, or a host that was off when the trigger fired. The earliest signal of silent failure today is when someone tries to restore. Adding a healthchecks.io dead-man URL pinged at the end of each successful run, with a ~26-hour SLA, closes most of that surface for the price of two curl calls and a free account.
  • Where:
    • deploy/backup-agentmemory.sh — append a curl -fsS -m 10 "$HEALTHCHECKS_URL" || true after the final echo "Backup written: ..." block. Make HEALTHCHECKS_URL an env var so the URL stays out of git.
    • O:\backups\pull-agentmemory-backups.ps1 (local script, not in repo) — same pattern at the end of the try block. Document in /root/docs/agentmemory/RUNBOOK.md.
    • Context: SESSION_REVIEW_2026-05-29.md §2 P4.

37. Derive user_id from auth context instead of accepting it from payload

  • Priority: P1
  • Status: closed
  • Severity: security
  • Why: The current model accepts user_id, agent_id, and run_id directly from the request body. The bearer token / OAuth flow gates the endpoint but does not bind the caller to a specific identity — any authenticated client can write or read records under any user_id it chooses. With a single human operator this is fine in practice (the threat model is "I authored every record"). The moment a second tenant is added, or an automation accepts user_id from an LLM-controlled input, records can be mis-attributed or read across users. Filter-based isolation by user_id works only as well as the discipline that fills the field.
  • Fix outline: Add an opt-in mode (AGENTMEMORY_ENFORCE_AUTH_USER_ID=1) that binds each access token to a user_id claim at token-issuance time (auth code grant) and refuses payloads where the requested user_id does not match the bound claim. Static API tokens grandfather through. Surface the claim under /.well-known/oauth-protected-resource and on the memory_health response so clients can introspect.
  • Where:
    • agentmemory/oauth.py::issue_access_token — accept and persist a bound_user_id per token.
    • agentmemory/api.py::_authorized_bearer — return the claim alongside the token validity bit.
    • agentmemory/runtime/operations.py::_execute_add and friends — when the env flag is set and a claim is present, reject payloads where source["user_id"] differs from the claim.
    • Context: SESSION_REVIEW_2026-05-29.md §3 F3.
  • Resolution: AGENTMEMORY_ENFORCE_AUTH_USER_ID=1, documented in docs/AUTH_IDENTITY_BINDING.md. The binding is stamped on the auth code, carried into the token pair, and preserved across refresh rotation; unbound credentials (static API token, tokens issued before a binding was configured) keep their previous behaviour.
  • Where it actually landed, and why it differs from the outline above: the outline proposed the check in _execute_add "and friends". It went instead into the wrapper OperationSpec.__post_init__ installs around every operation, because HTTP, MCP and CLI all dispatch through that one call — a per-operation check is a list that the next operation is added without. get/update/ delete carry no scope, so they resolve the stored record's own user_id first. New shared module: agentmemory/runtime/identity.py. New typed error: ProviderIdentityError → HTTP 403, same error_type over MCP.
  • Deliberately not done: the outline also proposed surfacing the claim under /.well-known/oauth-protected-resource and on memory_health for client introspection. That is useful and remains open as a smaller follow-up; it is not needed for enforcement and was left out rather than widened into scope. agent_id and run_id remain unbound — no evidenced requirement, and binding them would break multi-agent use under one identity.

38. Expose memory_type as a first-class filter

  • Priority: P2
  • Status: open
  • Severity: hygiene
  • Why: The memory_type field is part of the MemoryRecord shape and the input schema accepts it on add, but it is not exposed as a filter on memory_search or memory_list. Live inspection on 2026-05-29 found 0 of 47 production records populating the field — partly because callers have no read-side incentive to set it. Promoting it to a first-class filter, the same way user_id / agent_id / run_id already are, would give clients a structural way to separate categories of record (fact vs episode vs aggregate vs draft) without encoding the category as a free-form text prefix inside the body.
  • Fix outline: Add memory_type to the search/list input schemas and propagate it through validate_and_build_search_kwargs / validate_and_build_list_kwargs. mem0 already accepts it on read; localjson's filter implementation needs to be extended to match. Documentation should describe it as a category tag and stop short of prescribing a vocabulary — callers pick their own.
  • Where:
    • agentmemory/runtime/transport.py::validate_and_build_*_kwargs — accept and forward memory_type.
    • agentmemory/runtime/operations.py::OPERATIONS — add memory_type to the search / list input schemas alongside the scope fields.
    • agentmemory/providers/localjson.py::search_memory_page / list_memories_page — add a memory_type predicate.
    • tests/test_provider_contract_v1.py and the relevant provider tests — assert the filter narrows results.
    • Context: SESSION_REVIEW_2026-05-29.md §4.

39. Parametrize the dedup similarity threshold

  • Priority: P3
  • Status: open
  • Severity: hygiene
  • Why: memory_add(dedup=true) returns a pre-existing record when the semantic score against the new input is at least DEDUP_SCORE_THRESHOLD (agentmemory/runtime/operations.py, currently hardcoded to 0.92). Live verification on 2026-05-28 showed a clean paraphrase scoring 1.0 on identical input but failing to merge a sentence-level paraphrase ("DCR registration endpoint at POST /register, anonymous, rate-limited to 20 per hour per IP" vs. "The /register endpoint is anonymous and capped at 20 requests per hour per source IP") — both stayed in the store as separate records. 0.92 is a defensible conservative default ("don't lose user data"), but different domains have different tolerance: a chat-fact extractor may want 0.80 to aggressively collapse paraphrases, an evidence-log application may want 0.98 to never merge near-duplicates by mistake. Callers have no way to express this today.
  • Fix outline: accept an optional dedup_threshold field on the add input schema (float in [0, 1], default keeps current 0.92). Validate range, fall back to default when absent. Carry it through _maybe_dedup_existing and surface the threshold actually used in the returned dedup_score envelope so callers can tell what they got. Optionally: an AGENTMEMORY_DEFAULT_DEDUP_THRESHOLD env var for an install-wide default different from 0.92.
  • Where:
    • agentmemory/runtime/operations.py::DEDUP_SCORE_THRESHOLD / _maybe_dedup_existing — read threshold from source["dedup_threshold"] with fallback.
    • agentmemory/runtime/operations.py::OPERATIONS["add"].input_schema — add the field with description.
    • tests/test_agentmemory_operations.py — assert per-call override works and out-of-range values are rejected.
    • Context: SESSION_REVIEW_2026-05-29.md and the original 2026-05-28 live review notes.

40. Warn callers when infer=true drops meaningful content

  • Priority: P2
  • Status: open
  • Severity: hygiene
  • Why: When infer=true, mem0's LLM extracts a compressed fact from the input. Live test on 2026-05-28: input "I just shipped the OAuth refresh token feature with rotation and added a 30-day TTL on refresh tokens. The change went live at 8:31 UTC today" → stored as "Shipped the OAuth refresh token feature with rotation". The 30-day TTL detail and the timestamp were both silently dropped. We do expose transformed: true, original_text, stored_text, plus the additional_records array (item 36 in the closed set, post-Bug 1 fix). A caller paying attention can compare lengths or diff the strings — but most won't. The fan-out array catches the case where mem0 split into N records; this item is about the case where it collapsed instead. Two failure modes, currently one observable.
  • Fix outline: when infer=true is requested AND stored_text is materially shorter than original_text (heuristic: ratio below some threshold, e.g. 0.4, or character count delta above some absolute, e.g. 200 chars), add a field content_loss_warning to the response with the computed ratio and a short message ("LLM stored a compressed version; consider infer=false or add the dropped detail as a separate record"). Make the threshold configurable.
  • Where:
    • agentmemory/runtime/operations.py::_execute_add — extend the existing transformed=true enrichment block to also emit a warning field when the ratio is below threshold.
    • agentmemory/runtime/operations.py::OPERATIONS["add"].description — note the new warning field in the MCP tool description so clients see it without reading code.
    • tests/test_agentmemory_operations.py — cover the warning fires on a synthetic large-input / small-stored pair and stays absent when the ratio is reasonable.
    • Context: SESSION_REVIEW_2026-05-29.md; related to closed item 5 (infer=true observability) but a distinct concern.

41. memory_reconcile name is confusing; add an alias and clarify docs

  • Priority: P3
  • Status: open
  • Severity: hygiene
  • Why: "Reconcile" is overloaded. The 2026-05-28 live review noted that on first encounter it looked like the tool would reconcile drift between mem0 (the source of truth for records) and the scope_registry sidecar (our SQLite mirror). In fact it does conflict detection between contradictory claims (A says X at t1, B says ¬X at t2) — a different operation entirely. A reviewer unfamiliar with item 6 of this backlog would have to read the implementation to find out. Naming is a small fix with outsized return on caller comprehension.
  • Fix outline: add memory_find_conflicts as the primary MCP tool name and keep memory_reconcile as a deprecated alias. The underlying OperationSpec stays one entry; both names route to the same handler. Update the tool description to lead with "find contradictory claim pairs" and only mention "reconcile" as the legacy name. Schedule the alias for removal in a major version bump.
  • Where:
    • agentmemory/mcp.py and/or agentmemory/runtime/operations.py — register both names. Most providers register tools by iterating OPERATIONS — add an aliases field to OperationSpec or expose a deprecated copy.
    • docs/USE_CASES.md, examples/mcp-demo.md, agentmemory/runtime/operations.py tool description — switch the canonical name.
    • CHANGELOG.md — record the rename and the deprecation window.
    • Context: SESSION_REVIEW_2026-05-29.md; closed item 6 implemented the tool, this item improves its name.

42. Disable TTL acceptance by default; opt in via env var

  • Priority: P1
  • Status: closed (commit c89cc6f)
  • Severity: data-retention
  • Why: TTL is a terminal data-loss class with no clean recovery path (see SESSION_REVIEW_2026-05-29.md §2 P5). Restoring from backup does not bring TTL'd records back to a usable state because the read paths still filter them and the sweeper immediately re-deletes. Today's default is "accept any well-typed ttl_seconds / expires_at" — a single typo in units (6 instead of 6*60*60*24*180 for "six months") permanently deletes a record within ~10 minutes. Production currently has 0 of 47 records with TTL set, so switching the default closes the entire risk class at the API boundary without breaking anything in flight.
  • Fix outline: add an env var AGENTMEMORY_ALLOW_TTL (default unset / 0). When TTL is disabled and a request includes metadata.ttl_seconds or metadata.expires_at, raise ProviderValidationError with a clear message that says how to opt in. When enabled, behave exactly as today (subject to items 43-45 on top). Use reject semantics, not silent strip — silent strip is the bug-3 anti-pattern we just fixed for malformed values, and the same anti-pattern should not reappear at the feature gate.
  • Where:
    • agentmemory/runtime/lifecycle.py::resolve_expires_at / apply_expiry_to_metadata — guard at the top.
    • agentmemory/runtime/lifecycle.py::sweep_interval_seconds — also short-circuit to 0 (disabled) when AGENTMEMORY_ALLOW_TTL is unset, so the sweeper does not start.
    • .env.example — document the variable next to the existing AGENTMEMORY_OAUTH_DISABLE_DCR pattern.
    • README.md and CHANGELOG.md — note the default change.
    • tests/test_observability_lifecycle.py — existing TTL tests need a setUp that sets AGENTMEMORY_ALLOW_TTL=1 so they still exercise the enabled path. Add a new test asserting the default is reject.
    • Context: SESSION_REVIEW_2026-05-29.md §2 P5 mitigation 1.

43. Sanity-guard TTL values when TTL is enabled

  • Priority: P2
  • Status: open
  • Severity: data-retention
  • Why: With item 42 closing the accidental enable surface, this item closes the typo in units surface for operators that intentionally use TTL. A ttl_seconds: 6 (meant as six months) is syntactically valid today and would delete the record in six seconds. Likewise a ttl_seconds: 9_999_999_999 is almost certainly a paste accident, not intent. Bounding both ends costs nothing in the common case (sane minutes-to-months range) and rejects two realistic typo classes.
  • Fix outline: when TTL is enabled, reject ttl_seconds outside [AGENTMEMORY_MIN_TTL_SECONDS, AGENTMEMORY_MAX_TTL_SECONDS] (defaults 60 and 365 * 24 * 3600). For expires_at, reject timestamps further than AGENTMEMORY_MAX_TTL_SECONDS in the future, and refuse timestamps in the past (those are dead-on-arrival, an obvious caller bug).
  • Where:
    • agentmemory/runtime/lifecycle.py::resolve_expires_at — extend the existing validation block.
    • .env.example — document the two new bounds.
    • tests/test_observability_lifecycle.py — assert each bound.
    • Context: SESSION_REVIEW_2026-05-29.md §2 P5 mitigation 2.

44. Sweeper soft-deletes via archived flag, with a delayed hard-delete pass

  • Priority: P2
  • Status: open
  • Severity: data-retention
  • Why: Today the sweeper hard-deletes expired records by calling delete_memory. That is irrecoverable: even a clean restore from the data-volume backup does not revive the record, because expires_at is still in the past and the read path filters it. A two-stage cleanup gives a recovery window: stage 1 sets metadata.archived: true plus archived_reason: "ttl_expired" and archived_at: <iso> (read paths already filter archived records); stage 2 hard-deletes records that have been archived for longer than AGENTMEMORY_TTL_RECOVERY_DAYS (default 30). An operator who notices unexpected disappearance has 30 days to flip archived: false and bring records back.
  • Fix outline:
    • agentmemory/runtime/lifecycle.py::run_sweep_once — replace the delete_memory(id) call with a softer update_memory(id, metadata={archived: True, archived_reason: "ttl_expired", archived_at: utc_now()}).
    • Add _hard_delete_archived_older_than helper called on a slower interval (or piggybacked on the same sweep with a separate threshold check).
    • Make the recovery window configurable. Setting it to 0 reproduces today's behavior (no soft-delete window).
    • Coordinate with item 28 ("Admin memory views bypass runtime TTL filtering") — admin views should keep showing the archived records so an operator can browse and revive them.
    • Context: SESSION_REVIEW_2026-05-29.md §2 P5 mitigation 3.

45. Surface a signal when the sweeper deletes records

  • Priority: P3
  • Status: open
  • Severity: hygiene
  • Why: The sweeper deletes silently. With items 42-44 in place most of the harm is gone, but operators still benefit from a signal when a meaningful number of records disappear in a single sweep — it would have caught the "bot started passing TTL by mistake" scenario much earlier than a user noticing missing data weeks later.
  • Fix outline: at the end of a sweep cycle, write one structured log line per non-empty cycle: count of soft-deleted (or hard- deleted) ids, the providers they belonged to, and an indication of whether the count exceeds an alert threshold (AGENTMEMORY_SWEEPER_ALERT_THRESHOLD, default 10). Optionally invoke a configurable webhook (healthchecks.io fail URL, generic POST) when the threshold is exceeded, so the dead-man monitoring from item 36 can route an alert.
  • Where:
    • agentmemory/runtime/lifecycle.py::run_sweep_once — add the summary log and the optional webhook call.
    • agentmemory/runtime/metrics.py — add a counter for sweep deletions if metrics infrastructure has a home for it.
    • .env.example — document the threshold and webhook env vars.
    • Context: SESSION_REVIEW_2026-05-29.md §2 P5 mitigation 4.

46. Read-time stale_warning on records with passed metadata.stale_after

  • Priority: P2
  • Status: open
  • Severity: data-retention
  • Why: The most dangerous hallucination class in the production legal-case pool is the aggregate-shaped record dated 2026-05-27 with figure "ИТОГО 297 580 руб." (see SESSION_REVIEW §2 P1 and AGENT_FRICTION §6). The figure was current as of the record's creation date; once new financial movements happen after that date, it is wrong. Today the system has no representation of "current as of " — the staleness lives in the body text where the agent may or may not parse it. Adding a metadata.stale_after convention (free, doc-only) plus a read-time warning when that date has passed (this item) makes the hallucination class visible to the agent at the moment it tries to cite the record. It is the highest-leverage item in the DATA_DEGRADATION_DESIGN observability category because the signal is consumed by the agent in real time, not by an operator after the fact.
  • Fix outline: every read path that returns a MemoryRecord checks metadata.stale_after (or stale_at — pick the canonical name and document it). If present and parseable and in the past, add a stale_warning field to the returned envelope:
    {
      "id": "...",
      "memory": "...",
      "metadata": {...},
      "stale_warning": {
        "stale_since": "2026-05-27T00:00:00+00:00",
        "days_overdue": 2
      }
    }
    
    Non-destructive: nothing is filtered, nothing is hidden, nothing is marked archived. The record is returned exactly as today plus a warning flag the agent can consult. The same parsing helper from lifecycle.py::_parse_expires_at can be reused for the date parsing (canonicalises Z vs +00:00, handles unix seconds).
  • Where:
    • agentmemory/runtime/lifecycle.py — add a stale_warning_for(record) helper that returns the warning dict or None.
    • agentmemory/runtime/operations.py::_execute_get, _execute_search, _execute_list (and their _page variants) — walk the records, attach stale_warning where appropriate.
    • agentmemory/runtime/operations.py::OPERATIONS[…].description — document the new field for MCP consumers.
    • tests/test_observability_lifecycle.py or a new tests/test_stale_warning.py — cover: warning fires for past stale_after, absent for future stale_after, absent for missing field, robust to malformed values (no crash, no warning), handles both Z and +00:00 ISO forms.
    • Context: DATA_DEGRADATION_DESIGN §3.3, recommended sequence Step 1.

47. Pre-write similarity check: surface candidates for update instead of blind add

  • Priority: P3
  • Status: open
  • Severity: hygiene
  • Why: Pool 1 today has structural duplication: monthly bank-statement analyses (April, March, …) all share the same shape and overlap in content, and a period-wide aggregate exists alongside each of them. Future writes into the same pool will tend to compound this — a new May analysis will be a near-duplicate of April. Today the caller has no signal that "you are about to write a record that is 0.85 similar to record X in the same scope; did you mean to update X?". The natural place to surface that signal is at the moment of add. Mechanism is suggestive (not coercive): we never auto-merge or refuse the write — we just give the caller the chance to choose update over add.
  • Fix outline: opt-in via an on_similar field on the add input:
    on_similar: "warn" | "skip" | "merge_metadata"
    on_similar_threshold: 0.80   # default if 'warn'
    
    When warn, the runtime runs a single semantic search in the same scope before insert, returns the matching candidate (if any above the threshold) on a similar_existing field of the response, and still performs the insert. The caller can then decide whether to delete/update what was just written. skip returns the existing record (like dedup but at the user's chosen threshold). merge_metadata performs the write but merges the candidate's metadata into the new record. Default behaviour stays "always insert, no extra cost" so existing callers see no change.
  • Where:
    • agentmemory/runtime/operations.py::_execute_add and friends — add the pre-write search branch.
    • agentmemory/runtime/operations.py::OPERATIONS["add"].input_schema — document the new fields.
    • tests/test_agentmemory_operations.py — cover the three modes plus the "no similar found" path and the absent-field path.
    • Context: DATA_DEGRADATION_DESIGN §3.2. Activation deferred until duplication is visibly costing the pool — the tradeoff is one extra search per write, which is expensive enough that defaulting on is not justified at current scale.

48. /admin/pool-health endpoint: scope density, growth rate, age distribution

  • Priority: P3
  • Status: open
  • Severity: hygiene (observability)
  • Why: There is no first-class signal of pool health today. An operator who wants to know "is Pool 1 still healthy?" has to write bespoke queries against scope_registry. Once there are two pools or two operators, that becomes the wrong shape. An admin endpoint that returns the structured health snapshot for any scope (or for the whole provider) gives a single readable answer and forms the basis for any future alerting policy ("if any scope grows by more than X% in a day, warn"). Defer activation until at least one of: (a) the pool grows past ~100 records, (b) there is more than one operator, (c) a programmatic monitor needs to consume the data.
  • Fix outline: new GET endpoint /admin/pool-health (and /admin/pool-health?user_id=… / ?agent_id=…) returning:
    {
      "provider": "mem0",
      "scope": {"user_id": "topazd2", "agent_id": "family_court_..."},
      "total": 44,
      "first_seen_at": "...",
      "last_seen_at": "...",
      "growth_last_7d": 0,
      "growth_last_30d": 12,
      "with_metadata": {"any": 31, "stale_after": 3, "archived": 0},
      "age_distribution_days": {"0-7": 0, "7-30": 12, "30+": 32}
    }
    
    Authenticated under the existing admin gating. Read-only, no mutation.
  • Where:
    • agentmemory/runtime/admin.py — new pool_health() function with the SQL aggregations against scope_registry.
    • agentmemory/api.py::do_GET — route /admin/pool-health.
    • tests/test_agentmemory_admin.py — cover the aggregations.
    • Context: DATA_DEGRADATION_DESIGN §3.3.

49. Duplicate detection report: scheduled cluster pass over a scope

  • Priority: P3
  • Status: open
  • Severity: hygiene
  • Why: When a pool already has accumulated duplication (Pool 1 today, or any future pool that drifts in the same direction), the remediation step needs a target list — which records are candidates to be merged, archived, or deleted. Computing this is not the same as pre-write similarity (item 47, which runs once per write at fixed cost); it is a scheduled batch pass that embeds the whole scope and clusters cosine-close records. Output is a report, not a destructive action — the human or agent reviews it and decides what to do with each cluster. This unblocks the living-summary refactor (item 50) by giving it a concrete set of candidates to fold in.
  • Fix outline: new operation memory_find_duplicates(user_id, agent_id, threshold=0.85, limit=50). Pulls all records in scope, computes pairwise similarity via the provider's embedding (re-using whatever the provider exposes — mem0's search infrastructure can be used per-record), and returns cluster groups with summary stats. Read-only — never mutates the store. Exposed as both an admin endpoint and an MCP tool so the legal-case agent can call it before a synthesis pass.
  • Where:
    • agentmemory/runtime/operations.py — new OPERATIONS["find_duplicates"] entry.
    • Provider contract — find_duplicates_in_scope() method, default implementation in BaseMemoryProvider that does the naive pairwise pass; mem0 can override with a smarter qdrant-side clustering if useful.
    • tests/test_agentmemory_operations.py — cover at least: returns correct clusters for a hand-crafted set with one obvious dup pair; empty result on no-dup pool; respects threshold.
    • Context: DATA_DEGRADATION_DESIGN §3.3 and §4.1.

50. Living-summary workflow guide: docs + system-prompt template

  • Priority: P2
  • Status: open
  • Severity: hygiene (workflow)
  • Why: The structural problem in Pool 1 is not a missing feature — mem0 + AgentMemory already support everything needed to operate in a "living summary" workflow (one canonical record per topic, archived: true to retire chunks, update_memory to keep summaries current). What is missing is a documented pattern for how to use the runtime in that workflow, so an agent (or its operator) can adopt it without reverse-engineering the implications of every field. Without this doc, every new agent that uses AgentMemory will drift toward the "document store" anti-pattern Pool 1 exhibits.
  • Fix outline: new doc docs/USE_CASES.md (or extending an existing one) describing:
    • The two extremes ("dump every chunk" vs. "living summaries") with concrete write patterns.
    • The metadata convention (kind: "summary" | "evidence" | "context" | "draft", event_date, stale_after, supersedes) and the rationale for each field.
    • System-prompt fragments that an agent owner can paste into their own agent's prompt to adopt the convention.
    • A worked example using Pool 1 as the case study: pre-state (44 chunks), post-state (5-7 living summaries plus archived raw chunks), the migration path.
    • When to not use the living-summary pattern (small pools, one-shot notes, scratch contexts).
  • Where:
    • docs/USE_CASES.md — main writeup.
    • examples/ — a small example agent prompt fragment, importable.
    • README.md — link from the documentation map.
    • Context: DATA_DEGRADATION_DESIGN §3.4 and §4.2.

Closed (for reference)

  • DEFECT-01rerank not capability-aware: fixed in agentmemory/runtime/transport.py::validate_and_build_search_kwargs. Covered by tests in tests/test_defect_fixes.py and tests/test_post_remediation.py.
  • DEFECT-02 — PID/state files not populated under PID 1 in Docker: fixed in agentmemory/api.py::_record_supervisor_files + signal handlers.
  • DEFECT-03 — double-delete raising ProviderUnavailableError: fixed in _execute_delete (idempotent response) plus Mem0Provider._map_exception (message-pattern → MemoryNotFoundError).
  • DEFECT-04 — silent LLM rewrite on memory_add: fixed by flipping default to infer=false and surfacing transformed/original_text/ stored_text when infer=true differs.
  • DEFECT-05 — mem0 contract advertising sentinels: fixed in Mem0Provider.provider_contract and BaseMemoryProvider.provider_contract.

51. Decide whether DCR-created OAuth clients must receive a bound identity

  • Priority: P1

  • Status: open

  • Severity: security-policy

  • Why: AGENTMEMORY_ENFORCE_AUTH_USER_ID=1 (item 37) binds a request's scope to the identity its credential was issued for — but only for credentials that carry a binding. Credentials without one are exempt by design, which is what keeps existing single-owner installs working across the upgrade. Dynamic client registration is on by default and /oauth/authorize approves without a login or a consent step, so anyone reachable by the endpoint can register a client and receive an unbound token. An unbound token is exempt. The mode is therefore void unless AGENTMEMORY_OAUTH_DISABLE_DCR=1 and every client carries a binding.

    Reproduced end-to-end on 2026-08-05: anonymous POST /register → 201, GET /oauth/authorize → 302 with a code, POST /oauth/token → 200 with bound_user_id: None, then POST /add {"user_id": "someone-else"} → 200 and the record was written under that id.

    This is not a defect in the item 37 implementation. The exemption is deliberate and documented. What is undecided is the policy, and it should be decided rather than left as an interaction between two defaults that each look reasonable alone.

  • Options, none obviously right:

    1. Disable DCR by default. Breaks self-registering remote MCP clients (Claude.ai, ChatGPT connectors), which is why it is on.
    2. Require a login or consent step at /oauth/authorize, so there is a person to bind to. This is the honest fix and the largest one: AgentMemory would acquire an end-user identity concept it does not have today.
    3. Assign an identity at authorize time from operator configuration, refusing to issue tokens to clients with no configured binding while the mode is on.
    4. Keep the single-owner trust model explicitly, and state in the docs that the mode defends against a careless client rather than a hostile one.
  • Do not resolve this by describing the current state as full cross-user isolation. docs/AUTH_IDENTITY_BINDING.md already says it is not; that wording should survive whatever is decided here.

  • Related: item 37 (closed), commit 2ddd6d4.