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:
- Priority —
P0/P1/P2/P3 - Status —
open/in-progress/blocked/mitigated/closed - Severity —
bug(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.
-
Status: closed
-
Severity: bug
-
Why: When a client passes the wrong argument name to a tool, the
KeyErrorraised from insideOPERATIONS_BY_MCP_NAME[name].execute(source)is caught by the outerexcept KeyErrorthat 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_addwithtextasmessages(wrong field name). Surfaced as "Unknown tool" instead of a clear validation error. -
Where:
agentmemory/mcp.py—handle_request, thetools/callbranch:try: return success(request_id, handle_call(name, arguments)) except KeyError: return error(request_id, -32601, f"Unknown tool: {name}")
-
Fix:
tools/callnow resolves the tool before execution and returns-32601only when the tool name is unknown. Bad arguments are validated against the published schema and returned as structuredProviderValidationErrortool errors instead of JSON-RPC unknown-tool errors.
-
Status: closed
-
Severity: bug (abuse risk)
-
Why: Anyone with the bearer token (or a leaked OAuth client secret) can loop
memory_addand 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 permemory_addis 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.pynow enforces an in-process token bucket keyed by the presented bearer token for authenticated API requests and by OAuthclient_idfor/oauth/token. The default is 60 requests / minute, overridable viaAGENTMEMORY_RATE_LIMIT_PER_MINUTE. Overrun returns429with aRetry-Afterheader./health,/.well-known/oauth-*, and/oauth/authorizeremain unthrottled.tests/test_agentmemory_api.pycovers both bearer-auth and OAuth token-exchange rate limiting. -
Scope note:
/health,/.well-known/oauth-*, and/oauth/authorizestay unthrottled — they're either unauthenticated liveness or part of the discovery handshake that can burst legitimately.
-
Status: closed
-
Severity: hygiene
-
Why:
docker compose up -d --build --force-recreatehas been observed droppingagentmemoryoff the externalnetbird_netbirdnetwork even when--force-recreateis specified. Seen twice on this server (once on first deploy, once while verifying the post-remediation grammar fix). The same bug caused the 20-minutetca-weboutage documented in/opt/telegramchatanalyzer/POSTMORTEM_NETWORK_DETACH.md.We ship
deploy/redeploy.shwhich forces the re-attach idempotently — the symptom is gone — but the root cause is still there and could bite any future service that joinsnetbird_netbird. -
Where:
deploy/docker-compose.ymldeclares the external network correctly. The behavior is upstream indocker composev2. -
Fix: The repository now carries the missing operational bundle around the existing self-heal:
docs/COMPOSE_V2_NETWORK_DRIFT.mdcontains a minimal reproducer, issue draft, and repo policy for the defect.deploy/repro-compose-network-drift.shprovides a self-cleaning local reproducer for hosts that need verification before deploy changes.deploy/docker-compose.yml,deploy/redeploy.sh, anddocs/DEPLOY.mdnow 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.
-
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.shis only restorable onto a compatible Qdrant + embedding-dimension combination — a change to either breaks the restore.An
exporttool that walks the scope inventory and emits a JSONL file ofMemoryRecords, plus animportthat replays them viamemory_add, would buy provider-neutral portability and make backup/restore independent of the storage layer. -
Where: new
agentmemory/runtime/portability.py+ MCP toolsmemory_export/memory_import+ CLI wrappersagentmemory export-memories/agentmemory import-memories. -
Fix:
agentmemory/runtime/portability.pynow exports canonicalMemoryRecordJSONL by walkinglist_scopesplus per-scopelist_memoriesand 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 currentmemory_addpath withinfer=false, preserving scope fields andmemory_typewhile tagging metadata withsource: "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 intests/test_agentmemory_portability.py,tests/test_agentmemory_mcp_server.py,tests/test_agentmemory_operation_adapters.py,tests/test_provider_contract_v1.py, andtests/test_agentmemory_core.py. -
Pagination follow-up: The provider contract now includes
list_memories_page/search_memory_pagepage shapes andsupports_pagination.localjsonimplements real cursor paging;mem0keeps the safe single-page fallback until a backend-safe cursor strategy is available. Export useslist_memories_pagefor paginated providers, removing the record fixed-limit guard for those providers while keeping the guard for legacy non-paginated providers. Scope inventory now haslist_scopes_page, and export walks scope pages.
-
Status: closed
-
Severity: hygiene (decision, not a bug)
-
Why: DEFECT-04 was closed by flipping the default to
infer=false. The mechanism forinfer=truestill works and returnstransformed,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:
- Remove
infer=trueentirely. Commit to "the runtime stores what you sent, full stop". Simplest product story. - Add an example in
docs/USE_CASES.mdor a dedicated doc showinginfer=truefor fact extraction (e.g. from chat transcripts) with the surfacedtransformedmetadata.
Letting it sit in the middle — available but undocumented — is the worst of both worlds.
- Remove
-
Where:
agentmemory/runtime/operations.py(schema +_execute_add) + docs.
-
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.pywith a dedicated MCP tool (memory_reconcilereturning conflict pairs), and optional enforcement policy inmemory_add(warn / reject / supersede). -
Fix:
agentmemory/runtime/reconcile.pynow provides the first read-only memory hygiene pass.memory_reconcilelists 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 viaclaim_key/claim_valueorconflict_key/claim_value. CLI access is available throughagentmemory reconcile-memories. Enforcement policies (warn/reject/supersede) remain intentionally out of scope until the read-only signal is proven useful.
-
Status: closed
-
Severity: bug
-
Why: When
AGENTMEMORY_API_TOKENor 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 anowner_process_proxyprovider call those same endpoints throughagentmemory/runtime/http_client.py, but the internal client does not send anAuthorizationheader./healthcan 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::_requestnow propagatesAGENTMEMORY_API_TOKENas a bearer token for internal proxy requests.tests/test_agentmemory_http_client.pycovers every currentproxy_*method with auth enabled so future endpoint additions do not silently regress.
-
Status: closed
-
Severity: bug
-
Why:
LocalJsonProvideradvertises direct transport and protects read/modify/write with only an in-processthreading.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, andruntime_policy. -
Fix:
LocalJsonProvidernow wraps file reads/writes with a cross-process lock file and writes through a temp file followed byos.replace. The direct transport policy remains valid because the provider no longer relies only on an in-process lock.tests/test_localjson_provider.pycovers concurrent writes from separate Python processes.
-
Status: closed
-
Severity: bug (security footgun)
-
Why: The deployment compose requires
AGENTMEMORY_API_TOKEN, but the rootdocker-compose.ymlbinds the API to0.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.ymlnow requires a non-emptyAGENTMEMORY_API_TOKENand binds the published host port to127.0.0.1by default. External exposure requires an explicitAGENTMEMORY_BIND_ADDRoverride, with bearer auth still enabled.
-
Status: closed
-
Severity: bug
-
Why: DEFECT-04 flipped the runtime default to
infer=false, but the CLI adapter still maps add requests asinfer = 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 addnow exposes--inferas explicit opt-in and defaults toinfer=false. The old--no-inferflag is retained as a hidden no-op for compatibility with scripts that already pass it.
-
Status: closed
-
Severity: bug
-
Why: MCP tools expose
inputSchema, buttools/callsends arguments directly intomcp_operation_sourcewithout a validation step. Bad payloads therefore become Python exceptions such asKeyErrorinstead of structured validation errors. This compounds the existing "Unknown tool" masking issue. -
Where:
agentmemory/mcp.pyandagentmemory/runtime/operation_adapters.py::mcp_operation_source. -
Fix:
agentmemory/mcp.pynow 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 structuredProviderValidationErrortool results.
-
Status: closed
-
Severity: bug
-
Why:
Mem0Provider.list_scopesopens Qdrant'sstorage.sqlite, reads rows from the privatepointstable, and callspickle.loadson 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_payloadsandlist_scopes. -
Fix:
list_scopesnow reads from an AgentMemory-owned SQLite scope registry shared across providers.mem0andlocaljsonupdate the registry on add/update/delete, and legacymem0installs can be migrated with the explicitagentmemory rebuild-scope-registrycommand. The old Qdrantpicklereader remains only as the one-shot rebuild seed path, not a runtime inventory dependency.
-
Status: closed
-
Severity: bug
-
Why: API tests instantiate
BaseHTTPRequestHandlermanually viaHandler.__new__and do not isolateAGENTMEMORY_API_TOKENor OAuth env loaded from a local.env. With auth enabled,_require_authcallssend_responseon a fake handler missing fields such asrequestline, producing unrelated test failures. -
Where:
tests/test_agentmemory_api.py. -
Fix:
tests/test_agentmemory_api.pynow clears API token and OAuth env around each test, restores the caller's environment intearDown, and covers both/healthmodes explicitly: public liveness without auth and registry-backed health when an auth header is present.
-
Status: closed
-
Severity: bug (secret handling)
-
Why:
Mem0Provider._load_memorycallsos.environ.setdefault("OPENAI_API_KEY", api_key)and the same forOPENAI_BASE_URL. The key is already threaded intoconfig["llm"]andconfig["embedder"]a few lines below, so the environment mutation is redundant, but the side effect is real — any child process spawned after_load_memoryruns (for example, MCP clients launched byconnect-clientsor the sweeper thread's downstream helpers) inherits the key, and any library in the same process that readsOPENAI_API_KEYnow sees it. -
Where:
agentmemory/providers/mem0.py::_load_memory(≈ L260). -
Fix:
Mem0Provider._load_memoryno longer writesOPENAI_API_KEYorOPENAI_BASE_URLinto process-wideos.environ. The OpenRouter key is passed explicitly into bothllm.config.api_keyandembedder.config.api_keybeforeMemory.from_config.
-
Status: closed
-
Severity: bug (secret handling)
-
Why:
run_onboardingcallsrun_command(['configure', '--openrouter-api-key', key]). The key lands inargv, which is visible inps aux, Windows Task Manager's command-line column, shell history, CI job logs, and most process auditors. -
Where:
agentmemory/interactive.py::run_onboardingand theconfigurecommand inagentmemory/cli.py. -
Fix:
configurenow supports--openrouter-api-key-stdinand--openrouter-api-key-env NAMEin addition to the legacy argv form.run_onboardingnow 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.
-
Status: closed
-
Severity: bug
-
Why:
_execute_searchand_execute_listapplylifecycle_module.filter_unexpiredto the list returned by the provider afterlimithas already capped it. If the caller asked forlimit=10and 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
searchandlistnow retry with a larger providerlimitwhen TTL filtering removes items from the first batch. This keeps the observablelimitcontract 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.
-
Status: closed
-
Severity: bug (regression risk)
-
Why:
http_client.proxy_addhasinfer=Trueas its default andproxy_searchhasrerank=True. The project-wide defaults were flipped toinfer=Falsein DEFECT-04, andrerankis provider-capability-gated. All current call sites pass kwargs explicitly, so the defaults are inert today — but any future direct caller ofproxy_*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.pynow requires callers to passinfertoproxy_addandreranktoproxy_searchexplicitly. This turns future omissions into immediateTypeErrors 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.
-
Status: closed
-
Severity: bug (local DoS)
-
Why:
AgentMemoryHandler._read_jsonreadsint(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 MiBby default (overridable viaAGENTMEMORY_MAX_BODY_BYTES). Declared oversized bodies are rejected before reading, and bodies withoutContent-Lengthare read with a strict ceiling. HTTP endpoints return413 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.
-
Status: closed
-
Severity: bug (cross-platform correctness)
-
Why:
agentmemory/clients.pydefinesCLAUDE_DESKTOP_CONFIG,VSCODE_MCP,ROO_MCP,KILO_MCP,CLINE_*_MCPasPath.home() / "AppData" / "Roaming" / .... On macOS or Linux this produces paths like~/AppData/Roaming/Code/...— neither a real config location nor an empty miss.connect-clientsanddoctor-clientswill either skip them as non-existent or, worse, create garbage directories. -
Where:
agentmemory/clients.py— module-level path constants (L22–27). -
Fix:
agentmemory/clients.pynow resolves client config paths through platform-aware helpers instead of hardcoded WindowsAppData/Roamingconstants. Windows uses%APPDATA%(fallback~/AppData/Roaming), macOS uses~/Library/Application Support, and Linux uses$XDG_CONFIG_HOME(fallback~/.config).connect-clients,status-clients, anddoctor-clientsnow read those helpers at call time, so non-Windows runs no longer inspect or create fake~/AppData/Roaming/...trees.tests/test_agentmemory_clients.pycovers Windows, macOS, Linux, and the Linux lowercaseclaudefallback for existing setups.
-
Status: closed
-
Severity: bug
-
Why: Two non-owner processes that both call
ensure_api_runningat the same time each seeapi_is_healthy() == False, eachPopena 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 callsstop-apikills 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_runningnow 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.
-
Status: closed
-
Severity: bug
-
Why:
cli.py::stop_api_processusestaskkill /Fon Windows, which skips theSIGTERM-style handler that would otherwise flush the TTL sweeper and remove the PID/state files. The PID/state files are then removed bystop-apiitself, 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_processnow uses a two-phase shutdown path. On Windows it first callstaskkill /PID <pid>without/F, waits for the process to exit, and escalates to/Fonly if the grace period expires. On POSIX it now mirrors the same contract withSIGTERMfollowed by a bounded wait andSIGKILLfallback. Regression tests cover the POSIX path, Windows graceful success, and Windows forced escalation.
-
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 incontent[0].text, withisError: true. CLI printsstr(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.pyerror path (L102–103). -
Fix:
agentmemory/providers/base.pynow exposes the sharedprovider_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 remainerror_typeandmessage.
-
Status: closed
-
Severity: hygiene (observability correctness)
-
Why:
OperationSpec.__post_init__wrapsexecutewithmetrics_registry.timed(name)._execute_addreturns a pre-existing record when_maybe_dedup_existingfires, without inserting. Both paths incrementmemory_add.ok, so the counter is the sum of real inserts plus dedup reads. There is no separate counter for dedup hits, and the silentexcept Exception: return Noneinside_maybe_dedup_existinghides 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.pynow records explicit auxiliary eventsmemory_add.dedup_hit,memory_add.inserted, andmemory_add.dedup_probe_failedthrough the metrics registry. The originalmemory_add.okcounter 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, andtests/test_observability_lifecycle.pycovers dedup-hit and probe-failure paths.
-
Status: closed
-
Severity: bug (latent)
-
Why:
active_provider_runtime_policyinagentmemory/runtime/config.pyis@lru_cache'd. If the owner API process reloads a new config (provider switched frommem0tolocaljson, 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 likePOST /admin/reloadwould expose the divergence immediately. -
Where:
agentmemory/runtime/http_client.py::should_proxy_to_api(L26–33) andagentmemory/runtime/config.py::active_provider_*cached accessors. -
Fix:
agentmemory/runtime/config.pynow 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 manualclear_caches(). Regression tests cover both runtime policy and capability updates after an external config-file rewrite.
-
Priority: P1
-
Status: closed
-
Severity: data-retention bug
-
Why: Runtime
memory_searchandmemory_listfilter 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 throughmemory_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_pageand_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_pageandmemory_list_pagenow filter expired records before returning page payloads while preserving the providernext_cursor. Provider-neutral export also filters expired records when it consumes page readers directly. Regression coverage lives intests/test_observability_lifecycle.pyandtests/test_agentmemory_portability.py.
-
Priority: P1
-
Status: closed
-
Severity: data-retention bug
-
Why: The background sweeper calls
list_scopes(limit=500)and thenlist_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
mem0record 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_atvalue, and the TTL sweeper uses a registry-backed expired-id index instead of walkinglist_scopes(limit=500)pluslist_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.
-
Priority: P2
-
Status: closed
-
Severity: hygiene (scalability)
-
Why:
scope_registry.list_inventoryfetches every registry row for the provider, aggregates buckets in Python, sorts all buckets, and only then appliesitems[:limit]. Small calls such asmemory_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.
-
Priority: P2
-
Status: closed
-
Severity: bug (observability correctness)
-
Why:
list_admin_memoriescallsruntime.config.memory_listandmemory_searchdirectly 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_memoryalso treats expired records as absent. Regression tests cover list, stats, and direct admin-get behavior.
-
Priority: P1
-
Status: closed
-
Severity: hygiene (portability/scalability)
-
Why: Provider-neutral export now uses
list_memories_pagefor paginated record walks, but the first step is stilllist_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_pagewith an opaque cursor and a sharedScopeInventoryPageshape. Update export to walk pages. Keep the existinglist_scopesresponse as the backwards-compatible first-page API. -
Fix: Added provider-neutral
list_scopes_pagewith opaque cursors across provider base contract,localjson,mem0, runtime config, HTTP proxy, operation registry, MCP tool generation, HTTP/admin/scopes/page, and CLIlist-scopes-page. Provider-neutral export now walks scope pages instead of relying on the fixedEXPORT_SCOPE_LIMITguard. The legacylist_scopesresponse shape remains unchanged.
-
Priority: P3
-
Status: closed
-
Severity: bug (API validation)
-
Why: Some HTTP GET handlers parse
limitwith rawint(...). A non-numeric query value raisesValueErrorand falls into the generic exception handler, returning500instead of a structured400validation error. -
Where:
agentmemory/api.py—/admin/stats,/admin/memories, and other directint(params.get(...))query parsing sites. -
Fix outline: Move numeric query parsing into a small shared helper that raises
ProviderValidationErrorwith a clear field name and minimum-value check. Cover affected endpoints with bad-query tests. -
Fix:
agentmemory/api.pynow parses numeric query params through a shared helper that raises typedProviderValidationErrorwith field-specific messages./admin/statsand/admin/memoriesnow return structured400validation payloads for invalidlimitvalues instead of falling through to generic500, and regression tests cover both endpoints.
-
Priority: P2
-
Status: closed
-
Severity: bug (provider compatibility)
-
Why:
ProviderCapabilitiesincludessupports_updateandsupports_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/DeleteResultcontract still applies. If it declares no support, calls must fail consistently withProviderCapabilityError, and certification should treat that as valid. -
Fix:
BaseMemoryProvidernow provides defaultProviderCapabilityErrorimplementations for unsupported update/delete. Runtime operations validatesupports_update/supports_deletebefore 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.
-
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, andagentmemory/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.
-
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, anddocs/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, anddocs/future-memory-providers/README.mdnow document pagination, scope-registry maintenance, degraded registry semantics, and valid unsupported update/delete capability behavior.
-
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. Normallist_scopesuses AgentMemory's SQLite scope registry; the legacy Qdrant reader is only an explicit one-shot rebuild path. -
Where:
docs/WHAT_AGENTMEMORY_ACTUALLY_ADDS.mdscope 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.mdnow describes the AgentMemory-owned scope registry as the normal inventory path and limits backend inspection to explicit legacy rebuild/migration.
-
Priority: P3
-
Status: closed
-
Severity: hygiene (provider compatibility)
-
Why: Interactive setup prompts only advertise
mem0andlocaljson, and unknown input falls back tomem0. 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 tomem0, and delegates provider-specific prompt behavior toBaseMemoryProvider.onboarding_configuration()implementations.
- Priority: P1
- Status: open
- Severity: data-retention
- Why: The backup chain has two independent legs —
0 4 * * *server cron runningdeploy/backup-agentmemory.sh, and a Windows Task Scheduler job at 12:30 local runningpull-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 twocurlcalls and a free account. - Where:
deploy/backup-agentmemory.sh— append acurl -fsS -m 10 "$HEALTHCHECKS_URL" || trueafter the finalecho "Backup written: ..."block. MakeHEALTHCHECKS_URLan 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.
- Priority: P1
- Status: closed
- Severity: security
- Why: The current model accepts
user_id,agent_id, andrun_iddirectly 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 anyuser_idit 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 acceptsuser_idfrom an LLM-controlled input, records can be mis-attributed or read across users. Filter-based isolation byuser_idworks 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 auser_idclaim at token-issuance time (auth code grant) and refuses payloads where the requesteduser_iddoes not match the bound claim. Static API tokens grandfather through. Surface the claim under/.well-known/oauth-protected-resourceand on thememory_healthresponse so clients can introspect. - Where:
agentmemory/oauth.py::issue_access_token— accept and persist abound_user_idper token.agentmemory/api.py::_authorized_bearer— return the claim alongside the token validity bit.agentmemory/runtime/operations.py::_execute_addand friends — when the env flag is set and a claim is present, reject payloads wheresource["user_id"]differs from the claim.- Context:
SESSION_REVIEW_2026-05-29.md§3 F3.
- Resolution:
AGENTMEMORY_ENFORCE_AUTH_USER_ID=1, documented indocs/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 wrapperOperationSpec.__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/deletecarry no scope, so they resolve the stored record's ownuser_idfirst. New shared module:agentmemory/runtime/identity.py. New typed error:ProviderIdentityError→ HTTP 403, sameerror_typeover MCP. - Deliberately not done: the outline also proposed surfacing the claim under
/.well-known/oauth-protected-resourceand onmemory_healthfor 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_idandrun_idremain unbound — no evidenced requirement, and binding them would break multi-agent use under one identity.
- Priority: P2
- Status: open
- Severity: hygiene
- Why: The
memory_typefield is part of theMemoryRecordshape and the input schema accepts it on add, but it is not exposed as a filter onmemory_searchormemory_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 wayuser_id/agent_id/run_idalready are, would give clients a structural way to separate categories of record (factvsepisodevsaggregatevsdraft) without encoding the category as a free-form text prefix inside the body. - Fix outline: Add
memory_typeto the search/list input schemas and propagate it throughvalidate_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 forwardmemory_type.agentmemory/runtime/operations.py::OPERATIONS— addmemory_typeto thesearch/listinput schemas alongside the scope fields.agentmemory/providers/localjson.py::search_memory_page/list_memories_page— add amemory_typepredicate.tests/test_provider_contract_v1.pyand the relevant provider tests — assert the filter narrows results.- Context:
SESSION_REVIEW_2026-05-29.md§4.
- 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 leastDEDUP_SCORE_THRESHOLD(agentmemory/runtime/operations.py, currently hardcoded to0.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_thresholdfield 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_existingand surface the threshold actually used in the returneddedup_scoreenvelope so callers can tell what they got. Optionally: anAGENTMEMORY_DEFAULT_DEDUP_THRESHOLDenv var for an install-wide default different from 0.92. - Where:
agentmemory/runtime/operations.py::DEDUP_SCORE_THRESHOLD/_maybe_dedup_existing— read threshold fromsource["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.mdand the original 2026-05-28 live review notes.
- 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 exposetransformed: true,original_text,stored_text, plus theadditional_recordsarray (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=trueis requested ANDstored_textis materially shorter thanoriginal_text(heuristic: ratio below some threshold, e.g. 0.4, or character count delta above some absolute, e.g. 200 chars), add a fieldcontent_loss_warningto 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 existingtransformed=trueenrichment 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=trueobservability) but a distinct concern.
- 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_conflictsas the primary MCP tool name and keepmemory_reconcileas a deprecated alias. The underlyingOperationSpecstays 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.pyand/oragentmemory/runtime/operations.py— register both names. Most providers register tools by iteratingOPERATIONS— add analiasesfield toOperationSpecor expose a deprecated copy.docs/USE_CASES.md,examples/mcp-demo.md,agentmemory/runtime/operations.pytool 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.
- 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-typedttl_seconds/expires_at" — a single typo in units (6instead of6*60*60*24*180for "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 includesmetadata.ttl_secondsormetadata.expires_at, raiseProviderValidationErrorwith 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) whenAGENTMEMORY_ALLOW_TTLis unset, so the sweeper does not start..env.example— document the variable next to the existingAGENTMEMORY_OAUTH_DISABLE_DCRpattern.README.mdandCHANGELOG.md— note the default change.tests/test_observability_lifecycle.py— existing TTL tests need a setUp that setsAGENTMEMORY_ALLOW_TTL=1so 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.
- 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 attl_seconds: 9_999_999_999is 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_secondsoutside[AGENTMEMORY_MIN_TTL_SECONDS, AGENTMEMORY_MAX_TTL_SECONDS](defaults 60 and 365 * 24 * 3600). Forexpires_at, reject timestamps further thanAGENTMEMORY_MAX_TTL_SECONDSin 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.
- 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, becauseexpires_atis still in the past and the read path filters it. A two-stage cleanup gives a recovery window: stage 1 setsmetadata.archived: trueplusarchived_reason: "ttl_expired"andarchived_at: <iso>(read paths already filter archived records); stage 2 hard-deletes records that have been archived for longer thanAGENTMEMORY_TTL_RECOVERY_DAYS(default 30). An operator who notices unexpected disappearance has 30 days to fliparchived: falseand bring records back. - Fix outline:
agentmemory/runtime/lifecycle.py::run_sweep_once— replace thedelete_memory(id)call with a softerupdate_memory(id, metadata={archived: True, archived_reason: "ttl_expired", archived_at: utc_now()}).- Add
_hard_delete_archived_older_thanhelper 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.
- 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.
- 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 andAGENT_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 ametadata.stale_afterconvention (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 theDATA_DEGRADATION_DESIGNobservability 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
MemoryRecordchecksmetadata.stale_after(orstale_at— pick the canonical name and document it). If present and parseable and in the past, add astale_warningfield to the returned envelope: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{ "id": "...", "memory": "...", "metadata": {...}, "stale_warning": { "stale_since": "2026-05-27T00:00:00+00:00", "days_overdue": 2 } }lifecycle.py::_parse_expires_atcan be reused for the date parsing (canonicalisesZvs+00:00, handles unix seconds). - Where:
agentmemory/runtime/lifecycle.py— add astale_warning_for(record)helper that returns the warning dict orNone.agentmemory/runtime/operations.py::_execute_get,_execute_search,_execute_list(and their_pagevariants) — walk the records, attachstale_warningwhere appropriate.agentmemory/runtime/operations.py::OPERATIONS[…].description— document the new field for MCP consumers.tests/test_observability_lifecycle.pyor a newtests/test_stale_warning.py— cover: warning fires for paststale_after, absent for futurestale_after, absent for missing field, robust to malformed values (no crash, no warning), handles bothZand+00:00ISO forms.- Context:
DATA_DEGRADATION_DESIGN§3.3, recommended sequence Step 1.
- 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_similarfield on the add input:Whenon_similar: "warn" | "skip" | "merge_metadata" on_similar_threshold: 0.80 # default if 'warn'warn, the runtime runs a single semantic search in the same scope before insert, returns the matching candidate (if any above the threshold) on asimilar_existingfield of the response, and still performs the insert. The caller can then decide whether to delete/update what was just written.skipreturns the existing record (like dedup but at the user's chosen threshold).merge_metadataperforms 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_addand 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.
- 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:Authenticated under the existing admin gating. Read-only, no mutation.{ "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} } - Where:
agentmemory/runtime/admin.py— newpool_health()function with the SQL aggregations againstscope_registry.agentmemory/api.py::do_GET— route/admin/pool-health.tests/test_agentmemory_admin.py— cover the aggregations.- Context:
DATA_DEGRADATION_DESIGN§3.3.
- 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— newOPERATIONS["find_duplicates"]entry.- Provider contract —
find_duplicates_in_scope()method, default implementation inBaseMemoryProviderthat 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.
- 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: trueto retire chunks,update_memoryto 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.
- DEFECT-01 —
reranknot capability-aware: fixed inagentmemory/runtime/transport.py::validate_and_build_search_kwargs. Covered by tests intests/test_defect_fixes.pyandtests/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) plusMem0Provider._map_exception(message-pattern →MemoryNotFoundError). - DEFECT-04 — silent LLM rewrite on
memory_add: fixed by flipping default toinfer=falseand surfacingtransformed/original_text/stored_textwheninfer=truediffers. - DEFECT-05 — mem0 contract advertising sentinels: fixed in
Mem0Provider.provider_contractandBaseMemoryProvider.provider_contract.
-
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/authorizeapproves 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 unlessAGENTMEMORY_OAUTH_DISABLE_DCR=1and 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 withbound_user_id: None, thenPOST /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:
- Disable DCR by default. Breaks self-registering remote MCP clients (Claude.ai, ChatGPT connectors), which is why it is on.
- 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. - Assign an identity at authorize time from operator configuration, refusing to issue tokens to clients with no configured binding while the mode is on.
- 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.mdalready says it is not; that wording should survive whatever is decided here. -
Related: item 37 (closed), commit
2ddd6d4.