Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
## [Unreleased]

### Weekly research update (2026-08-01)
- **OpenAI base URL env-var fallback**: ``OpenAICompatProvider`` now
reads ``OPENAI_BASE_URL`` (and the legacy ``OPENAI_API_BASE``) when
the constructor argument is empty, so a self-hosted OpenAI-
compatible proxy (vLLM, LM Studio, llama.cpp server, OpenRouter)
works without ``--base-url`` on the CLI. Mirrors the official
OpenAI SDK env-var name and the convention every other
OpenAI-compatible provider in the Mem0 / LiteLLM ecosystem now
uses (see `mem0ai/mem0#6322`). Explicit constructor argument
still wins, so every existing call site and test is unchanged.
Pinned by four new cases in
``tests/test_llm_providers.py::OpenAICompatTests``.
- **Pre-push secret scanner drift fix**: ``scripts/scan_secrets.py``
was missing two patterns the in-process redaction layer
(``loop_memory/privacy/redact.py``) has shipped for a while — JWT
(``eyJ…eyJ…{sig}``) and ``Authorization: Bearer …``. The local
pre-push gate can now be silently no less strict than the runtime
redactor. Pinned by
``tests/scripts/test_scan_secrets.py`` (pattern catalogue,
placeholder filter, end-to-end scan).
- **Ecosystem research report**: ``docs/research/2026-08-01.md``
documents the adopt / defer / reject decisions from the
2026-07-25 → 2026-08-01 agent-memory survey. Mem0 PRs #6322,
#6700, #6701, #6703, #6704, #6705, #5980-#5988, #4878 reviewed.
OpenMemory PRs #191, #194 reviewed. Graphiti v0.29.3 reviewed.
Cognee v1.4.0 / v1.4.1 reviewed. Letta and langchain-ai/langmem
had no material code activity in the window. See report for
primary-source URLs and per-item rationale.

### Wiki preview modal (2026-07-31)
- **Knowledge preview popup**: clicking the card title (or the new
preview button) opens a read-only modal overlay with the page's
Expand Down
22 changes: 22 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ The selection chain at LLM-call time is:
`echo` rules engine so the UI stays usable offline. A red dot on the
top-bar **Models** chip indicates this fallback.

### `OPENAI_BASE_URL` for self-hosted proxies

The OpenAI-compatible client also reads `OPENAI_BASE_URL` (and the
legacy `OPENAI_API_BASE`) as a fallback when the constructor
argument is empty. Set either variable to point the LLM adapter at a
self-hosted proxy without touching the CLI:

```bash
# vLLM, LM Studio, llama.cpp server, OpenRouter, etc.
export OPENAI_BASE_URL="http://127.0.0.1:8000/v1"
export OPENAI_API_KEY="not-needed-for-local"
loop-memory consolidate-now
```

The explicit `base_url` argument (and the provider's
`default_base_url`) still win when set, so every existing script
and config keeps working. Pinned by
`tests/test_llm_providers.py::OpenAICompatTests`. Mirrors the
official OpenAI SDK env-var name and the convention the rest of
the OpenAI-compatible ecosystem has converged on (see
`mem0ai/mem0#6322`).

## Token limits (v2)

The default behaviour block is tuned for the new "completeness over
Expand Down
290 changes: 290 additions & 0 deletions docs/research/2026-08-01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
# 2026-08-01 — Weekly agent-memory ecosystem research

> **Scope.** Survey of public releases, changelogs, issues, and engineering
> blogs from roughly 2026-07-25 → 2026-08-01 that are relevant to Loop
> Memory. Each item lists the primary source, publication date, license
> (where applicable), and an explicit `adopt` / `defer` / `reject`
> decision with a one-line rationale. No project secrets, user paths,
> API keys, transcripts, or credential stores were consulted.
>
> **Method.** GitHub Releases & Issues APIs for Mem0, Letta, Zep, Graphiti,
> Cognee, OpenMemory, and langchain-ai/langmem; HN Algolia search for
> peer projects published in the same window. Primary sources only —
> no marketing reposts.

## Primary sources consulted

| Project | Source | License | Last activity in window |
| --- | --- | --- | --- |
| Mem0 | `github.com/mem0ai/mem0` (Releases, Issues, PRs) | Apache-2.0 | 2026-08-01 |
| Letta | `github.com/letta-ai/letta` (Releases, commits) | Apache-2.0 | 2026-07-30 |
| Zep / Graphiti | `github.com/getzep/graphiti` & `…/zep` | Apache-2.0 | 2026-07-30 |
| LangMem | `github.com/langchain-ai/langmem` (commits) | MIT | 2026-07-25 |
| OpenMemory | `github.com/CaviraOSS/OpenMemory` (PRs, commits) | MIT | 2026-07-28 |
| Cognee | `github.com/topoteretes/cognee` (Releases) | Apache-2.0 | 2026-07-31 |

