Releases: heichaowo/amem
Release list
openclaw-amem@1.3.0
Minor Changes
-
#63
9b22d73Thanks @heichaowo! - Let a host choose the engine's LLM provider, model and endpoint (Story 35).The engine's own LLM settings were frozen at module load:
PROVIDERandMODEL
were top-level consts, so the only way to change them was to set an environment
variable before the process started. A host embedding the engine had no way in.They now resolve per call, and
configureLlm({ provider, model, baseURL })lets a
host set them after import. The OpenClaw plugin wires this to three new config
keys —llmProvider,llmModel,llmBaseURL— soopenclaw.jsoncan point
amem's note construction, linking and evolution at a different model than the one
your agent session uses. Precedence, highest first: environment variable, then
plugin config, then the built-in default per provider. Configure nothing and
behaviour is exactly as before.Two deliberate choices worth naming. There is no way to inject an API key:
keys come from the environment only. Configuration arrives from a host config
file, and a key field would make the memory engine a channel for a user's gateway
credentials — endpoint and model are enough to route a call. And an environment
variable set to the empty string now counts as unset, so an exported-but-blank
AMEM_LLM_MODELcan no longer silently outrank a valid configured model.The engine still does not follow whichever model your agent session is using;
that needs host APIs this change does not depend on, and is tracked separately.
Following it is also not obviously desirable — these are cheap, high-frequency
utility calls, and inheriting a large reasoning model would make every memory
write slow and expensive. -
#71
78f2190Thanks @heichaowo! - Split the engine's LLM calls into afastand an optionalstrongtier (Story 42, PR 1/2).Published results are consistent that memory quality is mostly architecture-bound:
for fact extraction a cheap model scores within ~2 points of a strong one, and
retrieval method moves accuracy far more than write strategy does. There is one
exception — judging whether new information contradicts what is stored, where
the cheap/strong gap is large.So the calls now split by how hard they actually are.
fastruns note
construction, link judgement, neighbourhood refresh and the per-turn CRUD
decision.strongruns only merge adjudication and EVOLVE/CONFLICT/EXPAND/NEW
classification.Configure nothing and nothing changes.
strongfalls back tofastfield by
field, so the three useful shapes all work: set onlyllmStrongModelfor "same
endpoint, better model"; set all threellmStrong*fields to run the tiers on
entirely different backends (a local Ollama forfast, a hosted API for
strong); set none and the engine behaves exactly as before. There is
deliberately no built-in strong default — inventing one would start spending an
existing user's money without them asking.New config:
llmStrongProvider/llmStrongModel/llmStrongBaseURLand
llmCrudRole, plusAMEM_LLM_STRONG_PROVIDER/AMEM_LLM_STRONG_MODEL/
AMEM_LLM_STRONG_BASE_URL/AMEM_LLM_CRUD_ROLE.The CRUD decision defaults to
fasteven though it is a contradiction judgement:
it runs every turn, and its one destructive failure mode — overwriting the wrong
memory — is already handled architecturally by the update guard rather than by
model tier.llmCrudRole: "strong"moves it for operators who prefer that.SDK clients are now cached per base URL instead of as singletons, since the two
tiers may point at different backends. -
#74
c4f3e91Thanks @heichaowo! - Addsubjects— who a memory is about (Story 44).agent_idsays whose memory store a note lives in. It never said who the note
is about. For a companion that meets several people those are different
questions, and without the second one every player's memories land in the same
pool and contaminate each other's retrieval.Every note now carries
subjects, a list:[]— a fact about the world, or about the character itself. Visible to everyone.["alex"]— about one person. Surfaced only when scoped to them.["alex", "sam"]— a shared experience. Surfaced for either of them.
A list rather than a single value because shared experience is the normal case
for a companion, not an edge case: "we beat the dragon together" belongs to both
people, and splitting it into two near-identical notes would only give the
deduplicator something to merge back. The three-way visibility rule then falls
out of the shape, with no extra mode switch.searchMemory(query, topK, agentId, { subject })and thememory_search/
memory_addtools expose it. Scoping is applied inside the vector-store query,
so an out-of-scope memory is never fetched — and it is applied to BOTH retrieval
paths, semantic and keyword, since scoping only the vector side would leak
another person's memories through BM25.subjectsdefaults to empty, so existing memories are all world facts and stay
visible exactly as before; omittingsubjecton a search is today's behaviour.
Patch Changes
-
#60
d903552Thanks @heichaowo! - Enforce thewritersaccess rule on every write path (Access Protocol, Story 33).Notes have carried
owner/readers/writerssince per-agent isolation landed,
but onlyreaderswas enforced. Because the agent filter matches
agent_id == caller OR agent_id == 'shared', every query can return another
agent's shared note — and each mutation then wrote to it unchecked. An audit of
the engine found eight such write sites: high-similarity dedup, bidirectional
link generation, evolution (neighbour rewrite and strengthen), the plugin's
agent_end CRUD update and delete, the quality scan, and consolidation's link
rewriting. In the worst of them, one agent's write could silently overwrite the
content and embedding of another agent's shared memory.All eight now gate on a new exported rule,
canWrite(note, callerAgentId)— true
for the owner, an agent listed inwriters, orwriters: ['*']. Denial degrades
gracefully and never throws: dedup inserts the caller's own note instead of
overwriting, back-links and evolution skip that note, CRUD ops are logged and
skipped, and the quality scan no longer flags notes it cannot act on.
updateNoteContentandinvalidateNotetake an optionalcallerAgentIdfor
callers that hold only an id (they returnfalse, unwritten, when denied);
omitting it preserves existing behaviour, so consolidation and merge — already
scoped to their own private notes — are unchanged. -
#64
634d280Thanks @heichaowo! - Harden LLM response parsing and add a request timeout (Story 40, mem0 取经).Three robustness fixes for the engine's LLM layer, all in
llm.ts, prompted by
reading how mem0 handles the same problems:-
Strip reasoning scaffolding before JSON.parse. The engine accepts any
OpenAI-compatiblebaseURL, so it can be pointed at reasoning/open-weight
models (DeepSeek-R1, Qwen, LLaMA-3 via Ollama/vLLM) that wrap their output in
<think>…</think>blocks and chat special tokens (<|eot_id|>,<|im_end|>,
…). Those brokeJSON.parse, and every JSON task silently fell back to its
blank default on an otherwise-valid response — with nothing in the logs to say
why.stripReasoning()now removes them first, instripFences(covering the
five object-JSON tasks) and on the CRUD array path. This was a latent silent
degradation, not just a nicety. -
Tolerate a preamble before the JSON object.
parseJsonLoose()replaces the
four directJSON.parse(stripFences(raw))calls: on a parse failure it retries
against the first{…}region, recovering the common "Sure! Here is the JSON:"
preamble smaller models emit. It still throws when nothing parses, so every
caller's existing try/catch → safe-default path is unchanged. The CRUD path
already did array extraction and is untouched. -
Configurable client timeout. The SDK clients were built with no timeout, so
a slow or stuck endpoint (loaded vLLM, unreachable gateway) could hang the whole
addMemorypipeline indefinitely. NewAMEM_LLM_TIMEOUTenv var (default
30000 ms) andLlmConfig.timeoutMs, threaded into both client constructors.
No behaviour change for a well-formed response from a normal model. New tests
drive the real note-construction and CRUD functions with mocked SDKs; the
reasoning-strip and preamble-recovery tests were verified to fail against mutated
source (a no-opstripReasoning, a dropped brace fallback), so they are not
vacuous.Deliberately NOT changed, after comparing with mem0: the CRUD integer-index
referencing (amem's JSundefined+if (target)guard is already safer than
mem0's unguarded index), the three-layer dedup (stronger than mem0 v3's single
hash pass), and retry/fallback-on-error (mem0 has none either — a real runtime
fallback is designed separately, with Story 39). -
-
[#6...
@heichaowo/amem-core@0.4.0
Minor Changes
-
#60
d903552Thanks @heichaowo! - Enforce thewritersaccess rule on every write path (Access Protocol, Story 33).Notes have carried
owner/readers/writerssince per-agent isolation landed,
but onlyreaderswas enforced. Because the agent filter matches
agent_id == caller OR agent_id == 'shared', every query can return another
agent's shared note — and each mutation then wrote to it unchecked. An audit of
the engine found eight such write sites: high-similarity dedup, bidirectional
link generation, evolution (neighbour rewrite and strengthen), the plugin's
agent_end CRUD update and delete, the quality scan, and consolidation's link
rewriting. In the worst of them, one agent's write could silently overwrite the
content and embedding of another agent's shared memory.All eight now gate on a new exported rule,
canWrite(note, callerAgentId)— true
for the owner, an agent listed inwriters, orwriters: ['*']. Denial degrades
gracefully and never throws: dedup inserts the caller's own note instead of
overwriting, back-links and evolution skip that note, CRUD ops are logged and
skipped, and the quality scan no longer flags notes it cannot act on.
updateNoteContentandinvalidateNotetake an optionalcallerAgentIdfor
callers that hold only an id (they returnfalse, unwritten, when denied);
omitting it preserves existing behaviour, so consolidation and merge — already
scoped to their own private notes — are unchanged. -
#63
9b22d73Thanks @heichaowo! - Let a host choose the engine's LLM provider, model and endpoint (Story 35).The engine's own LLM settings were frozen at module load:
PROVIDERandMODEL
were top-level consts, so the only way to change them was to set an environment
variable before the process started. A host embedding the engine had no way in.They now resolve per call, and
configureLlm({ provider, model, baseURL })lets a
host set them after import. The OpenClaw plugin wires this to three new config
keys —llmProvider,llmModel,llmBaseURL— soopenclaw.jsoncan point
amem's note construction, linking and evolution at a different model than the one
your agent session uses. Precedence, highest first: environment variable, then
plugin config, then the built-in default per provider. Configure nothing and
behaviour is exactly as before.Two deliberate choices worth naming. There is no way to inject an API key:
keys come from the environment only. Configuration arrives from a host config
file, and a key field would make the memory engine a channel for a user's gateway
credentials — endpoint and model are enough to route a call. And an environment
variable set to the empty string now counts as unset, so an exported-but-blank
AMEM_LLM_MODELcan no longer silently outrank a valid configured model.The engine still does not follow whichever model your agent session is using;
that needs host APIs this change does not depend on, and is tracked separately.
Following it is also not obviously desirable — these are cheap, high-frequency
utility calls, and inheriting a large reasoning model would make every memory
write slow and expensive. -
#62
8da3791Thanks @heichaowo! - Enforce thereadersaccess rule on reads by id (Access Protocol, Story 36).Story 33 closed the write half of the protocol. The read half had one hole left:
getNote(id)fetches straight by UUID, so unlike every list and search path it
never went through theagent_idfilter and never consultedreaders. That
mattered because of how shared notes link: a shared note is returned to every
agent by every query, and itslinks[]can name its owner's private notes.
Evolution walks that neighbourhood and puts each neighbour's content into the
LLM prompt — so one shared note could carry its owner's private memory out to
any agent that linked to it.getNotenow takes an optionalreaderAgentId. When passed, an unreadable note
comes back asnull— indistinguishable from missing, so nothing leaks, and
every existing caller already handlesnull. Omitting it skips the check, so
internal callers that only ever hold their own ids are unchanged. The two
neighbourhood walks in evolution now pass the caller: the link expansion that
feedsllmEvolveNote, and the strengthen step, whose target ids come from the
model rather than from a filtered query.The rule ships as
canRead(note, callerAgentId)alongsidecanWrite— true for
the owner, an agent listed inreaders, orreaders: ['*']. Read and write are
independent: a shared note is typically public to read and closed to write.
Search and list paths were audited and need no change; they build their working
set from the agent-filteredlistNotes, so they cannot materialise a note the
caller may not see. -
#68
f52a083Thanks @heichaowo! - Make the CRUDUPDATEpath non-destructive (Story 41).When the
agent_endhook decides an existing memory should be updated, it picks
one from a numbered candidate list. Picking the wrong number was the engine's one
silent, unrecoverable failure: the index is valid and the note is usually one the
caller owns, so neither bounds-checking nor the access protocol catches it — and
updateNoteContentoverwrites content and embedding in place. (DELETEwas
never affected;invalidateNoteis a soft delete.)This is a documented failure class, not a hypothetical. mem0 removed its own CRUD
step partly because "overwrites sometimes erased key information from the original
fact", and Memory-R1 exists because vanilla LLMs mis-classify additive facts as
contradictions. The risk scales inversely with model capability, and the notes
reaching this step have already survived hash and vector dedup — the hardest
subset, exactly where a cheap model is least reliable.Two guards, both default-on:
isPlausibleUpdateTarget— before overwriting, check the replacement text is
plausibly about the memory it replaces (cosine ≥crudUpdateMinSim, default
0.35). Both embeddings are already in hand, so this costs one dot product and
no LLM call. On failure the fact is stored as a new memory instead: nothing
is lost, and consolidation can merge a duplicate later — whereas it can never
resurrect an overwritten note. Tunable viaAMEM_CRUD_UPDATE_MIN_SIMor the
crudUpdateMinSimplugin config; raise it for cheaper models.- History snapshot — an accepted overwrite now records the replaced text in the
note'sevolution_history(action: "crud_update", newoldContentfield), so
even a false negative stays recoverable. Only the caller-scoped path pays for
this; the dedup and merge paths pass nocallerAgentIdand are unchanged, so
they take no extra read.
The threshold is a heuristic, not a tuned constant — it sits just above the
0.3
bar the engine already uses for "related at all", because a legitimate update is
often a correction ("drinks tea" → "switched to coffee") that is related but not
near-identical. Set it to0to disable the check deliberately. -
#71
78f2190Thanks @heichaowo! - Split the engine's LLM calls into afastand an optionalstrongtier (Story 42, PR 1/2).Published results are consistent that memory quality is mostly architecture-bound:
for fact extraction a cheap model scores within ~2 points of a strong one, and
retrieval method moves accuracy far more than write strategy does. There is one
exception — judging whether new information contradicts what is stored, where
the cheap/strong gap is large.So the calls now split by how hard they actually are.
fastruns note
construction, link judgement, neighbourhood refresh and the per-turn CRUD
decision.strongruns only merge adjudication and EVOLVE/CONFLICT/EXPAND/NEW
classification.Configure nothing and nothing changes.
strongfalls back tofastfield by
field, so the three useful shapes all work: set onlyllmStrongModelfor "same
endpoint, better model"; set all threellmStrong*fields to run the tiers on
entirely different backends (a local Ollama forfast, a hosted API for
strong); set none and the engine behaves exactly as before. There is
deliberately no built-in strong default — inventing one would start spending an
existing user's money without them asking.New config:
llmStrongProvider/llmStrongModel/llmStrongBaseURLand
llmCrudRole, plusAMEM_LLM_STRONG_PROVIDER/AMEM_LLM_STRONG_MODEL/
AMEM_LLM_STRONG_BASE_URL/AMEM_LLM_CRUD_ROLE.The CRUD decision defaults to
fasteven though it is a contradiction judgement:
it runs every turn, and its one destructive failure mode — overwriting the wrong
memory — is already handled architecturally by the update guard rather than by
model tier.llmCrudRole: "strong"moves it for operators who prefer that.SDK clients are now cached per base URL instead of as singletons, since the two
tiers may point at different backends. -
[#72](https://github....
openclaw-amem@1.2.2
Patch Changes
- #57
eac161dThanks @heichaowo! - Replace the agent_end hook self-check with a deterministic config check. It used a
10-minute timer to guess whether the hook was "blocked", which mis-fired on an idle
gateway that had simply had no conversation, and only surfaced in the gateway log
(seen viaopenclaw completion --write-state). The plugin now reads the actual flag —
plugins.entries.<id>.hooks.allowConversationAccess— from the full OpenClaw config
at startup, so it knows for certain whether automatic memory write-back is on: no
timer, no heuristic, no idle false positives. When it is off, it logs once at startup
and appends a clearer, actionable notice to memory_search results so the assistant
relays it to the user. It stays silent if the config can't be read.
openclaw-amem@1.2.1
Patch Changes
-
#51
79075a6Thanks @heichaowo! - Declare the OpenAI-provider capability surface in the plugin manifest.0.3.0
added an OpenAI-compatible LLM path (readingAMEM_LLM_PROVIDERand
OPENAI_API_KEY, and able to reachapi.openai.comor any compatible gateway),
butopenclaw.plugin.jsonstill only declared the Anthropic surface. The two new
env vars and anopenaiendpoint class are now declared, so the manifest matches
what the code actually does — and ClawHub's scan can adjudicate the bundled
openaiSDK's env/network access against a declared capability instead of
holding the release. -
#53
51dca27Thanks @heichaowo! - Document the multi-provider LLM support prominently. The OpenAI-compatible
provider was only described in the configuration reference and the README's
security section; the plugin README's Requirements line and the docs
getting-started page still framed the LLM as Anthropic-only. Both now point at
a dedicated LLM provider section coveringAMEM_LLM_PROVIDER=anthropic|openai
and the OpenAI-compatible endpoints (OpenAI, DeepSeek, OpenRouter, Groq, Together,
Ollama, vLLM, LM Studio).
openclaw-amem@1.2.0
Minor Changes
-
#45
398a59cThanks @heichaowo! - Add an OpenAI-compatible LLM provider. SetAMEM_LLM_PROVIDER=openaito route
note construction, CRUD decisions, link judgment and memory evolution through the
Chat Completions API instead of the Anthropic Messages API, with
AMEM_LLM_BASE_URLpointing at any OpenAI-compatible endpoint — OpenAI, DeepSeek,
OpenRouter, Groq, Together, or a local server (Ollama, vLLM, LM Studio). The
default staysanthropic, so existing setups are unchanged.Reasoning models (
o1,o3,gpt-5) are handled automatically, and keyless
local servers work without an API key. In the plugin, theopenaiSDK is a
runtime dependency kept out of the bundle, so the download size is unchanged for
everyone on the default path.
Patch Changes
-
#50
d07f16cThanks @heichaowo! - Fix three issues in the OpenAI-compatible provider, found in pre-release review:OPENAI_API_KEYwas ignored. The client always passed an explicit key, so
the SDK never read the standardOPENAI_API_KEY— a user who set it (but not
AMEM_LLM_API_KEY) got 401 on every call. It now falls back to
OPENAI_API_KEY, then to the keyless-local placeholder.deepseek-reasonersent the wrong token parameter. A broad
includes('reason')match classified it as an OpenAI reasoning model and sent
max_completion_tokens, which DeepSeek's API does not accept. Reasoning
detection is now scoped to OpenAI's owno*/gpt-5names.AMEM_LLM_PROVIDERwith surrounding whitespace (e.g."openai "from a
.envfile) silently routed to the Anthropic path. The value is now trimmed,
and an unrecognised value logs a warning instead of failing invisibly.
@heichaowo/amem-core@0.3.0
Minor Changes
-
#45
398a59cThanks @heichaowo! - Add an OpenAI-compatible LLM provider. SetAMEM_LLM_PROVIDER=openaito route
note construction, CRUD decisions, link judgment and memory evolution through the
Chat Completions API instead of the Anthropic Messages API, with
AMEM_LLM_BASE_URLpointing at any OpenAI-compatible endpoint — OpenAI, DeepSeek,
OpenRouter, Groq, Together, or a local server (Ollama, vLLM, LM Studio). The
default staysanthropic, so existing setups are unchanged.Reasoning models (
o1,o3,gpt-5) are handled automatically, and keyless
local servers work without an API key. In the plugin, theopenaiSDK is a
runtime dependency kept out of the bundle, so the download size is unchanged for
everyone on the default path.
Patch Changes
-
#50
d07f16cThanks @heichaowo! - Fix three issues in the OpenAI-compatible provider, found in pre-release review:OPENAI_API_KEYwas ignored. The client always passed an explicit key, so
the SDK never read the standardOPENAI_API_KEY— a user who set it (but not
AMEM_LLM_API_KEY) got 401 on every call. It now falls back to
OPENAI_API_KEY, then to the keyless-local placeholder.deepseek-reasonersent the wrong token parameter. A broad
includes('reason')match classified it as an OpenAI reasoning model and sent
max_completion_tokens, which DeepSeek's API does not accept. Reasoning
detection is now scoped to OpenAI's owno*/gpt-5names.AMEM_LLM_PROVIDERwith surrounding whitespace (e.g."openai "from a
.envfile) silently routed to the Anthropic path. The value is now trimmed,
and an unrecognised value logs a warning instead of failing invisibly.
@heichaowo/amem-core@0.2.0
Minor Changes
-
f48a266Thanks @heichaowo! - First public release of the amem engine. Install it directly —npm i @heichaowo/amem-core—
to build memory on top of the A-MEM engine: notes that construct, link, and
evolve like a Zettelkasten, over Qdrant + local Transformers.js embeddings, with
hybrid (BM25 + dense) retrieval and graph expansion. No Python.The public API is deliberate: memory operations (
addMemory,addEpisodic,
searchMemory,consolidateMemories,scanLowQuality, …), the storage context,
the embedding-model lifecycle, and the domain types. Being0.x, the surface may
still shift.
openclaw-amem@1.1.5
Patch Changes
- #28
c8464beThanks @heichaowo! - Security: thememory_quality_scantool now treats itsoutputPathas a bare
filename written under the review directory, instead of a full path it writes
verbatim.generateReviewBatchpreviously used a caller-suppliedoutputPath
directly, so a prompt-injected agent could pass an absolute path or a../
traversal and overwrite an arbitrary file the process could write (CodeQL
js/path-injection). AnoutputPaththat carries any directory component is
now rejected; a bare filename lands underAMEM_REVIEW_DIR(default: the
current working directory). SetAMEM_REVIEW_DIRto choose the directory.
openclaw-amem@1.1.4
Patch Changes
-
#25
ec149d9Thanks @heichaowo! - Fix a phantomamem-core@0.1.0dependency that broke installation from ClawHub.The engine is bundled into the plugin's
distby tsup, butamem-corewas
still declared as aworkspace:*devDependency. On publish, pnpm rewrote that
toamem-core@0.1.0— a private package that does not exist on npm. ClawHub
extracts the tarball and runs a fullnpm install, which then 404s on it, so
the plugin could not be installed at all.The engine is now resolved by a build alias to its source (see
tsup.config.ts) instead of a package dependency, so it stays inlined in the
bundle while no longer appearing anywhere in the published manifest. -
7967915- Refresh the package description shown on npm and ClawHub — replace the stale "TypeScript rewrite" wording with a description of what the plugin actually does: an OpenClaw memory plugin implementing A-MEM, with evolving memory, graph linking, and hybrid retrieval.
openclaw-amem@1.1.3
Patch Changes
310ea62- Fix the broken logo image in the README as shown on npm and ClawHub — serve it fromraw.githubusercontent.cominstead of theamem.owo.lcGitHub Pages custom domain, which did not render reliably on the registry pages.