# Context offload

> Symbolic short-term memory: tool-result offload to refs, L1/L1.5/L2 pipelines, Mermaid canvas injection, mild vs aggressive compression ratios, and node_id recovery.

- Repository: TencentCloud/TencentDB-Agent-Memory
- GitHub: https://github.com/TencentCloud/TencentDB-Agent-Memory
- Human docs: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a
- Complete Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/llms-full.txt

## Source Files

- `src/offload/index.ts`
- `src/offload/types.ts`
- `src/offload/storage.ts`
- `src/offload/pipelines/l2-mermaid.ts`
- `src/offload/hooks/after-tool-call.ts`
- `src/offload/hooks/llm-input-l3.ts`
- `src/config.ts`

---

---
title: "Context offload"
description: "Symbolic short-term memory: tool-result offload to refs, L1/L1.5/L2 pipelines, Mermaid canvas injection, mild vs aggressive compression ratios, and node_id recovery."
---

Context offload is the **symbolic short-term memory** subsystem in `memory-tencentdb`. When `offload.enabled` is true, `registerOffload()` wires an OpenClaw **context engine** (`plugins.slots.contextEngine = "memory-tencentdb"`), tool/lifecycle hooks, and an on-disk ref/jsonl/MMD store so large tool results leave the live context as compact summaries and Mermaid task nodes while remaining recoverable by `node_id` and `result_ref`.

Long-term L0–L3 memory (conversation, atoms, scenes, persona) is a separate pipeline. Offload is an independent switch and data root; default data directory is `~/.openclaw/context-offload`.

<Info>
Offload requires OpenClaw host integration (context-engine slot + preferably the after-tool-call messages patch). It is not driven by the Hermes Gateway HTTP routes. Configuration and enable steps: [Enable context offload](/enable-context-offload).
</Info>

## Runtime model

Offload runs four cooperating stages plus optional skill synthesis<ANSWER>
---
title: "Context offload"
description: "Symbolic short-term memory: tool-result offload to refs, L1/L1.5/L2 pipelines, Mermaid canvas injection, mild vs aggressive compression ratios, and node_id recovery."
---

Context offload is the **symbolic short-term memory** subsystem under `src/offload/`. When `offload.enabled` is true, `registerOffload()` registers the `memory-tencentdb` **context engine** slot, buffers tool call/result pairs, writes full results to ref files, summarizes them into `offload-*.jsonl`, builds Mermaid task canvases (MMD), and compresses live conversation messages so the model keeps task direction without retaining raw tool logs.

It is **independent** of long-term L0–L3 memory (conversations, atoms, scenes, persona). Long-term capture and recall continue whether offload is on or off.

```mermaid
flowchart TB
  subgraph Host["OpenClaw host"]
    ATC["after_tool_call"]
    CE["Context engine assemble()"]
    BPB["before_prompt_build / llm_input"]
  end

  subgraph Pipelines["Offload pipelines"]
    L1["L1 summarize + write refs"]
    L15["L1.5 task boundary"]
    L2["L2 Mermaid + node_id map"]
    L3["L3 mild / aggressive / emergency"]
  end

  subgraph Disk["dataRoot / agentName"]
    REFS["refs/*.md"]
    JSONL["offload-sessionId.jsonl"]
    MMDS["mmds/*.mmd"]
    STATE["state.json"]
  end

  ATC --> L1
  L1 --> REFS
  L1 --> JSONL
  CE --> L15
  L15 --> MMDS
  L15 --> STATE
  L2 --> MMDS
  L2 --> JSONL
  CE --> L3
  BPB --> L3
  ATC --> L3
  JSONL --> L3
  MMDS --> L3
```

## Prerequisites

| Requirement | Detail |
|---|---|
| Plugin | `memory-tencentdb` installed and enabled |
| Config | `plugins.entries["memory-tencentdb"].config.offload.enabled: true` |
| Slot | `plugins.slots.contextEngine: "memory-tencentdb"` (required; mismatch disables all offload) |
| Patch | `scripts/openclaw-after-tool-call-messages.patch.sh` so `after_tool_call` carries `event.messages` (needed for in-loop L3 + MMD) |
| LLM for L1/L1.5/L2 | Local: `offload.model` or `agents.defaults.model` as `provider/model-id` with provider `baseUrl` + `apiKey`; or backend: `offload.backendUrl` |
| Version | Short-term compression path documented for package ≥ 0.3.4 |

Without the context-engine slot, registration aborts early. Without the messages patch, L3 from `after_tool_call` reports `patch_not_effective` and skips in-hook compression for that turn.

## Pipeline layers

Offload uses its own L1 / L1.5 / L2 / L3 naming. These are **not** the long-term memory L0–L3 layers.