## Material findings

### 1. Mem0 self-hosted server ↔ SDK default LLM drift (2026-08-01)

- **Source.** PR `mem0ai/mem0#6704` "fix: align default model names with
SDK defaults" merged 2026-08-01. Self-hosted `server/main.py` shipped
`gpt-4.1-nano-2025-04-14` while every SDK default was already
`gpt-5-mini` and the docs advertised `gpt-5-mini`. PR also refreshed
four stale docstring/help-text strings.
- **License.** Apache-2.0.
- **Applicability to Loop Memory.** Loop Memory's
`loop_memory/llm/providers.py` ships `default_model="gpt-4o-mini"`
for OpenAI and `"claude-3-5-haiku-latest"` for Anthropic. Both
resolve and the model picker is user-overridable, so the server /
SDK split that bit Mem0 (server pinned one value, SDKs pinned
another) does not exist here. The two string literals are still
"real" model ids and work today.
- **Decision.** `defer`. No behavioural bug, no user-visible drift
between CLI defaults and LLM adapter defaults. Revisit next cycle
only if either id is retired upstream (the Gemini issue in the same
week, where `gemini-2.0-flash` was retired on 2026-06-01, is the
kind of trigger worth watching).

### 2. Mem0 OpenClaw auto-capture drops `run_id` (2026-08-01)

- **Source.** Issue `mem0ai/mem0#6703` (and PR `…#6703`) opened
2026-08-01 — `buildAddOptions` only sets `user_id` and `agent_id`
for OpenClaw's auto-capture, silently dropping `run_id`, so
multi-tenant recall cannot isolate a single run.
- **License.** Apache-2.0.
- **Applicability.** Loop Memory's `ingest/loader.py` and
`cli/commands/diag.py` `openclaw-setup` watcher already keep
`source = "openclaw"` and don't currently propagate a per-run
id. Today every OpenClaw transcript is written to the same
`source`-scoped wiki/memory bucket, so a per-run split is not yet
a real user-visible need. The two loaders do differ (clawx
session files vs. daily log markdown) and merging them under one
id has been deferred to the user-feedback channel.
- **Decision.** `defer`. No regression today; revisit when the
per-run split becomes a real product request.

### 3. Mem0 reranker doc drift — labelled "LLM Score" but printed vector score (2026-07-31)

- **Source.** Issue `mem0ai/mem0#6705` — the docs for HuggingFace and
`llm_reranker` print `result["score"]` and label it as the
reranker's score, when the actual reranker score is in
`result["rerank_score"]`. Vector score never changed.
- **License.** Apache-2.0.
- **Applicability.** Loop Memory's web UI shows the `score` field
from `store.recall()` and labels it "score" in
`loop_memory/serve/static/js/components/`. The numeric value comes
from a single fused computation (token hits × importance ×
recency × usage), not two separate channels, so there is nothing
to mislabel.
- **Decision.** `reject`. The bug pattern doesn't exist in Loop
Memory; the score field is fused, not split.

### 4. Mem0 — multiple vector-store filter-injection fixes (2026-07-25 → 2026-07-31)

- **Source.** PRs `mem0ai/mem0#4878` (PGVector, Azure MySQL,
Neptune, Neptune Analytics), `#5986` (OpenSearch), `#5980`
(Elasticsearch term queries), `#5982` (Neptune openCypher),
`#5983` (Azure AI Search OData), `#5988` (Databricks
catalog/schema/table).
- **License.** Apache-2.0.
- **Applicability.** Loop Memory is a SQLite-only stack. Every
read and write path uses `?`-parameterised queries (see
`loop_memory/storage/sqlite_store.py:1147+` for the helper
`_like_clause`, plus every cursor call site in the same file).
There is no vector store, no graph store, no SQL string
concatenation in the search hot path. The pattern is already
covered.
- **Decision.** `reject`. Already addressed; nothing to lift.

### 5. Mem0 — pagination `delete_all` regression (2026-07-30 → 2026-07-31)

- **Source.** PRs `mem0ai/mem0#6701`-series — `delete_all()` only
read the first page of `list()`, so a vector store with more
than the default page size left orphan vectors. Backported to TS.
- **License.** Apache-2.0.
- **Applicability.** Loop Memory's `MemoryStore.delete_all()` is
a single `DELETE FROM <table>` per table inside one
`with self._conn() as c:` block, so there is no pagination to
miss. The 7 PRs around it were Mem0/vector-store-specific.
- **Decision.** `reject`. Pagination does not exist in the
delete path; nothing to copy.

### 6. Mem0 — `OPENAI_BASE_URL` env var name (2026-07-22)

