Many memory systems focus on cross-session recall. TencentDB-Agent-Memory is interesting because it also treats the current task as a memory pressure problem. Search results, stack traces, file snippets, and tool logs can become too heavy before the task is over. Deleting them loses evidence. Summarizing them blindly risks losing the one line that matters. The project answers with a layered rule: keep evidence in lower layers, keep structure in higher layers.
Scenario: a coding agent runs 8 test commands and 12 file searches
Raw context:
assistant tool_use -> tool_result(thousands of lines of test logs)
assistant tool_use -> tool_result(multiple source snippets)
assistant tool_use -> tool_result(error stack trace)
...
TencentDB-Agent-Memory path:
refs/*.md keep full raw tool results
offload-*.jsonl keep tool_call_id, summary, score, and result_ref
mmds/*.mmd merge tool calls into task-topology nodes
prompt keeps summaries and MMD, then drills down by result_ref when needed
The key idea is not compression alone. It is expandable compression. If the model later needs the raw
stack trace for a failed test, it does not have to guess what the summary lost. It follows
node_id, tool_call_id, and result_ref back to the source file.
That is the major difference from a one-way summary memory.
Reading contract. By the end, you should be able to explain what Context Offload actually moves out of the prompt, how refs, offload JSONL, and MMD task canvases connect, how the L0-L3 long-term pipeline generates recallable memory, how the Hermes provider turns the local Gateway into a health-checked sidecar, and how this project differs from Mem0, LangMem, and Cognee / Supermemory.
Evidence boundary. This article uses the public README, plugin config, and source code from TencentCloud/TencentDB-Agent-Memory. The source snapshot for this pass is 45e6e80. The repository supports OpenClaw and Hermes ecosystems. I focus on the public local plugin, Gateway, and provider implementation and do not infer private hosted backend behavior.
1. It Handles Two Different Context Pressures
1.1 The Current Task Gets Too Heavy, Long-Term History Gets Too Scattered
The README frames the project around layering and symbolic memory. On the short-term task side, raw tool
results are kept in refs/*.md, step summaries are written to jsonl, and the top
layer becomes a Mermaid task canvas with node_id. On the long-term personalization side, the
pyramid is L0 Conversation, L1 Atom, L2 Scenario, and
L3 Persona. The project states that lower layers preserve evidence while upper layers
preserve structure in the
core technology section.
These are two separate problems. Short-term Context Offload answers: this task is still active, but the prompt is too full. Long-term L0-L3 memory answers: next time this user appears, what should the agent remember? The first is like moving thick documents from the desk into indexed folders while keeping a task map on the desk. The second is like turning many conversations into facts, scenes, and a user profile.
Keep the two routes separate while reading this chapter. The tool-result route explains how the active task loses bulk without losing evidence. The long-term-fact route explains how user profile accumulates over time.
| Tool-Result Route | What Happens | How It Is Used Later |
|---|---|---|
| Trigger | after_tool_call sees a bulky tool output and creates a result_ref. |
The current prompt no longer carries the full tool log. |
| Stored record | Raw text goes into refs/*.md and offload JSONL. |
Evidence can be recovered by reference when the model or user needs the raw text. |
| Structured summary | MMD nodes keep the task graph, key conclusions, and references. | The model normally sees summaries and task structure, not bulky logs. |
| Long-Term Fact Route | What Happens | How It Is Used Later |
|---|---|---|
| Trigger | Multi-turn conversation becomes L0 Conversation. | The system looks for information worth reusing after this turn. |
| Stored record | L1 Atom entries are deduplicated and merged into L2 Scenario and L3 Persona. | Lower layers keep evidence; higher layers provide scenario navigation and user profile. |
| Recall entry | Before the next prompt, relevant atoms, scenes, and persona are recalled. | Persona enters system context; L1 hits enter the current user prompt prefix. |
1.2 It Is Closer to a Runtime Plugin Than a Hosted Memory API
The implementation boundary matters. TencentDB-Agent-Memory is an OpenClaw plugin. The README says the default local backend is SQLite + sqlite-vec and that enabling the plugin records conversations, extracts memories, summarizes scenes, generates user persona, and recalls context before the next turn. Short-term offload is an independent switch. See the quickstart and the plugin schema.
That makes it different from a context API such as Supermemory. Supermemory behaves more like an external
service that accepts content, processes connectors, and returns context. TencentDB-Agent-Memory lives
closer to the local agent runtime: it can rewrite the message array at after_tool_call,
before_prompt_build, and llm_input, while using files and SQLite as traceable
evidence stores.
The Hermes entry makes that runtime boundary more concrete. The provider README maps Hermes lifecycle calls
to Gateway endpoints: prefetch(query) synchronously calls POST /recall and
returns injectable <memory-context>; sync_turn(user, assistant) calls
POST /capture on a background daemon thread with at most 4 in-flight captures;
shutdown() or session end calls POST /session/end to flush pending pipeline work.
Hermes sees a Python memory provider, while the actual memory pipeline is still handled by the local Node
Gateway. See the
Hermes lifecycle mapping.
The provider also handles runtime safety around that HTTP boundary: circuit breaking, capture back-pressure,
Gateway auto-discovery, and supervised startup. supervisor.py mirrors Hermes-side environment
variables into the child Gateway process, which keeps Windows-native installs from maintaining two separate
config surfaces. It redirects child stdout/stderr into log files so a full pipe cannot deadlock the Gateway,
and it terminates the Windows process tree during shutdown. Those details matter because local memory is
only useful if the Gateway stays healthy, the provider can recover, and background capture cannot grow
without bound. See
Gateway startup and log handling
and shutdown and Windows process-tree handling.
2. Short-Term Memory: Context Offload Is Recoverable Unloading
2.1 First Move Heavy Tool Results Out of the Model View
The offload file layer is explicit. The comments in storage.ts say that each agent has its
own directory, the same agent shares mmds/, refs/, and state.json,
and every session gets an offload-<sessionId>.jsonl file. The path constructor shows
the default root, refsDir, mmdsDir, and session JSONL. See the
storage.ts design comment
and StorageContext constructor.
This is more than token shaving. Tool output has uneven density. Most lines are operational noise, but a few lines carry the decisive error, file path, data shape, or design decision. The project avoids keeping all of that in the model view forever. It stores raw evidence in external files and JSONL indexes, then lets the model see a compact summary and task structure.
export interface OffloadEntry {
timestamp: string;
node_id: string | null;
tool_call: string;
summary: string;
result_ref: string;
tool_call_id: string;
score?: number;
}
export async function writeRefMd(ctx, timestamp, toolName, content) {
const filename = `${isoToFilename(timestamp)}.md`;
const filePath = join(ctx.refsDir, filename);
await writeFile(filePath, header + safeContent, "utf-8");
return `refs/${filename}`;
}
A tool result is split into two parts here. The raw text goes into refs/*.md; the index goes
into OffloadEntry. summary gives the model a fast reading path,
result_ref keeps recovery possible, and score tells later L3 compression whether
the summary is safe enough to replace the original result.
2.2 L1 Writes Step Summaries, L2 Turns Steps Into Task Topology
Offload L1 is not the same as the long-term L1 memory layer. It is a summarizer for tool results.
l1-prompt.ts asks the LLM to combine each tool call/result pair into JSON fields such as
tool_call, summary, tool_call_id, timestamp, and score.
The prompt explicitly asks for the key finding, key action, concrete modification, or concrete error that
advanced or blocked the task. See the
L1 summarization prompt.
L2 then turns those offload entries into a Mermaid flowchart. l2-prompt.ts tells the model
not to keep a logbook. It should merge routine actions, preserve important turns and failed paths, and map
every tool_call_id to a node_id. The same prompt defines write and
replace modes and asks the MMD to stay within a compact character budget. See the
L2 MMD rules
and new offload entries format.
const req: L2Request = {
existingMmd,
newEntries: batch.map((e) => ({
tool_call_id: e.tool_call_id,
tool_call: e.tool_call,
summary: e.summary,
timestamp: e.timestamp,
})),
recentHistory,
currentTurn,
taskLabel,
mmdPrefix,
};
// L2 returns node_mapping, attaching each tool_call_id to a Mermaid node.
This explains why the MMD matters. It is not decorative. It turns many tool calls into a small number of
task nodes. Five consecutive file reads can become one "locate implementation boundary" node; a failed
test and the following fix should remain two connected nodes. As long as node_mapping is kept,
the model can drill down from the task graph to the exact tool calls.
refs/*.md
Long logs, errors, search results, and file snippets stay recoverable without staying in the prompt.
offload-*.jsonl
Each tool call gets a summary, timestamp, tool_call_id, reference path, and later node_id mapping.
mmds/*.mmd
Many tool calls become a small task graph. The model reads structure and drills down by node_id when needed.
2.3 L3 Actually Rewrites the Context Entering the Model
The real offload happens before model input. before-prompt-build.ts describes three phases:
re-apply confirmed mild replacements and aggressive deletions, run full L3 when token thresholds still
require it, and inject MMD into messages. The implementation reads offload entries, replaces confirmed
tool results with summaries, deletes older tool messages when the aggressive threshold is reached, injects
history MMD as compensation, and applies score-based mild replacements. See the
three-phase comment
and core logic.
MMD injection is the other important mechanism. mmd-injector.ts injects only the active MMD
during normal assembly. History MMDs are reserved for aggressive compression after messages have been
deleted, so they can stand in for lost conversational context. The injector budgets MMD tokens by
mmdMaxTokenRatio and inserts the marked user message at a suitable point. The model sees the
active task graph, not an ever-growing list of tool logs. See the
MMD injection strategy
and injection implementation.
// before_prompt_build has three stages
// 1. confirmed replaceable tool_result -> summary
if (entry && isToolResultMessage(msg)) {
replaceWithSummary(msg, entry);
msg._offloaded = true;
}
// 2. above the aggressive threshold, delete older tool messages and inject history MMD
const result = await aggressiveCompressUntilBelowThreshold(...);
// 3. inject the current active MMD
await injectMmdIntoMessages(messages, stateManager, logger, getContextWindow, pluginConfig);
[Offloaded Tool Result | node: 012-N3]
Summary: Test failures concentrate in connection-pool retry logic; timeout is reproducible
result_ref: refs/2026-07-02T10-21-33.md (read this file for full tool call and raw result)
L3 therefore has several strengths. Mild compression replaces tool results with summaries. Aggressive compression deletes older tool messages and injects history MMD to preserve task structure. Active MMD then enters the message array. The model sees why the current task is here, not every full command output.
3. Long-Term Memory: L0 to L3 Is a Separate Semantic Pyramid
3.1 L0 Keeps Conversation Evidence, L1 Extracts Structured Facts
On the long-term side, L0 is raw conversation and L1 is structured memory. The comment in
l1-extractor.ts gives the pipeline: read recent messages from L0, call the LLM for scene
segmentation and memory extraction, run batch conflict detection, and write L1 JSONL files. The
implementation applies a quality gate before sending messages to the LLM and separates new messages from a
small background window. See the
L1 extractor pipeline
and filtering and extraction steps.
const qualifiedMessages = messages.filter((m) => shouldExtractL1(m.content));
const newMessages = qualifiedMessages.slice(-maxNewMessages);
const backgroundMessages = qualifiedMessages.slice(...);
const scenes = await callLlmExtraction({
newMessages,
backgroundMessages,
previousSceneName,
});
// scenes -> memories -> L1 records
This shows that L1 does not turn every message into memory. L0 can preserve raw conversation, but L1 first applies a quality gate, then separates recent messages from a background window before calling the model. That reduces noise and gives the model enough scene context to extract durable facts.
Deduplication avoids a full JSONL scan. l1-dedup.ts says that v4 removed the JSONL Jaccard
fallback. Candidate recall now uses vector search first, falls back to FTS5 BM25, and skips conflict
detection entirely if neither is available. The practical point is to narrow “possibly conflicting old
memories” with retrieval, then ask the LLM to batch-judge store, skip, update, or merge. See the
dedup comment
and three-tier candidate strategy.
Before writing a new L1 memory:
1. vector recall finds top-k old-memory candidates
2. without vector capability, FTS5 BM25 retrieves keyword candidates
3. if neither is available, conflict detection is skipped and the memory is stored
4. the LLM compares only "new memory + candidate pools" and decides store / skip / update / merge
3.2 L2 Is Scene Diary, L3 Is User Persona
L2 is handled by SceneExtractor. It does not append L1 memories as a list. Instead, it lets
an LLM read and write Markdown scene files inside a restricted scene_blocks/ directory. The
file comment lays out the flow: backup and load scene index, build the prompt from memories and scene
context, run a tool-enabled runner in the sandboxed directory, then clean soft deletes, sync the index,
and update navigation. See the
SceneExtractor description.
L3 PersonaGenerator reads changed scene contents and generates or incrementally updates
persona.md. It strips old navigation, loads changed scenes, runs a tool-enabled model, and
appends fresh scene navigation afterward. The persona is therefore not a free-floating summary. It is an
abstraction built on top of L2 scene files. See
generateLocalPersona.
The newer prompts also turn output language into an explicit contract. L2 scene extraction detects the
dominant language from the new memories, then uses that language for scene filenames, Markdown headings,
and natural-language body text. META fields such as created, updated, and
summary, plus system markers such as [DELETED], remain in English. L3 applies
the same rule to persona.md: headings and narrative content follow the language of the changed
scenes, while the filename and structural markers stay stable. That affects memory usability directly. If
a user works in English or Japanese, the scene diary and persona should stay readable in that language,
while downstream code can still depend on stable field names and markers. See the
scene output language contract,
scene filename rules,
and persona output language contract.
3.3 Recall Separates Stable Profile From Dynamic Memories
Recall preserves the layering. The header of auto-recall.ts says L1 can be searched by
keyword, embedding, or hybrid strategy; L3 persona is injected; and L2 scene navigation is injected so the
model can decide whether to inspect a scene. During context assembly, L3 persona, L2 navigation, and the
memory tool guide go into stable system context. L1 relevant memories go into the current user prompt
prefix. This makes prompt caching more plausible because persona and navigation change slowly, while L1
hits are turn-specific. See the
auto-recall summary
and stable/dynamic split.
const stableParts: string[] = [];
if (personaContent) stableParts.push(`<user-persona>...</user-persona>`);
if (sceneNavigation) stableParts.push(`<scene-navigation>...</scene-navigation>`);
let prependContext: string | undefined;
if (memoryLines.length > 0) {
prependContext = `<relevant-memories>\n${memoryLines.join("\n")}\n</relevant-memories>`;
}
return { appendSystemContext, prependContext };
This split is important. Persona and Scene Navigation are slow-changing, so they fit at the end of the system prompt and can benefit from prompt caching. L1 recall changes every turn, so it belongs in the user prompt prefix. The system does not dump all memory into one block; it places memory by change frequency.
The storage layer supports the same idea. sqlite.ts manages both L1 structured memories and
L0 raw conversations: relational tables keep metadata, and sqlite-vec virtual tables support
cosine similarity. Recall uses keyword, embedding, or hybrid search; native hybrid is used when available,
otherwise keyword and embedding paths are combined with RRF. See the
SQLite store design comment
and recall search dispatcher.
4. Put It Back on the Agent Memory Map
In the “who owns history and who assembles the model view” map, TencentDB-Agent-Memory belongs after LangMem and before OpenViking. Like LangMem, it sits near the agent runtime. But LangMem's central question is whether memory writes happen in the hot path or in a background manager. TencentDB-Agent-Memory also manages the current task's tool history, using Context Offload to handle the case where a single task becomes too long before it ends.
| Project | Core question | How TencentDB-Agent-Memory differs |
|---|---|---|
| Mem0 | How an external memory layer writes, merges, and retrieves long-term memory. | It also handles current-task context offload, not only cross-session memory. |
| LangMem / LangGraph | Whether memory actions belong in the hot path or a background path. | It plugs tool-log compression, MMD task graphs, and token thresholds into runtime message handling. |
| OpenViking | How resources, memories, skills, and sessions enter one context tree. | It focuses on current tool-log offload and L0-L3 user memory; OpenViking expands the boundary to shared addressing and layered retrieval. |
| Cognee / Supermemory | How memory becomes a multi-source, multi-user platform layer. | It is closer to a local plugin and white-box filesystem than an external context API. |
That is why it deserves its own chapter. Many memory discussions assume the current context is already
assembled and only ask how long-term memory should be recalled. TencentDB-Agent-Memory opens that input
itself: what should stay visible to the model, what can be replaced by summary, what can be deleted but
represented by an MMD task graph, and what must remain recoverable through node_id.
5. Tradeoff: More Moving Parts for Recoverability
This design is not light. It introduces L1, L1.5, L2, and L3, several file directories, MMD, JSONL, SQLite, FTS, vector search, and runtime hooks that must receive enough message state. The benefit is a clear evidence chain. Short-term task state can drill down from MMD to JSONL to refs. Long-term persona can drill down from persona to scene, then to L1 atom and L0 conversation.
The new disableThinking option belongs in the same tradeoff. L1/L2/L3 extraction and offload
summarization are utility LLM calls: they need structured, consistent outputs more than they need a long
reasoning path on every request. The config separates llm.disableThinking from
offload.disableThinking. createNoThinkFetch then rewrites chat-completion request
bodies for vLLM, DeepSeek, DashScope, OpenAI o-series, Anthropic, Kimi, Gemini, and related endpoints. It
only touches requests whose body contains a messages array, so embedding and other non-chat
requests pass through unchanged. OpenAI o-series can only be lowered to
reasoning_effort: "low", not fully disabled. This optimization helps latency and cost; it does
not replace the quality gate, deduplication, or recovery chain. See
LLM and offload config,
disableThinking strategies,
and the fetch wrapper.
That makes it best suited for long-running agents with heavy tool use and a need for white-box traceability. For simple support chat, a memory API may be cheaper to operate. For coding agents, database agents, or research agents, tool logs are part of the task asset. Context Offload is valuable because it lets the agent keep direction without carrying every raw artifact in the model view.
Sources
- TencentCloud/TencentDB-Agent-Memory
- README: layering, short-term offload, L0-L3, and recoverability
- OpenClaw plugin schema: standalone LLM and Context Offload config
- Hermes provider: lifecycle mapping, circuit breaker, back-pressure, and auto-discovery
- Hermes supervisor: Gateway environment, logs, and startup
- Context Offload storage paths
- Offload L1 summarization prompt
- Offload L2 MMD prompt
- before_prompt_build L3 compression
- Long-term L1 extraction pipeline
- Scene prompt: output language and filename contracts
- disableThinking fetch wrapper
- Auto-recall search and context assembly