| Layer | Role | Trigger | Output |
|---|---|---|---|
| **L1** | Summarize tool pairs; write full results to refs | Pending pairs ≥ `forceTriggerThreshold` (default 4), or flush before L1.5 / assemble | `OffloadEntry` rows in `offload-*.jsonl`, `refs/*.md` |
| **L1.5** | Task boundary: long vs short, active MMD file | Context engine `assemble` (and collect-mode hooks) | In-memory boundaries + `activeMmdFile` in state |
| **L2** | Build/patch Mermaid canvas; assign `node_id` | Independent poll: null-count ≥ `l2NullThreshold` **or** timeout ≥ `l2TimeoutSeconds` | `mmds/*.mmd`, `node_id` backfill on jsonl |
| **L3** | Compress live messages using offload summaries | Token utilisation ≥ mild / aggressive / emergency ratios | In-place message rewrites; optional history MMD injection |
| **L4** (optional) | `/create-skill` from an MMD | User command + backend/local skill path | `skills/<name>/SKILL.md` under the agent data dir |

### L1 — refs and summaries

1. `after_tool_call` buffers a `ToolPair` (skips heartbeats, approval-pending, already-processed ids).
2. L1 flushes batches (backend max 5 pairs; local path uses the same client interface).
3. **L1.1** always writes the raw tool result locally via `writeRefMd` → `refs/<timestamp>.md`, path recorded as `result_ref` (e.g. `refs/2026-08-04T12-00-00.md`).
4. LLM (or fallback stub) produces `tool_call`, `summary`, optional replaceability `score` (0–10), and `node_id: null` until L2.

On LLM failure, fallback entries still keep `result_ref` so recovery is possible without a summary.

### L1.5 — task boundary

L1.5 judges whether work is a **long** task (canvas + L2) or **short** (no MMD, L2 skips). Results push `L15Boundary` segments over the jsonl entry index range.

- **long** → `targetMmd` set; MMD injection becomes ready after settle.
- **short** → `targetMmd: null`; eligible null entries are not L2-bound.
- Fail-safe after one retry: treat as short so L2 is not blocked forever.
- L2 poll waits for `l15Settled` (force-settle after 60s if assemble never ran).

### L2 — Mermaid and `node_id`

L2 does **not** chain directly off L1. `checkL2Trigger` selects entries with `node_id === null` or aged `node_id === "wait"`, only when the L1.5 boundary is `long` with a `targetMmd`.

| Condition | Default | Behavior |
|---|---|---|
| A — null count | `l2NullThreshold: 4` | Eligible null entries ≥ threshold |
| B — timeout | `l2TimeoutSeconds: 300` | Elapsed since `lastL2TriggerTime`; with `l2TimeTriggerRequiresNewOffload: true`, needs a null row newer than last L2 |
| Wait retry | `l2WaitRetrySeconds: 120` | `wait` rows re-enter the batch after age ≥ retry seconds |

Node id shape: `\d{3}-N\d+` (e.g. `003-N12`). After L2, `backfillNodeIds` maps `tool_call_id → node_id`. Unmapped `wait` rows fall back to the most frequent mapped id or the highest N id present in the MMD text for that prefix.

### L3 — mild, aggressive, emergency

L3 runs from the context engine `assemble()` path and from hooks (`llm_input`, `before_prompt_build`, `after_tool_call` when messages are present). Token counts use configurable tiktoken/heuristic modes; system overhead defaults to ~12% of the context window when not measured.

| Stage | Threshold (of context window) | Action |
|---|---|---|
| **Mild** | ≥ `mildOffloadRatio` (default **0.5**) | Score-cascade: replace tool results (and pure tool_use assistants) with summaries for non-current-task / high-score entries in the scan window (`mildOffloadScanRatio` default 0.7). Cascade scores from 7 down to 1; prefers higher L1 `score`. Skips replacement if summary text is larger than the original. Marks status `offloaded: true`. |
| **Aggressive** | ≥ `aggressiveCompressRatio` (default **0.85**) | Delete oldest message prefix until under threshold (`aggressiveDeleteRatio` default 0.4 of message-token mass per round; keep ≥ 2 messages). Marks ids `deleted`. Injects **history** MMDs for deleted tool_call ids (budget `mmdMaxTokenRatio` default 0.2). |
| **Emergency** | ≥ `emergencyCompressRatio` (default **0.95**) or forced after stalled aggressive | Hard delete/truncate toward `emergencyTargetRatio` (default 0.6). |

Current-task protection uses node ids from the active MMD: mild compression prefers non-current-task tool uses; aggressive deletion still prefers dropping history while preserving pairing integrity where possible.

**Mild replacement text** (agent-visible stub):