- **Source.** PR `mem0ai/mem0#6322` — `OpenAIStructuredLLM` now
reads `OPENAI_BASE_URL` (was `OPENAI_API_BASE`) to match the
official OpenAI SDK and the rest of the OpenAI-compatible
providers.
- **License.** Apache-2.0.
- **Applicability.** Loop Memory's `OpenAICompatProvider` reads
`OPENAI_API_KEY` and `LOOP_MEMORY_API_KEY` (see
`loop_memory/llm/providers.py:~178`) but does not currently read
`OPENAI_BASE_URL` for the OpenAI endpoint itself — the
`base_url` is constructor-arg only. Users running a self-hosted
OpenAI-compatible proxy currently have to pass `--base-url` on
the CLI.
- **Decision.** `adopt` (low-risk, one-line env-var fallback). See
**Code change** below.

### 7. OpenMemory — Chinese memory sector classification (2026-07-28)

- **Source.** PR `CaviraOSS/OpenMemory#194` "Add Chinese memory
sector classification" merged 2026-07-28. Adds Chinese patterns
to the five-sector (episodic / semantic / procedural / emotional
/ reflective) classifier that was previously English-only.
- **License.** MIT.
- **Applicability.** Loop Memory does not have a "memory sector"
abstraction; the analogous concept is `kind="fact" | "summary"`
in `MemoryItem`, and the distilled knowledge is grouped by
`slug` (e.g. `preferences-…`, `decision-…`, `project-…`)
through the `wiki_system_prompt()` schema, not by sector.
Chinese / English / Japanese output locale IS already
first-class in `loop_memory/wiki/prompts.py`.
- **Decision.** `defer`. Not a gap in Loop Memory's design — the
current shape (fact/summary + slug-dimensioned wiki) is a
different abstraction, and lifting the rule list verbatim
would create a parallel "sector" dimension that nobody asked
for. Re-evaluate if Loop Memory adds multi-sector memory items
later.

### 8. OpenMemory — `opm` CLI argument parsing crash (2026-07-22)

- **Source.** PR `CaviraOSS/OpenMemory#191` "fix(cli): repair
opm query and stop flags leaking into memory text". Three bugs:
`query` read `r.memories` instead of `r.matches`; positional
text was `argv.slice(1, argv.indexOf('--'))` which silently
drops the last arg when `--` is absent; `--help` was stored as
memory content.
- **License.** MIT.
- **Applicability.** Loop Memory's CLI (`loop_memory/cli/main.py`
+ `commands/*.py`) takes positional text in `run_recall`,
`run_ask`, and `run_inject`. None of those three commands
defines its own `--flag` set, so the "flag names leaking into
positional text" bug does not exist. The unknown-flag handling
is centralised in `main.main()` (returns `2` on unknown
command, prints `__doc__` on `-h/--help`).
- **Decision.** `reject`. Bug pattern is not present.

### 9. Graphiti — FalkorDB fixes and reranker provider selection (2026-07-27 → 2026-07-30)

- **Source.** Release `getzep/graphiti@v0.29.3` (2026-07-27) plus
MCP PR `…#1698` (2026-07-30) adding explicit `reranker.provider`
configuration (auto / openai / azure_openai / gemini / bge).
- **License.** Apache-2.0.
- **Applicability.** Loop Memory does not ship a graph DB driver
and does not consume a graph-reranker API. The selection logic
is interesting (auto-fallback chain LLM → embedder → local
BGE), but adding it would mean introducing a new optional
dependency, which the `CONTRIBUTING.md` ground rules
explicitly forbid.
- **Decision.** `defer`. Architectural mismatch + dependency
growth prohibition.

### 10. Cognee — search result previews + clearer ingestion errors (2026-07-31)

- **Source.** Release `topoteretes/cognee@v1.4.1` "Reliability
& Search Improvements". Adds short preview snippets to search
results and clearer, actionable ingestion error messages.
- **License.** Apache-2.0.
- **Applicability.** Loop Memory's `store.recall()` already
returns a `preview` field on every memory / wiki row
(truncated to 240 chars at write time in
`loop_memory/storage/sqlite_store.py`), and the web UI Timeline
pane renders it. The 240-char cap is conservative; users with
Chinese text occasionally see truncation mid-character (the
slice is a Python byte/char count, not a grapheme cluster
count). A Cognee-style "preview" is therefore not a feature
gap, but the boundary handling is.
- **Decision.** `defer` (recorded; tracked under the existing
`test_memories_pagination.py` scope). Grapheme-aware
truncation deserves a separate, more involved change.

### 11. Letta — single commit in the window (2026-07-30)

