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.

TencentDB Agent Memory layered mechanism diagram with context offload above and L0 to L3 long-term memory below
The core idea is not “one more memory database”. It is a shared layering strategy for two pressures: offload the current task, then distill long-term user memory.

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.

Raw evidence: refs/*.md

Long logs, errors, search results, and file snippets stay recoverable without staying in the prompt.

Step index: offload-*.jsonl

Each tool call gets a summary, timestamp, tool_call_id, reference path, and later node_id mapping.

Task canvas: 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