```text
[Offloaded Tool Result | node: 003-N12]
Summary: <L1 summary>
result_ref: refs/<file>.md (read this file for full tool call and raw result)
```

Assistant tool_use blocks become compact `{ _offloaded: true, node_id, tool_call }` inputs.

## Data model and disk layout

Default root: `~/.openclaw/context-offload` (`offload.dataDir` override). Session key `agent:<name>:<sessionId>` → per-agent directory.

:::files
~/.openclaw/context-offload/
└── <agentName>/
    ├── state.json                 # active MMD, L2 cursor, last offloaded tool_call_id
    ├── sessions-registry.json     # sessionKey → real sessionId / offload file
    ├── offload-<sessionId>.jsonl  # per-session OffloadEntry lines
    ├── refs/
    │   └── <timestamp>.md         # full tool results (result_ref)
    ├── mmds/
    │   └── <NNN>-<label>.mmd      # Mermaid canvases
    └── skills/                    # optional L4 skill outputs
:::

### OffloadEntry fields

| Field | Type | Meaning |
|---|---|---|
| `timestamp` | string | ISO time from tool result |
| `node_id` | `string \| null` | L2 node id, `"wait"` while mapping pending, or `null` pre-L2 |
| `tool_call` | string | Short command description |
| `summary` | string | L1 LLM summary used by mild L3 |
| `result_ref` | string | Relative path under agent dir (`refs/...`) |
| `tool_call_id` | string | Provider tool call id (underscore-normalized for lookup) |
| `score` | number? | 0–10 replaceability; higher = safer to replace original |
| `session_key` | string? | Owning session |

Shared across sessions for the same agent: `mmds/`, `refs/`, `state.json`. L2 aggregation can read all `offload-*.jsonl` in the agent dir.

## Mermaid canvas injection

| Path | When | What is injected |
|---|---|---|
| Active MMD | L1.5 settled + `activeMmdFile` set | Single synthetic user message with `_mmdContextMessage: "active"`, wrapped in `<current_task_context>`, containing the live flowchart (`done` / `doing` / `todo`) |
| History MMDs | After aggressive L3 deletes tool results | MMDs linked via deleted entries’ `node_id` prefixes; inserted at history insertion point; token-capped by `mmdMaxTokenRatio` |

`injectMmdIntoMessages` (assemble / before_prompt_build) injects **only the active** MMD. History MMDs are L3-aggressive-only so completed tasks stay navigable after deletion. Insertion points avoid splitting tool_use / tool_result pairs.

Active injection also updates mid-loop on `after_tool_call` when L2 rewrites the MMD file between tools.

## node_id recovery and drill-down

Recovery is deterministic from context stubs back to disk:

1. **In context:** summary line carries `node: <node_id>` and `result_ref: refs/...`.
2. **jsonl index:** match `tool_call_id` or `node_id` in `offload-*.jsonl`.
3. **Full evidence:** read `result_ref` under the agent data dir (`readRefMd`).
4. **Canvas:** open the MMD file named in state / injection text; node ids match flowchart nodes.

`markOffloadStatus` records whether an id was mildly offloaded (`true`) or aggressively deleted (`"deleted"`). Fast-path re-apply on later turns re-applies known replacements if the host reloads uncompacted history.

```text
Mermaid node (003-N12)
        │
        ▼
offload-*.jsonl  { node_id, tool_call_id, summary, result_ref }
        │
        ▼
refs/<timestamp>.md   ← full tool result
```

## Configuration

Plugin path: `plugins.entries["memory-tencentdb"].config.offload` (parsed by `parseConfig` into `OffloadConfig`).

### Enable skeleton

```jsonc
{
  "plugins": {
    "slots": {
      "contextEngine": "memory-tencentdb"
    },
    "entries": {
      "memory-tencentdb": {
        "enabled": true,
        "config": {
          "offload": {
            "enabled": true
            // "mode": "local" | "backend" | "collect"
            // "model": "provider/model-id"
            // "mildOffloadRatio": 0.5
            // "aggressiveCompressRatio": 0.85
            // "mmdMaxTokenRatio": 0.2
          }
        }
      }
    }
  }
}
```

### Modes

| `mode` | L1/L1.5/L2 | L3 compression | Context engine slot |
|---|---|---|---|
| `local` (default if no `backendUrl`) | Direct OpenAI-compatible LLM via provider config | Yes | Registers |
| `backend` | Remote `backendUrl` (+ optional `backendApiKey`, `X-User-Id`) | Yes | Registers |
| `collect` | Async pipelines for data collection | **Disabled** | Does **not** take the slot (legacy compaction remains) |

If `backendUrl` is set and `mode` is omitted, parseConfig selects `backend`.