- **Source.** `letta-ai/letta#3418` "Update AI_POLICY.md". No code
changes in the last 7 days.
- **License.** Apache-2.0.
- **Applicability.** None.
- **Decision.** `reject` (nothing to lift).

### 12. langchain-ai/langmem — only Dependabot bumps (2026-07-25)

- **Source.** Last 3 commits on `langchain-ai/langmem` are
`Build(deps): Bump the uv group across 1 directory with 2
updates`.
- **License.** MIT.
- **Applicability.** None — pure dependency bumps in transitive
libraries.
- **Decision.** `reject`.

## Smaller observations (no action)

- **Mem0 PR #6700 (2026-08-01)** — TS `createWebhook` now accepts
an explicit `projectId`; `deleteWebhook` rejects empty
webhookId. Not applicable: Loop Memory's webhook surface is
one integration (`install-hooks`) and goes through the local
CLI, not a web client.
- **Mem0 PR #6506 (2026-07-25)** — `parse_vision_messages` no
longer drops system messages; image description uses the
configured LLM. Not applicable: Loop Memory's ingest path is
text-only by design (the loader contract treats transcripts
as plain text).
- **HN noise in the window.** Several "Show HN" memory layers
(`MemoryStack`, `Mwe-MCP`, `Sekha`, `Moltis`, `Memory.plugin`,
`MenteDB`, `Forensic`) all claim a LoCoMo / LongMemEval
number without a reproducible harness. Treat as
non-actionable until each ships a public eval suite.

## Code change this week

One small, high-confidence improvement, motivated by Mem0 PR
`#6322` (item 6):

- **`loop_memory/llm/providers.py`** — `OpenAICompatProvider`
now reads `OPENAI_BASE_URL` (after the constructor argument)
so a self-hosted OpenAI-compatible proxy (vLLM, LM Studio,
llama.cpp server, OpenRouter) works without the `--base-url`
flag. The existing `base_url` argument still wins, preserving
every existing call site and test. Same precedence ordering
the official OpenAI SDK uses.

A parallel improvement in `scripts/scan_secrets.py` extends the
local pre-commit secret scanner with two patterns the
in-process redaction layer (`loop_memory/privacy/redact.py`) has
shipped for a while — JWT (`eyJ…eyJ…{sig}`) and `Bearer …` in
HTTP headers — so the local pre-push gate can no longer be
silently less strict than the runtime redactor. New
`tests/scripts/test_scan_secrets.py` pins both positive and
negative cases.

Both changes are minimal, dependency-free, and exercised by new
pytest cases.

## Items intentionally not adopted this cycle

- **LangGraph / Letta / Cognee rerankers and graph drivers.**
Would force new optional dependencies and a new write path
(`graph` tables exist in Loop Memory but only as a lightweight
`entities` / `entity_mentions` SQLite pair). Not justified by
the current recall quality.
- **OpenMemory Chinese sector classifier.** Different
abstraction; would be a parallel dimension.
- **Switching the SQLite store to MySQL/Postgres-backed
vector search.** Out of project scope (MIT core stays
zero-dep at runtime).
- **Updating default model names to `gpt-5-mini` / a current
Anthropic haiku.** No behaviour bug today; the id picker is
user-overridable.
- **i18n / XSS hardening items from `docs/development-plan.md`.**
Larger, documented work; will be tracked via separate PRs.

## Provenance

- All URLs are public primary sources (GitHub release pages,
GitHub PR / issue threads, GitHub commit history).
- No private repos, internal docs, customer data, transcripts,
or credential stores were accessed.
- The `~/.loop_memory/`, `~/.codex/`, shell history, `.env`
files, and keychain entries are all out of scope per the
weekly-research safety rules.
11 changes: 10 additions & 1 deletion loop_memory/llm/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,16 @@ def __init__(
) -> None:
self.model = model
self.api_key = api_key or os.environ.get("OPENAI_API_KEY") or os.environ.get("LOOP_MEMORY_API_KEY")
self.base_url = (base_url or "https://api.openai.com/v1").rstrip("/")
# OPENAI_BASE_URL lets users point the OpenAI-compatible client at
# a self-hosted proxy (vLLM, LM Studio, llama.cpp server,
# OpenRouter) without passing --base-url. Mirrors the official
# OpenAI SDK env-var convention and matches the other
# OpenAI-compatible providers that already read this variable
# (see mem0ai/mem0#6322). Explicit constructor argument still
# wins, so every existing call site is unchanged.
env_base_url = os.environ.get("OPENAI_BASE_URL") or os.environ.get("OPENAI_API_BASE")
effective_base_url = base_url or env_base_url or "https://api.openai.com/v1"
self.base_url = effective_base_url.rstrip("/")
self.timeout = timeout

def complete(self, history: ChatHistory, **kwargs) -> str:
Expand Down
Loading
Loading