### Key fields and defaults

| Field | Default | Notes |
|---|---|---|
| `enabled` | `false` | Master switch |
| `mode` | `local` / auto-`backend` | See table above |
| `model` | host default | `provider/model-id` |
| `temperature` | `0.2` | Offload LLM only |
| `disableThinking` | `false` | Local mode thinking-disable strategies |
| `forceTriggerThreshold` | `4` | Pending pairs → L1 |
| `maxPairsPerBatch` | `20` | L1 batch cap (backend chunk size 5) |
| `defaultContextWindow` | `200000` | Fallback if model window unknown |
| `l2NullThreshold` | `4` | L2 condition A |
| `l2TimeoutSeconds` | `300` | L2 condition B |
| `mildOffloadRatio` | `0.5` | Mild L3 gate |
| `aggressiveCompressRatio` | `0.85` | Aggressive L3 gate |
| `mmdMaxTokenRatio` | `0.2` | History MMD token budget |
| `dataDir` | `~/.openclaw/context-offload` | Absolute override |
| `backendUrl` / `backendApiKey` | unset | Backend L1–L4 |
| `backendTimeoutMs` | `120000` (parse default) | HTTP timeout |
| `offloadRetentionDays` | `0` | Reclaim; values in `(0,3)` forced to `0`; min effective **3** |
| `logMaxSizeMb` | `50` | Truncate oversized `*.log` under data root |
| `userId` | machine IPv4 fallback | Backend `X-User-Id` |

Additional knobs exist on the internal `PluginConfig` (mild scan/score ratios, emergency ratios, L2 wait retry, tiktoken encoding). They apply when wired through the offload plugin config path; primary surface is the `OffloadConfig` group above.

### Operational scripts

```bash
# Patch OpenClaw so after_tool_call receives messages
bash scripts/openclaw-after-tool-call-messages.patch.sh

# Optional one-shot enable/disable (sets slot, offload.enabled, patch check)
bash scripts/setup-offload.sh --enable --user-id <id> --backend-url <url> [--backend-api-key <key>]
bash scripts/setup-offload.sh --status
bash scripts/setup-offload.sh --disable
```

Re-run the patch after OpenClaw upgrades.

## Retention reclaim

When `offloadRetentionDays >= 3`, a delayed scheduler (~5 minutes after start, then daily-style cadence) runs `reclaimOffloadData`:

1. Delete aged `offload-*.jsonl` by mtime  
2. Delete orphan `refs/*.md` not referenced by remaining jsonl  
3. Delete aged `mmds/*.mmd` (protect active MMD)  
4. Truncate oversized debug logs  
5. Prune stale `sessions-registry.json` entries  

`0` or invalid values disable reclaim entirely.

## Failure modes

| Symptom | Likely cause | Check |
|---|---|---|
| All offload no-ops | `plugins.slots.contextEngine` ≠ `memory-tencentdb` or slot owned by another plugin | Config + log: `Context engine slot occupied` / `not assigned` |
| No mild/aggressive after tools | Messages patch missing | Log: `patch check: NOT EFFECTIVE`; re-run patch script |
| L1/L1.5/L2 silent; L3 still runs | No model / missing provider key / missing `backendUrl` | Log: `LLM client not available` |
| L2 never runs | L1.5 stuck short or not settled; no long-task nulls | Wait 60s force-settle; confirm long task + null count |
| Stubs without full recovery | Missing `refs/` file or wrong agent dataDir | Resolve `result_ref` under agent dir |
| Backend mode fails | Bad URL/auth/timeout | `backendTimeoutMs`, API key, `userId` |

## Relationship to long-term memory

| Concern | Context offload | Memory layers (L0–L3) |
|---|---|---|
| Lifetime | Session / task window | Multi-session durable store |
| Primary artifacts | refs, jsonl, mmds | conversations, records, scene_blocks, persona |
| Storage backends | Local filesystem | sqlite or tcvdb |
| Goal | Token budget + task canvas | Searchable long-term knowledge |

Offload does not replace hybrid recall tools; it keeps the **current** context compact while preserving a drill-down path to raw tool output.

## Next

<CardGroup>
  <Card title="Enable context offload" href="/enable-context-offload">
    Turn on offload.enabled, register plugins.slots.contextEngine, apply the after-tool-call messages patch, and verify Mermaid injection.
  </Card>
  <Card title="Memory layers" href="/memory-layers">
    Long-term L0 conversation, L1 atom, L2 scene, and L3 persona model and pipeline scheduling.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full memory-tencentdb schema including the offload group, defaults, and validation.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Offload patch missing, disabled plugin, and related failure checklists.
  </Card>
</CardGroup>
