# TencentDB Agent Memory Documentation

> Technical reference for the @tencentdb-agent-memory/memory-tencentdb four-layer agent memory system: OpenClaw plugin, Hermes Gateway HTTP API, L0–L3 pipeline, context offload, storage backends, CLIs, and operations.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a)
- [GitHub repository](https://github.com/TencentCloud/TencentDB-Agent-Memory)

## Repository Metadata

- Repository: TencentCloud/TencentDB-Agent-Memory

- Generated: 2026-08-04T19:25:39.445Z
- Updated: 2026-08-04T19:43:12.716Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 24

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/01-overview.md) - What TencentDB Agent Memory exposes, host integration surfaces (OpenClaw plugin and Hermes Gateway), runtime assumptions, and the first docs routes to follow.
- 02. [Installation](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/02-installation.md) - Prerequisites (Node >= 22.16, OpenClaw >= 2026.3.13), npm package install, OpenClaw plugin install and update commands, and postinstall patch behavior.
- 03. [Quickstart](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/03-quickstart.md) - Zero-config OpenClaw enable path: plugins install, openclaw.json memory-tencentdb.enabled, gateway restart, data-dir success signals, and optional offload slot registration.
- 04. [Hermes setup](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/04-hermes-setup.md) - Install and run memory_tencentdb with Hermes: Docker greenfield image, attach to existing Hermes, provider directory naming, Gateway auto-discovery, and health verification.
- 05. [Memory layers](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/05-memory-layers.md) - Repo-specific L0 conversation, L1 atom, L2 scene, and L3 persona model: what each layer stores, how the pipeline schedules L1→L2→L3, and drill-down paths between layers.
- 06. [Context offload](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/06-context-offload.md) - 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.
- 07. [Storage backends](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/07-storage-backends.md) - sqlite (SQLite + sqlite-vec + FTS5) vs tcvdb store backends, BM25 sparse encoding, hybrid RRF retrieval, embedding service roles, and factory selection constraints.
- 08. [TdaiCore and host adapters](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/08-tdaicore-and-host-adapters.md) - Host-neutral TdaiCore facade, OpenClawHostAdapter vs StandaloneHostAdapter, LLM runner boundaries, and how in-process OpenClaw hooks map to Gateway HTTP handlers.
- 09. [Configure OpenClaw](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/09-configure-openclaw.md) - Enable the plugin in ~/.openclaw/openclaw.json, daily and advanced tuning groups (capture, pipeline, recall, persona), timezone, and verification after gateway restart.
- 10. [Enable context offload](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/10-enable-context-offload.md) - Turn on offload.enabled, register plugins.slots.contextEngine, apply the after-tool-call messages patch, tune compression ratios, and confirm Mermaid injection paths.
- 11. [Configure embedding](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/11-configure-embedding.md) - OpenAI-compatible embedding providers, required fields (baseUrl, apiKey, model, dimensions), sendDimensions for BGE-M3-style backends, timeouts, and graceful keyword-only degradation when provider is none.
- 12. [Use Tencent VectorDB](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/12-use-tencent-vectordb.md) - Switch storeBackend to tcvdb, required tcvdb.url / apiKey / database fields, BM25 language, server-side embedding model, and CA PEM path for HTTPS instances.
- 13. [Seed historical conversations](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/13-seed-historical-conversations.md) - Import conversation JSON via openclaw memory-tdai seed or POST /seed: input formats A/B, flags, config overrides, output directory layout, and L0→L1→L2→L3 execution path.
- 14. [Secure the Gateway](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/14-secure-the-gateway.md) - Optional TDAI_GATEWAY_API_KEY Bearer auth, TDAI_CORS_ORIGINS allow-list, non-loopback host warnings, and Hermes client MEMORY_TENCENTDB_GATEWAY_API_KEY alignment.
- 15. [Plugin configuration reference](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/15-plugin-configuration-reference.md) - Full memory-tencentdb config schema: field types, defaults, enums, validation rules (retention, embedding, llm, offload, tcvdb, bm25, report), and parseConfig behavior.
- 16. [Agent tools](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/16-agent-tools.md) - tdai_memory_search and tdai_conversation_search (OpenClaw) plus Hermes memory_tencentdb_* tool schemas: parameters, hybrid/embedding/keyword strategies, combined 3-call-per-turn limit, and response shapes.
- 17. [Gateway HTTP API](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/17-gateway-http-api.md) - TdaiGateway routes: GET /health, POST /recall, /capture, /search/memories, /search/conversations, /session/end, /seed — request and response fields, auth exceptions, and error envelope.
- 18. [CLI reference](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/18-cli-reference.md) - Package bin commands and OpenClaw memory-tdai namespace: migrate-sqlite-to-tcvdb, export-tencent-vdb, read-local-memory, seed flags, and memory-tencentdb-ctl subcommands.
- 19. [Environment variables](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/19-environment-variables.md) - Gateway, LLM, Hermes supervisor, install root, and data-dir env keys: TDAI_*, MEMORY_TENCENTDB_*, MODEL_*, resolution order for tdai-gateway.json, and client vs server API-key names.
- 20. [Migrate SQLite to TCVDB](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/20-migrate-sqlite-to-tcvdb.md) - Offline migrate-sqlite-to-tcvdb workflow: dry-run, layer selection (l0/l1/profile), TCVDB connection flags, config rewrite, manifest updates, and rollback-safe verification.
- 21. [Gateway lifecycle](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/21-gateway-lifecycle.md) - Operate the Node Gateway with memory-tencentdb-ctl: standalone vs hermes modes, start/stop/status/health/logs, config llm/embedding/vdb writes, and path layout under ~/.memory-tencentdb.
- 22. [Inspect local memory](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/22-inspect-local-memory.md) - Query L0–L3 artifacts with read-local-memory, export TCVDB collections with export-tencent-vdb, diagnostic export script, and on-disk layout (conversations, records, scene_blocks, persona, vectors.db).
- 23. [Troubleshooting](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/23-troubleshooting.md) - Source-backed failure modes: plugin disabled, no recall, embedding 400/matryoshka, retention cleanup, offload patch missing, Gateway circuit breaker, auth 401, and log/probe checklist.
- 24. [Contributing](https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/24-contributing.md) - Local development with openclaw plugins install --link, build and test scripts, project layout, CI expectations, and contribution workflow from CONTRIBUTING.md.

## Source File Index

- `.github/workflows/pr-ci.yml`
- `bin/migrate-sqlite-to-tcvdb.mjs`
- `bin/read-local-memory.mjs`
- `CHANGELOG.md`
- `CONTRIBUTING.md`
- `docker/opensource/Dockerfile.hermes`
- `docker/opensource/README-hermes.md`
- `hermes-plugin/memory/memory_tencentdb/__init__.py`
- `hermes-plugin/memory/memory_tencentdb/client.py`
- `hermes-plugin/memory/memory_tencentdb/plugin.yaml`
- `hermes-plugin/memory/memory_tencentdb/README.md`
- `hermes-plugin/memory/memory_tencentdb/supervisor.py`
- `hermes-plugin/memory/memory_tencentdb/tests/test_memory_tencentdb_recovery.py`
- `index.ts`
- `LICENSE`
- `openclaw.plugin.json`
- `package.json`
- `README.md`
- `scripts/bugfix-20260423/BUGFIX-20260423-SOP.md`
- `scripts/export-diagnostic.sh`
- `scripts/export-tencent-vdb/export-tencent-vdb.ts`
- `scripts/install_hermes_memory_tencentdb.sh`
- `scripts/memory-tencentdb-ctl.sh`
- `scripts/migrate-sqlite-to-tcvdb/cli-entry.ts`
- `scripts/migrate-sqlite-to-tcvdb/config-write.ts`
- `scripts/migrate-sqlite-to-tcvdb/manifest-write.ts`
- `scripts/migrate-sqlite-to-tcvdb/README.md`
- `scripts/migrate-sqlite-to-tcvdb/sqlite-to-tcvdb.ts`
- `scripts/openclaw-after-tool-call-messages.patch.sh`
- `scripts/read-local-memory/read-local-memory.ts`
- `scripts/README.memory-tencentdb-ctl.md`
- `scripts/setup-offload.sh`
- `SKILL-DIAGNOSTIC-EXPORT.md`
- `SKILL-MIGRATION.md`
- `SKILL.md`
- `src/adapters/index.ts`
- `src/adapters/openclaw/host-adapter.ts`
- `src/adapters/standalone/host-adapter.ts`
- `src/adapters/standalone/llm-runner.ts`
- `src/cli/commands/seed.ts`
- `src/cli/index.ts`
- `src/cli/README.md`
- `src/config.ts`
- `src/core/conversation/l0-recorder.ts`
- `src/core/persona/persona-generator.ts`
- `src/core/record/l1-extractor.ts`
- `src/core/report/reporter.ts`
- `src/core/scene/scene-extractor.ts`
- `src/core/seed/input.ts`
- `src/core/seed/seed-runtime.ts`
- `src/core/seed/types.ts`
- `src/core/store/bm25-local.ts`
- `src/core/store/embedding.ts`
- `src/core/store/factory.ts`
- `src/core/store/search-utils.ts`
- `src/core/store/sqlite.ts`
- `src/core/store/tcvdb-client.ts`
- `src/core/store/tcvdb.ts`
- `src/core/store/types.ts`
- `src/core/tdai-core.ts`
- `src/core/tools/conversation-search.ts`
- `src/core/tools/memory-search.ts`
- `src/core/types.ts`
- `src/gateway/config.ts`
- `src/gateway/server.ts`
- `src/gateway/types.ts`
- `src/offload/hooks/after-tool-call.ts`
- `src/offload/hooks/llm-input-l3.ts`
- `src/offload/index.ts`
- `src/offload/pipelines/l2-mermaid.ts`
- `src/offload/storage.ts`
- `src/offload/types.ts`
- `src/utils/ensure-hook-policy.ts`
- `src/utils/env.ts`
- `src/utils/manifest.ts`
- `src/utils/memory-cleaner.ts`
- `src/utils/no-think-fetch.ts`
- `src/utils/pipeline-factory.ts`
- `tsdown.config.ts`
- `vitest.config.ts`

---

## 01. Overview

> What TencentDB Agent Memory exposes, host integration surfaces (OpenClaw plugin and Hermes Gateway), runtime assumptions, and the first docs routes to follow.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/01-overview.md
- Generated: 2026-08-04T18:30:17.520Z

### Source Files

- `README.md`
- `package.json`
- `index.ts`
- `openclaw.plugin.json`
- `src/core/tdai-core.ts`
- `src/gateway/server.ts`

---
title: "Overview"
description: "What TencentDB Agent Memory exposes, host integration surfaces (OpenClaw plugin and Hermes Gateway), runtime assumptions, and the first docs routes to follow."
---

`@tencentdb-agent-memory/memory-tencentdb` (v0.3.6) is a four-layer agent memory package for OpenClaw and Hermes. It exposes long-term memory (L0 conversation → L1 atoms → L2 scenes → L3 persona), optional symbolic short-term context offload, hybrid recall tools, a host-neutral `TdaiCore` facade, and a Node.js HTTP Gateway for non-OpenClaw hosts. Default storage is local `sqlite` + `sqlite-vec` + FTS5; optional `tcvdb` targets Tencent VectorDB. LLM and embedding calls use OpenAI-compatible endpoints you supply (BYOK).

## What the package exposes

| Surface | Identifiers | Role |
| :--- | :--- | :--- |
| OpenClaw plugin | id `memory-tencentdb`, CLI namespace `memory-tdai` | In-process capture, recall, tools, optional offload |
| Agent tools (OpenClaw) | `tdai_memory_search`, `tdai_conversation_search` | L1 memory and L0 conversation search |
| Agent tools (Hermes) | `memory_tencentdb_memory_search`, `memory_tencentdb_conversation_search` | Same capabilities over Gateway HTTP |
| Core facade | `TdaiCore` | Host-neutral recall, capture, search, session end, pipeline |
| Host adapters | `OpenClawHostAdapter`, `StandaloneHostAdapter` | OpenClaw plugin API vs standalone/Gateway runtime |
| Gateway HTTP | `TdaiGateway` on `127.0.0.1:8420` by default | Hermes sidecar: `/health`, `/recall`, `/capture`, `/search/*`, `/session/end`, `/seed` |
| Package bins | `migrate-sqlite-to-tcvdb`, `export-tencent-vdb`, `read-local-memory` | Offline migrate, export, inspect |
| Ops scripts | `scripts/memory-tencentdb-ctl.sh`, `scripts/install_hermes_memory_tencentdb.sh` | Gateway lifecycle and Hermes install |
| Hermes provider | `hermes-plugin/memory/memory_tencentdb/` | Python client + supervisor; provider key `memory_tencentdb` |

Plugin metadata lives in `openclaw.plugin.json` (`activation.onStartup: true`, config schema for capture/pipeline/recall/embedding/offload/tcvdb). Package entry is `index.ts` → built `dist/index.mjs`; engines require **Node `>=22.16.0`**. OpenClaw compat in `package.json` pins `pluginApi` / `minGatewayVersion` to `>=2026.3.13` (README badge matches; peerDependency allows `openclaw >=2026.3.7`).

## Host integration surfaces

### OpenClaw (in-process)

`index.ts` is a thin shell: parse plugin config, construct `OpenClawHostAdapter` + `TdaiCore`, register tools/hooks/CLI. Core logic stays in `src/core/tdai-core.ts`.

| Host event | Core method | Behavior |
| :--- | :--- | :--- |
| `before_prompt_build` | `handleBeforeRecall` | Auto-recall L1 (+ persona context) into the prompt |
| `agent_end` (success) | `handleTurnCommitted` | L0 capture and pipeline scheduling |
| Tool execute | `searchMemories` / `searchConversations` | Agent-driven search |
| `gateway_stop` / process exit | `destroy` | Full teardown (not session-scoped) |

Zero-config enable path:

```jsonc
// ~/.openclaw/openclaw.json
{
  "memory-tencentdb": {
    "enabled": true
  }
}
```

```bash
openclaw plugins install @tencentdb-agent-memory/memory-tencentdb
openclaw gateway restart
```

Data directory resolves under the OpenClaw state dir as `memory-tdai` (typically `~/.openclaw/memory-tdai/`). On enable, the plugin auto-captures turns, runs L1→L2→L3 extraction when configured, and injects recall before the next turn.

Context offload is **separate** and default-off (`offload.enabled: false`). Enabling it requires `plugins.slots.contextEngine: "memory-tencentdb"`, the after-tool-call messages patch (`postinstall` / `scripts/openclaw-after-tool-call-messages.patch.sh`), and `offload.enabled: true`.

### Hermes (Gateway sidecar)

Hermes does not load TypeScript in-process. The Python provider `memory_tencentdb` starts or discovers the Node Gateway and maps lifecycle hooks to HTTP:

| Hermes call | Gateway route |
| :--- | :--- |
| `prefetch(query)` | `POST /recall` |
| `sync_turn(user, assistant)` | `POST /capture` (background, back-pressured) |
| `on_session_end` / shutdown | `POST /session/end` |
| Tool schemas | Client-side; execute via `/search/memories` and `/search/conversations` |

Provider directory name **must** be `memory_tencentdb` (underscore). Config aliases include `memory-tencentdb` and legacy `tdai`. Default Gateway listen address is `http://127.0.0.1:8420`. Auth is optional: set `TDAI_GATEWAY_API_KEY` on the server and `MEMORY_TENCENTDB_GATEWAY_API_KEY` (or fallback `TDAI_GATEWAY_API_KEY`) on the Hermes client.

```mermaid
flowchart TB
  subgraph Hosts["Host runtimes"]
    OC["OpenClaw plugin<br/>index.ts"]
    HM["Hermes MemoryTencentdbProvider<br/>hermes-plugin/memory/memory_tencentdb"]
  end

  subgraph Core["Host-neutral core"]
    OCA["OpenClawHostAdapter"]
    SHA["StandaloneHostAdapter"]
    TC["TdaiCore"]
    OCA --> TC
    SHA --> TC
  end

  subgraph Stores["Storage"]
    SQL[("sqlite + sqlite-vec + FTS5")]
    VDB[("tcvdb")]
    FS["JSONL / Markdown<br/>conversations, records,<br/>scene_blocks, persona"]
  end

  subgraph Ext["External BYOK services"]
    LLM["OpenAI-compatible LLM"]
    EMB["OpenAI-compatible embeddings<br/>or TCVDB server-side embed"]
  end

  OC --> OCA
  HM -->|"HTTP TdaiGateway<br/>:8420"| SHA
  TC --> SQL
  TC --> VDB
  TC --> FS
  TC --> LLM
  TC --> EMB
```

## Memory model (summary)

Two complementary systems share the package:

| System | Layers / artifacts | Default |
| :--- | :--- | :--- |
| Long-term layered memory | **L0** conversations (JSONL + search) → **L1** episodic atoms → **L2** scene blocks (Markdown) → **L3** persona (`persona.md`) | Capture/extraction/recall on; pipeline schedules L1 then delayed L2/L3 |
| Symbolic short-term (offload) | Tool results → `refs/*.md` → L1/L1.5/L2 pipelines → Mermaid canvas with `node_id` recovery | Off by default |

Drill-down is intentional: persona/scenes for direction; atoms/conversations for evidence; Mermaid `node_id` for raw offloaded tool payloads. Heterogeneous storage: lower layers favor DB/JSONL retrieval; upper layers favor readable Markdown.

## Runtime assumptions

| Requirement | Value |
| :--- | :--- |
| Node.js | `>= 22.16.0` |
| OpenClaw (plugin path) | `>= 2026.3.13` recommended (plugin API / gateway compat) |
| Default store | `storeBackend: "sqlite"` (no remote DB required) |
| Embedding | `embedding.provider` default `"none"` → keyword/hybrid degrades without vectors |
| LLM for L1/L2/L3 | OpenClaw host model by default; optional `llm.enabled` standalone OpenAI-compatible API |
| Hermes path | Node Gateway process + LLM credentials (`TDAI_LLM_*` or `tdai-gateway.json`) |
| Gateway auth | Disabled by default; `GET /health` always unauthenticated |
| Gateway host | Default loopback; non-loopback without API key logs a loud warning |
| License | MIT |

<Warning>
Do not expose the Gateway on a non-loopback interface without `TDAI_GATEWAY_API_KEY`. Auth and CORS are opt-in so existing local sidecars keep working.
</Warning>

## Storage and data layout

`initDataDirectories` creates under the plugin/Gateway data dir:

```text
<dataDir>/
  conversations/   # L0
  records/         # L1
  scene_blocks/    # L2
  .metadata/
  .backup/
  # plus vectors.db / persona artifacts depending on backend and pipeline
```

| Host | Typical data root |
| :--- | :--- |
| OpenClaw | `<openclaw-state-dir>/memory-tdai` (often `~/.openclaw/memory-tdai`) |
| Gateway / Hermes | `$TDAI_DATA_DIR` default `~/.memory-tencentdb/memory-tdai` |
| Offload (optional) | `~/.openclaw/context-offload` unless `offload.dataDir` is set |

Factory selection (`createStoreBundle`): `sqlite` uses local vectors and client-side embeddings when configured; `tcvdb` requires `tcvdb.url`, `tcvdb.apiKey`, and `tcvdb.database`, uses server-side embedding (`NoopEmbeddingService` client-side), and optional BM25 sparse encoding (`bm25.language` default `"zh"`).

## Gateway routes (inventory)

| Method | Path | Auth when key set | Purpose |
| :--- | :--- | :--- | :--- |
| `GET` | `/health` | No | Status, version, uptime, store readiness |
| `POST` | `/recall` | Yes | Prefetch memory context |
| `POST` | `/capture` | Yes | Commit a user/assistant turn |
| `POST` | `/search/memories` | Yes | L1 search |
| `POST` | `/search/conversations` | Yes | L0 search |
| `POST` | `/session/end` | Yes | Flush one session (not full process destroy) |
| `POST` | `/seed` | Yes | Batch historical import |

Error envelope: `{ "error": "<message>" }`. Health returns `status: "ok" | "degraded"` based on vector store presence.

## Configuration entry points

| Context | Where |
| :--- | :--- |
| OpenClaw plugin | `~/.openclaw/openclaw.json` → `memory-tencentdb` / nested `config` |
| Schema + defaults | `openclaw.plugin.json` → `configSchema` |
| Gateway file | `TDAI_GATEWAY_CONFIG`, `./tdai-gateway.{yaml,json}`, or `<dataDir>/tdai-gateway.{yaml,json}` |
| Gateway env | `TDAI_GATEWAY_PORT` (8420), `TDAI_GATEWAY_API_KEY`, `TDAI_CORS_ORIGINS`, `TDAI_DATA_DIR`, `TDAI_LLM_*` |
| Hermes client | `MEMORY_TENCENTDB_GATEWAY_*`, `memory.provider: memory_tencentdb` in `~/.hermes/config.yaml` |

Daily tuning defaults that matter on day one: `recall.strategy: "hybrid"`, `recall.maxResults: 5`, `pipeline.everyNConversations: 5`, `pipeline.enableWarmup: true`, `persona.triggerEveryN: 50`, `storeBackend: "sqlite"`, `offload.enabled: false`.

## CLI and package scripts

| Command | Role |
| :--- | :--- |
| `openclaw plugins install/update @tencentdb-agent-memory/memory-tencentdb` | Install / upgrade plugin |
| `openclaw memory-tdai …` | Plugin CLI (seed and related) |
| `migrate-sqlite-to-tcvdb` | Offline SQLite → TCVDB |
| `export-tencent-vdb` | Export TCVDB collections |
| `read-local-memory` | Inspect local L0–L3 artifacts |
| `memory-tencentdb-ctl` (script) | start/stop/status/health/logs for Gateway |
| `npm test` / `npm run build` | Vitest + tsdown |

`postinstall` best-effort applies the OpenClaw after-tool-call messages patch for offload correctness (`|| true` so install never fails hard).

## Choose a path

<CardGroup>
  <Card title="Quickstart (OpenClaw)" href="/quickstart">
    Install plugin, enable `memory-tencentdb`, restart gateway, confirm data-dir activity.
  </Card>
  <Card title="Installation" href="/installation">
    Node/OpenClaw prerequisites, npm package install, update commands, postinstall patch.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Docker greenfield image, attach to existing Hermes, provider directory naming, health checks.
  </Card>
  <Card title="Memory layers" href="/memory-layers">
    L0–L3 model, pipeline schedule, drill-down paths.
  </Card>
</CardGroup>

## Next

<CardGroup>
  <Card title="TdaiCore and host adapters" href="/tdai-core-adapters">
    Facade methods, OpenClaw vs Standalone adapters, hook-to-HTTP mapping.
  </Card>
  <Card title="Context offload" href="/context-offload">
    Symbolic short-term memory, Mermaid injection, compression ratios.
  </Card>
  <Card title="Storage backends" href="/storage-backends">
    sqlite vs tcvdb, BM25, hybrid RRF, embedding roles.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Request/response fields, auth exceptions, error envelope.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full schema, defaults, validation, parseConfig behavior.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Disabled plugin, empty recall, embedding 400, Gateway auth, offload patch gaps.
  </Card>
</CardGroup>

---

## 02. Installation

> Prerequisites (Node >= 22.16, OpenClaw >= 2026.3.13), npm package install, OpenClaw plugin install and update commands, and postinstall patch behavior.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/02-installation.md
- Generated: 2026-08-04T18:29:57.861Z

### Source Files

- `package.json`
- `README.md`
- `SKILL.md`
- `scripts/openclaw-after-tool-call-messages.patch.sh`
- `openclaw.plugin.json`
- `CONTRIBUTING.md`

---
title: "Installation"
description: "Prerequisites (Node >= 22.16, OpenClaw >= 2026.3.13), npm package install, OpenClaw plugin install and update commands, and postinstall patch behavior."
---

`@tencentdb-agent-memory/memory-tencentdb` installs as an OpenClaw plugin (plugin id `memory-tencentdb`) or as a standalone npm package for Hermes Gateway layouts. Package metadata pins Node `>=22.16.0`, OpenClaw plugin API / gateway `>=2026.3.13`, and a `postinstall` hook that best-effort patches OpenClaw for context-offload message injection.

## Prerequisites

| Requirement | Constraint | Where declared |
| :--- | :--- | :--- |
| Node.js | `>= 22.16.0` | `package.json` `engines.node` |
| OpenClaw (in-process host) | `>= 2026.3.13` | `openclaw.compat.pluginApi` / `minGatewayVersion`, README, `SKILL.md` |
| npm / pnpm | Any current installer that can resolve the package | CONTRIBUTING / Hermes install paths |

Verify before install:

```bash
node -v
openclaw --version
```

If either check fails, upgrade Node or OpenClaw first.

<Note>
`peerDependencies.openclaw` is listed as `>=2026.3.7` and marked optional so pure npm / Hermes installs do not hard-fail. For the OpenClaw plugin path, treat **`>=2026.3.13`** as the supported floor (`pluginApi` and `minGatewayVersion`).
</Note>

Optional peers (not required for basic install):

| Peer | Role |
| :--- | :--- |
| `openclaw` | Host when running as an OpenClaw plugin (optional peer) |
| `node-llama-cpp` | Optional local LLM path (`^3.16.2`) |

## Package identity

| Field | Value |
| :--- | :--- |
| npm name | `@tencentdb-agent-memory/memory-tencentdb` |
| Plugin id | `memory-tencentdb` |
| Command alias | `memory-tdai` |
| Entry (OpenClaw) | `./index.ts` via `package.json` `openclaw.extensions` |
| Main (published) | `./dist/index.mjs` |
| License | MIT |

Published layout includes `dist/`, `bin/`, `src/`, `hermes-plugin/`, `openclaw.plugin.json`, Gateway/control scripts (`scripts/memory-tencentdb-ctl.sh`, `scripts/install_hermes_memory_tencentdb.sh`), and the offload patch/setup scripts.

## Install paths

Choose the host surface you are targeting:

| Path | Use when |
| :--- | :--- |
| OpenClaw plugin install | Primary path: long-term memory hooks inside OpenClaw Gateway |
| npm package only | Hermes attach, tooling bins, or custom Gateway process layout |
| Linked local clone | Development (`openclaw plugins install --link .`) |

Hermes Docker / attach steps are documented on [Hermes setup](/hermes-setup). This page covers package acquisition and the OpenClaw install surface.

### OpenClaw plugin install

<Steps>
  <Step title="Install the plugin">
```bash
openclaw plugins install @tencentdb-agent-memory/memory-tencentdb
```
  </Step>
  <Step title="Restart the gateway">
```bash
openclaw gateway restart
```
  </Step>
  <Step title="Enable the plugin (zero-config minimum)">
Edit `~/.openclaw/openclaw.json`:

```json
{
  "memory-tencentdb": {
    "enabled": true
  }
}
```

Defaults use local `SQLite + sqlite-vec`. Full enablement, data-dir signals, and optional offload slot registration are on [Quickstart](/quickstart).
  </Step>
</Steps>

### Update the OpenClaw plugin

Prefer the native OpenClaw updater. Semantic version ranges in ad-hoc package managers can leave the plugin disabled.

```bash
openclaw plugins update @tencentdb-agent-memory/memory-tencentdb
```

`SKILL.md` also accepts the short id form:

```bash
openclaw plugins update memory-tencentdb
```

Restart the gateway after update. If you use context offload, re-run the after-tool-call patch after upgrading OpenClaw itself (see below).

### npm package install (non-OpenClaw / Hermes layout)

For a standalone tree under `~/.memory-tencentdb` (Hermes attach pattern):

```bash
mkdir -p ~/.memory-tencentdb
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
npm init -y --silent
npm install @tencentdb-agent-memory/memory-tencentdb@latest --omit=dev
cp -r node_modules/@tencentdb-agent-memory/memory-tencentdb \
      ~/.memory-tencentdb/tdai-memory-openclaw-plugin
rm -rf "$TEMP_DIR"

cd ~/.memory-tencentdb/tdai-memory-openclaw-plugin
npm install --omit=dev
npm install tsx
```

Automated variant: `scripts/install_hermes_memory_tencentdb.sh` (requires Hermes already installed, Node `>= 22`). Path defaults:

| Env | Default |
| :--- | :--- |
| `MEMORY_TENCENTDB_ROOT` | `~/.memory-tencentdb` |
| `TDAI_INSTALL_DIR` | `$MEMORY_TENCENTDB_ROOT/tdai-memory-openclaw-plugin` |
| `TDAI_DATA_DIR` | `$MEMORY_TENCENTDB_ROOT/memory-tdai` |

### Development link install

From a repository clone:

```bash
git clone https://github.com/TencentCloud/TencentDB-Agent-Memory.git
cd TencentDB-Agent-Memory
npm install
openclaw plugins install --link .
```

OpenClaw loads `.ts` sources at runtime on Node 22.16+ (type stripping). After edits, restart the gateway. Contribution workflow details: [Contributing](/contributing).

## postinstall patch behavior

`package.json` defines:

```json
"postinstall": "bash scripts/openclaw-after-tool-call-messages.patch.sh 2>/dev/null || true"
```

| Behavior | Detail |
| :--- | :--- |
| When it runs | After every npm/pnpm install of this package |
| Failure mode | Errors are swallowed (`2>/dev/null \|\| true`) so install succeeds without OpenClaw |
| Script | `scripts/openclaw-after-tool-call-messages.patch.sh` |
| Purpose | Inject `messages: ctx.params.session?.messages` into OpenClaw `after_tool_call` hook events so context-offload can read full tool-call history |

### What the patch does

1. Resolves the OpenClaw package root via `which openclaw`, pnpm shims, or common global module paths (nvm / npm / yarn / volta layouts).
2. Scans `$OPENCLAW_DIR/dist/**/*.js` for `after_tool_call` + `durationMs`.
3. Inserts session messages into the hook event object (four match strategies; first success wins).
4. Skips files already patched (idempotent).
5. Writes first-time backups as `*.pre-offload-patch.bak`.
6. Exits `0` if at least one file was patched or already skipped; exits `1` only when nothing matched.

Manual invocation (required after OpenClaw upgrades, or when postinstall could not find OpenClaw):

```bash
# Auto-detect OpenClaw install
bash scripts/openclaw-after-tool-call-messages.patch.sh

# Or pass the package root explicitly
bash scripts/openclaw-after-tool-call-messages.patch.sh /path/to/openclaw

# Debug matching
DEBUG=1 bash scripts/openclaw-after-tool-call-messages.patch.sh
```

Restart OpenClaw after a successful patch.

<Warning>
The patch is only required for **context offload** correctness (tool-result history on `after_tool_call`). Core L0→L3 long-term memory does not depend on it. Apply once per OpenClaw install; re-apply after upgrading OpenClaw.
</Warning>

Related enablement: set `offload.enabled`, register `plugins.slots.contextEngine: "memory-tencentdb"`, and optionally use `scripts/setup-offload.sh` — see [Enable context offload](/enable-context-offload).

## Install verification (OpenClaw)

After install + enable + `openclaw gateway restart`:

| Signal | Expected |
| :--- | :--- |
| Gateway logs | Lines with `[memory-tdai]` prefix |
| Data directory | `~/.openclaw/state/memory-tdai/` |
| Layout | At least `conversations/`, `records/`, `scene_blocks/`, `vectors.db` |
| Tools | `tdai_memory_search`, `tdai_conversation_search` available to the agent |

## Common install failures

| Symptom | Check |
| :--- | :--- |
| Plugin silent / no logs | `memory-tencentdb.enabled` is `true` in `~/.openclaw/openclaw.json`; gateway restarted |
| Plugin disabled after upgrade | Prefer `openclaw plugins update …` over range-based package upgrades |
| Offload cannot see tool history | Re-run `openclaw-after-tool-call-messages.patch.sh` after OpenClaw upgrade |
| `postinstall` did nothing | Expected when OpenClaw is not on PATH; run the patch script manually with an explicit path |
| Node too old | Upgrade to Node `>= 22.16.0` |
| OpenClaw too old | Upgrade to `>= 2026.3.13` for plugin API / gateway compatibility |

Broader runtime failures (recall, embedding, gateway auth): [Troubleshooting](/troubleshooting).

## Next

<CardGroup>
  <Card title="Quickstart" href="/quickstart">
    Enable the plugin, restart the gateway, and confirm data-dir success signals.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Docker greenfield image or attach to an existing Hermes install.
  </Card>
  <Card title="Enable context offload" href="/enable-context-offload">
    Offload flags, contextEngine slot, and after-tool-call patch confirmation.
  </Card>
  <Card title="Configure OpenClaw" href="/configure-openclaw">
    Daily and advanced tuning groups after a working install.
  </Card>
  <Card title="Contributing" href="/contributing">
    Linked local install, build, and contribution workflow.
  </Card>
</CardGroup>

---

## 03. Quickstart

> Zero-config OpenClaw enable path: plugins install, openclaw.json memory-tencentdb.enabled, gateway restart, data-dir success signals, and optional offload slot registration.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/03-quickstart.md
- Generated: 2026-08-04T18:30:46.418Z

### Source Files

- `README.md`
- `SKILL.md`
- `index.ts`
- `src/config.ts`
- `src/utils/pipeline-factory.ts`
- `openclaw.plugin.json`

---
title: "Quickstart"
description: "Zero-config OpenClaw enable path: plugins install, openclaw.json memory-tencentdb.enabled, gateway restart, data-dir success signals, and optional offload slot registration."
---

The OpenClaw path for `@tencentdb-agent-memory/memory-tencentdb` is install → set `memory-tencentdb.enabled` → restart the gateway. With no further plugin fields, the runtime uses a local `sqlite` store, turns on L0 capture / L1 extraction / auto-recall, keeps `embedding.provider` at `none` (keyword path only), and leaves context offload off until you opt in.

## Prerequisites

| Requirement | Constraint |
| :--- | :--- |
| Node.js | `>= 22.16.0` (`package.json` `engines`) |
| OpenClaw | Plugin API / gateway `>= 2026.3.13` (`openclaw.compat` / peer guidance) |
| Host config | Writable `~/.openclaw/openclaw.json` (or your `OPENCLAW_STATE_DIR` root) |

```bash
node -v
openclaw --version
```

Upgrade the host before installing the plugin if either check fails.

## Zero-config enable path

<Steps>
<Step title="Install the plugin">
```bash
openclaw plugins install @tencentdb-agent-memory/memory-tencentdb
```

Prefer OpenClaw’s native update command for upgrades so the plugin is not disabled by semantic version ranges:

```bash
openclaw plugins update @tencentdb-agent-memory/memory-tencentdb
```

`postinstall` best-effort runs `scripts/openclaw-after-tool-call-messages.patch.sh` (failures are ignored). That patch is required for full context-offload behavior; long-term memory works without it.
</Step>

<Step title="Enable in openclaw.json">
Edit `~/.openclaw/openclaw.json` so the plugin is on:

```json
{
  "memory-tencentdb": {
    "enabled": true
  }
}
```

Empty functional config is valid: `parseConfig({})` applies defaults. Host-level `enabled` is separate from nested feature toggles such as `capture.enabled` / `recall.enabled` / `offload.enabled`.
</Step>

<Step title="Restart the gateway">
```bash
openclaw gateway restart
```

`register()` runs on gateway start (and on later reloads). It parses plugin config, creates `…/memory-tdai` data dirs, wires `TdaiCore` + `OpenClawHostAdapter`, registers tools/hooks, and optionally registers offload when `offload.enabled` is true.
</Step>

<Step title="Confirm success signals">
Check logs and the data directory (next section). Then run 2–3 chat turns with memorable facts and, in a later turn, call `tdai_memory_search` / `tdai_conversation_search`.
</Step>
</Steps>

## What runs with defaults

| Surface | Default | Behavior |
| :--- | :--- | :--- |
| `storeBackend` | `sqlite` | Local SQLite + sqlite-vec under the plugin data dir |
| `capture.enabled` | `true` | L0 conversation JSONL on `agent_end` |
| `extraction.enabled` | `true` | Background L1 atom extraction |
| `recall.enabled` | `true` | Auto-recall before prompt build (`hybrid`, `maxResults=5`, `scoreThreshold=0.3`) |
| `pipeline.everyNConversations` | `5` | L1 batch every N turns; warm-up starts at 1 and doubles to N |
| `pipeline.l1IdleTimeoutSeconds` | `600` | Idle L1 trigger |
| `embedding.provider` | `none` | No remote vectors; keyword / hybrid degrades without embeddings |
| `offload.enabled` | `false` | Context offload not registered |
| `llm.enabled` | `false` | L1/L2/L3 use the OpenClaw host model |

Plugin id: `memory-tencentdb`. Log tag: `[memory-tdai]`. CLI namespace: `memory-tdai` (via `commandAliases` / `openclaw memory-tdai …`).

## Data directory layout

State root:

```text
{OPENCLAW_STATE_DIR || ~/.openclaw}/memory-tdai/
```

Resolution order: host `runtime.state.resolveStateDir()` → env `OPENCLAW_STATE_DIR` → `~/.openclaw`.

On registration, `initDataDirectories` creates:

:::files
memory-tdai/
├── conversations/     # L0 JSONL
├── records/           # L1 JSONL
├── scene_blocks/      # L2 scene markdown
├── .metadata/         # instance_id, manifest, checkpoints
└── .backup/           # rolling backups
:::

After store init (sqlite backend), expect `vectors.db` in the same directory. `persona.md` appears after L3 persona generation, not at first install.

**Startup log markers** (debug/info, depending on host log level):

- `[memory-tdai] Registering plugin ...`
- `[memory-tdai] Data dir: …/memory-tdai (all subdirectories initialized)`
- `[memory-tdai] Plugin registration complete (v3.1 — TdaiCore). …`

Incomplete remote embedding config logs `[memory-tdai] [EMBEDDING CONFIG ERROR] …` and continues without vectors.

## Smoke test

1. Chat 2–3 turns with concrete preferences or constraints.
2. Start a new turn and watch for auto-recall injection (when memories exist and scores clear the threshold).
3. From the agent, call:

| Tool | Role |
| :--- | :--- |
| `tdai_memory_search` | Search L1 long-term memories |
| `tdai_conversation_search` | Search L0 conversation text |

Combined limit: **3 calls per turn** across both tools.

4. Confirm hits for content just captured (keyword path works without embedding).

## Optional: enable context offload

Offload is independent of long-term memory. Requires plugin **≥ 0.3.4** for the documented OpenClaw short-term path.

### 1 — Config

```json
{
  "memory-tencentdb": {
    "enabled": true,
    "config": {
      "offload": {
        "enabled": true
      }
    }
  }
}
```

Equivalent under the full OpenClaw tree: `plugins.entries.memory-tencentdb.config.offload.enabled = true` (what `scripts/setup-offload.sh` writes).

Default offload data dir: `~/.openclaw/context-offload` (override with `offload.dataDir`).

### 2 — Register the context-engine slot

```json
{
  "plugins": {
    "slots": {
      "contextEngine": "memory-tencentdb"
    }
  }
}
```

Without this slot, OpenClaw does not route context-engine traffic to the plugin.

### 3 — Apply the after-tool-call messages patch

```bash
# From the installed package directory, or a clone that contains scripts/
bash scripts/openclaw-after-tool-call-messages.patch.sh
```

Idempotent per OpenClaw install. Re-run after upgrading OpenClaw. Injects `session.messages` into `after_tool_call` so offload can see tool history.

### 4 — Restart and verify

```bash
openclaw gateway restart
```

Expect offload registration logs when enabled: `Offload enabled, registering offload module...` / `Offload module registered successfully`.

For a scripted enable path with backend URL and user id, use `scripts/setup-offload.sh --enable …` (also sets `agents.defaults.compaction.mode` to `safeguard`). Full tuning: [Enable context offload](/enable-context-offload).

## Common failures

| Symptom | Check |
| :--- | :--- |
| No `[memory-tdai]` logs | `memory-tencentdb.enabled` is true; gateway restarted after edit |
| Config ignored | File is `~/.openclaw/openclaw.json` (or state dir override); nested fields under plugin `config` for entries-style layouts |
| Capture but no recall | `recall.enabled`, `scoreThreshold` not too high; enough L1 records exist |
| No vector hits | Default `embedding.provider=none` is keyword-only; configure embedding four-tuple when needed |
| Offload inactive | `offload.enabled`, `plugins.slots.contextEngine`, patch applied, gateway restart |
| Behavior stuck after edits | Restart gateway again; avoid overwriting unrelated plugin entries |

## Next

<CardGroup>
  <Card title="Configure OpenClaw" href="/configure-openclaw">
    Daily and advanced tuning: capture, pipeline, recall, persona, timezone.
  </Card>
  <Card title="Enable context offload" href="/enable-context-offload">
    Slot registration, patch, compression ratios, Mermaid injection.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    OpenAI-compatible providers, dimensions, sendDimensions, keyword-only degradation.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Docker or attach path when the host is Hermes instead of OpenClaw.
  </Card>
  <Card title="Installation" href="/installation">
    Prerequisites, npm/package install details, postinstall patch behavior.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Disabled plugin, no recall, embedding 400, offload patch, auth.
  </Card>
</CardGroup>

---

## 04. Hermes setup

> Install and run memory_tencentdb with Hermes: Docker greenfield image, attach to existing Hermes, provider directory naming, Gateway auto-discovery, and health verification.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/04-hermes-setup.md
- Generated: 2026-08-04T18:30:16.378Z

### Source Files

- `README.md`
- `hermes-plugin/memory/memory_tencentdb/README.md`
- `hermes-plugin/memory/memory_tencentdb/__init__.py`
- `hermes-plugin/memory/memory_tencentdb/plugin.yaml`
- `hermes-plugin/memory/memory_tencentdb/supervisor.py`
- `docker/opensource/Dockerfile.hermes`
- `docker/opensource/README-hermes.md`
- `scripts/install_hermes_memory_tencentdb.sh`

---
title: "Hermes setup"
description: "Install and run memory_tencentdb with Hermes: Docker greenfield image, attach to existing Hermes, provider directory naming, Gateway auto-discovery, and health verification."
---

Hermes uses the Python provider under `hermes-plugin/memory/memory_tencentdb/` as a thin HTTP client and process supervisor. Capture, L1–L3 extraction, storage, and recall run in the Node.js Gateway (`src/gateway/server.ts`) on `127.0.0.1:8420` by default. Choose **Docker greenfield** (single image) or **attach to an existing Hermes** install.

```text
Hermes Agent (Python)
  MemoryManager
    └─ memory_tencentdb provider
         ├─ GatewaySupervisor  (start / health / log tail)
         └─ MemoryTencentdbSdkClient  →  HTTP 127.0.0.1:8420
                                              │
                                    memory-tencentdb Gateway (Node)
                                      └─ L0 → L1 → L2 → L3 core
```

| Hermes surface | Gateway route | Behavior |
|---|---|---|
| `prefetch(query)` | `POST /recall` | Sync; injects `<memory-context>` |
| `sync_turn(user, assistant)` | `POST /capture` | Background (max 4 in-flight) |
| `shutdown` / `on_session_end` | `POST /session/end` | Flush pipeline |
| `get_tool_schemas()` | — | `memory_tencentdb_memory_search`, `memory_tencentdb_conversation_search` |

<Note>
Provider directory name must be exactly `memory_tencentdb` (underscore). Hermes uses that directory name as the provider key. Config aliases `memory-tencentdb` and `tdai` resolve to the same provider; they are not valid directory names.
</Note>

## Prerequisites

| Requirement | Notes |
|---|---|
| Node.js ≥ 22 | Gateway runtime; Docker image installs Node 22 |
| Hermes Agent | Official install, or bundled in the Docker image |
| OpenAI-compatible LLM | Required for L1/L2/L3; Gateway reads `TDAI_LLM_*` or `tdai-gateway.json` |
| npm package | `@tencentdb-agent-memory/memory-tencentdb` (Docker and install script pull `@latest`) |

## Path A — Docker greenfield

Image build does **not** `COPY` local source. It installs the npm package, Hermes via the official installer, and symlinks the provider into Hermes bundled plugins.

### Build and run

```bash
cd docker/opensource

docker build -f Dockerfile.hermes -t hermes-memory .

docker run -d \
  --name hermes-memory \
  --restart unless-stopped \
  -p 8420:8420 \
  -e MODEL_API_KEY="your-api-key" \
  -e MODEL_BASE_URL="https://api.lkeap.cloud.tencent.com/v1" \
  -e MODEL_NAME="deepseek-v3.2" \
  -e MODEL_PROVIDER="custom" \
  -v hermes_data:/opt/data \
  hermes-memory
```

Image defaults: `MODEL_NAME=deepseek-v3.2`, `MODEL_BASE_URL=https://api.lkeap.cloud.tencent.com/v1`, `MODEL_PROVIDER=custom`. With that stack you can pass only `MODEL_API_KEY`. Any OpenAI-compatible endpoint works by setting the four `MODEL_*` vars.

### What the container does at start

1. Maps `MODEL_*` → `TDAI_LLM_*` for the Gateway.
2. Writes `/opt/data/config.yaml` with model settings and `memory.provider: memory_tencentdb`.
3. Writes `/opt/data/.env` with `OPENAI_API_KEY` for Hermes.
4. Starts the Gateway in the foreground on `:8420` (keeps the container alive).

Interact with Hermes:

```bash
docker exec -it hermes-memory hermes
```

### Docker environment

| Variable | Default | Role |
|---|---|---|
| `MODEL_API_KEY` | _(required at run)_ | Shared LLM key for Hermes + Gateway |
| `MODEL_BASE_URL` | Tencent LKE OpenAI path | LLM base URL |
| `MODEL_NAME` | `deepseek-v3.2` | Model id |
| `MODEL_PROVIDER` | `custom` | Hermes provider label |
| `TDAI_GATEWAY_PORT` | `8420` | Gateway listen port |
| `TDAI_GATEWAY_HOST` | `0.0.0.0` | Gateway bind (healthcheck uses localhost) |
| `TDAI_DATA_DIR` | `/opt/data/tdai-memory` | L0–L3 data root |
| `HERMES_HOME` | `/opt/data` | Hermes config/sessions home |
| `MEMORY_TENCENTDB_GATEWAY_HOST` | `127.0.0.1` | Plugin → Gateway host |
| `MEMORY_TENCENTDB_GATEWAY_PORT` | `8420` | Plugin → Gateway port |

### Docker data layout

```text
/opt/data/
├── tdai-memory/       # Gateway data (SQLite, scenes, persona, …)
├── sessions/
├── skills/
├── config.yaml        # generated at start
├── .env               # generated at start
└── gateway.log        # when present
```

## Path B — Attach to existing Hermes

Use this when Hermes is already installed on the host. Prefer `scripts/install_hermes_memory_tencentdb.sh` after Hermes is present, or follow the manual steps below.

### Install script (recommended)

```bash
# After Hermes install; Node ≥ 22 on PATH
bash scripts/install_hermes_memory_tencentdb.sh
```

The script:

1. Migrates legacy `~/tdai-memory-openclaw-plugin` and `~/memory-tdai` into `~/.memory-tencentdb/` when needed.
2. Downloads `@tencentdb-agent-memory/memory-tencentdb@latest` into `~/.memory-tencentdb/tdai-memory-openclaw-plugin`.
3. Runs `npm install` (and installs `tsx` if missing).
4. Symlinks the provider into `$HERMES_AGENT_DIR/plugins/memory/memory_tencentdb` (default `$HERMES_HOME/hermes-agent`).
5. Writes `MEMORY_TENCENTDB_GATEWAY_*` into `/etc/profile.d/memory-tencentdb-env.sh` and `~/.hermes/.env` (for systemd / interactive shells).
6. Does **not** auto-enable the provider; you must set `memory.provider` yourself.

Override roots with `MEMORY_TENCENTDB_ROOT`, `TDAI_INSTALL_DIR`, `TDAI_DATA_DIR`, `HERMES_AGENT_DIR`, `INSTALL_AS_USER`.

### Manual attach

<Steps>
<Step title="Install the npm package under the canonical root">
```bash
mkdir -p ~/.memory-tencentdb
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
npm init -y --silent
npm install @tencentdb-agent-memory/memory-tencentdb@latest --omit=dev
cp -r node_modules/@tencentdb-agent-memory/memory-tencentdb \
      ~/.memory-tencentdb/tdai-memory-openclaw-plugin
rm -rf "$TEMP_DIR"

cd ~/.memory-tencentdb/tdai-memory-openclaw-plugin
npm install --omit=dev
npm install tsx
```
</Step>
<Step title="Link the provider (underscore directory name)">
```bash
rm -rf ~/.hermes/hermes-agent/plugins/memory/memory_tencentdb
ln -sf ~/.memory-tencentdb/tdai-memory-openclaw-plugin/hermes-plugin/memory/memory_tencentdb \
       ~/.hermes/hermes-agent/plugins/memory/memory_tencentdb
```

Developer symlink from a git checkout:

```bash
ln -s "$(pwd)/hermes-plugin/memory/memory_tencentdb" \
      <hermes-agent-checkout>/plugins/memory/memory_tencentdb
```
</Step>
<Step title="Enable the provider in Hermes config">
In `~/.hermes/config.yaml`:

```yaml
memory:
  provider: memory_tencentdb
  # aliases still accepted: memory-tencentdb, tdai
```
</Step>
<Step title="Configure Gateway env and LLM">
In `~/.hermes/.env` (authoritative for systemd-managed Hermes):

```bash
MEMORY_TENCENTDB_GATEWAY_CMD="sh -c 'cd ~/.memory-tencentdb/tdai-memory-openclaw-plugin && exec npx tsx src/gateway/server.ts'"
MEMORY_TENCENTDB_GATEWAY_HOST="127.0.0.1"
MEMORY_TENCENTDB_GATEWAY_PORT="8420"

# Gateway-side LLM (what src/gateway/config.ts actually reads)
TDAI_LLM_API_KEY="sk-..."
TDAI_LLM_BASE_URL="https://api.openai.com/v1"
TDAI_LLM_MODEL="gpt-4o"
```

Or write LLM (and other Gateway settings) to `~/.memory-tencentdb/memory-tdai/tdai-gateway.json`:

```json
{
  "llm": {
    "baseUrl": "https://your-api-endpoint/v1",
    "apiKey": "your-api-key",
    "model": "your-model-name"
  }
}
```
</Step>
<Step title="Start or let the supervisor start the Gateway">
See [Gateway startup options](#gateway-startup-options) below.
</Step>
</Steps>

### Default path map (host)

| Path | Purpose |
|---|---|
| `~/.memory-tencentdb/` | Unified install root (`MEMORY_TENCENTDB_ROOT`) |
| `…/tdai-memory-openclaw-plugin/` | Package extract + `node_modules` + Gateway source |
| `…/memory-tdai/` | Gateway data dir default (`TDAI_DATA_DIR`) |
| `…/memory-tdai/tdai-gateway.json` | Optional Gateway config file |
| `~/.hermes/config.yaml` | Hermes memory provider selection |
| `~/.hermes/.env` | Env for Hermes (and supervised Gateway child) |
| `~/.hermes/logs/memory_tencentdb/` | Supervisor stdout/stderr logs (default) |

## Provider discovery and directory naming

Hermes scans, in order:

1. **Bundled** — `<hermes-agent>/plugins/memory/<name>/` (preferred for this package).
2. **User** — `$HERMES_HOME/plugins/<name>/` (default `~/.hermes/plugins/`).

Requirements inside the provider dir:

- Directory name: `memory_tencentdb`
- Files: `__init__.py`, `plugin.yaml`, plus `client.py` / `supervisor.py`
- `__init__.py` must contain `MemoryProvider` or `register_memory_provider` (both present)

`plugin.yaml` metadata:

```yaml
name: memory_tencentdb
display_name: memory-tencentdb
hooks:
  - on_memory_write
  - on_session_end
aliases:
  - tdai
  - memory-tencentdb
```

Verify discovery from a Hermes checkout:

```bash
python -c 'from plugins.memory import discover_memory_providers; \
           [print(n, a) for n, _, a in discover_memory_providers()]'
# expect: memory_tencentdb ...
```

## Gateway startup options

| Option | When to use | Mechanism |
|---|---|---|
| **A — Auto-discovery** | Install at a well-known path; no `MEMORY_TENCENTDB_GATEWAY_CMD` | Provider finds `src/gateway/server.ts`, `Popen`s via `sh -c 'cd … && pnpm exec tsx …'` |
| **B — Explicit CMD** | Pin path; install script; systemd PATH issues | Set `MEMORY_TENCENTDB_GATEWAY_CMD` |
| **C — External process** | Docker CMD, `memory-tencentdb-ctl`, manual `npx tsx` | Provider sees `/health` and skips spawn |

### Auto-discovery search order

When `MEMORY_TENCENTDB_GATEWAY_CMD` is unset:

1. In-tree: plugin root’s `src/gateway/server.ts` (via `__file__` → parents)
2. `~/.memory-tencentdb/tdai-memory-openclaw-plugin/src/gateway/server.ts` (canonical)
3. `~/tdai-memory-openclaw-plugin/src/gateway/server.ts` (legacy)
4. `~/.hermes/plugins/tdai-memory-openclaw-plugin/src/gateway/server.ts` (legacy)

Success logs:

```text
memory-tencentdb Gateway command auto-discovered: …/src/gateway/server.ts
```

`MEMORY_TENCENTDB_GATEWAY_CMD` always wins over discovery.

### Supervisor behavior

- If `/health` already returns `status` in `ok` | `degraded`, attach and do not spawn.
- Otherwise spawn `MEMORY_TENCENTDB_GATEWAY_CMD` (or discovered command), wait up to **30s**, poll every **0.5s**.
- Gateway init runs on a **background thread** so Hermes is not blocked up to 30s; early turns no-op until ready.
- Logs: `MEMORY_TENCENTDB_LOG_DIR` or `~/.hermes/logs/memory_tencentdb` → `gateway.stdout.log` / `gateway.stderr.log`.
- Child stdout/stderr go to files (not pipes) to avoid pipe-buffer deadlock.
- Circuit breaker: **5** consecutive failures → pause **60s**.
- Capture back-pressure: max **4** in-flight `sync_turn` threads.
- Watchdog (~**10s**) can revive a dead Gateway without restarting Hermes.

### Day-2 ops with `memory-tencentdb-ctl`

```bash
# hermes mode: logs under ~/.hermes, enable-hermes-memory available
memory-tencentdb-ctl --hermes start
memory-tencentdb-ctl --hermes health
memory-tencentdb-ctl --hermes enable-hermes-memory
```

Default mode is **standalone** (does not touch `~/.hermes`). Use `--hermes` or `MEMORY_TENCENTDB_MODE=hermes` when integrating with Hermes. See [Gateway lifecycle](/gateway-ops).

## Environment variables (Hermes client)

<ParamField body="MEMORY_TENCENTDB_GATEWAY_HOST" type="string">
Default `127.0.0.1`. Gateway host for the Python client.
</ParamField>

<ParamField body="MEMORY_TENCENTDB_GATEWAY_PORT" type="string">
Default `8420`. Invalid or out-of-range values fall back to `8420`.
</ParamField>

<ParamField body="MEMORY_TENCENTDB_GATEWAY_CMD" type="string">
Shell command the supervisor `Popen`s. Install script resolves absolute `node` and uses `node --import tsx/esm` so systemd does not depend on nvm PATH.
</ParamField>

<ParamField body="MEMORY_TENCENTDB_LOG_DIR" type="string">
Supervisor log directory. Default `~/.hermes/logs/memory_tencentdb`.
</ParamField>

<ParamField body="MEMORY_TENCENTDB_GATEWAY_API_KEY" type="string">
Client Bearer token. Fallback: `TDAI_GATEWAY_API_KEY`. Does **not** enable Gateway auth; set `TDAI_GATEWAY_API_KEY` / `server.apiKey` on the Gateway separately.
</ParamField>

<ParamField body="TDAI_DATA_DIR" type="string">
Gateway data root. Not owned by the Python provider. Default `~/.memory-tencentdb/memory-tdai` (legacy `~/memory-tdai` if present).
</ParamField>

<ParamField body="TDAI_LLM_API_KEY" type="string" required>
Gateway LLM key (or set via `tdai-gateway.json` `llm.apiKey`). Required for L1/L2/L3.
</ParamField>

Data directory is resolved only inside the Gateway (`TDAI_DATA_DIR` → config file `data.baseDir` → defaults). `MEMORY_TENCENTDB_DATA_DIR` is not read.

## Health verification

Expected Gateway health statuses: `ok` or `degraded`.

```bash
# Host attach or published Docker port
curl -s http://127.0.0.1:8420/health

# Docker container
docker exec hermes-memory curl -s http://localhost:8420/health

# Optional recall smoke test
curl -s -X POST http://127.0.0.1:8420/recall \
  -H "Content-Type: application/json" \
  -d '{"query":"test","session_key":"debug"}'
```

| Check | Signal |
|---|---|
| Provider linked | Path ends with `plugins/memory/memory_tencentdb` |
| Provider enabled | `memory.provider: memory_tencentdb` in Hermes config |
| Gateway process | Listen on configured port; `/health` ok/degraded |
| Auto-discovery | Log line `Gateway command auto-discovered` |
| Tools visible | LLM sees `memory_tencentdb_*` tools after init |
| Auth (if enabled) | Client and Gateway share the same secret |

`GET /health` stays unauthenticated even when `TDAI_GATEWAY_API_KEY` is set (orchestrator probes).

## Troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `memory-tencentdb Gateway not available` | No CMD, no discovery hit, nothing on :8420, or crash | Check `gateway.stderr.log`; set `MEMORY_TENCENTDB_GATEWAY_CMD`; enable DEBUG for discovery path list |
| Wrong checkout started | Discovery preference order | Set `MEMORY_TENCENTDB_GATEWAY_CMD` explicitly |
| Provider missing from discovery | Hyphen dir name, wrong path, missing files | Use underscore dir; ensure `__init__.py` + `plugin.yaml` |
| Search tools empty | No Gateway and no CMD/PORT env | Set `MEMORY_TENCENTDB_GATEWAY_CMD` or PORT so schemas register optimistically |
| Circuit breaker tripped | 5 consecutive Gateway errors | Fix Gateway/LLM; wait 60s |
| Capture backlog | ≥4 in-flight syncs | Inspect L1/LLM timeouts on Gateway |
| Auth 401 | Gateway key set, client key missing/mismatch | Align `TDAI_GATEWAY_API_KEY` and `MEMORY_TENCENTDB_GATEWAY_API_KEY` |
| systemd PATH / nvm | `node`/`npx` not found | Re-run install script (absolute `node` in CMD) or use full path |
| Docker: no memory | Missing `MODEL_API_KEY` or volume not mounted | Pass `-e MODEL_API_KEY` and `-v …:/opt/data` |

Docker debug:

```bash
docker exec hermes-memory cat /opt/data/config.yaml
docker exec hermes-memory env | grep -E '(MODEL_|TDAI_LLM_)'
docker exec hermes-memory cat /opt/data/tdai-memory/gateway.log
```

## Related pages

<CardGroup>
<Card title="Overview" href="/overview">
Host surfaces: OpenClaw plugin vs Hermes Gateway.
</Card>
<Card title="Gateway lifecycle" href="/gateway-ops">
`memory-tencentdb-ctl` start/stop/status/health and path layout.
</Card>
<Card title="Secure the Gateway" href="/secure-gateway">
Bearer auth, CORS, client/server key alignment.
</Card>
<Card title="Gateway HTTP API" href="/gateway-http-api">
`/health`, `/recall`, `/capture`, search, seed routes.
</Card>
<Card title="Environment variables" href="/environment-variables">
`TDAI_*`, `MEMORY_TENCENTDB_*`, `MODEL_*` resolution order.
</Card>
<Card title="Agent tools" href="/agent-tools">
Hermes `memory_tencentdb_*` tool schemas and limits.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Broader failure modes and probe checklist.
</Card>
</CardGroup>

---

## 05. Memory layers

> Repo-specific L0 conversation, L1 atom, L2 scene, and L3 persona model: what each layer stores, how the pipeline schedules L1→L2→L3, and drill-down paths between layers.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/05-memory-layers.md
- Generated: 2026-08-04T19:25:39.435Z

### Source Files

- `src/core/tdai-core.ts`
- `src/core/conversation/l0-recorder.ts`
- `src/core/record/l1-extractor.ts`
- `src/core/scene/scene-extractor.ts`
- `src/core/persona/persona-generator.ts`
- `src/core/types.ts`
- `src/utils/pipeline-factory.ts`

---
title: "Memory layers"
description: "Repo-specific L0 conversation, L1 atom, L2 scene, and L3 persona model: what each layer stores, how the pipeline schedules L1→L2→L3, and drill-down paths between layers."
---

TDAI memory is organized into four layers that live under the plugin data directory: L0 raw conversation messages (`conversations/*.jsonl`), L1 extracted memory atoms (`records/*.jsonl` plus the vector store), L2 scene blocks (`scene_blocks/*.md` with a synced `scene_index.json`), and a single L3 persona document (`persona.md`). `MemoryPipelineManager` (`src/utils/pipeline-manager.ts`) schedules the L1→L2→L3 promotion asynchronously after each captured turn, and `TdaiCore` (`src/core/tdai-core.ts`) wires the layer runners identically for the OpenClaw in-process host and the standalone Gateway/Hermes host.

## Layer summary

| Layer | Producer | Storage | Unit | Trigger |
|-------|----------|---------|------|---------|
| L0 | `recordConversation` (`src/core/conversation/l0-recorder.ts`) | `conversations/YYYY-MM-DD.jsonl` + optional L0 vectors | One sanitized message per JSONL line | Every committed turn (`agent_end` / `sync_turn`) |
| L1 | `extractL1Memories` (`src/core/record/l1-extractor.ts`) | `records/YYYY-MM-DD.jsonl` + vector store | Typed memory atom (`persona` / `episodic` / `instruction`) | Conversation threshold, idle timeout, or shutdown flush |
| L2 | `SceneExtractor` (`src/core/scene/scene-extractor.ts`) | `scene_blocks/*.md` + `scene_index.json` | Scene block (META header + Markdown body) | Downward-only timer after L1 completes |
| L3 | `PersonaGenerator` (`src/core/persona/persona-generator.ts`) | `persona.md` | Persona document with appended scene navigation | `PersonaTrigger` after L2 (every N memories or explicit request) |

```mermaid
flowchart TB
  subgraph Host["Host events (OpenClaw hook / Gateway HTTP)"]
    TURN["handleTurnCommitted (agent_end / sync_turn)"]
    RECALL["handleBeforeRecall (before_prompt_build / prefetch)"]
  end

  subgraph Pipeline["MemoryPipelineManager (serial queues L1/L2/L3)"]
    L1R["L1 runner — text-only LLM extraction"]
    L2R["L2 runner — SceneExtractor (tools, sandboxed)"]
    L3R["L3 runner — PersonaTrigger + PersonaGenerator"]
  end

  subgraph Data["dataDir"]
    L0[("conversations/*.jsonl (L0)")]
    L1S[("records/*.jsonl + vector store (L1)")]
    L2S[("scene_blocks/*.md + scene_index.json (L2)")]
    L3S[("persona.md (L3)")]
  end

  TURN -->|"recordConversation + notifyConversation"| L0
  L0 -->|"grouped by sessionId, cursor-incremental"| L1R --> L1S
  L1S -->|"updatedAt cursor"| L2R --> L2S
  L2S -->|"changed scenes since last persona"| L3R --> L3S
  L3S -->|"persona + scene navigation → system prompt"| RECALL
  L1S -->|"hybrid search hits → prepend context"| RECALL
```

## L0 — conversation record

`recordConversation` runs on every committed turn. It captures only incremental messages using two protection layers: a position slice based on `originalUserMessageCount` (the message count cached at `before_prompt_build`, immune to timestamp drift after gateway restarts) and an `afterTimestamp` cursor fallback. The user message that OpenClaw pollutes with injected `prependContext` is replaced with the cached clean `originalUserText`.

Each surviving message is sanitized (`sanitizeText`), assistant replies have fenced code blocks stripped (`stripCodeBlocks`), inline base64 image data URIs are collapsed to `[image]`, and noise is dropped by `shouldCaptureL0`. Records are appended as flat JSONL, one message per line, into a per-day file shared by all sessions:

```json title="conversations/2026-08-04.jsonl (one L0MessageRecord per line)"
{"sessionKey":"agent:main","sessionId":"s-01","recordedAt":"2026-08-04T09:12:33.512Z","id":"msg_1754298753000_a1b2c3","role":"user","content":"...","timestamp":1754298753000}
```

The `id` field is load-bearing: L1 extraction reports `source_message_ids` that point back to these L0 message IDs. When the vector store is available, L0 messages are also indexed for `tdai_conversation_search`; when it is not, L1 falls back to reading the JSONL files directly via `readConversationMessagesGroupedBySessionId` (groups per `sessionId`, since one `sessionKey` can span multiple conversation instances after `/reset`).

## L1 — memory atoms

`extractL1Memories` turns buffered L0 messages into structured atoms with a single text-only LLM call (`taskId: "l1-extraction"`, 180 s timeout). The stages are:

1. **Quality gate** — `shouldExtractL1` filters length/symbol/prompt-injection noise. L0 deliberately captures everything; strictness lives here.
2. **Windowing** — the newest 10 messages are the extraction target, with up to 5 older messages as background context, plus `previousSceneName` for scene continuity across batches.
3. **Scene-segmented extraction** — the LLM returns a JSON array of `{scene_name, message_ids, memories[]}` segments; invalid types are dropped, legacy names normalized (`episode`→`episodic`, `preference`→`persona`).
4. **Batch dedup** — when `extraction.enableDedup` is on (default), `batchDedup` recalls similar existing records (vector top-K via `embedding.conflictRecallTopK`) and decides store/merge per atom; on dedup failure everything is stored as new.
5. **Write** — `writeMemory` appends to `records/YYYY-MM-DD.jsonl` and the vector store.

A stored `MemoryRecord` (`src/core/record/l1-writer.ts`) carries the cross-layer link fields:

<ResponseField name="type" type='"persona" | "episodic" | "instruction"'>Memory category; `priority` is 0–100 (−1 marks a strict global instruction).</ResponseField>
<ResponseField name="scene_name" type="string">Scene assigned during extraction — the upward link toward L2 scene blocks.</ResponseField>
<ResponseField name="source_message_ids" type="string[]">L0 message IDs this atom was distilled from — the downward drill-down link.</ResponseField>
<ResponseField name="sessionKey / sessionId" type="string">Provenance: conversation channel and instance identifiers.</ResponseField>

Per LLM call, at most `extraction.maxMemoriesPerSession` atoms are kept (config default 20).

## L2 — scene blocks

`SceneExtractor.extract()` is an agentic step: the LLM runs with **tools enabled** and its `workspaceDir` sandboxed to `scene_blocks/`, so it can only read and write `.md` scene files — checkpoints, `scene_index.json`, and `persona.md` are physically invisible to it. Each run:

1. Backs up `scene_blocks/` (`persona.sceneBackupCount`, default 10 backups) and restores the backup if the LLM run fails.
2. Feeds new L1 records (incremental via the `updatedAt` cursor tracked by the pipeline manager) plus summaries of existing scenes with a capacity counter. A tiered warning enforces `persona.maxScenes` (default 15): near the limit CREATE is discouraged, at the limit the LLM must MERGE scenes first.
3. Cleans up after the LLM: files containing only `[DELETED]` or an empty META body are removed (the LLM has no `exec` tool; soft-delete markers are its only deletion mechanism), filenames are normalized, and `syncSceneIndex` rebuilds `scene_index.json` from disk.
4. Refreshes the scene-navigation section of `persona.md` and parses the LLM output for an out-of-band `[PERSONA_UPDATE_REQUEST]reason[/PERSONA_UPDATE_REQUEST]` signal, which is persisted to the checkpoint for L3.

Scene files use a META-delimited format parsed by `src/core/scene/scene-format.ts`:

```text title="scene_blocks/<scene>.md"
-----META-START-----
created: 2026-07-30 10:02
updated: 2026-08-04 09:15
summary: One-line scene summary used in navigation and prompts
heat: 120
-----META-END-----

<Markdown scene body: profile, event timeline, stage conclusions>
```

`heat` counts recall hits for the scene and drives navigation ordering.

## L3 — persona

`createL3Runner` first consults `PersonaTrigger` (fires every `persona.triggerEveryN` processed memories, default 50, or on a persisted persona-update request). `PersonaGenerator.generateLocalPersona()` then:

1. Reads the existing `persona.md` (navigation stripped) to pick `first` vs `incremental` mode.
2. Diffs the scene index against `last_persona_time` in the checkpoint and preloads the full content of changed scene blocks into the prompt; with no changes and an existing persona, generation is skipped.
3. Runs the LLM with tools enabled and `workspaceDir` set to the data directory (180 s timeout) — the LLM writes `persona.md` directly.
4. Post-processes: strips any navigation the LLM added, escapes XML-like tags for safe prompt injection, appends fresh scene navigation, and writes the final file. `persona.md` is backed up before each run (`persona.backupCount`, default 3).

## Pipeline scheduling

`MemoryPipelineManager` owns three serial queues (concurrency 1 each) and per-session timers. Config keys live under the `pipeline` group (`src/config.ts`):

| Config key | Default | Role |
|------------|---------|------|
| `pipeline.everyNConversations` | `5` | L1 conversation-count threshold |
| `pipeline.enableWarmup` | `true` | New-session threshold ramps 1 → 2 → 4 → 8 → … → `everyNConversations` |
| `pipeline.l1IdleTimeoutSeconds` | `600` | Resettable idle timer that flushes below-threshold buffers through L1 |
| `pipeline.l2DelayAfterL1Seconds` | `10` | L2 fire-time advance after an L1 completion |
| `pipeline.l2MinIntervalSeconds` | `900` | Floor between L2 runs per session |
| `pipeline.l2MaxIntervalSeconds` | `3600` | Guaranteed L2 poll interval for active sessions |
| `pipeline.sessionActiveWindowHours` | `24` | Sessions idle longer than this stop L2 polling until the next L1 event |

L1 has three trigger paths: the (warm-up-adjusted) conversation threshold, the idle timeout, and a shutdown flush; failed L1 runs retry after 30 s, up to 5 consecutive attempts per session. The L2 timer is **downward-only** — its fire time can move earlier but never later: after L1 completes it advances to `max(now + delayAfterL1, lastL2 + minInterval)`, and after each L2 run it resets to `now + maxInterval`. L3 runs after L2 completes behind a global mutex with a pending-flag dedup, so concurrent requests collapse into one persona generation.

```mermaid
stateDiagram-v2
    [*] --> Buffering : notifyConversation()
    Buffering --> L1 : count ≥ threshold (warm-up 1→2→4→…)
    Buffering --> L1 : idle l1IdleTimeoutSeconds
    Buffering --> L1 : shutdown flush
    L1 --> L2armed : advance timer to max(now+delay, lastL2+minInterval)
    L2armed --> L2 : timer fires (session active)
    L2armed --> Cancelled : session cold > sessionActiveWindowHours
    Cancelled --> L2armed : next L1 event re-arms
    L2 --> L3 : enqueue (global mutex, pending-flag dedup)
    L2 --> L2armed : reset timer to now + maxInterval
    L3 --> [*] : persona.md written, checkpoint advanced
```

Progress is durable: `CheckpointManager` persists per-session L1 cursors (max `recordedAtMs` of processed L0 batches), the last scene name for continuity, L2 `updatedAt` cursors, `scenes_processed` / `total_processed` counters, and `last_persona_time`. `TdaiCore.handleSessionEnd` flushes exactly one session's buffered work without touching other sessions or the shared scheduler; full teardown is `destroy()` only.

<Note>
Runner boundaries differ by layer: L1 (and dedup) use a **text-only** LLM runner (`enableTools: false`), while L2 and L3 require a **tool-enabled** runner (`enableTools: true`). `TdaiCore.wirePipelineRunners` selects the OpenClaw embedded runner or `StandaloneLLMRunnerFactory` (direct OpenAI-compatible HTTP) based on `llm.enabled` and the host type.
</Note>

## Drill-down paths between layers

Recall (`src/core/hooks/auto-recall.ts`) injects the layers top-down and gives the agent explicit paths back down:

- **L3 → prompt**: `persona.md` (persona + scene navigation + a memory-tools guide) becomes the stable `appendSystemContext`; L1 search hits (keyword FTS5 BM25, embedding, or hybrid RRF) become the per-turn `prependContext`.
- **L3 → L2**: the scene-navigation section of `persona.md` lists each scene's absolute `scene_blocks/<file>.md` path, heat, and summary, so the agent can `read_file` a full scene on demand (progressive disclosure).
- **L2 → L1**: scene blocks are consolidations of L1 atoms; `tdai_memory_search` supports a `scene` filter to retrieve the underlying atoms for a scene.
- **L1 → L0**: each `MemoryRecord` carries `source_message_ids` referencing L0 message IDs, and `tdai_conversation_search` retrieves the raw conversation lines for exact wording and timelines.

The injected tools guide caps `tdai_memory_search` + `tdai_conversation_search` at 3 combined calls per turn, and recall as a whole is bounded by `recall.timeoutMs` (default 5000 ms) — on timeout the turn proceeds with no memory injection rather than blocking the user.

## On-disk layout

:::files
dataDir/                      # e.g. ~/.openclaw/memory-tdai/
├── conversations/            # L0 — daily JSONL shards, one message per line
│   └── 2026-08-04.jsonl
├── records/                  # L1 — daily JSONL shards of MemoryRecords
│   └── 2026-08-04.jsonl
├── scene_blocks/             # L2 — META + Markdown scene files (LLM sandbox)
│   └── <scene>.md
├── persona.md                # L3 — persona + scene navigation
├── .metadata/                # scene_index.json, recall_checkpoint.json
└── .backup/                  # scene_blocks/ and persona.md backups
:::

## Related pages

<CardGroup cols={2}>
  <Card title="Storage backends" href="/storage-backends">How L0/L1 vectors, FTS5 BM25, and hybrid RRF retrieval work across sqlite and tcvdb.</Card>
  <Card title="Configure OpenClaw" href="/configure-openclaw">Tune the capture, pipeline, recall, and persona config groups behind these layers.</Card>
  <Card title="Inspect local memory" href="/inspect-local-memory">Query L0–L3 artifacts on disk with read-local-memory and the diagnostic export.</Card>
  <Card title="Agent tools" href="/agent-tools">tdai_memory_search and tdai_conversation_search parameters, strategies, and the 3-call limit.</Card>
  <Card title="Seed historical conversations" href="/seed-history">Run the same L0→L1→L2→L3 path offline over imported conversation JSON.</Card>
  <Card title="Context offload" href="/context-offload">The separate symbolic short-term memory pipelines that complement these layers.</Card>
</CardGroup>

---

## 06. 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.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/06-context-offload.md
- Generated: 2026-08-04T18:31:05.052Z

### 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>

---

## 07. Storage backends

> sqlite (SQLite + sqlite-vec + FTS5) vs tcvdb store backends, BM25 sparse encoding, hybrid RRF retrieval, embedding service roles, and factory selection constraints.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/07-storage-backends.md
- Generated: 2026-08-04T18:30:57.043Z

### Source Files

- `src/core/store/factory.ts`
- `src/core/store/types.ts`
- `src/core/store/sqlite.ts`
- `src/core/store/tcvdb.ts`
- `src/core/store/embedding.ts`
- `src/core/store/bm25-local.ts`
- `src/core/store/search-utils.ts`

---
title: "Storage backends"
description: "sqlite (SQLite + sqlite-vec + FTS5) vs tcvdb store backends, BM25 sparse encoding, hybrid RRF retrieval, embedding service roles, and factory selection constraints."
---

`createStoreBundle()` in `src/core/store/factory.ts` selects the memory persistence stack from `storeBackend` (`sqlite` | `tcvdb`, default `sqlite`) and returns a `StoreBundle`: `store` (`IMemoryStore`), `embedding` (`IEmbeddingService`), optional `bm25Encoder`, and a `storeSnapshot` for manifest binding. Upper layers (hooks, tools, pipeline, `TdaiCore`) depend only on `IMemoryStore` and capability flags—not on concrete backends.

## Architecture

```mermaid
flowchart TB
  subgraph callers [Callers]
    AR[auto-recall / tools]
    AC[auto-capture]
    PF[pipeline-factory]
  end

  subgraph factory [createStoreBundle]
    CFG["config.storeBackend"]
  end

  subgraph sqlite_path [sqlite default]
    VS[VectorStore]
    DB["dataDir/vectors.db"]
    VEC[sqlite-vec vec0]
    FTS[FTS5 BM25]
    EMB[OpenAIEmbeddingService or none]
  end

  subgraph tcvdb_path [tcvdb]
    TMS[TcvdbMemoryStore]
    CLI[TcvdbClient HTTP]
    SSE[server-side embedding]
    BM25[BM25LocalEncoder sparse]
    HYB["hybridSearch dense+sparse+RRF k=60"]
  end

  PF --> factory
  CFG -->|sqlite| VS
  CFG -->|tcvdb| TMS
  VS --> DB
  VS --> VEC
  VS --> FTS
  factory --> EMB
  TMS --> CLI
  TMS --> SSE
  TMS --> BM25
  CLI --> HYB
  AR --> VS
  AR --> TMS
  AC --> VS
  AC --> TMS
```

| Concern | `sqlite` | `tcvdb` |
| --- | --- | --- |
| Implementation | `VectorStore` (`sqlite.ts`) | `TcvdbMemoryStore` (`tcvdb.ts`) |
| On-disk / remote | `{dataDir}/vectors.db` | Tencent VectorDB HTTP API |
| Dense vectors | Client embed → `l1_vec` / `l0_vec` (sqlite-vec) | Collection `embedding` + server-side `embeddingItems` |
| Keyword / sparse | FTS5 + jieba tokenization | Client BM25 sparse vectors (`sparse_vector`) |
| Hybrid RRF | Client-side: FTS list + vector list → `rrfMerge` / local RRF | Native `/document/hybridSearch` with `rerank: { method: "rrf", k: 60 }` |
| Embedding service | Remote OpenAI-compatible when configured; else none / keyword-only | Always `NoopEmbeddingService` (dims `0`) |
| `supportsDeferredEmbedding` | `true` (metadata first, background `updateL0Embedding`) | not set (server embeds on upsert/search) |
| Profile sync (L2/L3) | local files primarily | remote `profiles` collection + optional sync APIs |

## Factory selection

`createStoreBundle(config, { dataDir, logger })` always builds the BM25 encoder from `config.bm25` first, then switches on `config.storeBackend`.

### `storeBackend: "tcvdb"`

Hard requirements (throws if missing):

| Field | Constraint |
| --- | --- |
| `tcvdb.url` | Required |
| `tcvdb.apiKey` | Required |
| `tcvdb.database` | Required (unique database name) |

Other fields passed into `TcvdbMemoryStore`: `username` (default `"root"`), `embeddingModel` (default `"bge-large-zh"`), `timeout` (default `10000`), optional `caPemPath`.

Returned bundle:

- `store`: `TcvdbMemoryStore`
- `embedding`: `NoopEmbeddingService`
- `bm25Encoder`: from `createBM25Encoder` when `bm25.enabled`
- `storeSnapshot`: `{ type: "tcvdb", tcvdbUrl, tcvdbDatabase, tcvdbAlias? }`

### `storeBackend: "sqlite"` (default)

Any value other than `"tcvdb"` resolves to `"sqlite"` in `parseConfig`.

Embedding construction (client-side only):

- Created when `embedding.enabled && embedding.provider !== "local" && embedding.apiKey`
- Otherwise no embedding service (vector search off; FTS may still work)
- `dimensions` from config; `0` when `provider === "none"` defers sqlite-vec tables

Returned bundle:

- `store`: `VectorStore(path.join(dataDir, "vectors.db"), dimensions, logger)`
- `embedding`: remote service or effectively absent
- `bm25Encoder`: still created if enabled (used primarily by tcvdb paths; sqlite keyword search uses FTS5, not sparse vectors)
- `storeSnapshot`: `{ type: "sqlite", sqlitePath: relative "vectors.db" }`

```json
{
  "storeBackend": "sqlite",
  "embedding": {
    "provider": "none"
  }
}
```

```json
{
  "storeBackend": "tcvdb",
  "tcvdb": {
    "url": "http://10.0.1.1:8100",
    "apiKey": "<key>",
    "database": "agent_memory_prod",
    "embeddingModel": "bge-large-zh",
    "username": "root",
    "timeout": 10000
  },
  "bm25": {
    "enabled": true,
    "language": "zh"
  }
}
```

<Warning>
Switching `storeBackend` or TCVDB database binding after data exists is a migration concern. Use the offline migrate path rather than pointing a live process at a different backend without rewriting data.
</Warning>

## `IMemoryStore` contract

All backends implement the same surface in `src/core/store/types.ts`:

- Lifecycle: `init`, `isDegraded`, `getCapabilities`, `close`
- L1 CRUD / search: `upsertL1`, `deleteL1*`, `searchL1Vector`, `searchL1Fts`, optional `searchL1Hybrid`
- L0 CRUD / search: `upsertL0`, optional `updateL0Embedding`, `searchL0Vector`, `searchL0Fts`
- Optional profile ops: `pullProfiles`, `syncProfiles`, `deleteProfiles` (TCVDB)
- `reindexAll` (meaningful for sqlite client embeddings; TCVDB no-ops with server-side embedding)
- Fault tolerance: methods return empty/`false` on failure unless documented otherwise

### Capability flags

```ts
interface StoreCapabilities {
  vectorSearch: boolean;
  ftsSearch: boolean;
  nativeHybridSearch: boolean;
  sparseVectors: boolean;
}
```

| Flag | sqlite (`VectorStore`) | tcvdb (`TcvdbMemoryStore`) |
| --- | --- | --- |
| `vectorSearch` | `vecTablesReady` (false when dimensions=`0` / no vec0) | always `true` |
| `ftsSearch` | FTS5 created successfully | `!!bm25Encoder` |
| `nativeHybridSearch` | always `false` | `!!bm25Encoder` |
| `sparseVectors` | always `false` | `!!bm25Encoder` |

Callers use these to pick strategy and degrade:

- Auto-recall hybrid: if `nativeHybridSearch`, single `searchL1Hybrid` call; else parallel keyword + embedding + client RRF
- Tools (`tdai_memory_search`, conversation search): FTS + vector in parallel, RRF when both produce hits
- Keyword path needs `isFtsAvailable()` (sqlite FTS5 or tcvdb BM25)

## sqlite backend

### Layout

Single SQLite file: `{pluginDataDir}/vectors.db` (Node `node:sqlite` + `sqlite-vec`).

| Layer | Metadata table | Vector | FTS |
| --- | --- | --- | --- |
| L1 | `l1_records` | `l1_vec` (vec0, cosine) | FTS5 virtual table |
| L0 | `l0_conversations` | `l0_vec` | FTS5 virtual table |

Writes use BEGIN/COMMIT for metadata + vector atomicity. vec0 has no `ON CONFLICT`; upsert is delete + insert. WAL mode is enabled.

### Vector path

- Requires configured dimensions and a remote embedding provider with `apiKey`
- Cosine similarity score: `1.0 - cosine_distance`
- When `embedding.provider` is `"none"`, dimensions resolve to `0`, vec0 tables stay deferred, `vectorSearch` is false → keyword/FTS-only

### FTS5 keyword path

- Index/query tokenization prefers `@node-rs/jieba` `cutForSearch`; falls back to Unicode regex
- Write-side: space-joined tokens in FTS content; query-side: OR-joined quoted phrases via `buildFtsQuery`
- Chinese stop-word filter reduces noise
- FTS ranks map to 0–1 via `bm25RankToScore`
- If FTS5 is unavailable at init, `ftsAvailable` stays false and keyword search is skipped (no O(N) full-scan fallback)

### Deferred embedding

`supportsDeferredEmbedding = true`:

1. Capture writes L0 metadata (+ FTS) with `upsertL0(record, undefined)`
2. Background path calls `updateL0Embedding(recordId, embedding)` when the client embed finishes

### Degradation

If sqlite-vec fails to load or init fails hard, the store enters degraded mode; pipeline-factory may drop the store and fall back to keyword-only behavior for higher layers.

## tcvdb backend

### Collections

Names are prefixed with the configured database to avoid cross-database collisions:

| Logical layer | Collection suffix | Notes |
| --- | --- | --- |
| L1 | `{database}_l1_memories` | Server embedding on field `text` → `vector`; sparse `sparse_vector` |
| L0 | `{database}_l0_conversations` | Server embedding on `message_text` |
| L2/L3 profiles | `{database}_profiles` | Embedding disabled; FLAT dummy vector; versioned rows |

Vector index: prefer `DISK_FLAT` (COSINE, dimension 1024); on API error 15113 / DISK_FLAT unsupported, fall back to HNSW (`M=16`, `efConstruction=200`). Sparse index: inverted, metric `IP`.

### Server-side embedding

- Collection config enables embedding with model `tcvdb.embeddingModel` (default `bge-large-zh`)
- Upsert sends text fields only; dense vectors are generated server-side
- Search uses `embeddingItems` / ANN on the text field—not client `Float32Array`s
- Factory therefore installs `NoopEmbeddingService` (`getDimensions() === 0`, provider `"noop"`)
- Capture skips local embed when dimensions are 0
- `reindexAll` is a no-op (server embeds; rebuild would require drop/recreate)

### Time and filters

- Times stored as `uint64` epoch ms; ISO conversion is internal
- Scalar filters: `session_key`, `session_id`, `type`, `agent_id`, time fields, etc.
- Expired-delete has an 80% safety block (refuses mass delete above threshold)

### Init

- Creates database (idempotent); after create, waits ~5s before collections
- Init failures set `degraded = true`; methods return empty/false

## BM25 sparse encoding

Local encoder (`bm25-local.ts`) uses `@tencentdb-agent-memory/tcvdb-text` (`BM25Encoder.default(language)`).

| Config | Type | Default | Role |
| --- | --- | --- | --- |
| `bm25.enabled` | boolean | `true` | When false, encoder is omitted |
| `bm25.language` | `"zh"` \| `"en"` | `"zh"` | Pre-trained tokenizer/params |

API:

- `encodeTexts(texts)` — document upsert (TF-based sparse vectors)
- `encodeQueries(texts)` — search-time sparse queries (IDF-based)

On TCVDB:

- Upsert attaches `sparse_vector` when encode succeeds
- Hybrid search builds `match` on `sparse_vector` from `encodeQueries`
- Without BM25: dense-only `/document/search` with `embeddingItems`
- With BM25: full hybrid path (capabilities mark FTS/hybrid/sparse true)

BM25 is independent of the client embedding provider: TCVDB dense is server-side; sparse is always client-encoded when enabled.

## Hybrid RRF retrieval

RRF constant: **k = 60** (shared paper default).

### Score fusion

Per ranked list, item score contribution is `1 / (k + rank + 1)`. Scores sum across lists; result sorts by descending RRF score.

Shared helper: `rrfMerge` in `search-utils.ts`. Auto-recall hybrid, memory-search, and conversation-search implement the same formula (some with local copies).

### Strategy matrix (`recall.strategy`)

| Strategy | Behavior |
| --- | --- |
| `keyword` | FTS5 (sqlite) or sparse/hybrid path (tcvdb when BM25 available) |
| `embedding` | Client embed + vector search (sqlite); tcvdb needs query text for server embed |
| `hybrid` (default) | Native hybrid if `nativeHybridSearch`; else parallel keyword + embedding + client RRF |

Fallbacks:

- If embedding service is missing for `embedding`/`hybrid` (sqlite with `provider: none`), auto-recall falls back to keyword
- Tools report effective strategy: `"hybrid"` | `"embedding"` | `"fts"` | `"none"`
- Score threshold default `0.3` (`recall.scoreThreshold`); small FTS result sets may bypass threshold when all ranks are low (IDF edge case)

### TCVDB native hybrid request shape

When BM25 is present:

1. ANN: `fieldName: "text"`, `data: [queryText]` (server embed)
2. Match: `fieldName: "sparse_vector"`, sparse from `encodeQueries`
3. `rerank: { method: "rrf", k: 60 }`
4. Single `hybridSearch` HTTP call

Auto-recall short-circuits to this path when `getCapabilities().nativeHybridSearch` is true—avoids a redundant local `embed()` and second HTTP round-trip.

## Embedding service roles

| Role | Class | When |
| --- | --- | --- |
| Remote OpenAI-compatible | `OpenAIEmbeddingService` | sqlite + provider ≠ `local`/`none` + `apiKey` |
| Local GGUF (internal) | `LocalEmbeddingService` | Not exposed as user config; `provider: "local"` is forced off in `parseConfig` |
| Server-side noop | `NoopEmbeddingService` | Always with tcvdb |
| Disabled | no service / dims 0 | `provider: "none"` (default) or invalid remote config |

Remote config fields that matter for sqlite vector quality:

| Field | Notes |
| --- | --- |
| `baseUrl`, `apiKey`, `model`, `dimensions` | Required for remote; missing fields set `configError` and disable embedding |
| `sendDimensions` | Default `true` (Matryoshka); set `false` for BGE-M3-style backends that reject `dimensions` |
| `timeoutMs` / `recallTimeoutMs` / `captureTimeoutMs` | Per-path timeouts |
| `maxInputChars` | Default `5000` truncate |

`EmbeddingProviderInfo` (`provider` + `model`) drives sqlite reindex detection when the provider/model/dimensions change.

Provider neutrality: any OpenAI-compatible HTTP embedding endpoint works for sqlite; TCVDB uses the instance’s built-in model name (`tcvdb.embeddingModel`) instead of a separate client provider. No particular cloud host is hard-coded beyond the TCVDB HTTP client URL you configure.

## Config field reference

<ParamField body="storeBackend" type="string" required>
Enum: `sqlite` | `tcvdb`. Default `sqlite`.
</ParamField>

<ParamField body="tcvdb.url" type="string" required>
VectorDB instance URL (required when backend is `tcvdb`).
</ParamField>

<ParamField body="tcvdb.apiKey" type="string" required>
API key for VectorDB.
</ParamField>

<ParamField body="tcvdb.database" type="string" required>
Database name; prefixes collection names.
</ParamField>

<ParamField body="tcvdb.username" type="string">
Default `root`.
</ParamField>

<ParamField body="tcvdb.embeddingModel" type="string">
Server-side model; default `bge-large-zh`.
</ParamField>

<ParamField body="tcvdb.timeout" type="number">
HTTP timeout ms; default `10000`.
</ParamField>

<ParamField body="tcvdb.caPemPath" type="string">
Optional CA PEM path for HTTPS instances.
</ParamField>

<ParamField body="bm25.enabled" type="boolean">
Default `true`. Disables sparse vectors / native hybrid when false.
</ParamField>

<ParamField body="bm25.language" type="string">
`zh` or `en`; default `zh`.
</ParamField>

<ParamField body="embedding.provider" type="string">
Default `none` (keyword-only on sqlite). Remote names are OpenAI-compatible endpoints.
</ParamField>

<ParamField body="recall.strategy" type="string">
`hybrid` | `embedding` | `keyword`; default `hybrid`.
</ParamField>

## Operational notes

| Topic | Behavior |
| --- | --- |
| Manifest | First init writes store binding; config drift is logged against `storeSnapshot` |
| Degraded store | `isDegraded()` true → pipeline may null out store; searches return empty |
| Migration | Offline `migrate-sqlite-to-tcvdb` moves L0/L1/profile data and can rewrite config |
| Inspection | `read-local-memory` for sqlite artifacts; `export-tencent-vdb` for TCVDB collections |
| Gateway / Hermes | Same factory path via standalone host adapter and `tdai-gateway.json` / ctl config |

### Failure modes

| Symptom | Likely cause |
| --- | --- |
| Factory throw on start | Missing `tcvdb.url` / `apiKey` / `database` |
| No vector hits on sqlite | `embedding.provider` is `none` or missing remote fields; dims 0 |
| Embedding HTTP 400 | Matryoshka/`dimensions` rejected → set `sendDimensions: false` |
| Hybrid collapses to dense-only (tcvdb) | `bm25.enabled: false` or encode failure |
| Keyword empty | FTS5 not available (sqlite) or BM25 off (tcvdb) |
| Store degraded | sqlite-vec load failure or TCVDB init HTTP errors |

## Next

<CardGroup>
  <Card title="Use Tencent VectorDB" href="/use-tcvdb">
    Switch `storeBackend` to `tcvdb`, required connection fields, BM25 language, server embedding model, and CA PEM.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    OpenAI-compatible providers, dimensions, `sendDimensions`, timeouts, and keyword-only degradation.
  </Card>
  <Card title="Migrate SQLite to TCVDB" href="/migrate-to-tcvdb">
    Offline migrate-sqlite-to-tcvdb, layer selection, config rewrite, and verification.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full schema for storeBackend, tcvdb, bm25, embedding, and recall.
  </Card>
  <Card title="Inspect local memory" href="/inspect-local-memory">
    `vectors.db`, L0–L3 layout, and export tools for both backends.
  </Card>
  <Card title="Agent tools" href="/agent-tools">
    Hybrid / embedding / keyword strategies on `tdai_memory_search` and conversation search.
  </Card>
</CardGroup>

---

## 08. TdaiCore and host adapters

> Host-neutral TdaiCore facade, OpenClawHostAdapter vs StandaloneHostAdapter, LLM runner boundaries, and how in-process OpenClaw hooks map to Gateway HTTP handlers.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/08-tdaicore-and-host-adapters.md
- Generated: 2026-08-04T18:30:04.100Z

### Source Files

- `src/core/tdai-core.ts`
- `src/core/types.ts`
- `src/adapters/index.ts`
- `src/adapters/openclaw/host-adapter.ts`
- `src/adapters/standalone/host-adapter.ts`
- `src/adapters/standalone/llm-runner.ts`
- `index.ts`

---
title: TdaiCore and host adapters
description: Host-neutral TdaiCore facade, OpenClawHostAdapter vs StandaloneHostAdapter, LLM runner boundaries, and how in-process OpenClaw hooks map to Gateway HTTP handlers.
summary: TdaiCore is the single host-neutral entry point for recall, capture, search, session flush, and pipeline lifecycle. Hosts inject a HostAdapter (OpenClaw in-process or Standalone Gateway) plus an LLMRunnerFactory so the same core runs under OpenClaw hooks or Hermes HTTP.
---

`TdaiCore` is the host-neutral facade for TencentDB Agent Memory. OpenClaw (in-process plugin) and Hermes (Gateway HTTP sidecar) both call the same methods; only the adapter layer differs.

This page is an **architecture pattern** page: contracts, wiring, lifecycle, and the OpenClaw-hook ↔ Gateway-route map.

## Architecture

```mermaid
flowchart TB
  subgraph hosts [Host shells]
    OC["index.ts OpenClaw plugin<br/>hooks + tools"]
    GW["TdaiGateway HTTP<br/>POST /recall /capture ..."]
    HER["Hermes memory_tencentdb<br/>prefetch / sync_turn"]
  end

  subgraph adapters [Adapter layer]
    OCA["OpenClawHostAdapter<br/>hostType: openclaw"]
    SHA["StandaloneHostAdapter<br/>hostType: standalone"]
    OCL["OpenClawLLMRunnerFactory<br/>CleanContextRunner / embedded agent"]
    STL["StandaloneLLMRunnerFactory<br/>OpenAI-compatible AI SDK"]
  end

  subgraph core [TdaiCore]
    TC["handleBeforeRecall<br/>handleTurnCommitted<br/>searchMemories / searchConversations<br/>handleSessionEnd / destroy"]
    PM["MemoryPipelineManager<br/>L1 → L2 → L3"]
    ST["IMemoryStore + EmbeddingService"]
  end

  OC --> OCA
  OCA --> OCL
  OCA --> TC
  GW --> SHA
  HER --> GW
  SHA --> STL
  SHA --> TC
  TC --> PM
  TC --> ST
```

**Design rules** (from `src/core/types.ts`):

1. Core depends only on `HostAdapter`, `LLMRunner` / `LLMRunnerFactory`, `Logger`, and `RuntimeContext` — never on OpenClaw or Express.
2. Each host supplies one `HostAdapter` and its own runner factory.
3. `RuntimeContext` is the identity/path bundle (`userId`, `sessionId`, `sessionKey`, `platform`, `dataDir`, `workspaceDir`).

## TdaiCore facade

Construct once, `initialize()`, then call the same APIs from hooks or HTTP.

```ts
// OpenClaw (in-process)
const adapter = new OpenClawHostAdapter({ api, pluginDataDir, openclawConfig: api.config });
const core = new TdaiCore({ hostAdapter: adapter, config: parsedCfg, sessionFilter });
await core.initialize();

// Gateway / Hermes sidecar
const adapter = new StandaloneHostAdapter({ dataDir, llmConfig, logger, platform: "gateway" });
const core = new TdaiCore({ hostAdapter: adapter, config: memoryCfg });
await core.initialize();
```

### Constructor options

| Field | Type | Role |
| --- | --- | --- |
| `hostAdapter` | `HostAdapter` | Runtime context, logger, LLM factory |
| `config` | `MemoryTdaiConfig` | Parsed plugin/gateway memory config |
| `sessionFilter` | `SessionFilter` (optional) | Skip internal/benchmark agents |
| `instanceId` | `string` (optional) | Metrics / reporting instance id |

### Public methods

| Method | Purpose | Typical caller |
| --- | --- | --- |
| `initialize()` | Data dirs, store init, optional pipeline manager + deferred runner wire | Plugin register / Gateway `start()` |
| `handleBeforeRecall(userText, sessionKey)` | Prefetch L1/L3 context for the next turn | OpenClaw `before_prompt_build`, `POST /recall` |
| `handleTurnCommitted(turn)` | L0 capture + pipeline notify | OpenClaw `agent_end`, `POST /capture` |
| `searchMemories(params)` | L1 structured search | `tdai_memory_search`, `POST /search/memories` |
| `searchConversations(params)` | L0 conversation search | `tdai_conversation_search`, `POST /search/conversations` |
| `handleSessionEnd(sessionKey)` | Flush **one** session’s buffered work (process stays up) | Hermes session end, `POST /session/end` |
| `destroy()` | Process teardown: drain bg tasks, destroy scheduler, close stores | OpenClaw `gateway_stop`, Gateway `stop()` |

### Result shapes

**`RecallResult`** (recall):

- `prependContext` — dynamic L1 text for the user prompt  
- `appendSystemContext` — stable system append (persona, scene nav, tool guide)  
- `recalledL1Memories`, `recalledL3Persona`, `recallStrategy` — metrics  

**`CaptureResult`** (capture):

- `l0RecordedCount`, `schedulerNotified`, `l0VectorsWritten`, `filteredMessages`

**Search returns** `{ text, total, strategy? }` as agent-ready formatted text plus counts.

### Lifecycle rules you must not conflate

| Event | Scope | Call |
| --- | --- | --- |
| Conversation ends (other sessions continue) | One `sessionKey` | `handleSessionEnd` → `scheduler.flushSession` |
| Host/process exit | Entire process | `destroy()` (scheduler, stores, embedding, bg drain) |

`handleSessionEnd` must not rebuild or destroy the global scheduler; concurrent Gateway sessions share one `TdaiCore` instance.

`handleTurnCommitted` races under concurrent HTTP captures: scheduler start uses a shared `schedulerStartPromise` so concurrent callers await the same start sequence. Capture may register fire-and-forget L0 embedding work in `bgTasks`; `destroy()` drains that set (5s hard timeout) before closing stores.

## HostAdapter contract

```ts
interface HostAdapter {
  readonly hostType: "openclaw" | "hermes" | "standalone";
  getRuntimeContext(): RuntimeContext;
  getLogger(): Logger;
  getLLMRunnerFactory(): LLMRunnerFactory;
}
```

Core uses the factory for extraction runners and the logger/dataDir from the adapter. Prefer host-specific helpers only at the shell (`buildRuntimeContextForSession` / `buildRuntimeContextForRequest`).

## OpenClawHostAdapter vs StandaloneHostAdapter

| Concern | `OpenClawHostAdapter` | `StandaloneHostAdapter` |
| --- | --- | --- |
| `hostType` | `"openclaw"` | `"standalone"` (default platform `"gateway"`) |
| Package deps | `openclaw/plugin-sdk` | None of OpenClaw |
| Logger | `api.logger` | Injected console/`Logger` |
| Data dir | `pluginDataDir` (e.g. under OpenClaw state `memory-tdai`) | Gateway `data.baseDir` |
| LLM path | `OpenClawLLMRunnerFactory` → `CleanContextRunner` / embedded agent | `StandaloneLLMRunnerFactory` → Vercel AI SDK + OpenAI-compatible HTTP |
| Session identity | Defaults empty; hooks pass session via `buildRuntimeContextForSession` | Defaults empty; handlers use `buildRuntimeContextForRequest` |
| Extra accessors | `getPluginApi()`, `getOpenClawConfig()`, `getPluginDataDir()` | — |

**OpenClaw construction** (`index.ts`):

- Resolves `pluginDataDir` from OpenClaw state dir + `memory-tdai`
- Builds adapter with `api`, `pluginDataDir`, `openclawConfig: api.config`
- Shell owns prompt caches, tools, hooks, cleaner, reporter; core owns memory algorithms

**Standalone construction** (`TdaiGateway`):

- Builds adapter with `dataDir`, `llmConfig` from gateway config, console logger
- One long-lived `TdaiCore` for all routes

## LLM runner boundaries

Core never calls OpenAI or OpenClaw agent APIs directly. Pipeline wiring asks the factory for runners:

| Stage | `enableTools` | Why |
| --- | --- | --- |
| L1 extraction / L1 dedup | `false` | Pure text JSON extraction |
| L2 scene / L3 persona | `true` | File tools: read/write/edit under a workspace |

### OpenClawLLMRunner

- Wraps `CleanContextRunner` (embedded Pi agent path).
- Tools map to host allow-lists (`read` / `write` / `edit` when enabled; tools disabled when not).

### StandaloneLLMRunner

- `ai` + `@ai-sdk/openai` against any OpenAI-compatible `baseUrl` / `apiKey` / `model` (BYOK).
- With tools: sandboxed `read_file`, `write_to_file`, `replace_in_file` relative to `workspaceDir` (path escape rejected).
- Optional `disableThinking` strategies for vLLM, DeepSeek, DashScope, OpenAI reasoning, Anthropic/Kimi, Gemini.

### Which factory TdaiCore wires

In `wirePipelineRunners()`:

```text
useStandaloneRunner = config.llm.enabled || hostAdapter.hostType !== "openclaw"
```

| Host | Default runners | Override |
| --- | --- | --- |
| OpenClaw | Host `OpenClawLLMRunner` (via `openclawConfig` into pipeline factory helpers) | If `config.llm.enabled`, swap in `StandaloneLLMRunnerFactory` from `config.llm` |
| Gateway / non-OpenClaw | Always standalone factory from host LLM config | — |

When standalone is active, core creates:

- L1 runner: `createRunner({ enableTools: false })`
- L2/L3 runner: `createRunner({ enableTools: true })`

When OpenClaw path is active without `llm.enabled`, L1/L2/L3 helpers receive `openclawConfig` and use the embedded path internally (no host-neutral runner instance passed).

## OpenClaw hooks ↔ Gateway HTTP

Same `TdaiCore` methods; different transport.

| Capability | OpenClaw (in-process) | Gateway HTTP | Hermes client |
| --- | --- | --- | --- |
| Recall | `before_prompt_build` → `handleBeforeRecall` | `POST /recall` | `prefetch()` |
| Capture | `agent_end` (success only) → `handleTurnCommitted` | `POST /capture` | `sync_turn()` |
| L1 search | Tool `tdai_memory_search` | `POST /search/memories` | tool → HTTP |
| L0 search | Tool `tdai_conversation_search` | `POST /search/conversations` | tool → HTTP |
| Session flush | *(no dedicated OpenClaw session-end hook; process uses stop)* | `POST /session/end` | session end |
| Process teardown | `gateway_stop` → `destroy()` | `TdaiGateway.stop()` → `destroy()` | supervisor stop |
| Health | n/a (plugin process) | `GET /health` (no auth) | health probes |
| Seed | CLI `memory-tdai seed` | `POST /seed` | ops/import paths |

### OpenClaw shell extras (not in TdaiCore)

The plugin shell around core still owns:

- Original prompt + message-count cache (`pendingOriginalPrompts`) so capture records pre-injection user text
- Recall result cache for `agent_turn` metrics
- `before_message_write` strip of `<relevant-memories>` from persisted user transcripts
- Session filter / empty `sessionKey` skip
- Lazy embedding warmup on first conversation
- Embedded-agent prewarm on first capture when scheduler not yet started
- Optional local retention cleaner and reporter init

### Gateway field mapping (compact)

| Core / domain | HTTP body / response |
| --- | --- |
| `handleBeforeRecall(query, session_key)` | Request: `query`, `session_key`; response: `context` ← `appendSystemContext`, `strategy`, `memory_count` |
| `handleTurnCommitted` | Request: `user_content`, `assistant_content`, `session_key`, optional `messages` / `session_id`; response: `l0_recorded`, `scheduler_notified` |
| `searchMemories` | Request: `query`, optional `limit` / `type` / `scene`; response: `results`, `total`, `strategy` |
| `searchConversations` | Request: `query`, optional `limit` / `session_key`; response: `results`, `total` |
| `handleSessionEnd` | Request: `session_key`; response: `{ flushed: true }` |

Auth: optional Bearer via gateway API key (all routes except `GET /health`). Details: [Secure the Gateway](/secure-gateway), [Gateway HTTP API](/gateway-http-api).

## Degraded modes

| Condition | Behavior |
| --- | --- |
| Store init fails | Log warn; recall/search degrade; pipeline runners still wire (JSONL fallback, no embedding) |
| `extraction.enabled` false | No pipeline manager; capture may still write L0 depending on capture config |
| Embedding missing / not ready | Keyword/BM25 paths may still work; warmup deferred until first conversation on OpenClaw |
| `destroy()` with stuck bg embed | 5s drain timeout, then close stores with residual-write warning |

## File layout

```
src/
├── core/
│   ├── tdai-core.ts      # TdaiCore facade
│   ├── types.ts          # HostAdapter, LLMRunner, RuntimeContext
│   ├── hooks/            # auto-recall, auto-capture
│   ├── tools/            # memory + conversation search
│   └── store/            # sqlite / tcvdb backends
├── adapters/
│   ├── openclaw/         # OpenClawHostAdapter + OpenClawLLMRunner*
│   └── standalone/       # StandaloneHostAdapter + StandaloneLLMRunner*
├── gateway/
│   └── server.ts         # TdaiGateway → StandaloneHostAdapter + TdaiCore
index.ts                  # OpenClaw plugin shell → OpenClawHostAdapter + TdaiCore
```

## Related pages

<CardGroup cols={2}>
  <Card title="Overview" href="/overview">
    Host integration surfaces and runtime assumptions.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Full request/response schemas for TdaiGateway routes.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Sidecar install, auto-discovery, and health checks.
  </Card>
  <Card title="Agent tools" href="/agent-tools">
    OpenClaw and Hermes tool schemas backed by TdaiCore search.
  </Card>
  <Card title="Memory layers" href="/memory-layers">
    L0–L3 model and pipeline scheduling behind capture.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    memory-tencentdb-ctl start/stop/status for the standalone host.
  </Card>
</CardGroup>

---

## 09. Configure OpenClaw

> Enable the plugin in ~/.openclaw/openclaw.json, daily and advanced tuning groups (capture, pipeline, recall, persona), timezone, and verification after gateway restart.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/09-configure-openclaw.md
- Generated: 2026-08-04T18:32:06.870Z

### Source Files

- `src/config.ts`
- `openclaw.plugin.json`
- `README.md`
- `SKILL.md`
- `index.ts`
- `src/utils/ensure-hook-policy.ts`

---
title: "Configure OpenClaw"
description: "Enable the plugin in ~/.openclaw/openclaw.json, daily and advanced tuning groups (capture, pipeline, recall, persona), timezone, and verification after gateway restart."
---

OpenClaw loads `@tencentdb-agent-memory/memory-tencentdb` (plugin id `memory-tencentdb`, CLI alias `memory-tdai`) from `~/.openclaw/openclaw.json`. On gateway start, `register()` calls `parseConfig(api.pluginConfig)` and wires capture, extraction, pipeline, recall, persona, and optional embedding under `{stateDir}/memory-tdai` (default `~/.openclaw/memory-tdai`).

<Note>
Install the package first (`openclaw plugins install @tencentdb-agent-memory/memory-tencentdb`). Prerequisites: Node `>= 22.16`, OpenClaw `>= 2026.3.13`. See [Installation](/installation) and [Quickstart](/quickstart) for the zero-config path.
</Note>

## Prerequisites

| Requirement | Check |
| :--- | :--- |
| Plugin installed | `openclaw plugins install @tencentdb-agent-memory/memory-tencentdb` |
| OpenClaw / Node | `openclaw --version`, `node -v` |
| Config file | `~/.openclaw/openclaw.json`, or path from `OPENCLAW_CONFIG_PATH` / `$OPENCLAW_STATE_DIR/openclaw.json` |

## Enable the plugin

Minimal enable (all other fields use defaults from `parseConfig`):

```jsonc
// ~/.openclaw/openclaw.json
{
  "memory-tencentdb": {
    "enabled": true
  }
}
```

OpenClaw may also store the entry under the plugins registry. The migration writer and hook-policy patch use this shape:

```jsonc
{
  "plugins": {
    "entries": {
      "memory-tencentdb": {
        "enabled": true,
        "hooks": {
          "allowConversationAccess": true
        },
        "config": {
          // tuning groups go here — see below
        }
      }
    }
  }
}
```

Runtime reads **plugin config** as `api.pluginConfig` and passes it to `parseConfig`. Host `enabled` is an OpenClaw entry flag (outside `parseConfig`). Tuning groups (`capture`, `pipeline`, …) live in the plugin config object.

<Warning>
Config changes apply only after a gateway restart:

```bash
openclaw gateway restart
```
</Warning>

## Config model

`parseConfig({})` is valid. Groups and defaults are defined in `src/config.ts` and exposed in `openclaw.plugin.json`.

```text
openclaw.json
└── memory-tencentdb (or plugins.entries.memory-tencentdb)
    ├── enabled                 # host enable flag
    ├── hooks.allowConversationAccess  # capture hook gate (OpenClaw ≥ 2026.4.24)
    └── config / plugin fields  # parseConfig input
        ├── timezone
        ├── storeBackend          # "sqlite" | "tcvdb"
        ├── capture / extraction / persona / pipeline / recall
        ├── embedding / tcvdb / bm25 / llm / report / offload
        └── memoryCleanup       # derived from capture.l0l1RetentionDays + cleanTime
```

## Daily tuning

Use these for most production setups.

### Timezone

<ParamField body="timezone" type="string" default="system">
User/LLM-facing timestamps and local day boundaries. Accepts `"system"` (process TZ), IANA names (`Asia/Shanghai`, `Europe/Berlin`, `UTC`), or UTC offsets (`+08:00`, `-05:30`). Invalid values fall back to system TZ with a warning. Storage instants (SQLite/TCVDB) stay UTC.
</ParamField>

```json
{
  "memory-tencentdb": {
    "enabled": true,
    "timezone": "Asia/Shanghai"
  }
}
```

### Capture (L0)

| Field | Default | Description |
| :--- | :--- | :--- |
| `capture.enabled` | `true` | Register `agent_end` auto-capture |
| `capture.excludeAgents` | `[]` | Glob patterns; matched agents skip capture, recall, and pipeline |
| `capture.l0l1RetentionDays` | `0` | L0/L1 local TTL days; `0` = no cleanup |
| `capture.allowAggressiveCleanup` | `false` | Allow retention of 1–2 days |
| `capture.cleanTime` | `"03:00"` | Daily cleanup clock (`HH:mm`); invalid values fall back to `03:00` |

Retention rules:

- `l0l1RetentionDays <= 0` → cleanup disabled  
- `>= 3` → accepted  
- `1` or `2` → accepted only if `allowAggressiveCleanup: true`; otherwise treated as disabled  

### Extraction (L1)

| Field | Default | Description |
| :--- | :--- | :--- |
| `extraction.enabled` | `true` | Background L1 extraction |
| `extraction.enableDedup` | `true` | Smart dedup / conflict detection |
| `extraction.maxMemoriesPerSession` | `20` | Cap per L1 pass |
| `extraction.model` | *(host default)* | Optional `provider/model` |

### Pipeline (L1→L2→L3 schedule)

| Field | Default | Description |
| :--- | :--- | :--- |
| `pipeline.everyNConversations` | `5` | L1 every N conversation rounds |
| `pipeline.enableWarmup` | `true` | New sessions: 1→2→4→…→`everyN` |
| `pipeline.l1IdleTimeoutSeconds` | `600` | Idle seconds before L1 |
| `pipeline.l2DelayAfterL1Seconds` | `10` | Delay after L1 before L2 |
| `pipeline.l2MinIntervalSeconds` | `900` | Min gap between L2 runs (per session) |
| `pipeline.l2MaxIntervalSeconds` | `3600` | Max L2 poll interval for active sessions |
| `pipeline.sessionActiveWindowHours` | `24` | Stop L2 polling after this idle window |

### Recall

| Field | Default | Description |
| :--- | :--- | :--- |
| `recall.enabled` | `true` | `before_prompt_build` auto-recall |
| `recall.maxResults` | `5` | Max injected memories |
| `recall.maxCharsPerMemory` | `0` | Per-memory char cap; `0` = unlimited |
| `recall.maxTotalRecallChars` | `0` | Total L1 inject budget; `0` = unlimited |
| `recall.scoreThreshold` | `0.3` | Minimum score |
| `recall.strategy` | `"hybrid"` | `embedding` \| `keyword` \| `hybrid` (invalid → `hybrid`) |
| `recall.timeoutMs` | `5000` | On timeout, skip inject and log a warning |

### Persona (L2/L3)

| Field | Default | Description |
| :--- | :--- | :--- |
| `persona.triggerEveryN` | `50` | Persona generation every N new memories |
| `persona.maxScenes` | `15` | Max scene blocks |
| `persona.backupCount` | `3` | Persona backup count |
| `persona.sceneBackupCount` | `10` | Scene backup count |
| `persona.model` | *(host default)* | Optional `provider/model` |

### Recommended daily template

```json
{
  "memory-tencentdb": {
    "enabled": true,
    "timezone": "system",
    "capture": {
      "enabled": true,
      "excludeAgents": [],
      "l0l1RetentionDays": 90,
      "cleanTime": "03:00"
    },
    "extraction": {
      "enabled": true,
      "enableDedup": true,
      "maxMemoriesPerSession": 20
    },
    "pipeline": {
      "everyNConversations": 5,
      "enableWarmup": true,
      "l1IdleTimeoutSeconds": 600,
      "l2DelayAfterL1Seconds": 10,
      "l2MinIntervalSeconds": 900,
      "l2MaxIntervalSeconds": 3600,
      "sessionActiveWindowHours": 24
    },
    "recall": {
      "enabled": true,
      "maxResults": 5,
      "scoreThreshold": 0.3,
      "strategy": "hybrid",
      "timeoutMs": 5000
    },
    "persona": {
      "triggerEveryN": 50,
      "maxScenes": 15,
      "backupCount": 3,
      "sceneBackupCount": 10
    }
  }
}
```

## Advanced tuning (summary)

These groups are optional for day-to-day use; full field types and validation live in [Plugin configuration reference](/plugin-config-reference).

| Group | Role | Defaults (high level) |
| :--- | :--- | :--- |
| `storeBackend` | `sqlite` (default) or `tcvdb` | Local SQLite + sqlite-vec unless TCVDB is configured |
| `embedding` | Vector search (OpenAI-compatible remote) | `provider: "none"` → keyword-only; incomplete remote config disables vectors without crashing |
| `llm` | Standalone OpenAI-compatible LLM for L1/L2/L3 | `enabled: false` (uses host LLM) |
| `offload` | Context offload / short-term compression | `enabled: false` — see [Enable context offload](/enable-context-offload) |
| `bm25` | Sparse encoding (mainly TCVDB hybrid) | `enabled: true`, `language: "zh"` |
| `report` | Structured metric logs | `enabled: false`, `type: "local"` |

<Info>
Default embedding is off (`provider: "none"`). Hybrid recall then degrades to keyword paths. To enable vectors, set `apiKey`, `baseUrl`, `model`, and `dimensions` together. See [Configure embedding](/configure-embedding).
</Info>

## Hook policy auto-patch

On OpenClaw **≥ 2026.4.24**, non-bundled plugins need `plugins.entries.memory-tencentdb.hooks.allowConversationAccess: true` or `agent_end` capture is silently blocked.

On gateway start, when the host version is parseable and ≥ `2026.4.24`, the plugin:

1. Tries SDK `mutateConfigFile` (may restart the gateway automatically).
2. Falls back to writing `openclaw.json` and logging a restart requirement.
3. Skips auto-write if the config uses `$include` (manual edit required).

Manual verification:

```bash
python3 -m json.tool ~/.openclaw/openclaw.json | grep -A2 allowConversationAccess
```

Expected:

```text
"allowConversationAccess": true
```

under `plugins.entries.memory-tencentdb.hooks`.

## Restart and verification

<Steps>
  <Step title="Apply config">
    Edit `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`). Only change the `memory-tencentdb` entry / plugin config.
  </Step>
  <Step title="Restart gateway">
    ```bash
    openclaw gateway restart
    ```
  </Step>
  <Step title="Confirm plugin logs">
    Gateway logs should include `[memory-tdai]` lines. On successful parse you get a config summary such as `capture=…`, `recall=…`, `pipeline=(everyN=…)`, `persona(…)`, `memoryCleanup(…)`.
  </Step>
  <Step title="Confirm data directory">
    Default data root:

    ```text
    ~/.openclaw/memory-tdai/
    ├── conversations/
    ├── records/
    ├── scene_blocks/
    ├── .metadata/
    ├── .backup/
    ├── persona.md          # after L3 runs
    └── vectors.db          # after store init (sqlite backend)
    ```

    Override state root with `OPENCLAW_STATE_DIR`; the plugin still appends `memory-tdai`.
  </Step>
  <Step title="Smoke the memory path">
    1. Hold a 2–3 turn conversation with memorable preferences or constraints.  
    2. Start a new turn and check for auto-recall injection.  
    3. Call tools `tdai_memory_search` and `tdai_conversation_search`.  
  </Step>
</Steps>

### Success signals

| Signal | Expected |
| :--- | :--- |
| Log tag | `[memory-tdai]` present after restart |
| Config parse | Debug log lists capture/recall/pipeline/persona flags |
| Data dir | `~/.openclaw/memory-tdai/{conversations,records,scene_blocks}` created at register |
| Hook policy (new hosts) | `allowConversationAccess: true` in openclaw.json |
| Tools | `tdai_memory_search`, `tdai_conversation_search` available when capture or recall is enabled |

### Failure modes

| Symptom | Likely cause |
| :--- | :--- |
| No `[memory-tdai]` logs | Plugin not enabled or gateway not restarted |
| Capture never runs | Missing `allowConversationAccess` on OpenClaw ≥ 2026.4.24 |
| Capture/recall skipped for empty session | Empty `sessionKey` — plugin refuses unstable fallback keys |
| Records but no recall | `recall.enabled: false` or `scoreThreshold` too high |
| No vector hits | `embedding.provider` is `none` or remote four-tuple incomplete (`configError` logged) |
| Config ignored | Edited wrong file; confirm `OPENCLAW_CONFIG_PATH` / `OPENCLAW_STATE_DIR` |
| History thinned | `l0l1RetentionDays` + cleaner schedule (`cleanTime`) |

## Security notes

- Treat `embedding.apiKey`, `llm.apiKey`, `tcvdb.apiKey`, and offload backend keys as secrets; prefer env injection and placeholders in shared configs.
- Limit edits to the `memory-tencentdb` plugin entry so other OpenClaw plugins are not overwritten.
- `capture.excludeAgents` is the supported way to keep bench/judge agents out of the memory pipeline.

## Next

<CardGroup>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full schema: types, defaults, enums, and parseConfig validation rules.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    OpenAI-compatible providers, dimensions, sendDimensions, and keyword-only degradation.
  </Card>
  <Card title="Enable context offload" href="/enable-context-offload">
    offload.enabled, contextEngine slot, after-tool-call patch, compression ratios.
  </Card>
  <Card title="Use Tencent VectorDB" href="/use-tcvdb">
    storeBackend tcvdb, required connection fields, BM25 language, CA PEM.
  </Card>
  <Card title="Agent tools" href="/agent-tools">
    tdai_memory_search and tdai_conversation_search parameters and limits.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Disabled plugin, no recall, embedding 400, retention, and hook policy failures.
  </Card>
</CardGroup>

---

## 10. Enable context offload

> Turn on offload.enabled, register plugins.slots.contextEngine, apply the after-tool-call messages patch, tune compression ratios, and confirm Mermaid injection paths.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/10-enable-context-offload.md
- Generated: 2026-08-04T18:33:03.819Z

### Source Files

- `README.md`
- `src/offload/index.ts`
- `src/config.ts`
- `openclaw.plugin.json`
- `scripts/openclaw-after-tool-call-messages.patch.sh`
- `scripts/setup-offload.sh`
- `index.ts`

---
title: "Enable context offload"
description: "Turn on offload.enabled, register plugins.slots.contextEngine, apply the after-tool-call messages patch, tune compression ratios, and confirm Mermaid injection paths."
---

Context offload is the plugin’s **symbolic short-term memory** path: when `offload.enabled` is true, `index.ts` calls `registerOffload()`, which registers hooks plus an OpenClaw `contextEngine` implementation (`memory-tencentdb`). Tool results are written under `~/.openclaw/context-offload` (or `offload.dataDir`), condensed via L1/L1.5/L2 into Mermaid canvases, and re-injected into the live message list so long tool loops stay under the context window.

Offload is **independent of long-term L0–L3 memory**. You can run layered memory without offload; enabling offload is optional and defaults to off.

<Warning>
Without `plugins.slots.contextEngine: "memory-tencentdb"`, the module logs a warning and sets an internal reject flag: **all offload functions are disabled**, even if `offload.enabled` is true.
</Warning>

## Prerequisites

| Requirement | Notes |
| :--- | :--- |
| OpenClaw plugin installed | `openclaw plugins install @tencentdb-agent-memory/memory-tencentdb` (OpenClaw ≥ 2026.3.13, Node ≥ 22.16) |
| Plugin enabled | Long-term memory may already be on; offload is a separate switch |
| Writable config | `~/.openclaw/openclaw.json` |
| Patch targets | OpenClaw `dist/**` must contain `after_tool_call` hook event construction (patch script rewrites those files) |

## What enablement turns on

| Surface | Behavior when enabled |
| :--- | :--- |
| `registerOffload(api, cfg.offload)` | Registers `after_tool_call`, `before_prompt_build` / L3 paths, L1/L1.5/L2 pipelines, reclaim scheduler |
| `api.registerContextEngine("memory-tencentdb", …)` | Claims the context-engine slot; `OffloadContextEngine.assemble()` owns context assembly (not used in `mode: "collect"`) |
| Storage under data root | Per-agent `refs/`, `mmds/`, `offload-<sessionId>.jsonl`, `state.json` |
| L3 compression | Mild score-cascade replacement and aggressive deletion driven by token ratios vs context window |
| Mermaid injection | Active MMD via `_mmdContextMessage`; history MMD after aggressive deletion |

```text
~/.openclaw/openclaw.json
  plugins.slots.contextEngine = "memory-tencentdb"
  plugins.entries.memory-tencentdb.config.offload.enabled = true
        │
        ▼
  index.ts → registerOffload()
        │
        ├─ hooks: after_tool_call / before_prompt_build / llm_input L3
        ├─ registerContextEngine("memory-tencentdb")  [unless mode=collect]
        └─ dataRoot: ~/.openclaw/context-offload/<agent>/
              refs/*.md   mmds/*.mmd   offload-<session>.jsonl   state.json
```

## Enable procedures

Use either the one-shot script (recommended when you also want the patch + compaction defaults applied together) or manual JSON edits.

### Option A — `setup-offload.sh` (all-in-one)

From the installed package (or repo checkout):

```bash
# Enable (backend-oriented example used by the script)
bash scripts/setup-offload.sh --enable \
  --user-id "<userId>" \
  --backend-url "http://host:port" \
  [--backend-api-key "<token>"]

# Status / disable
bash scripts/setup-offload.sh --status
bash scripts/setup-offload.sh --disable
```

Enable steps the script performs (in order):

1. **Patch** — runs `scripts/openclaw-after-tool-call-messages.patch.sh`; non-zero exit aborts enable (exit code 2).
2. **Slot** — sets `plugins.slots.contextEngine` to `memory-tencentdb`.
3. **Offload config** — sets `plugins.entries.memory-tencentdb.config.offload` with `enabled`, `backendUrl`, `userId`, default `backendTimeoutMs` 120000, optional `backendApiKey`.
4. **Compaction** — sets `agents.defaults.compaction.mode` to `safeguard`.

Backs up `openclaw.json` to `openclaw.json.bak.<timestamp>` before writing. Restart the OpenClaw gateway after success.

<Note>
`--enable` requires `--user-id` and `--backend-url` (URL must start with `http://` or `https://`). Local-only offload without a backend is configured manually (see Option B + `mode` / omit `backendUrl`).
</Note>

### Option B — Manual config

#### 1. Turn on offload

OpenClaw stores plugin config under `plugins.entries` (what `setup-offload.sh` and `src/offload/types.ts` document). Minimal local mode:

```json
{
  "plugins": {
    "entries": {
      "memory-tencentdb": {
        "enabled": true,
        "config": {
          "offload": {
            "enabled": true
          }
        }
      }
    }
  }
}
```

With optional tuning and backend routing:

```json
{
  "plugins": {
    "entries": {
      "memory-tencentdb": {
        "enabled": true,
        "config": {
          "offload": {
            "enabled": true,
            "mode": "local",
            "model": "openai/your-model-id",
            "mildOffloadRatio": 0.5,
            "aggressiveCompressRatio": 0.85,
            "mmdMaxTokenRatio": 0.2,
            "defaultContextWindow": 200000,
            "dataDir": "/absolute/path/to/context-offload"
          }
        }
      }
    }
  }
}
```

| Key | Type | Default | Role |
| :--- | :--- | :--- | :--- |
| `enabled` | boolean | `false` | Master switch; false skips `registerOffload` entirely |
| `mode` | `"local"` \| `"backend"` \| `"collect"` | auto: `backend` if `backendUrl` set, else `local` | Where L1/L1.5/L2 LLM work runs; `collect` skips L3 + context engine registration |
| `model` | string | host default model | `provider/model-id` for local offload LLM |
| `temperature` | number | `0.2` | Offload LLM temperature |
| `disableThinking` | boolean \| strategy string | `false` | Local-mode only: disable reasoning/thinking on supported providers |
| `forceTriggerThreshold` | number | `4` | Pending tool pairs that force L1 |
| `dataDir` | string | `~/.openclaw/context-offload` | Absolute data root override |
| `defaultContextWindow` | number | `200000` | Window used when host does not supply one |
| `maxPairsPerBatch` | number | `20` | Max tool pairs per L1 batch |
| `l2NullThreshold` | number | `4` | Trigger L2 when `node_id=null` entries reach this count |
| `l2TimeoutSeconds` | number | `300` | Trigger L2 if none ran for this many seconds |
| `mildOffloadRatio` | number | `0.5` | Mild L3 when tokens ≥ window × ratio |
| `aggressiveCompressRatio` | number | `0.85` | Aggressive L3 when tokens ≥ window × ratio |
| `mmdMaxTokenRatio` | number | `0.2` | Token budget fraction for MMD injection |
| `backendUrl` | string | — | Remote offload API base; implies `mode: "backend"` when `mode` omitted |
| `backendApiKey` | string | — | Backend auth token |
| `backendTimeoutMs` | number | `120000` (runtime parse) | Backend call timeout (ms) |
| `offloadRetentionDays` | number | `0` | Reclaim sessions/refs/mmds older than N days; `0` off; effective min `3` |
| `logMaxSizeMb` | number | `50` | Cap for offload `*.log` files under data root |
| `userId` | string | machine primary IPv4 | Sent as `X-User-Id` on backend requests |

#### 2. Register the context engine slot

```json
{
  "plugins": {
    "slots": {
      "contextEngine": "memory-tencentdb"
    }
  }
}
```

Slot ID **must** equal the plugin id `memory-tencentdb` (not a legacy `openclaw-context-offload` name). If the slot is missing or owned by another engine, registration aborts and offload hooks become no-ops.

#### 3. Apply the after-tool-call messages patch

The `after_tool_call` hook needs `event.messages` (session message list). Stock OpenClaw does not always attach it; the patch injects:

```js
messages: ctx.params.session?.messages
```

into the `after_tool_call` hook event object (after `durationMs`).

```bash
# Auto-run on package install (failures ignored)
# package.json → "postinstall": "bash scripts/openclaw-after-tool-call-messages.patch.sh 2>/dev/null || true"

# Explicit / re-apply after OpenClaw upgrade
bash scripts/openclaw-after-tool-call-messages.patch.sh
# or with explicit install root:
bash scripts/openclaw-after-tool-call-messages.patch.sh /path/to/openclaw
```

| Property | Detail |
| :--- | :--- |
| Resolve OpenClaw | `which openclaw` → walk to package root; pnpm shim parsing; common global paths |
| Targets | All `dist/**/*.js` containing `after_tool_call` + nearby `durationMs` |
| Idempotent | Skips files already containing the injection; exit 0 if patched or already skipped |
| Backup | First write creates `*.pre-offload-patch.bak` |
| Debug | `DEBUG=1 bash scripts/openclaw-after-tool-call-messages.patch.sh` |
| Exit codes | `0` success/skip; `1` no file patched and none already patched |

Without an effective patch, L3 on `after_tool_call` logs:

`after_tool_call patch check: NOT EFFECTIVE` with status `missing_field` or `empty_messages`, and skips compression for that turn (`patch_not_effective` may be reported to a backend store).

#### 4. Restart gateway

```bash
openclaw gateway restart
```

## Compression ratios

L3 compares estimated working tokens to the context window:

| Threshold | Formula | Action |
| :--- | :--- | :--- |
| Mild | `floor(window × mildOffloadRatio)` default **0.5** | `compressByScoreCascade` — replace lower-priority tool results with summaries |
| Aggressive | `floor(window × aggressiveCompressRatio)` default **0.85** | `aggressiveCompressUntilBelowThreshold` — delete history, then inject history Mermaid |
| MMD budget | `floor(window × mmdMaxTokenRatio)` default **0.2** | Cap for injected Mermaid token cost |

Tune for long-horizon sessions:

```json
"offload": {
  "enabled": true,
  "mildOffloadRatio": 0.4,
  "aggressiveCompressRatio": 0.8,
  "mmdMaxTokenRatio": 0.15,
  "defaultContextWindow": 200000
}
```

Lower ratios compress earlier; higher ratios keep more raw tool text longer (more tokens, more detail).

## Mermaid injection paths

Confirm these code paths when debugging “canvas never appears in context”:

| Path | When | What |
| :--- | :--- | :--- |
| `injectMmdIntoMessages` | `before_prompt_build` / assemble / `llm_input` | Injects **active** MMD only; marker `_mmdContextMessage: "active"` |
| `maybeUpdateMmdInMessages` | Each `after_tool_call` | Re-reads active MMD file; re-injects if content fingerprint changed |
| `buildHistoryMmdInjection` | After aggressive L3 deletion | Injects **history** MMD as replacement for deleted turns (`_mmdInjection`) |
| `findActiveMmdInsertionPoint` | Active inject | After latest user message (second half), without splitting tool_use / tool_result pairs |
| `findHistoryMmdInsertionPoint` | History inject | Immediately before active MMD, else same heuristic as active |

Injection readiness requires L1.5 settled and `stateManager.isMmdInjectionReady()`. With `waitForL15: true`, inject is skipped until judgment completes.

On-disk canvas files live at:

```text
<dataRoot>/<agentName>/mmds/*.mmd
<dataRoot>/<agentName>/refs/*.md          # full tool results
<dataRoot>/<agentName>/offload-<sessionId>.jsonl
<dataRoot>/<agentName>/state.json
```

Default `dataRoot` is `~/.openclaw/context-offload` (`DEFAULT_DATA_ROOT` in `src/offload/storage.ts`).

## Modes

| `offload.mode` | L1/L1.5/L2 | L3 compression | `registerContextEngine` |
| :--- | :--- | :--- | :--- |
| `local` (default without `backendUrl`) | Local LLM (`offload.model` or host default) | Yes | Yes (slot required) |
| `backend` (default when `backendUrl` set) | Remote `backendUrl` | Yes | Yes |
| `collect` | Yes (async data collection) | **No** | **No** — legacy compaction; remove slot or switch mode if slot is set |

## Verification checklist

<Steps>
  <Step title="Config status">
    Run `bash scripts/setup-offload.sh --status` and confirm:

    - Context Engine Slot: `memory-tencentdb`
    - Offload enabled
    - Mild / aggressive ratios as expected
    - Compaction: `safeguard` if you used the setup script
  </Step>
  <Step title="Patch health">
    Re-run the patch script; expect “已经 patch 过，跳过” / exit 0. After a tool-heavy turn, gateway logs should **not** repeatedly show `patch check: NOT EFFECTIVE`.
  </Step>
  <Step title="Registration logs">
    On gateway start with offload on, look for context-offload diagnostics such as successful `registerContextEngine` and absence of:

    - `Config plugins.slots.contextEngine=... ALL offload functions disabled`
    - `Context engine slot occupied — ALL offload functions disabled`
  </Step>
  <Step title="Data dir artifacts">
    After a multi-tool session, expect under `~/.openclaw/context-offload/<agent>/`:

    - `refs/*.md` for offloaded tool bodies
    - `offload-*.jsonl` entries
    - `mmds/*.mmd` when L2 has produced a canvas
  </Step>
  <Step title="Injection markers">
    In debug logs, active inject reports `mmd-injector: injected active MMD…`; history inject only after aggressive L3. Markers `_mmdContextMessage` / `_mmdInjection` must be skipped by L0 capture (plugin already filters them).
  </Step>
</Steps>

## Disable

```bash
bash scripts/setup-offload.sh --disable
# or set offload.enabled=false and remove plugins.slots.contextEngine
openclaw gateway restart
```

Disable sets `offload.enabled` to false and deletes `plugins.slots.contextEngine` (and empty `plugins.slots`). It does not uninstall the messages patch; that remains until you restore `*.pre-offload-patch.bak` or reinstall OpenClaw.

## Troubleshooting (enable-specific)

| Symptom | Likely cause | Action |
| :--- | :--- | :--- |
| Offload logs “slot not assigned… ALL offload disabled” | Missing/wrong `plugins.slots.contextEngine` | Set to `memory-tencentdb`, restart |
| “slot occupied” / `ok: false` | Another context engine owns the slot | Free the slot or change owner config |
| `patch check: NOT EFFECTIVE` every tool call | Patch missing after OpenClaw upgrade | `bash scripts/openclaw-after-tool-call-messages.patch.sh`; `DEBUG=1` if exit 1 |
| `setup-offload.sh` exits 2 | Patch script failed or missing | Fix OpenClaw path/version; do not leave enable half-applied |
| No `mmds/` / no canvas in context | L1.5 not settled, L2 thresholds not met, or inject not ready | Check `forceTriggerThreshold`, `l2NullThreshold`, `l2TimeoutSeconds`; confirm not `mode: "collect"` |
| Backend L1/L2 silent | `mode` backend/collect without `backendUrl` | Set `backendUrl` (and optional `backendApiKey` / `userId`) |
| Compaction fights offload | Context engine not registered; host compaction active | Ensure slot + non-collect mode; setup script sets `compaction.mode=safeguard` |

For full short-term architecture (L1/L1.5/L2 pipelines, `node_id` recovery), see [Context offload](/context-offload). For the complete config schema, see [Plugin configuration reference](/plugin-config-reference).

## Next

<CardGroup>
  <Card title="Context offload" href="/context-offload">
    Symbolic short-term memory: tool-result refs, L1/L1.5/L2, Mermaid canvas, mild vs aggressive compression, node_id recovery.
  </Card>
  <Card title="Configure OpenClaw" href="/configure-openclaw">
    Daily and advanced plugin tuning (capture, pipeline, recall, persona) and post-restart verification.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full memory-tencentdb schema including offload field types, defaults, and parseConfig rules.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Source-backed failure modes including offload patch missing and no-recall checks.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Zero-config OpenClaw enable path and optional offload slot registration.
  </Card>
</CardGroup>

---

## 11. Configure embedding

> OpenAI-compatible embedding providers, required fields (baseUrl, apiKey, model, dimensions), sendDimensions for BGE-M3-style backends, timeouts, and graceful keyword-only degradation when provider is none.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/11-configure-embedding.md
- Generated: 2026-08-04T18:31:47.193Z

### Source Files

- `src/core/store/embedding.ts`
- `src/config.ts`
- `openclaw.plugin.json`
- `README.md`
- `SKILL.md`
- `src/core/store/factory.ts`

---
title: "Configure embedding"
description: "OpenAI-compatible embedding providers, required fields (baseUrl, apiKey, model, dimensions), sendDimensions for BGE-M3-style backends, timeouts, and graceful keyword-only degradation when provider is none."
---

`memory-tencentdb` drives dense vector search through the `embedding` config group. By default `provider` is `"none"`: the plugin stays online and degrades to keyword/FTS recall. Any other non-local provider value is treated as an OpenAI-compatible remote API, with an explicit `zeroentropy` branch and a `qclaw` proxy path.

## Default behavior

Zero-config leaves embedding off:

| Setting | Default | Effect |
| --- | --- | --- |
| `embedding.provider` | `"none"` | Vector search disabled |
| `embedding.enabled` | schema default `true`, but forced `false` when `provider` is `"none"` | No remote embed client is created |
| `embedding.dimensions` | `0` when provider is `"none"` | SQLite `vec0` tables are deferred until a real provider is configured |
| `recall.strategy` | `"hybrid"` | Falls back to `"keyword"` when no embedding service is available |

Incomplete remote config does **not** crash the plugin. `parseConfig` sets `embedding.configError`, forces `enabled: false`, and OpenClaw logs `[EMBEDDING CONFIG ERROR] ...` at startup. Search continues on the keyword path.

## Provider modes

| `provider` value | Mode | Required fields | Notes |
| --- | --- | --- | --- |
| `"none"` (default) | Off | none | Keyword/FTS only |
| `"openai"`, `"deepseek"`, `"azure"`, or any other string except specials below | OpenAI-compatible remote | `baseUrl`, `apiKey`, `model`, `dimensions` | `POST {baseUrl}/embeddings` |
| `"zeroentropy"` | ZeroEntropy native | same four fields | `POST {baseUrl}/models/embed` with `input_type: "query"` |
| `"qclaw"` | Proxied OpenAI-compatible | four fields + `proxyUrl` | Request goes to `proxyUrl`; original URL sent as `Remote-URL` header |
| `"local"` | Not user-configurable | — | Treated as disabled; `configError` asks for a remote provider |

With `storeBackend: "tcvdb"`, the store factory installs `NoopEmbeddingService` and relies on TCVDB server-side embedding (`tcvdb.embeddingModel`) instead of the remote client configured under `embedding.*`.

## Config fields

Configured under OpenClaw at `plugins.entries["memory-tencentdb"].config.embedding`, or under Gateway at `memory.embedding` in `tdai-gateway.json` / YAML (same shape; Gateway reuses `parseConfig`).

<ParamField body="enabled" type="boolean" default="true">
Schema default is `true`, but `provider: "none"` forces effective disable. Incomplete remote config also forces `false`.
</ParamField>

<ParamField body="provider" type="string" default="none">
Embedding backend label. `"none"` disables vectors; other values (except `"local"`) select remote clients. `"zeroentropy"` and `"qclaw"` use specialized request paths.
</ParamField>

<ParamField body="baseUrl" type="string" required>
OpenAI-compatible API base (no trailing slash required; service strips trailing `/`). Example: `https://api.openai.com/v1`. Remote calls hit `{baseUrl}/embeddings` unless provider is `zeroentropy` or `qclaw`.
</ParamField>

<ParamField body="apiKey" type="string" required>
Bearer token for the embeddings API (`Authorization: Bearer …`).
</ParamField>

<ParamField body="model" type="string" required>
Model id sent in the request body (for example `text-embedding-3-small`, `bge-m3`, `zembed-1`).
</ParamField>

<ParamField body="dimensions" type="number" required>
Output vector size. Must match the model (and any Matryoshka truncation the backend supports). Stored and used for SQLite `vec0` layout.
</ParamField>

<ParamField body="sendDimensions" type="boolean" default="true">
When `true`, the request body includes `"dimensions": <n>` (OpenAI Matryoshka-style). Set `false` for fixed-dimension OSS backends such as BGE-M3 that reject Matryoshka with HTTP 400.
</ParamField>

<ParamField body="proxyUrl" type="string">
Required only when `provider` is `"qclaw"`. Fetch target becomes `proxyUrl`; header `Remote-URL` is set to `{baseUrl}/embeddings`.
</ParamField>

<ParamField body="maxInputChars" type="number" default="5000">
Per-text character cap before truncation (warning logged). Applies to remote OpenAI-compatible and ZeroEntropy paths.
</ParamField>

<ParamField body="timeoutMs" type="number" default="10000">
Default per-call HTTP timeout (ms). Transient failures retry up to 3 times with exponential backoff (500ms × attempt). HTTP 4xx (except 429) is not retried.
</ParamField>

<ParamField body="recallTimeoutMs" type="number">
Optional override for user-facing recall embeds. Falls back to `timeoutMs`. Prefer a shorter value (for example `3000`).
</ParamField>

<ParamField body="captureTimeoutMs" type="number">
Optional override for background capture/L1-dedup embeds. Falls back to `timeoutMs`. Can be longer (for example `15000`).
</ParamField>

<ParamField body="conflictRecallTopK" type="number" default="5">
Top-K candidates pulled during L1 conflict/dedup detection when vectors are available.
</ParamField>

## Enable a remote provider

### OpenClaw (`~/.openclaw/openclaw.json`)

```json
{
  "plugins": {
    "entries": {
      "memory-tencentdb": {
        "enabled": true,
        "config": {
          "embedding": {
            "enabled": true,
            "provider": "openai",
            "baseUrl": "https://api.openai.com/v1",
            "apiKey": "sk-...",
            "model": "text-embedding-3-small",
            "dimensions": 1536,
            "sendDimensions": true,
            "timeoutMs": 10000,
            "recallTimeoutMs": 3000,
            "captureTimeoutMs": 15000
          },
          "recall": {
            "strategy": "hybrid"
          }
        }
      }
    }
  }
}
```

Restart the OpenClaw gateway after edits so `parseConfig` and the store factory re-run.

### BGE-M3 and other fixed-dimension backends

OpenAI-compatible hosts that reject Matryoshka need `sendDimensions: false`. Keep `dimensions` equal to the model’s fixed size for local SQLite storage:

```json
{
  "embedding": {
    "enabled": true,
    "provider": "openai",
    "baseUrl": "http://your-host:port/v1",
    "apiKey": "<KEY>",
    "model": "bge-m3",
    "dimensions": 1024,
    "sendDimensions": false
  }
}
```

Without `sendDimensions: false`, backends often return HTTP 400 with a body containing `does not support matryoshka representation`.

### ZeroEntropy

```json
{
  "embedding": {
    "provider": "zeroentropy",
    "baseUrl": "https://api.zeroentropy.dev/v1",
    "apiKey": "<KEY>",
    "model": "zembed-1",
    "dimensions": 640,
    "sendDimensions": true
  }
}
```

`dimensions` for `zembed-1` must be in the server Matryoshka set (`2560`, `1280`, `640`, `320`, `160`, `80`, `40`) when sent. Set `sendDimensions: false` to omit the field.

### Gateway / Hermes (`memory-tencentdb-ctl`)

```bash
# Enable OpenAI-compatible embedding
memory-tencentdb-ctl config embedding \
  --provider openai \
  --api-key "sk-xxxx" \
  --base-url "https://api.openai.com/v1" \
  --model "text-embedding-3-small" \
  --dimensions 1536 \
  --restart

# qclaw requires --proxy-url
memory-tencentdb-ctl config embedding \
  --provider qclaw \
  --api-key "..." \
  --base-url "https://api.openai.com/v1" \
  --model "text-embedding-3-small" \
  --dimensions 1536 \
  --proxy-url "http://127.0.0.1:PORT" \
  --restart

# Disable → keyword/BM25-only
memory-tencentdb-ctl config embedding --provider none --restart
```

Writes go to `$TDAI_DATA_DIR/tdai-gateway.json` at `memory.embedding.{provider,baseUrl,apiKey,model,dimensions,enabled,proxyUrl?}`. Validation matches `parseConfig`: non-`none` providers require the four core fields; missing fields abort the write.

## Runtime path (SQLite)

```text
parseConfig(embedding)
        │
        ├─ provider=none / incomplete / local  → enabled=false, dimensions=0
        │                                         no EmbeddingService
        │                                         recall → keyword/FTS only
        │
        └─ remote + valid four-tuple
                  │
                  ▼
         createStoreBundle (sqlite)
                  │
                  ├─ createEmbeddingService(...)
                  │     openai-compatible → OpenAIEmbeddingService
                  │     zeroentropy       → ZeroEntropyEmbeddingService
                  └─ VectorStore(vectors.db, dimensions)
```

TCVDB path: store uses server embedding; client-side `embedding.*` is not used for dense vectors.

### Request body (OpenAI-compatible)

```json
{
  "input": ["text..."],
  "model": "text-embedding-3-small",
  "dimensions": 1536
}
```

`dimensions` is omitted when `sendDimensions` is `false`. Responses are L2-normalized client-side. Batch size caps at 256 texts per request.

## Keyword-only degradation

| Condition | Behavior |
| --- | --- |
| `provider: "none"` | Embedding disabled; SQLite skips `vec0` creation |
| Missing `apiKey` / `baseUrl` / `model` / `dimensions` | `configError` set; plugin continues without vectors |
| `provider: "local"` | Disabled at config entry; error asks for a remote provider |
| `provider: "qclaw"` without `proxyUrl` | Same graceful disable + `configError` |
| `recall.strategy` is `embedding` or `hybrid` but no service | Auto-recall warns and uses `keyword` |
| Embedding API failure at search time | Search logs a warning and returns empty for that path |

FTS5 keyword search remains available on the SQLite store when FTS is healthy. Hybrid RRF on SQLite needs both keyword and embedding; without embedding, only the keyword branch runs.

## Timeouts and retries

| Control | Default | Scope |
| --- | --- | --- |
| `embedding.timeoutMs` | `10000` | Per embedding HTTP call |
| `embedding.recallTimeoutMs` | unset → `timeoutMs` | Auto-recall / user-facing embeds |
| `embedding.captureTimeoutMs` | unset → `timeoutMs` | Background L1 dedup / capture embeds |
| `recall.timeoutMs` | `5000` | Whole auto-recall budget (separate from embed HTTP) |
| Retries | 3 retries after first attempt | Network errors, 5xx, 429, timeouts; not other 4xx |

## Verification

<Steps>
<Step title="Confirm config shape">
Ensure all four remote fields are set and `provider` is not `"none"`. For BGE-M3-style hosts, set `sendDimensions: false`.
</Step>
<Step title="Restart the host">
OpenClaw: restart the gateway. Hermes/standalone: `memory-tencentdb-ctl restart` (or `config embedding ... --restart`).
</Step>
<Step title="Check startup logs">
Absence of `[EMBEDDING CONFIG ERROR]` means parse accepted the remote block. Presence of that tag means vectors are disabled and the missing-field message is in the log.
</Step>
<Step title="Exercise recall">
With `recall.strategy: "hybrid"` (default), semantic queries should return L1 hits after conversations have been captured and extracted. If only keyword matches work, the embed client is still missing or failing.
</Step>
</Steps>

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Startup `[EMBEDDING CONFIG ERROR]` | Incomplete remote quartet or qclaw without `proxyUrl` | Fill `baseUrl`, `apiKey`, `model`, `dimensions` (+ `proxyUrl` for qclaw) |
| HTTP 400 + matryoshka message | Backend rejects `dimensions` body field | `"sendDimensions": false` |
| No vector results, keyword works | `provider: "none"`, disabled service, or API errors | Enable remote provider; inspect embed HTTP errors in logs |
| Dimension mismatch after switching models | SQLite `vec0` built for previous dims | Align `dimensions` with the new model; may require re-index / fresh `vectors.db` for a clean layout |
| Slow or empty auto-recall | Embed latency vs `recall.timeoutMs` | Lower `recallTimeoutMs`, raise overall `recall.timeoutMs`, or fix backend latency |
| Expect client embeds with TCVDB | Wrong mental model | TCVDB uses `NoopEmbeddingService` + `tcvdb.embeddingModel` |

## Related pages

<CardGroup>
<Card title="Configure OpenClaw" href="/configure-openclaw">
Plugin enablement, daily tuning groups, and gateway restart verification.
</Card>
<Card title="Storage backends" href="/storage-backends">
SQLite vs TCVDB, hybrid RRF, and where embedding services attach.
</Card>
<Card title="Use Tencent VectorDB" href="/use-tcvdb">
Server-side embedding model and TCVDB connection fields.
</Card>
<Card title="Plugin configuration reference" href="/plugin-config-reference">
Full `memory-tencentdb` schema, defaults, and `parseConfig` rules.
</Card>
<Card title="Gateway lifecycle" href="/gateway-ops">
`memory-tencentdb-ctl config embedding` and data-dir layout.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Embedding 400 / matryoshka and broader failure checklist.
</Card>
</CardGroup>

---

## 12. Use Tencent VectorDB

> Switch storeBackend to tcvdb, required tcvdb.url / apiKey / database fields, BM25 language, server-side embedding model, and CA PEM path for HTTPS instances.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/12-use-tencent-vectordb.md
- Generated: 2026-08-04T18:32:15.853Z

### Source Files

- `src/core/store/tcvdb.ts`
- `src/core/store/tcvdb-client.ts`
- `src/core/store/factory.ts`
- `src/config.ts`
- `openclaw.plugin.json`
- `scripts/export-tencent-vdb/export-tencent-vdb.ts`

---
title: "Use Tencent VectorDB"
description: "Switch storeBackend to tcvdb, required tcvdb.url / apiKey / database fields, BM25 language, server-side embedding model, and CA PEM path for HTTPS instances."
---

Set `storeBackend` to `tcvdb` so memory storage uses Tencent Cloud VectorDB (`TcvdbMemoryStore`) instead of local SQLite. Dense vectors are produced **on the VectorDB instance** from collection embedding config; sparse BM25 vectors are encoded **in-process** via `@tencentdb-agent-memory/tcvdb-text`; hybrid recall is a single `/document/hybridSearch` call with RRF.

<Info>
Default backend is `sqlite`. TCVDB is optional and requires a reachable VectorDB instance plus plugin (or Gateway) credentials. Client-side OpenAI-compatible `embedding.*` is **not** used for TCVDB dense vectors — the factory installs `NoopEmbeddingService` when `storeBackend` is `tcvdb`.
</Info>

## Prerequisites

- A Tencent Cloud VectorDB instance URL reachable from the OpenClaw gateway or TDAI Gateway host
- VectorDB account and API key (HTTP `Authorization: Bearer account=<username>&api_key=<apiKey>`)
- A **unique database name** you control (required at factory time; not auto-filled from instance id in runtime validation)
- For `https://` endpoints with a private CA: a readable CA PEM file path
- Plugin already installed and enabled (`memory-tencentdb` / OpenClaw `plugins.entries`)

## What changes when you switch

| Concern | `sqlite` (default) | `tcvdb` |
| --- | --- | --- |
| Dense vectors | Client embedding provider + local `vectors.db` / sqlite-vec | Server-side model on the collection (`tcvdb.embeddingModel`) |
| Sparse / keyword | Local FTS5 | Local BM25 sparse vectors + inverted index on `sparse_vector` |
| Hybrid recall | Parallel FTS + embed, client RRF | Native hybrid: dense `ann` + sparse `match` + `rerank.method: "rrf"` |
| Client embedding service | Remote provider when configured | `NoopEmbeddingService` (`dimensions` 0; skips local embed on capture) |
| L0/L1/profile store | SQLite tables | Collections `{database}_l0_conversations`, `{database}_l1_memories`, `{database}_profiles` |
| On-disk `vectors.db` | Used | Not used for vector storage |

Collection names are prefixed with the database name so multiple databases on one instance do not collide.

## Configure OpenClaw plugin

Edit `~/.openclaw/openclaw.json` under the plugin config object (plugin id `memory-tencentdb`):

```json
{
  "plugins": {
    "entries": {
      "memory-tencentdb": {
        "enabled": true,
        "config": {
          "storeBackend": "tcvdb",
          "tcvdb": {
            "url": "http://10.0.1.1:8100",
            "username": "root",
            "apiKey": "YOUR-VDB-API-KEY",
            "database": "agent_memory_prod",
            "alias": "primary",
            "embeddingModel": "bge-large-zh",
            "timeout": 10000,
            "caPemPath": "/etc/ssl/vdb-ca.pem"
          },
          "bm25": {
            "enabled": true,
            "language": "zh"
          },
          "recall": {
            "strategy": "hybrid"
          }
        }
      }
    }
  }
}
```

Restart the OpenClaw gateway after saving.

### Minimal required config

```json
{
  "storeBackend": "tcvdb",
  "tcvdb": {
    "url": "http://10.0.1.1:8100",
    "apiKey": "YOUR-VDB-API-KEY",
    "database": "agent_memory_prod"
  }
}
```

`createStoreBundle` **throws** if any of these are missing when `storeBackend === "tcvdb"`:

- `tcvdb.url`
- `tcvdb.apiKey`
- `tcvdb.database`

## Configure TDAI Gateway (Hermes / standalone)

For Gateway-hosted memory (`tdai-gateway.json` under `$TDAI_DATA_DIR`, default `~/.memory-tencentdb/memory-tdai`):

```bash
memory-tencentdb-ctl config vdb \
  --url "http://xxx-vdb.tencentclb.com:8100" \
  --username root \
  --api-key "YOUR-VDB-API-KEY" \
  --database "openclaw_memory" \
  --alias "primary" \
  --embedding-model "bge-large-zh" \
  --ca-pem "/etc/ssl/vdb-ca.pem" \
  --restart
```

Behavior:

- Writes `$.memory.tcvdb.{url, username, apiKey, database, alias?, caPemPath?, embeddingModel?}`
- By default also sets `$.memory.storeBackend` to `"tcvdb"` (skip with `--no-set-backend`)
- `--url` must start with `http://` or `https://`
- `--ca-pem` is validated as readable; only the path is stored (file is not copied)

Switch back to SQLite without deleting credentials:

```bash
memory-tencentdb-ctl config vdb-off --restart
# clear credentials as well:
memory-tencentdb-ctl config vdb-off --purge-creds --restart
```

## Field reference

### `storeBackend`

<ParamField body="storeBackend" type="string" default="sqlite">
Enum: `sqlite` | `tcvdb`. Parsed as `tcvdb` only when the value is exactly `"tcvdb"`; any other value falls back to `sqlite`.
</ParamField>

### `tcvdb` object

<ParamField body="url" type="string" required>
Instance base URL (e.g. `http://10.0.1.1:80` or external CLB host). Trailing slashes are stripped by the HTTP client.
</ParamField>

<ParamField body="apiKey" type="string" required>
VectorDB API key. Sent as `Authorization: Bearer account=<username>&api_key=<apiKey>`.
</ParamField>

<ParamField body="database" type="string" required>
Database name. Created idempotently on store init if missing. Must be unique for your deployment; used as collection name prefix.
</ParamField>

<ParamField body="username" type="string" default="root">
Account name for the Bearer auth header.
</ParamField>

<ParamField body="alias" type="string">
Optional human label stored in manifest / `database.json` snapshots for identification.
</ParamField>

<ParamField body="embeddingModel" type="string" default="bge-large-zh">
**Server-side** embedding model bound to L0/L1 collections at create time (`embedding.model` on the collection). Dense search passes query text as `embeddingItems` / ann `data`; the instance embeds them.
</ParamField>

<ParamField body="timeout" type="number" default="10000">
Per-request timeout in milliseconds (`AbortSignal.timeout`). Export CLI default is 30000; plugin/runtime default is 10000.
</ParamField>

<ParamField body="caPemPath" type="string">
Filesystem path to a CA certificate PEM file. Used only when `url` is `https://`. Loaded once at client construction into an undici `Agent` with `connect.ca`. Load failure is logged; HTTPS may then fail TLS verification.
</ParamField>

### `bm25` object (hybrid sparse path)

BM25 is primarily useful with `tcvdb` (native hybrid). Defaults still apply for both backends.

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | `true` | When false, no encoder is created; TCVDB falls back to dense-only `/document/search` |
| `language` | `"zh"` \| `"en"` | `"zh"` | Pretrained BM25 params via `BM25Encoder.default(language)` (jieba-wasm tokenization in-package) |

```json
"bm25": { "enabled": true, "language": "en" }
```

Use `language: "en"` for English-dominant corpora (pair with an English-capable server embedding model such as `bge-large-en-v1.5` when your instance supports it).

## Runtime behavior

### Store factory

`createStoreBundle(config, { dataDir, logger })`:

1. Always builds optional BM25 encoder from `config.bm25`
2. On `storeBackend: "tcvdb"`, validates url / apiKey / database, constructs `TcvdbMemoryStore`, returns `embedding: NoopEmbeddingService` and store snapshot `{ type: "tcvdb", tcvdbUrl, tcvdbDatabase, tcvdbAlias? }`

### Init and collections

On `init()` the store:

1. `POST /database/list` then `/database/create` if needed  
2. Waits **5s** after a newly created database before creating collections  
3. Creates L1 and L0 collections with server embedding enabled on `text` / `message_text` respectively  
4. Creates profiles collection with embedding **disabled** (scalar + stub vector index)  
5. Prefers vector index type `DISK_FLAT` (dimension **1024**, metric **COSINE**); on API code `15113` or “DISK_FLAT not support” messages, falls back to `HNSW` (`M: 16`, `efConstruction: 200`)  
6. Always adds inverted index on `sparse_vector` (metric `IP`) for L0/L1  
7. Uses `shardNum: 1`, `replicaNum: 2`  
8. Sets `degraded = true` on init failure; subsequent ops return empty/false instead of throwing

### Upsert path

- L1/L0 documents store text fields; dense vectors are generated by the instance  
- If BM25 is enabled, `encodeTexts` fills `sparse_vector` before `/document/upsert` (`buildIndex: true`)  
- Capture skips local embed when embedding dimensions are 0 (noop / server-side path)

### Search path

- With BM25: `hybridSearch` with `ann` on field `text`/`message_text` (query string → server embed), `match` on `sparse_vector` (query sparse), `rerank: { method: "rrf", k: 60 }`, `readConsistency: "strongConsistency"`  
- Without BM25: dense-only `/document/search` with `embeddingItems`  
- Auto-recall short-circuits to `searchL1Hybrid` when `getCapabilities().nativeHybridSearch` is true (single HTTP call; no redundant local embed)

### HTTP client

- Retries up to **2** times on 5xx / timeout (not on 4xx API codes)  
- Logs one info line per successful path with latency (e.g. `/document/hybridSearch 85ms`)  
- Errors surface as `TcvdbApiError` with `apiCode` when the body `code !== 0`

## HTTPS and CA PEM

```text
https://instance-host
        │
        ▼
TcvdbClient constructor
  if url starts with https:// AND caPemPath set
    → fs.readFileSync(caPemPath)
    → undici Agent({ connect: { ca } })
    → request(..., { dispatcher })
```

- HTTP instances do not use `caPemPath`  
- If HTTPS is required by your network but `caPemPath` is omitted, TLS uses the process default trust store  
- Migration flag: `--tcvdb-ca-pem <path>`  
- ctl flag: `--ca-pem <path>`

## Embedding configuration interaction

| Config | Role with `tcvdb` |
| --- | --- |
| `tcvdb.embeddingModel` | **Authoritative** dense model on L0/L1 collections |
| `embedding.provider` / `baseUrl` / `apiKey` / `model` / `dimensions` | Not used for TCVDB dense vectors (noop service). Still relevant if you switch back to `sqlite` |
| `recall.strategy` | Prefer `"hybrid"` to use native hybrid; `"keyword"` uses sparse/FTS-style path; `"embedding"` uses dense path through store APIs |

Do not expect changing OpenAI-compatible `embedding.*` alone to change TCVDB dense vectors; recreate or migrate collections only when changing `embeddingModel` on a greenfield database.

## Verification

<Steps>
  <Step title="Confirm backend selection">
    After gateway restart, check plugin/Gateway logs for:

    ```text
    [memory-tdai][factory] Store created: backend=tcvdb, database=<name>, model=<embeddingModel>, bm25=enabled|disabled
    ```
  </Step>
  <Step title="Confirm init">
    Look for client debug/info around database/collection create, or degraded errors:

    ```text
    [memory-tdai][tcvdb] Initialized: db=<name>, model=<model>
    ```
  </Step>
  <Step title="Probe collections (optional)">
    ```bash
    npx export-tencent-vdb \
      --url "http://10.0.1.1:8100" \
      --username root \
      --api-key "YOUR-VDB-API-KEY" \
      --database "agent_memory_prod" \
      --probe
    ```

    Expect collections named `{database}_l1_memories`, `{database}_l0_conversations`, `{database}_profiles`.
  </Step>
  <Step title="Exercise hybrid recall">
    Run a session that should auto-recall, or call search tools. Logs may show:

    ```text
    [hybrid-native] Single-call hybrid: N results in Xms
    [memory-tdai][tcvdb-client] /document/hybridSearch Xms
    ```
  </Step>
  <Step title="Check manifest binding">
    Under the plugin data dir, `.metadata/manifest.json` store binding should report `type: "tcvdb"` with url/database (and optional alias). Mismatches vs current config are reported via store-binding diff helpers at startup.
  </Step>
</Steps>

## Export and inspect

Export collections to JSONL (skips dense `vector` by default; always includes `sparse_vector`):

```bash
npx export-tencent-vdb \
  --url "http://10.0.1.1:8100" \
  --username root \
  --api-key "YOUR-VDB-API-KEY" \
  --database "agent_memory_prod" \
  -o ./vdb-export-2026-08-04

# include dense vectors
npx export-tencent-vdb ... --include-vectors
```

Output layout:

```text
./vdb-export-YYYY-MM-DD/
├── <collection>.jsonl
├── schemas.json
└── export-meta.json
```

## Migrate existing SQLite data

To move local `vectors.db` into TCVDB and rewrite plugin config/manifest, use the offline migrator (see full workflow on the migrate page):

```bash
npx migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --bm25-language zh \
  --yes
```

English corpus example: `--tcvdb-embedding-model bge-large-en-v1.5 --bm25-language en`. Dense-only: `--no-bm25-enabled`. HTTPS: `--tcvdb-ca-pem /path/to/ca.pem`.

## Failure modes

| Symptom | Likely cause | What to check |
| --- | --- | --- |
| Startup throw: requires `tcvdb.url` and `tcvdb.apiKey` | Missing required fields | Plugin config / `tdai-gateway.json` |
| Startup throw: requires `tcvdb.database` | Empty database name | Set a unique name before enable |
| Store degraded; empty recall | Init HTTP failure, wrong URL/auth, network | Client logs; probe with export `--probe` |
| Hybrid degrades to dense-only | `bm25.enabled: false` or encoder init failure | `bm25` config and package `@tencentdb-agent-memory/tcvdb-text` |
| HTTPS TLS errors | Missing/wrong CA | `caPemPath` / `--ca-pem` readability and PEM content |
| Collection create fails then HNSW | Instance lacks `DISK_FLAT` | Expected fallback; look for debug “DISK_FLAT not supported … falling back to HNSW” |
| Auth failures | Bad username/apiKey | Bearer format uses `account=` + `api_key=` |
| Client embedding 400s still appear | Unrelated `embedding.*` remote calls or leftover sqlite path | Confirm `storeBackend` and factory log line show `backend=tcvdb` |

## Related pages

<CardGroup>
  <Card title="Storage backends" href="/storage-backends">
    sqlite vs tcvdb factory selection, BM25, hybrid RRF, embedding service roles.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    OpenAI-compatible embedding fields used primarily by the sqlite backend.
  </Card>
  <Card title="Migrate SQLite to TCVDB" href="/migrate-to-tcvdb">
    Offline migrate-sqlite-to-tcvdb flags, layers, config rewrite, verification.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full schema for storeBackend, tcvdb, bm25, and parseConfig defaults.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    memory-tencentdb-ctl config vdb / vdb-off and tdai-gateway.json layout.
  </Card>
  <Card title="Inspect local memory" href="/inspect-local-memory">
    export-tencent-vdb and on-disk layout for diagnostics.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Broader failure checklist including store and recall issues.
  </Card>
</CardGroup>

---

## 13. Seed historical conversations

> Import conversation JSON via openclaw memory-tdai seed or POST /seed: input formats A/B, flags, config overrides, output directory layout, and L0→L1→L2→L3 execution path.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/13-seed-historical-conversations.md
- Generated: 2026-08-04T18:32:31.370Z

### Source Files

- `src/cli/README.md`
- `src/cli/commands/seed.ts`
- `src/cli/index.ts`
- `src/core/seed/input.ts`
- `src/core/seed/seed-runtime.ts`
- `src/core/seed/types.ts`
- `src/gateway/types.ts`

---
title: "Seed historical conversations"
description: "Import conversation JSON via openclaw memory-tdai seed or POST /seed: input formats A/B, flags, config overrides, output directory layout, and L0→L1→L2→L3 execution path."
---

`openclaw memory-tdai seed` and Gateway `POST /seed` import historical conversation JSON into a dedicated data directory, then run the same capture and pipeline path used at runtime: per-round L0 write, L1 atom extraction on `everyNConversations` boundaries, with L2/L3 runners wired for scene and persona work.

## Surfaces

| Surface | Entry | Host |
|---------|--------|------|
| OpenClaw CLI | `openclaw memory-tdai seed --input <file>` | Plugin CLI under the `memory-tdai` namespace |
| Gateway HTTP | `POST /seed` | `TdaiGateway` (auth required when `TDAI_GATEWAY_API_KEY` / `server.apiKey` is set; only `GET /health` skips auth) |

Both paths share validation (`loadAndValidateInput` / `validateAndNormalizeRaw`) and execution (`executeSeed`). The CLI loads a file; the Gateway accepts an in-body JSON payload.

## Prerequisites

- OpenClaw plugin installed and CLI registered, **or** a running Gateway with memory + LLM config.
- Input JSON in Format A or Format B (below).
- For L1/L2/L3 LLM steps: plugin or Gateway `llm` settings with a usable `apiKey` (seed uses `StandaloneLLMRunnerFactory` when `llm.enabled` and `llm.apiKey` are set).
- Empty or new output directory (resume from checkpoint is **not** implemented).

## CLI: seed command

```bash
openclaw memory-tdai seed --input <file> [options]
```

### Flags

| Flag | Required | Default | Description |
|------|----------|---------|-------------|
| `--input <file>` | yes | — | Path to conversation JSON |
| `--output-dir <dir>` | no | `<stateDir>/memory-tdai-seed-<YYYYMMDD-HHmmss>` | Pipeline data directory |
| `--session-key <key>` | no | from input, else `seed-user` | Fallback when a session omits `sessionKey` |
| `--config <file>` | no | plugin config only | JSON override, two-level deep-merged onto plugin config |
| `--strict-round-role` | no | `false` | Each round must include at least one `user` and one `assistant` |
| `--yes` | no | `false` | Skip interactive timestamp auto-fill confirmation |

### Examples

```bash
# Basic
openclaw memory-tdai seed --input conversations.json

# Explicit output dir
openclaw memory-tdai seed --input data.json --output-dir ./seed-output

# Config override + non-interactive
openclaw memory-tdai seed --input data.json --config seed-config.json --yes

# Strict roles
openclaw memory-tdai seed --input data.json --strict-round-role --yes
```

### Output directory rules

1. Explicit `--output-dir` is resolved with `path.resolve`.
2. Default is under OpenClaw `stateDir`: `memory-tdai-seed-<timestamp>`.
3. If the directory exists and contains `.metadata/checkpoint.json` → exit with error (resume not implemented).
4. If the directory exists and is non-empty without a checkpoint → exit with error.

## Gateway: POST /seed

:::endpoint POST /seed Batch-seed historical conversations (blocking)

Same Format A/B payload as the CLI, wrapped in a request envelope. Execution is **blocking** and can run for minutes on large inputs.

### Request fields

<ParamField body="data" type="object | array" required>
Seed payload: Format A `{ sessions: [...] }` or Format B `[...]`.
</ParamField>

<ParamField body="session_key" type="string">
Fallback session key when sessions omit `sessionKey`.
</ParamField>

<ParamField body="strict_round_role" type="boolean">
Require user + assistant in every round (default: false).
</ParamField>

<ParamField body="auto_fill_timestamps" type="boolean">
When all messages lack timestamps, auto-fill with monotonic epoch ms (default: **true**; no interactive prompt).
</ParamField>

<ParamField body="config_override" type="object">
Two-level deep-merge onto gateway memory config. Gateway always injects its `llm` block (`enabled: true` plus gateway LLM fields) before merge.
</ParamField>

### Response fields

<ResponseField name="sessions_processed" type="number">
Sessions in the normalized input.
</ResponseField>

<ResponseField name="rounds_processed" type="number">
Rounds fed through capture.
</ResponseField>

<ResponseField name="messages_processed" type="number">
Total messages in input.
</ResponseField>

<ResponseField name="l0_recorded" type="number">
L0 rows actually recorded.
</ResponseField>

<ResponseField name="duration_ms" type="number">
Wall-clock duration of `executeSeed`.
</ResponseField>

<ResponseField name="output_dir" type="string">
`<data.baseDir>/seed-<YYYYMMDD-HHmmss>`.
</ResponseField>

### Errors

| Status | Condition |
|--------|-----------|
| `400` | Missing `data` |
| `400` | Validation failure — body includes `error` string and `validation_errors` array |
| `401` | Auth enabled and Bearer token missing/invalid |
| `404` | Wrong method/path |

:::

<RequestExample>
```bash
curl -sS -X POST "http://127.0.0.1:18790/seed" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -d '{
    "data": {
      "sessions": [{
        "sessionKey": "user-alice",
        "conversations": [[
          { "role": "user", "content": "Hello", "timestamp": 1711929600000 },
          { "role": "assistant", "content": "Hi!", "timestamp": 1711929601000 }
        ]]
      }]
    },
    "auto_fill_timestamps": true
  }'
```
</RequestExample>

<ResponseExample>
```json
{
  "sessions_processed": 1,
  "rounds_processed": 1,
  "messages_processed": 2,
  "l0_recorded": 1,
  "duration_ms": 12345,
  "output_dir": "/home/user/.memory-tencentdb/memory-tdai/seed-20260401-220000"
}
```
</ResponseExample>

## Input formats

### Format A — object wrapper

```json
{
  "sessions": [
    {
      "sessionKey": "user-alice",
      "sessionId": "conv-001",
      "conversations": [
        [
          { "role": "user", "content": "Hello", "timestamp": 1711929600000 },
          { "role": "assistant", "content": "Hi!", "timestamp": 1711929601000 }
        ],
        [
          { "role": "user", "content": "What is the weather?" },
          { "role": "assistant", "content": "Sunny." }
        ]
      ]
    }
  ]
}
```

### Format B — top-level array

```json
[
  {
    "sessionKey": "user-alice",
    "conversations": [
      [
        { "role": "user", "content": "Hello" },
        { "role": "assistant", "content": "Hi!" }
      ]
    ]
  }
]
```

### Field reference

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `sessionKey` | string | yes (non-empty) | User / channel identity |
| `sessionId` | string | no | Auto `crypto.randomUUID()` if omitted |
| `conversations` | message[][] | yes | Outer array = rounds; inner = messages in that round |
| `role` | string | yes | Non-empty string (`user` / `assistant` expected; strict mode enforces both per round) |
| `content` | string | yes | Non-empty after trim |
| `timestamp` | number \| string | no | Epoch **integer** ms, or ISO 8601 string |

## Validation pipeline

Six layers; failures throw `SeedValidationError` (CLI exits `1`; Gateway returns `400`).

| Layer | Stage id | Checks |
|-------|----------|--------|
| 1 | `file` | CLI only: exists, non-empty, valid JSON |
| 2 | `top_level` | Format A (`sessions` array) or Format B (array) |
| 3 | `session` | Non-empty `sessionKey`; `conversations` is 2D array; at least one session |
| 4 | `round` | Each round is a non-empty message array; optional strict user+assistant |
| 5 | `message` | `role` and `content` required; `timestamp` integer ms or parseable ISO |
| 6 | `timestamp_consistency` | **All** messages have timestamps, **or none** — mixed is rejected |

### Timestamp fill

- **All present** → keep as-is (ISO strings → epoch ms).
- **All missing** → CLI prompts (or `--yes` auto-fills); Gateway auto-fills when `auto_fill_timestamps` is true (default).
- **Fill strategy** → single global monotonic counter starting at `Date.now()`, +100 ms per message across all sessions (required when multiple sessions share one `sessionKey` so L0 capture cursors do not drop later sessions).

Normalization defaults: missing `sessionKey` → `--session-key` / `session_key` / `"seed-user"`; missing `sessionId` → UUID.

## Config overrides

CLI `--config` and Gateway `config_override` use the same **two-level deep merge**: if both base and override values for a key are plain objects, shallow-merge fields; otherwise override replaces the value.

Typical seed acceleration override:

```json
{
  "pipeline": {
    "everyNConversations": 3,
    "enableWarmup": false,
    "l1IdleTimeoutSeconds": 2,
    "l2DelayAfterL1Seconds": 1,
    "l2MinIntervalSeconds": 1,
    "l2MaxIntervalSeconds": 10
  }
}
```

Isolated TCVDB database for a seed run:

```json
{
  "storeBackend": "tcvdb",
  "tcvdb": {
    "database": "my_seed_test_db"
  },
  "pipeline": {
    "everyNConversations": 3,
    "enableWarmup": false,
    "l1IdleTimeoutSeconds": 2
  }
}
```

Gateway path also forces `llm` from gateway settings before applying `config_override`.

## Execution path (L0 → L1 → L2 → L3)

```mermaid
flowchart TB
  subgraph Input
    A[Format A/B JSON]
    V[validate + normalize]
    A --> V
  end

  subgraph SeedRuntime["executeSeed"]
    P[createPipeline + L2/L3 runners]
    S[scheduler.start]
    R[For each session × round]
    L0[performAutoCapture L0]
    W1["waitForL1Idle every N rounds"]
    WT[session tail + final L1 idle]
    D[pipeline.destroy]
    M[manifest.seed update]
    V --> P --> S --> R
    R --> L0
    L0 --> W1
    W1 --> R
    R --> WT --> D --> M
  end

  subgraph Artifacts["outputDir"]
    C[conversations/ L0]
    Rec[records/ L1]
    Sc[scene_blocks/ L2]
    Pe[persona/ L3]
    Vdb[vectors.db or tcvdb]
  end

  L0 --> C
  W1 --> Rec
  P -.->|scheduled, not fully awaited| Sc
  P -.->|scheduled, not fully awaited| Pe
  P --> Vdb
```

### Behavior details

1. **`captureStartTimestamp = 0`** — disables live cold-start filtering so historical messages are not dropped.
2. **Per round** — messages mapped to `{ role, content, timestamp }` and passed to `performAutoCapture` with the seed `outputDir` as `pluginDataDir`.
3. **L1 batching** — after every `pipeline.everyNConversations` rounds **within a session**, seed polls until L1 is idle (`l1Idle`, no buffered messages, conversation count 0). Without this pause, all rounds would pile into one L1 batch.
4. **Session tail + global wait** — residual L1 work drained per session, then once for all session keys.
5. **L2/L3** — runners are attached via `createL2Runner` / `createL3Runner` (same factory path as live runtime), but seed **only waits for L1 idle** before `pipeline.destroy()`. In-flight L2 scene / L3 persona work may be cut short; latest L2/L3 artifacts are not guaranteed on every run.
6. **SIGINT** — first Ctrl+C finishes the current round and shuts down; second forces exit.

### Summary output (CLI)

```
Sessions / Rounds / Messages / L0 recorded / Duration / Output path
```

Progress stages reported during the run: `l0_captured`, `l1_waiting`.

## Output directory layout

:::files
```
<output-dir>/
├── conversations/          # L0 JSONL
├── records/                # L1 JSONL
├── scene_blocks/           # L2 scene blocks (if L2 completed before destroy)
├── persona/                # L3 persona artifacts (if L3 completed before destroy)
├── vectors.db              # SQLite + sqlite-vec (storeBackend=sqlite only)
├── .metadata/
│   ├── manifest.json       # store binding + seed run record
│   └── checkpoint.json     # pipeline progress (presence blocks re-seed into same dir)
└── .backup/                # rolling backups when enabled by runtime
```
:::

### manifest.seed

On success, seed appends to `.metadata/manifest.json`:

```json
{
  "version": 1,
  "createdAt": "2026-04-01T22:00:00.000Z",
  "store": {
    "type": "sqlite",
    "sqlite": { "path": "vectors.db" }
  },
  "seed": {
    "inputFile": "conversations.json",
    "sessions": 3,
    "rounds": 42,
    "messages": 128,
    "startedAt": "2026-04-01T22:00:00.000Z",
    "completedAt": "2026-04-01T22:05:30.000Z"
  }
}
```

`inputFile` is the basename only (CLI). Live runtime directories keep `seed: null`.

## CLI vs Gateway differences

| Concern | CLI | Gateway |
|---------|-----|---------|
| Input source | File (`--input`) | Body field `data` |
| Timestamp missing | Interactive confirm unless `--yes` | Auto-fill default true |
| Output dir | `--output-dir` or `stateDir/memory-tdai-seed-*` | Always `data.baseDir/seed-*` |
| Config base | OpenClaw plugin config | Gateway memory + injected `llm` |
| openclawConfig | Passed from plugin CLI context | `{}` |
| Progress | stdout progress line | debug logs only |

## Failure modes

| Symptom | Cause | Action |
|---------|--------|--------|
| `Input file not found` / empty / JSON parse | Layer 1 file errors | Fix path and JSON |
| `Unrecognized input format` | Not Format A or B | Wrap with `sessions` or use an array of sessions |
| Timestamp consistency failed | Mixed presence of `timestamp` | Add timestamps to all messages or strip all |
| `--strict-round-role` errors | Round missing user or assistant | Fix rounds or drop the flag |
| Output directory not empty / checkpoint exists | Resume not implemented | New directory or clean target |
| Config override file missing / not object | CLI merge helper | Valid JSON object path |
| Long run, sparse L2/L3 | L1-only idle wait then destroy | Re-run with longer pipeline windows, or inspect partial artifacts; full L1+L2+L3 idle wait not yet exposed |
| Gateway `401` | API key set, missing Bearer | Align `TDAI_GATEWAY_API_KEY` client header |

## Verify after seed

<Steps>
  <Step title="Confirm summary counts">
    CLI box or Gateway JSON: `sessions_processed`, `rounds_processed`, `l0_recorded` &gt; 0 for non-empty input.
  </Step>
  <Step title="Inspect on-disk layers">
    Check `conversations/`, `records/`, and `.metadata/manifest.json` under the reported `output_dir`. Use [Inspect local memory](/inspect-local-memory) tools (`read-local-memory`) against that directory.
  </Step>
  <Step title="Optional: search via tools or Gateway">
    Point live config or tools at the seed data dir / TCVDB database if you seeded into a shared backend; otherwise treat the seed directory as an isolated corpus.
  </Step>
</Steps>

## Next

<CardGroup>
  <Card title="Memory layers" href="/memory-layers">
    L0 conversation, L1 atom, L2 scene, L3 persona model and drill-down paths.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Full route list including POST /seed request and response shapes.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    memory-tdai seed flags and other package bin commands.
  </Card>
  <Card title="Inspect local memory" href="/inspect-local-memory">
    Query L0–L3 artifacts and on-disk layout after a seed run.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Pipeline, LLM, embedding, and store fields usable in seed overrides.
  </Card>
  <Card title="Use Tencent VectorDB" href="/use-tcvdb">
    storeBackend tcvdb and database isolation for seed test DBs.
  </Card>
</CardGroup>

---

## 14. Secure the Gateway

> Optional TDAI_GATEWAY_API_KEY Bearer auth, TDAI_CORS_ORIGINS allow-list, non-loopback host warnings, and Hermes client MEMORY_TENCENTDB_GATEWAY_API_KEY alignment.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/14-secure-the-gateway.md
- Generated: 2026-08-04T18:33:57.921Z

### Source Files

- `README.md`
- `src/gateway/config.ts`
- `src/gateway/server.ts`
- `hermes-plugin/memory/memory_tencentdb/client.py`
- `hermes-plugin/memory/memory_tencentdb/README.md`

---
title: "Secure the Gateway"
description: "Optional TDAI_GATEWAY_API_KEY Bearer auth, TDAI_CORS_ORIGINS allow-list, non-loopback host warnings, and Hermes client MEMORY_TENCENTDB_GATEWAY_API_KEY alignment."
---

The standalone **TdaiGateway** (`src/gateway/server.ts`) is an optional Node HTTP sidecar for Hermes (default `127.0.0.1:8420`). Security is **opt-in**: by default all routes are open and no CORS headers are sent. Operators enable a shared-secret Bearer gate with `TDAI_GATEWAY_API_KEY` / `server.apiKey`, restrict browser origins with `TDAI_CORS_ORIGINS` / `server.corsOrigins`, and align the Hermes Python client with `MEMORY_TENCENTDB_GATEWAY_API_KEY` (same secret, client-side only).

<Warning>
Auth and CORS both default **off**. Existing loopback-only deployments keep working without changes. Bind to a non-loopback address (`0.0.0.0`, LAN IP) without `TDAI_GATEWAY_API_KEY` only if you intentionally accept unauthenticated access to capture, recall, search, and seed.
</Warning>

## Security model

| Control | Server config | Env | Default | Effect |
| :--- | :--- | :--- | :--- | :--- |
| Bearer auth | `server.apiKey` | `TDAI_GATEWAY_API_KEY` | unset (disabled) | When set, every route **except** `GET /health` and `OPTIONS` requires `Authorization: Bearer <key>` |
| CORS allow-list | `server.corsOrigins` | `TDAI_CORS_ORIGINS` (comma-separated) | `[]` | Empty → **no** `Access-Control-Allow-*` headers; browsers block cross-origin calls |
| Bind address | `server.host` | `TDAI_GATEWAY_HOST` | `127.0.0.1` | Loopback-only by default; non-loopback without apiKey triggers a loud startup WARN |
| Port | `server.port` | `TDAI_GATEWAY_PORT` | `8420` | HTTP listen port |

Resolution for the shared secret: env `TDAI_GATEWAY_API_KEY` overrides `server.apiKey` from the config file. CORS is inverted: an explicit yaml/json `server.corsOrigins` (including `[]`) wins over `TDAI_CORS_ORIGINS` so shell leakage cannot force CORS on.

Config file discovery (first hit wins): `TDAI_GATEWAY_CONFIG` path → `./tdai-gateway.yaml` or `./tdai-gateway.json` in CWD → same names under the data dir (`TDAI_DATA_DIR` / default `~/.memory-tencentdb/memory-tdai`).

## Enable Bearer auth

### Gateway side

Set the same secret the clients will send. Prefer env for secrets; yaml is fine if the file is not world-readable.

```bash
export TDAI_GATEWAY_API_KEY="replace-with-a-long-random-secret"
export TDAI_GATEWAY_HOST="127.0.0.1"   # keep loopback unless you need remote clients
export TDAI_GATEWAY_PORT="8420"
```

Or in `tdai-gateway.yaml` / `tdai-gateway.json`:

```yaml
server:
  host: "127.0.0.1"
  port: 8420
  apiKey: "${TDAI_GATEWAY_API_KEY}"   # whole-string ${VAR} leaves expand from the process env
```

String leaves that are exactly `${VAR_NAME}` expand from `process.env` at load time (missing vars become empty strings).

### Route gate behavior

When `server.apiKey` is a non-empty string:

| Request | Auth required? | Failure |
| :--- | :--- | :--- |
| `GET /health` | No | — |
| `OPTIONS *` (CORS preflight) | No | Returns `204` after CORS header application |
| `POST /recall`, `/capture`, `/search/memories`, `/search/conversations`, `/session/end`, `/seed` | Yes | HTTP **401** |
| Any other path after auth | — | HTTP **404** if path unknown |

Header rules (`checkAuth`):

1. Missing or non-`Bearer ` `Authorization` → `{"error":"Unauthorized: missing Bearer token"}` (401).
2. Present token compared with `crypto.timingSafeEqual` after UTF-8 buffer length check → wrong token → `{"error":"Unauthorized: invalid token"}` (401).
3. Leading/trailing spaces on the provided token are stripped before compare; empty after strip is invalid.

When `apiKey` is unset, `checkAuth` is a no-op (legacy open mode).

### Call a protected route

```bash
curl -sS -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"preferences","session_key":"agent:main:main"}' \
  http://127.0.0.1:8420/recall
```

Health stays open for orchestrators:

```bash
curl -sS http://127.0.0.1:8420/health
# {"status":"ok"|"degraded","version":"…","uptime":…,"stores":{…}}
```

## Hermes client alignment

The Hermes `memory_tencentdb` provider is an HTTP **client**. Auth on the wire is independent of whether the Gateway process was spawned by the supervisor, Docker, or systemd.

### Env names

| Variable | Who reads it | Role |
| :--- | :--- | :--- |
| `TDAI_GATEWAY_API_KEY` | Gateway (`loadGatewayConfig`) | Enables enforcement + secret to match against |
| `MEMORY_TENCENTDB_GATEWAY_API_KEY` | Hermes provider / SDK client only | Outbound `Authorization: Bearer …` on every request (including `GET /health`) |
| `TDAI_GATEWAY_API_KEY` (fallback) | Hermes `_resolve_gateway_api_key()` | Used only if `MEMORY_TENCENTDB_GATEWAY_API_KEY` is unset — shared env-file convenience |

The Gateway **never** reads `MEMORY_TENCENTDB_GATEWAY_API_KEY`. That name is plugin-side only.

### Resolution order (Hermes)

`_resolve_gateway_api_key()` walks:

1. `MEMORY_TENCENTDB_GATEWAY_API_KEY` (strip whitespace; empty → try next)
2. `TDAI_GATEWAY_API_KEY`
3. Neither set → no `Authorization` header (matches open Gateway)

`MemoryTencentdbSdkClient` strips the key again and attaches `Authorization: Bearer <key>` from `_build_headers` on both GET and POST.

### Supervisor does not inject the secret

`GatewaySupervisor` accepts `api_key` for the **client** half only. When it `Popen`s the Node sidecar it copies `os.environ` and deliberately **does not** set `TDAI_GATEWAY_API_KEY` from that client argument. Consequences:

- If both processes already share an env file that sets `TDAI_GATEWAY_API_KEY`, the child inherits enforcement automatically.
- If you only set `MEMORY_TENCENTDB_GATEWAY_API_KEY`, the client sends Bearer tokens but the Gateway stays open unless you also configure Gateway-side auth (env, yaml, Docker `-e`, unit file).

```bash
# Both ends — same secret
export TDAI_GATEWAY_API_KEY="replace-with-a-long-random-secret"
export MEMORY_TENCENTDB_GATEWAY_API_KEY="$TDAI_GATEWAY_API_KEY"
# Or set only TDAI_GATEWAY_API_KEY once when Hermes and Gateway share the process environment
```

Provider config metadata exposes `gateway_api_key` as optional/secret with `env_var: MEMORY_TENCENTDB_GATEWAY_API_KEY`.

## CORS allow-list

### Defaults

- `corsOrigins: []` → `applyCorsHeaders` returns immediately; **no** CORS response headers.
- Browsers then apply same-origin policy and block cross-origin XHR/fetch.
- Non-browser clients (Hermes Python, `curl`, in-process tools) are unaffected by CORS.

### Configure

```bash
export TDAI_CORS_ORIGINS="https://app.example.com,https://admin.example.com"
```

```yaml
server:
  corsOrigins:
    - "https://app.example.com"
    - "https://admin.example.com"
  # or comma-separated string: "https://a,https://b"
  # or [] to force CORS off even if TDAI_CORS_ORIGINS is set in the shell
```

### Match behavior

| Config | Response headers |
| :--- | :--- |
| `[]` (default) | None |
| `["*"]` | `Access-Control-Allow-Origin: *`, methods `GET, POST, OPTIONS`, headers `Content-Type, Authorization`; startup WARN |
| Explicit list, request `Origin` in list | Echo that origin + methods/headers + `Vary: Origin` |
| Explicit list, origin missing or not listed | No allow-origin headers; `Vary: Origin` only |

`OPTIONS` always completes with **204** after CORS application (no body). Allowed methods/headers are fixed as above when CORS is enabled.

## Startup security posture

On listen success, `logSecurityPosture()` logs one summary line (never logs the key material):

```text
Security posture: auth=ENABLED (Bearer)|disabled host=<host> cors=no-headers|wildcard(*)|allowlist(N)
```

Additional warnings:

| Condition | Log |
| :--- | :--- |
| Auth disabled | WARN: `TDAI_GATEWAY_API_KEY is NOT set` — routes open to anyone who can reach the port |
| Host not loopback (`127.0.0.1` / `localhost` / `::1`) **and** auth disabled | Second WARN naming the bind host and listing sensitive routes |
| `corsOrigins` contains `*` | WARN: every browser origin can call the gateway |

Loopback classification is exact string equality on the configured host, not a network interface scan.

## Recommended deployment patterns

<Tabs>
  <Tab title="Local Hermes (loopback)">
Keep defaults: `TDAI_GATEWAY_HOST=127.0.0.1`, no apiKey, empty CORS. Sufficient when only the local Hermes process talks to the sidecar.

```bash
# Optional hardening even on localhost
export TDAI_GATEWAY_API_KEY="$(openssl rand -hex 32)"
export MEMORY_TENCENTDB_GATEWAY_API_KEY="$TDAI_GATEWAY_API_KEY"
```
  </Tab>
  <Tab title="Docker / remote bind">
Official Hermes Docker docs default `TDAI_GATEWAY_HOST=0.0.0.0` so the port is reachable inside the network namespace. Treat that as a **non-loopback** bind: enable Bearer auth and do not publish the port publicly without a reverse proxy or network policy.

```bash
export TDAI_GATEWAY_HOST="0.0.0.0"
export TDAI_GATEWAY_API_KEY="<secret>"
export MEMORY_TENCENTDB_GATEWAY_API_KEY="<secret>"
# Prefer internal Docker network + curl health from the same container
```
  </Tab>
  <Tab title="Browser UI calling Gateway">
Set a concrete `server.corsOrigins` allow-list (not `*`). Clients must still send Bearer if auth is enabled. Prefer loopback or private network + TLS terminator in front of the Node process; the Gateway itself speaks plain HTTP.
  </Tab>
</Tabs>

## Enable checklist

<Steps>
  <Step title="Choose bind host">
Prefer `127.0.0.1` unless remote clients require otherwise. Non-loopback without auth logs a loud WARN at startup.
  </Step>
  <Step title="Set Gateway secret">
Export `TDAI_GATEWAY_API_KEY` or set `server.apiKey` in `tdai-gateway.yaml` / `.json`. Restart the Gateway process so `loadGatewayConfig` reloads.
  </Step>
  <Step title="Align Hermes client">
Set `MEMORY_TENCENTDB_GATEWAY_API_KEY` to the **same** secret (or rely on shared `TDAI_GATEWAY_API_KEY` fallback). Do not assume the supervisor copies the client `api_key` into the child env.
  </Step>
  <Step title="Configure CORS only if browsers call the API">
Leave empty for sidecar-only use. For browser UIs, list exact origins; avoid `*`.
  </Step>
  <Step title="Verify">
Confirm startup line `auth=ENABLED (Bearer)`, health without token, protected route without token returns 401, protected route with token succeeds.
  </Step>
</Steps>

## Verification

```bash
# 1) Health open
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8420/health
# expect 200

# 2) Protected route without token
curl -sS -o /dev/null -w "%{http_code}\n" \
  -H "Content-Type: application/json" \
  -d '{"query":"x","session_key":"s"}' \
  http://127.0.0.1:8420/recall
# expect 401 when auth enabled

# 3) Protected route with token
curl -sS -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"x","session_key":"s"}' \
  http://127.0.0.1:8420/recall
# expect 200 (or 400 only if body invalid after auth)
```

Inspect Gateway logs for:

```text
Security posture: auth=ENABLED (Bearer) host=127.0.0.1 cors=no-headers
```

Hermes client failures on auth show as HTTP errors from the SDK (`Gateway … returned 401: …`) and feed the provider circuit breaker after repeated failures.

## Failure modes

| Symptom | Likely cause | Fix |
| :--- | :--- | :--- |
| `401 Unauthorized: missing Bearer token` | Client not sending header | Set `MEMORY_TENCENTDB_GATEWAY_API_KEY` or pass `Authorization: Bearer` |
| `401 Unauthorized: invalid token` | Secret mismatch, trailing newline, wrong env var | Align secrets; both sides strip whitespace, but different strings still fail |
| Hermes capture/recall fails after enabling Gateway auth | Client-only key unset | Export Hermes key; supervisor does not auto-enable Gateway enforcement from client `api_key` |
| Gateway still open after setting only `MEMORY_TENCENTDB_GATEWAY_API_KEY` | Gateway does not read that name | Set `TDAI_GATEWAY_API_KEY` / `server.apiKey` on the Gateway process |
| Browser CORS errors | Empty allow-list or origin not listed | Add exact origin to `server.corsOrigins` / `TDAI_CORS_ORIGINS` |
| Loud WARN on Docker start | `0.0.0.0` without apiKey | Expected; set `TDAI_GATEWAY_API_KEY` before publishing the port |
| `OPTIONS` works but POST blocked by browser | Origin not echoed | Confirm request Origin is literally in the allow-list (no trailing slash surprises) |

## What this page does not cover

- OpenClaw **in-process** plugin path (no Gateway HTTP auth — hooks run inside the OpenClaw process).
- TLS / mTLS, reverse-proxy auth, or network policies (operator responsibility outside this package).
- LLM, embedding, or Tencent VectorDB API keys (`TDAI_LLM_*`, embedding `apiKey`, `tcvdb.apiKey`) — separate secrets from Gateway Bearer auth.

## Related pages

<CardGroup cols={2}>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Route inventory, request/response fields, auth exceptions, and error envelope.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full `TDAI_*` / `MEMORY_TENCENTDB_*` catalog and client vs server API-key names.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    Start/stop/status/health with `memory-tencentdb-ctl` and `~/.memory-tencentdb` layout.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Install provider, auto-discovery, and health verification before enabling auth.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth 401, circuit breaker, and log/probe checklist.
  </Card>
</CardGroup>

---

## 15. Plugin configuration reference

> Full memory-tencentdb config schema: field types, defaults, enums, validation rules (retention, embedding, llm, offload, tcvdb, bm25, report), and parseConfig behavior.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/15-plugin-configuration-reference.md
- Generated: 2026-08-04T18:33:36.005Z

### Source Files

- `openclaw.plugin.json`
- `src/config.ts`
- `README.md`
- `src/utils/no-think-fetch.ts`
- `src/utils/memory-cleaner.ts`
- `src/core/report/reporter.ts`

---
title: "Plugin configuration reference"
description: "Full memory-tencentdb config schema: field types, defaults, enums, validation rules (retention, embedding, llm, offload, tcvdb, bm25, report), and parseConfig behavior."
---

`memory-tencentdb` accepts a flat object of functional groups. Host JSON Schema lives in `openclaw.plugin.json` (`configSchema`, `additionalProperties: true`). Runtime resolution and validation are owned by `parseConfig()` in `src/config.ts`, which always returns a fully filled `MemoryTdaiConfig`. Empty input `{}` is valid zero-config: every field has a default. Invalid or incomplete remote embedding settings do not throw; they set `embedding.configError` and degrade to non-vector search.

## Config surfaces

| Host | Where the object lives | How it is parsed |
| :--- | :--- | :--- |
| OpenClaw | `~/.openclaw/openclaw.json` → `memory-tencentdb` / plugin `config` | `api.pluginConfig` → `parseConfig(raw)` in plugin `register()` |
| Gateway (Hermes / standalone) | `tdai-gateway.yaml` / `.json` key `memory` | `loadGatewayConfig()` → `parseConfig(memory)` |
| Seed CLI / HTTP | `pluginConfig` override on seed requests | Same `parseConfig` |

Enable the plugin separately from tunables:

```jsonc
// ~/.openclaw/openclaw.json
{
  "memory-tencentdb": {
    "enabled": true,
    "config": {
      "timezone": "Asia/Shanghai",
      "storeBackend": "sqlite",
      "recall": { "strategy": "hybrid", "maxResults": 5 }
    }
  }
}
```

Some install docs place groups at the plugin root (`memory-tencentdb.capture`, …). Runtime always consumes whatever object OpenClaw exposes as `pluginConfig`; groups are the keys listed below, not the outer `enabled` flag.

Gateway reuses the same groups under `memory`:

```jsonc
// ~/.memory-tencentdb/memory-tdai/tdai-gateway.json (excerpt)
{
  "llm": { "baseUrl": "https://api.openai.com/v1", "apiKey": "...", "model": "gpt-4o" },
  "memory": {
    "storeBackend": "sqlite",
    "recall": { "enabled": true, "strategy": "hybrid" }
  }
}
```

Gateway-only keys (`server.*`, top-level `llm`, `data.baseDir`) are **not** part of `MemoryTdaiConfig`. See [Environment variables](/environment-variables) and [Gateway lifecycle](/gateway-ops).

## `parseConfig` behavior

```ts
parseConfig(raw: Record<string, unknown> | undefined): MemoryTdaiConfig
```

| Rule | Behavior |
| :--- | :--- |
| Missing root | `raw ?? {}` |
| Missing groups | Treated as `{}` (`obj()` helper) |
| String fields | Non-empty trimmed strings only (`str`); optional allow empty via `optStr` |
| Numbers | Finite `number` only; non-numbers ignored → default |
| Booleans | Strict `typeof === "boolean"` |
| String arrays | Filters to non-empty strings |
| Unknown keys | Ignored (schema allows extras; parser only reads known paths) |
| Fail-soft | Embedding misconfig → `configError` + `enabled: false`, plugin keeps running |
| Fail-hard | Parser itself rarely throws; host registration logs parse failures if unexpected |

Resolved shape is `MemoryTdaiConfig`: top-level `timezone`, `storeBackend`, plus groups `capture`, `extraction`, `persona`, `pipeline`, `recall`, `embedding`, `tcvdb`, `bm25`, `memoryCleanup` (derived), `report`, `llm`, `offload`.

## Top-level fields

<ParamField body="timezone" type="string" default="system">
User/LLM-facing timestamps and local-day boundaries. Values: `"system"`, IANA name (`Asia/Shanghai`), or UTC offset (`+08:00`, `-05:30`). Storage instants remain UTC.
</ParamField>

<ParamField body="storeBackend" type="string" default="sqlite">
Enum at schema: `sqlite` \| `tcvdb`. Parser maps anything other than `"tcvdb"` to `"sqlite"`.
</ParamField>

## `capture` — L0 recording and local retention

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `true` | Auto-capture conversations |
| `excludeAgents` | `string[]` | `[]` | Glob patterns; matched agents skip capture, recall, and pipeline |
| `l0l1RetentionDays` | `number` | `0` | TTL for local L0/L1 files; `0` = no cleanup |
| `allowAggressiveCleanup` | `boolean` | `false` | Required for retention of `1` or `2` days |
| `cleanTime` | `string` | `"03:00"` | Daily cleaner schedule `HH:mm` / `H:mm` |

### Retention validation

| `l0l1RetentionDays` | `allowAggressiveCleanup` | Resulting `capture.l0l1RetentionDays` | `memoryCleanup` |
| :--- | :--- | :--- | :--- |
| `≤ 0` | any | `0` | `enabled: false`, `retentionDays` undefined |
| `≥ 3` | any | value as-is | `enabled: true`, `retentionDays` set |
| `1` or `2` | `true` | value as-is | enabled |
| `1` or `2` | `false` (default) | forced to `0` | disabled |

`cleanTime` is normalized by `normalizeCleanTime`: must match `^(\d{1,2}):(\d{2})$`, hour `0–23`, minute `0–59`. Invalid values fall back to `"03:00"`. Examples: `"3:05"` → `"03:05"`; `"24:00"`, `"3:5"`, `"abc"` → invalid.

### Derived `memoryCleanup`

Not a user-facing schema group. Built as:

```ts
{
  retentionDays,           // undefined when cleanup off
  enabled: retentionDays != null,
  cleanTime,               // normalized HH:mm
}
```

`LocalMemoryCleaner` deletes aged shards under `conversations/` (L0) and `records/` (L1), with floor guards (`MIN_RETAIN_L0 = 50`, `MIN_RETAIN_L1 = 20` records). Cutoff uses local calendar days, not rolling 24h windows.

## `extraction` — L1 atom extraction

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `true` | Background L1 extraction |
| `enableDedup` | `boolean` | `true` | Vector/keyword conflict detection |
| `maxMemoriesPerSession` | `number` | `20` | Cap per L1 pass |
| `model` | `string` | omit | `provider/model`; else host default model |

## `persona` — L2 scenes / L3 profile

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `triggerEveryN` | `number` | `50` | Persona rebuild every N new memories |
| `maxScenes` | `number` | `15` | Max scene blocks |
| `backupCount` | `number` | `3` | Persona backup generations |
| `sceneBackupCount` | `number` | `10` | Scene block backups |
| `model` | `string` | omit | `provider/model` for persona LLM |

## `pipeline` — L1→L2→L3 scheduling

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `everyNConversations` | `number` | `5` | L1 every N conversation rounds |
| `enableWarmup` | `boolean` | `true` | Threshold 1→2→4→…→`everyN` for new sessions |
| `l1IdleTimeoutSeconds` | `number` | `600` | L1 after idle |
| `l2DelayAfterL1Seconds` | `number` | `10` | Delay L2 after L1 completes |
| `l2MinIntervalSeconds` | `number` | `900` | Min gap between L2 runs per session |
| `l2MaxIntervalSeconds` | `number` | `3600` | Max L2 poll interval while session active |
| `sessionActiveWindowHours` | `number` | `24` | Stop L2 polling after inactivity |

## `recall` — auto-recall injection

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `true` | Auto-recall before turn |
| `maxResults` | `number` | `5` | Max memories returned |
| `maxCharsPerMemory` | `number` | `0` | Per-item char cap; `0` = unlimited |
| `maxTotalRecallChars` | `number` | `0` | Total inject budget; `0` = unlimited |
| `scoreThreshold` | `number` | `0.3` | Minimum score |
| `strategy` | `string` | `"hybrid"` | `embedding` \| `keyword` \| `hybrid` |
| `timeoutMs` | `number` | `5000` | Overall recall timeout; on exceed, skip inject + warn |

`validateStrategy` whitelists only the three enums; unknown strings fall back to `"hybrid"`.

## `embedding` — vector provider

Schema marks remote fields as required in description, but the parser **never throws** on missing fields.

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `true` (schema) | Forced `false` when provider is effectively disabled |
| `provider` | `string` | `"none"` | See provider rules below |
| `baseUrl` | `string` | `""` | Required for remote / `qclaw` |
| `apiKey` | `string` | `""` | Required for remote / `qclaw` |
| `model` | `string` | `""` / model when remote | Required for remote / `qclaw` |
| `dimensions` | `number` | `0` when provider `none` | Must be `> 0` for remote |
| `sendDimensions` | `boolean` | `true` | Include `dimensions` in request body (OpenAI Matryoshka). Set `false` for BGE-M3-style backends that return HTTP 400 matryoshka errors |
| `proxyUrl` | `string` | omit | **Required** when `provider === "qclaw"` |
| `conflictRecallTopK` | `number` | `5` | Dedup conflict recall size |
| `maxInputChars` | `number` | `5000` | Truncate embed input |
| `timeoutMs` | `number` | `10000` | Per-call timeout (retries up to 3 in embed client) |
| `recallTimeoutMs` | `number` | omit | Overrides `timeoutMs` on user-facing recall path |
| `captureTimeoutMs` | `number` | omit | Overrides on background capture/dedup path |
| `modelCacheDir` | `string` | omit | Internal; not in plugin schema |
| `configError` | `string` | omit | Filled by parser on invalid remote config |

### Provider resolution

| `provider` | Outcome |
| :--- | :--- |
| `"none"` (default) | `enabled = false`, `dimensions = 0` (skip vec0 table creation until a real provider is set) |
| `"local"` | Treated as disabled; `configError` explains local is not user-exposed |
| `"qclaw"` | Requires `proxyUrl`, `baseUrl`, `apiKey`, `model`, `dimensions > 0`; missing → disable + `configError` |
| any other string | OpenAI-compatible remote; requires `apiKey`, `baseUrl`, `model`, `dimensions > 0`; missing → disable + `configError` (plugin continues, keyword-only) |

Incomplete remote config example (plugin still starts):

```text
[memory-tdai] [EMBEDDING CONFIG ERROR] Remote embedding provider 'openai' requires 'apiKey', 'baseUrl', 'model', and 'dimensions' to be set. Missing: apiKey, dimensions. Embedding has been disabled.
```

Valid OpenAI-style example:

```json
{
  "embedding": {
    "enabled": true,
    "provider": "openai",
    "baseUrl": "https://api.openai.com/v1",
    "apiKey": "<KEY>",
    "model": "text-embedding-3-small",
    "dimensions": 1536,
    "sendDimensions": true,
    "timeoutMs": 10000,
    "recallTimeoutMs": 3000,
    "captureTimeoutMs": 15000
  }
}
```

BGE-M3 / fixed-dimension backend:

```json
{
  "embedding": {
    "provider": "openai",
    "baseUrl": "http://localhost:8080/v1",
    "apiKey": "not-needed",
    "model": "bge-m3",
    "dimensions": 1024,
    "sendDimensions": false
  }
}
```

## `tcvdb` — only when `storeBackend: "tcvdb"`

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `url` | `string` | `""` | Instance URL (required for real use) |
| `username` | `string` | `"root"` | Account |
| `apiKey` | `string` | `""` | Required for real use |
| `database` | `string` | `""` | Auto-generated from instance id when empty |
| `alias` | `string` | `""` | Optional friendly name for `database.json` |
| `embeddingModel` | `string` | `"bge-large-zh"` | Server-side embedding model |
| `timeout` | `number` | `10000` | Request timeout ms |
| `caPemPath` | `string` | omit | CA PEM path for HTTPS |

`parseConfig` does **not** reject empty `url`/`apiKey` at parse time; store factory fails later if backend cannot connect. Prefer setting all required fields before switching backend.

## `bm25` — sparse encoding (esp. tcvdb hybrid)

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `true` | Local BM25 sparse vectors via `@tencentdb-agent-memory/tcvdb-text` |
| `language` | `string` | `"zh"` | Enum `zh` \| `en`; parser maps non-`en` to `zh` |

## `llm` — standalone OpenAI-compatible override

When `enabled: true`, L1/L2/L3 extraction bypasses the host LLM runner (e.g. OpenClaw embedded agent) and calls the configured API directly. Default is host LLM.

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `false` | Standalone mode switch |
| `baseUrl` | `string` | `"https://api.openai.com/v1"` | OpenAI-compatible base |
| `apiKey` | `string` | `""` | Auth key |
| `model` | `string` | `"gpt-4o"` | Model id |
| `maxTokens` | `number` | `4096` | Max output tokens |
| `timeoutMs` | `number` | `120000` | Request timeout |
| `disableThinking` | `boolean \| string` | `false` | See thinking strategies |

Gateway top-level `llm` (in `tdai-gateway.json`) is a separate object used by `StandaloneHostAdapter` and does not require `llm.enabled`. Plugin `config.llm` is the OpenClaw in-process override.

## `offload` — context compression (independent switch)

Default `enabled: false` so long-term memory works without context-engine slot registration.

| Field | Type | Default (runtime) | In `openclaw.plugin.json`? | Notes |
| :--- | :--- | :--- | :---: | :--- |
| `enabled` | `boolean` | `false` | yes | Master switch |
| `mode` | `"local" \| "backend" \| "collect"` | auto | **no** | Explicit, else `backendUrl` present → `backend`, else `local` |
| `model` | `string` | omit | yes | `provider/model`; else host default |
| `temperature` | `number` | `0.2` | yes | LLM temperature |
| `disableThinking` | `boolean \| string` | `false` | yes | Local-mode only strategies |
| `forceTriggerThreshold` | `number` | `4` | yes | Tool pairs before force L1 |
| `dataDir` | `string` | omit → `~/.openclaw/context-offload` | yes | Absolute custom root |
| `defaultContextWindow` | `number` | `200000` | yes | Window size for ratio triggers |
| `maxPairsPerBatch` | `number` | `20` | yes | L1 batch size |
| `l2NullThreshold` | `number` | `4` | yes | `node_id=null` count → L2 |
| `l2TimeoutSeconds` | `number` | `300` | yes | Idle L2 trigger |
| `mildOffloadRatio` | `number` | `0.5` | yes | Mild compression fraction of window |
| `aggressiveCompressRatio` | `number` | `0.85` | yes | Aggressive trigger |
| `mmdMaxTokenRatio` | `number` | `0.2` | yes | Mermaid inject budget |
| `backendUrl` | `string` | omit | yes | Remote L1/L1.5/L2/L4 service |
| `backendApiKey` | `string` | omit | yes | Backend auth |
| `backendTimeoutMs` | `number` | **`120000`** | yes (schema default **10000**) | Prefer runtime default |
| `offloadRetentionDays` | `number` | `0` | **no** | Session/ref/MMD TTL; see below |
| `logMaxSizeMb` | `number` | `50` | **no** | Cap for `*.log` under data root; `0` disables |
| `userId` | `string` | omit | **no** | `X-User-Id` for backend; else primary non-loopback IPv4 |

### Offload mode

| Mode | Behavior |
| :--- | :--- |
| `local` | Call LLM via AI SDK with `offload.model` or host model |
| `backend` | Route L1/L1.5/L2/L4 through `backendUrl` |
| `collect` | Async L1/L1.5/L2 for data collection; disables L3 compression; does **not** take `contextEngine` slot |

### `offloadRetentionDays` normalization

| Input | Stored |
| :--- | :--- |
| `≤ 0` | `0` (disabled) |
| `(0, 3)` | `0` (invalid → forced off) |
| `≥ 3` | as-is |

Minimum effective retention is **3** days (no aggressive 1–2 path unlike L0/L1 capture retention).

Enable path (also needs `plugins.slots.contextEngine: "memory-tencentdb"` and after-tool-call patch) is covered in [Enable context offload](/enable-context-offload).

## `report` — metrics

| Field | Type | Default | Notes |
| :--- | :--- | :--- | :--- |
| `enabled` | `boolean` | `false` | Reporting off by default |
| `type` | `string` | `"local"` | Only `"local"` implemented |

`initReporter` with `type: "local"` logs structured JSON via the host logger:

```json
{
  "tag": "METRIC",
  "category": "plugin",
  "plugin": "memory-tdai",
  "instanceId": "<uuid>",
  "pluginVersion": "<semver>",
  "ts": "<ISO-8601>",
  "event": "<name>"
}
```

Unknown `type` → reporting stays disabled (debug log). `report()` never throws into business paths. Instance id persists under `<pluginDataDir>/.metadata/instance_id`.

## `disableThinking` strategies

Shared by `llm.disableThinking` and `offload.disableThinking` via `normalizeDisableThinking()`:

| Value | Effect |
| :--- | :--- |
| `false` / omit | No wrapper (`globalThis.fetch`) |
| `true` | Alias for `"vllm"` |
| `"vllm"` | `chat_template_kwargs.enable_thinking = false` |
| `"deepseek"` | top-level `enable_thinking: false` |
| `"dashscope"` | top-level `enable_thinking: false` (Qwen / DashScope) |
| `"openai"` | `reasoning_effort: "low"` (cannot fully disable o-series thinking) |
| `"anthropic"` / `"kimi"` | `thinking: { type: "disabled" }` |
| `"gemini"` | `thinking_config: { thinking_budget: 0 }` |
| unknown string | Warn + treat as `false` |

Only chat bodies with a `messages` array are mutated; embedding and other requests pass through.

## Schema vs runtime differences

Use **`parseConfig` defaults and validation** as operational truth. Notable gaps:

| Topic | Schema (`openclaw.plugin.json`) | Runtime (`parseConfig`) |
| :--- | :--- | :--- |
| Extra keys | Allowed (`additionalProperties: true`) | Ignored unless known |
| Offload `mode`, `offloadRetentionDays`, `logMaxSizeMb`, `userId` | Not listed | Fully supported |
| `offload.backendTimeoutMs` default | `10000` | `120000` |
| `memoryCleanup` | Not listed | Derived from `capture.*` |
| `embedding.modelCacheDir` / `configError` | Not listed | Internal / diagnostic |
| Invalid retention 1–2 | Documented | Silently disabled unless `allowAggressiveCleanup` |
| Incomplete embedding | Documented as required fields | Soft-fail with `configError` |

## Worked examples

### Zero-config

```json
{ "memory-tencentdb": { "enabled": true } }
```

Resolved highlights: `storeBackend: "sqlite"`, `embedding.provider: "none"` (keyword-only), capture/recall/extraction on, offload off, report off.

### Production-ish OpenClaw block

```jsonc
{
  "memory-tencentdb": {
    "enabled": true,
    "config": {
      "timezone": "Asia/Shanghai",
      "storeBackend": "sqlite",
      "capture": {
        "enabled": true,
        "excludeAgents": ["bench-judge-*"],
        "l0l1RetentionDays": 90,
        "cleanTime": "03:00"
      },
      "extraction": {
        "enabled": true,
        "enableDedup": true,
        "maxMemoriesPerSession": 20
      },
      "pipeline": {
        "everyNConversations": 5,
        "enableWarmup": true,
        "l1IdleTimeoutSeconds": 600
      },
      "recall": {
        "enabled": true,
        "maxResults": 5,
        "scoreThreshold": 0.3,
        "strategy": "hybrid",
        "timeoutMs": 5000
      },
      "persona": {
        "triggerEveryN": 50,
        "maxScenes": 15
      },
      "embedding": {
        "provider": "openai",
        "baseUrl": "https://api.openai.com/v1",
        "apiKey": "<EMBEDDING_KEY>",
        "model": "text-embedding-3-small",
        "dimensions": 1536
      },
      "bm25": { "enabled": true, "language": "zh" },
      "llm": {
        "enabled": false
      },
      "offload": {
        "enabled": false
      },
      "report": {
        "enabled": false,
        "type": "local"
      }
    }
  }
}
```

### TCVDB backend

```json
{
  "storeBackend": "tcvdb",
  "tcvdb": {
    "url": "http://10.0.1.1:8100",
    "username": "root",
    "apiKey": "<TCVDB_KEY>",
    "database": "tdai_memory",
    "embeddingModel": "bge-large-zh",
    "timeout": 10000,
    "caPemPath": "/path/to/ca.pem"
  },
  "bm25": { "enabled": true, "language": "zh" }
}
```

### Standalone LLM for extraction

```json
{
  "llm": {
    "enabled": true,
    "baseUrl": "https://api.deepseek.com/v1",
    "apiKey": "<KEY>",
    "model": "deepseek-v3",
    "maxTokens": 4096,
    "timeoutMs": 120000,
    "disableThinking": "deepseek"
  }
}
```

### Offload with backend + retention

```json
{
  "offload": {
    "enabled": true,
    "mode": "backend",
    "backendUrl": "https://offload-api.example.com",
    "backendApiKey": "<TOKEN>",
    "backendTimeoutMs": 120000,
    "mildOffloadRatio": 0.5,
    "aggressiveCompressRatio": 0.85,
    "mmdMaxTokenRatio": 0.2,
    "offloadRetentionDays": 14,
    "logMaxSizeMb": 50,
    "userId": "ops-user-1"
  }
}
```

## Config validation checklist

| Check | Expected |
| :--- | :--- |
| Plugin enabled | `memory-tencentdb.enabled: true` + gateway restart |
| Logs | `[memory-tdai] Config parsed: capture=…, recall=…, offload=…` |
| Embedding error | No `[EMBEDDING CONFIG ERROR]` if vector search is intended |
| Retention 1–2 | Only works with `allowAggressiveCleanup: true` |
| Offload | `offload.enabled` **and** `plugins.slots.contextEngine: "memory-tencentdb"` |
| TCVDB | Non-empty `tcvdb.url` + `apiKey` before `storeBackend: "tcvdb"` |
| Strategy typos | Unknown `recall.strategy` → silent `"hybrid"` |
| Thinking strategy typos | Unknown `disableThinking` → warn + `false` |

## Related pages

<CardGroup>
  <Card title="Configure OpenClaw" href="/configure-openclaw">
    Enable the plugin, daily vs advanced tuning groups, timezone, and post-restart checks.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    OpenAI-compatible providers, required fields, `sendDimensions`, and keyword-only degradation.
  </Card>
  <Card title="Enable context offload" href="/enable-context-offload">
    `offload.enabled`, contextEngine slot, after-tool-call patch, and compression ratios.
  </Card>
  <Card title="Use Tencent VectorDB" href="/use-tcvdb">
    `storeBackend: tcvdb`, connection fields, BM25 language, and HTTPS CA path.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Gateway `TDAI_*` / `MEMORY_TENCENTDB_*` keys and `tdai-gateway.json` resolution order.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    No recall, embedding 400/matryoshka, retention cleanup, and config-related failure modes.
  </Card>
</CardGroup>

---

## 16. Agent tools

> tdai_memory_search and tdai_conversation_search (OpenClaw) plus Hermes memory_tencentdb_* tool schemas: parameters, hybrid/embedding/keyword strategies, combined 3-call-per-turn limit, and response shapes.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/16-agent-tools.md
- Generated: 2026-08-04T18:33:59.538Z

### Source Files

- `index.ts`
- `src/core/tools/memory-search.ts`
- `src/core/tools/conversation-search.ts`
- `openclaw.plugin.json`
- `hermes-plugin/memory/memory_tencentdb/__init__.py`
- `src/gateway/types.ts`

---
title: "Agent tools"
description: "tdai_memory_search and tdai_conversation_search (OpenClaw) plus Hermes memory_tencentdb_* tool schemas: parameters, hybrid/embedding/keyword strategies, combined 3-call-per-turn limit, and response shapes."
---

OpenClaw registers **`tdai_memory_search`** (L1) and **`tdai_conversation_search`** (L0) via `api.registerTool()`. Hermes exposes the same Gateway-backed search under **`memory_tencentdb_memory_search`** and **`memory_tencentdb_conversation_search`**. Both hosts call `TdaiCore.searchMemories` / `searchConversations`, which run hybrid FTS + embedding search with automatic degradation and Reciprocal Rank Fusion (RRF).

## Host surfaces

| Host | L1 tool | L0 tool | How results return |
|------|---------|---------|-------------------|
| OpenClaw plugin | `tdai_memory_search` | `tdai_conversation_search` | Tool `content[].text` (markdown-ish lines) + `details` |
| Hermes `memory_tencentdb` | `memory_tencentdb_memory_search` | `memory_tencentdb_conversation_search` | JSON string from `handle_tool_call` (Gateway body) |
| Gateway HTTP | `POST /search/memories` | `POST /search/conversations` | JSON `{ results, total, strategy? }` |

`openclaw.plugin.json` declares `contracts.tools`: `["tdai_memory_search", "tdai_conversation_search"]`.

Hermes does **not** serve the `tdai_*` names. Older transcripts that call `tdai_memory_search` through Hermes get `{"error":"Unknown tool: …"}`.

```text
OpenClaw agent                  Hermes agent
     │                               │
     ├─ tdai_memory_search           ├─ memory_tencentdb_memory_search
     └─ tdai_conversation_search     └─ memory_tencentdb_conversation_search
              │                               │
              ▼                               ▼
         TdaiCore (in-process)         HTTP client → Gateway
              │                               │
              └──────────► executeMemorySearch / executeConversationSearch
```

## Registration and availability

### OpenClaw

Tools register only when **`recall.enabled` or `capture.enabled`** is true. If both are false, neither tool is registered.

### Hermes

`get_tool_schemas()` returns both schemas when:

- the provider is initialized / Gateway is available, or
- `MEMORY_TENCENTDB_GATEWAY_CMD` or `MEMORY_TENCENTDB_GATEWAY_PORT` is set (pre-init optimistic registration so routing works before `initialize()` finishes).

Otherwise it returns `[]`.

Tool execution goes through `MemoryTencentdbSdkClient` → `POST /search/memories` or `POST /search/conversations` (default base `http://127.0.0.1:8420`).

## Combined 3-call-per-turn limit

OpenClaw soft-limits the two tools **together** to **3 calls per turn**:

1. **Tool descriptions** — both tools include: *“Limit: tdai_memory_search and tdai_conversation_search share a combined limit of 3 calls per turn. Stop searching after 3 total attempts.”*
2. **Auto-recall guide** — when recall injects context, `<memory-tools-guide>` tells the model the same combined cap: first miss may retry with different keywords/tools; after 3 total attempts, stop and answer from what is already known.

There is **no hard enforcer** in `execute` yet. Registration comments mark a planned `before_tool_call` + early-return hard limit as TODO. Enforcement today is prompt-only.

Hermes tool descriptions and `system_prompt_block()` name the tools and purpose but **do not** encode the 3-call combined limit.

## Search strategies

Agent tools do **not** take a `strategy` argument. Strategy is chosen from runtime capability and which path returns hits:

| Reported `strategy` | When |
|---------------------|------|
| `hybrid` | FTS and embedding both return ≥1 candidate; lists merged with RRF (`k = 60`) |
| `embedding` | Only vector path has hits (or both empty and embedding is configured) |
| `fts` | Only FTS/keyword path has hits |
| `none` | Empty query, missing store, or neither embedding nor FTS available |

Behavior notes:

- FTS and embedding run **in parallel**.
- Over-fetch: `candidateK = limit * 3` (conversation search with `session_key` uses `limit * 4` before filter).
- Secondary filters (`type`, `scene`, `session_key`) apply **after** merge, then results are trimmed to `limit`.
- Single-path failure is non-fatal; the other path can still succeed.
- If neither embedding nor FTS is available, tools return empty results with a `message` asking for an embedding provider (e.g. `embedding.provider = openai_compatible`) or FTS support.

This is separate from **auto-recall** `recall.strategy` (`hybrid` / `embedding` / `keyword` in config). Tool code reports keyword-only path as `fts`, not `keyword`.

## `tdai_memory_search` / `memory_tencentdb_memory_search`

Searches **L1** structured long-term memories (preferences, events, instructions).

### Parameters

| Parameter | Type | Required | Default / bounds | OpenClaw | Hermes schema |
|-----------|------|----------|------------------|----------|---------------|
| `query` | string | yes | — | yes | yes |
| `limit` | number / integer | no | default `5`, clamp `[1, 20]` | yes | yes |
| `type` | string enum | no | — | `persona` \| `episodic` \| `instruction` | same |
| `scene` | string | no | substring match on `scene_name` (case-insensitive) | yes | **not in schema** (Gateway/client support `scene`; Hermes tool path does not pass it) |

OpenClaw clamps with `Math.min(Math.max(Number(params.limit) || 5, 1), 20)`. Hermes uses `_coerce_limit`: accepts int/float/numeric strings, rejects bool, clamps `[1, 20]`, falls back to `5` on garbage.

### Internal result items

| Field | Meaning |
|-------|---------|
| `id` | L1 `record_id` |
| `content` | Memory text |
| `type` | Memory type |
| `priority` | Priority; `< 0` treated as global instruction in formatter |
| `scene_name` | Scene association |
| `score` | Native score, or RRF score when `hybrid` |
| `created_at` / `updated_at` | Mapped from `timestamp_start` / `timestamp_end` |

### Formatted text (OpenClaw + Gateway `results`)

```text
Found N matching memories:

- **[type]** (priority: P) [scene: name] (score: 0.123)
  <content>
```

Empty: `No matching memories found.`  
Config/capability failure may return the embedding/FTS configuration message instead.

### OpenClaw tool return

```json
{
  "content": [{ "type": "text", "text": "<formatted string>" }],
  "details": { "count": 3, "strategy": "hybrid" }
}
```

On exception: text `Memory search failed: …`, `details: { error }`.

### Hermes tool return

JSON-serialized Gateway body, typically:

```json
{
  "results": "<formatted string>",
  "total": 3,
  "strategy": "hybrid"
}
```

Errors: missing `query`, Gateway disconnected, circuit breaker open, or `Tool call failed: …` as `{"error":"…"}`.

## `tdai_conversation_search` / `memory_tencentdb_conversation_search`

Searches **L0** raw conversation messages when structured L1 is insufficient (exact wording, timeline, dialogue context).

### Parameters

| Parameter | Type | Required | Default / bounds | OpenClaw | Hermes schema |
|-----------|------|----------|------------------|----------|---------------|
| `query` | string | yes | — | yes | yes |
| `limit` | number / integer | no | default `5`, clamp `[1, 20]` | yes | yes |
| `session_key` | string | no | exact match on `session_key` after merge | yes | **not in schema** (Gateway/client support it; Hermes tool path does not pass it) |

### Internal result items

| Field | Meaning |
|-------|---------|
| `id` | L0 `record_id` |
| `session_key` | Session id |
| `role` | `user` or `assistant` |
| `content` | Single message text (`message_text`) |
| `score` | Native or RRF score |
| `recorded_at` | Timestamp |

### Formatted text

```text
Found N matching message(s):

---
**[role]** Session: <session_key> [recorded_at] (score: 0.123)

<message content>
```

Empty: `No matching conversation messages found.`

### OpenClaw tool return

```json
{
  "content": [{ "type": "text", "text": "<formatted string>" }],
  "details": { "count": 2 }
}
```

Note: conversation tool `details` does **not** include `strategy` (unlike memory search). Failures use text `Conversation search failed: …`.

### Hermes / Gateway response

```json
{
  "results": "<formatted string>",
  "total": 2
}
```

Gateway conversation search response type has no `strategy` field (memory search does).

## Gateway HTTP bridge

Agent tools and HTTP share the same core methods.

:::endpoint POST /search/memories L1 memory search
**Body**

- `query` (string, required)
- `limit` (number, optional)
- `type` (string, optional)
- `scene` (string, optional)

**200 response**

- `results` (string) — formatted text from `formatSearchResponse`
- `total` (number)
- `strategy` (string)

**400** — missing `query` → `{ "error": "Missing required field: query" }`
:::

:::endpoint POST /search/conversations L0 conversation search
**Body**

- `query` (string, required)
- `limit` (number, optional)
- `session_key` (string, optional)

**200 response**

- `results` (string) — formatted text from `formatConversationSearchResponse`
- `total` (number)

**400** — missing `query`
:::

Auth and CORS follow Gateway config (`TDAI_GATEWAY_API_KEY`, etc.). Hermes attaches `Authorization: Bearer …` when `MEMORY_TENCENTDB_GATEWAY_API_KEY` (or fallback) is set.

## Limit and type coercion

| Rule | OpenClaw | Hermes |
|------|----------|--------|
| Default `limit` | `5` | `5` (`_DEFAULT_SEARCH_LIMIT`) |
| Max `limit` | `20` | `20` (`_MAX_SEARCH_LIMIT`) |
| Min `limit` | `1` | `1` |
| Non-numeric / bool `limit` | `Number(...) \|\| 5` then clamp | warn + default `5` (bool rejected) |

Missing required `query`:

- OpenClaw: empty string → empty result set (`strategy: "none"`).
- Hermes: explicit `{"error":"Missing required parameter: query"}`.

## Failure modes

| Condition | Behavior |
|-----------|----------|
| Both recall and capture disabled (OpenClaw) | Tools not registered |
| Vector store missing | Empty results, `strategy: "none"` |
| No embedding and no FTS | Empty + configuration `message` |
| Embedding or FTS throws mid-search | That path empty; other path may still win |
| Hermes Gateway down / not connected | JSON error + hint; recovery may run in background |
| Hermes circuit breaker open | `{"error":"… circuit breaker open."}` |
| OpenClaw execute exception | Soft fail text to the model; metrics report `success: false` |

## When to use which tool

| Need | Tool |
|------|------|
| Preferences, rules, structured facts (L1) | Memory search |
| Exact past wording, raw turns, timeline (L0) | Conversation search |
| Full scene dossier after navigation paths appear | Host `read_file` on scene paths (OpenClaw guide), not these search tools |

Auto-recall already injects a short L1 slice plus persona/scene navigation when enabled; tools are for **on-demand drill-down** when that slice is not enough. Truncated recall lines point agents at `tdai_memory_search` / `tdai_conversation_search`.

## Related pages

<CardGroup>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Full route list including `/search/memories` and `/search/conversations` request and error envelopes.
  </Card>
  <Card title="Memory layers" href="/memory-layers">
    L0 conversation vs L1 atom layers that these tools query.
  </Card>
  <Card title="Storage backends" href="/storage-backends">
    FTS5, embeddings, BM25, and hybrid RRF used by the search engines.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    Embedding provider setup and keyword-only degradation when provider is `none`.
  </Card>
  <Card title="TdaiCore and host adapters" href="/tdai-core-adapters">
    How OpenClaw tools and Gateway handlers share `TdaiCore.searchMemories` / `searchConversations`.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Install `memory_tencentdb` and verify Gateway tool routing.
  </Card>
</CardGroup>

---

## 17. Gateway HTTP API

> TdaiGateway routes: GET /health, POST /recall, /capture, /search/memories, /search/conversations, /session/end, /seed — request and response fields, auth exceptions, and error envelope.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/17-gateway-http-api.md
- Generated: 2026-08-04T18:34:02.456Z

### Source Files

- `src/gateway/server.ts`
- `src/gateway/types.ts`
- `src/gateway/config.ts`
- `src/core/tdai-core.ts`
- `hermes-plugin/memory/memory_tencentdb/client.py`

---
title: "Gateway HTTP API"
description: "TdaiGateway routes: GET /health, POST /recall, /capture, /search/memories, /search/conversations, /session/end, /seed — request and response fields, auth exceptions, and error envelope."
---

`TdaiGateway` (`src/gateway/server.ts`) is a Node.js native `http` server that exposes host-neutral `TdaiCore` capabilities over HTTP for Hermes and other sidecar clients. It does not use Express or Fastify. Default bind is `127.0.0.1:8420`. All bodies are JSON; all successful responses are JSON with `Content-Type: application/json`.

## Endpoint inventory

| Method | Path | Auth | Core call | Purpose |
|--------|------|------|-----------|---------|
| `GET` | `/health` | Never required | Store readiness | Liveness / readiness probe |
| `POST` | `/recall` | Bearer when configured | `handleBeforeRecall` | Prefetch memory context for a turn |
| `POST` | `/capture` | Bearer when configured | `handleTurnCommitted` | Record a user/assistant turn (L0 + pipeline notify) |
| `POST` | `/search/memories` | Bearer when configured | `searchMemories` | L1 structured memory search |
| `POST` | `/search/conversations` | Bearer when configured | `searchConversations` | L0 conversation search |
| `POST` | `/session/end` | Bearer when configured | `handleSessionEnd` | Flush session pipeline buffers |
| `POST` | `/seed` | Bearer when configured | `executeSeed` | Batch import historical conversations |
| `OPTIONS` | `*` | Not gated | — | CORS preflight; always `204` |

Unknown method/path combinations return **404** with the error envelope. Uncaught handler errors return **500**.

## Base URL, defaults, and clients

| Setting | Default | Env / config |
|---------|---------|----------------|
| Host | `127.0.0.1` | `TDAI_GATEWAY_HOST` / `server.host` |
| Port | `8420` | `TDAI_GATEWAY_PORT` / `server.port` |
| Data dir | `~/.memory-tencentdb/memory-tdai` (legacy `~/memory-tdai` if still present) | `TDAI_DATA_DIR` / `data.baseDir` / `MEMORY_TENCENTDB_ROOT` |
| API key | unset (auth off) | `TDAI_GATEWAY_API_KEY` / `server.apiKey` |
| CORS | no headers | `TDAI_CORS_ORIGINS` / `server.corsOrigins` |

Hermes uses `MemoryTencentdbSdkClient` (`hermes-plugin/memory/memory_tencentdb/client.py`) with default `base_url=http://127.0.0.1:8420` and optional Bearer from `MEMORY_TENCENTDB_GATEWAY_API_KEY` (fallback `TDAI_GATEWAY_API_KEY`). Client and Gateway secrets are configured independently.

```text
Hermes / HTTP client
        │  JSON + optional Authorization: Bearer
        ▼
  TdaiGateway (Node http)
        │  StandaloneHostAdapter + TdaiCore
        ▼
  L0/L1/L2/L3 store + pipeline
```

## Authentication

Auth is **opt-in**. When `server.apiKey` / `TDAI_GATEWAY_API_KEY` is unset, every route is open (legacy default). Startup logs a WARN if auth is disabled, and a louder WARN if the bind host is non-loopback without a key.

When a key is set:

- Every route **except** `GET /health` and `OPTIONS` requires `Authorization: Bearer <apiKey>`.
- Comparison uses constant-time equality (`crypto.timingSafeEqual`) after length check.
- Missing/malformed header → **401** `{ "error": "Unauthorized: missing Bearer token" }`.
- Wrong token → **401** `{ "error": "Unauthorized: invalid token" }`.

```http
Authorization: Bearer your-shared-secret
Content-Type: application/json
```

## CORS

| `corsOrigins` | Behavior |
|---------------|----------|
| `[]` (default) | No `Access-Control-*` headers; browsers block cross-origin |
| `["*"]` | Permissive `Access-Control-Allow-Origin: *` (dev only; startup WARN) |
| Explicit list | Echo request `Origin` only if listed; set `Vary: Origin` |

Allowed methods when CORS headers are emitted: `GET, POST, OPTIONS`. Allowed headers: `Content-Type, Authorization`.

## Error envelope

Standard error body:

```json
{
  "error": "human-readable message",
  "code": "optional-string"
}
```

Handlers typically set only `error`. Status codes:

| Status | When |
|--------|------|
| 400 | Missing required fields; invalid JSON body; seed validation failure |
| 401 | Auth enabled and Bearer missing/invalid |
| 404 | Unknown route |
| 500 | Unhandled exception (message from `Error.message`) |

`POST /seed` validation failures may include an extra field (not in the common type):

```json
{
  "error": "Seed validation failed",
  "validation_errors": [ /* stage/path details from SeedValidationError */ ]
}
```

Invalid JSON → **500** path via parse rejection message `"Invalid JSON body"` (thrown from the body parser and caught by the request router).

---

## GET /health

Always reachable without auth. Used by orchestrators (Docker/k8s health, `memory-tencentdb-ctl health`, Hermes startup).

### Response

| Field | Type | Meaning |
|-------|------|---------|
| `status` | `"ok" \| "degraded"` | `"ok"` if vector store is present; else `"degraded"` |
| `version` | string | Gateway version string (currently `"0.1.0"` in server) |
| `uptime` | number | Seconds since listen |
| `stores.vectorStore` | boolean | Vector/memory store initialized |
| `stores.embeddingService` | boolean | Embedding service initialized |

<RequestExample>
```bash
curl -sS http://127.0.0.1:8420/health
```
</RequestExample>

<ResponseExample>
```json
{
  "status": "ok",
  "version": "0.1.0",
  "uptime": 120,
  "stores": {
    "vectorStore": true,
    "embeddingService": true
  }
}
```
</ResponseExample>

---

## POST /recall

Prefetch memory context for the upcoming agent turn. Maps to `TdaiCore.handleBeforeRecall(query, session_key)`.

### Request body

<ParamField body="query" type="string" required>
User / turn text used for retrieval.
</ParamField>

<ParamField body="session_key" type="string" required>
Session identifier for scoped recall.
</ParamField>

<ParamField body="user_id" type="string">
Accepted by the request type and Hermes client; the current Gateway handler does not pass it into `TdaiCore`.
</ParamField>

### Response body

<ResponseField name="context" type="string">
System context string to append (`appendSystemContext`); empty string if none.
</ResponseField>

<ResponseField name="strategy" type="string">
Optional recall strategy label from core.
</ResponseField>

<ResponseField name="memory_count" type="number">
Count of recalled L1 memories (`recalledL1Memories.length`).
</ResponseField>

### Errors

- **400** — missing `query` or `session_key`

<RequestExample>
```bash
curl -sS -X POST http://127.0.0.1:8420/recall \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_KEY' \
  -d '{"query":"What did we decide about retention?","session_key":"agent:main:main"}'
```
</RequestExample>

<ResponseExample>
```json
{
  "context": "## Recalled memories\n...",
  "strategy": "hybrid",
  "memory_count": 3
}
```
</ResponseExample>

---

## POST /capture

Record one completed turn (sync path). Maps to `TdaiCore.handleTurnCommitted`. Starts the pipeline scheduler on first capture if needed.

### Request body

<ParamField body="user_content" type="string" required>
User message text.
</ParamField>

<ParamField body="assistant_content" type="string" required>
Assistant message text.
</ParamField>

<ParamField body="session_key" type="string" required>
Session key for L0 storage and pipeline state.
</ParamField>

<ParamField body="session_id" type="string">
Optional session id forwarded to capture.
</ParamField>

<ParamField body="user_id" type="string">
Accepted by the type/Hermes client; not consumed by the current handler.
</ParamField>

<ParamField body="messages" type="array">
Optional full message list. Default when omitted: `[{role:"user",content:user_content},{role:"assistant",content:assistant_content}]`.
</ParamField>

### Response body

<ResponseField name="l0_recorded" type="number">
Number of L0 rows recorded for this turn.
</ResponseField>

<ResponseField name="scheduler_notified" type="boolean">
Whether the extraction pipeline was notified.
</ResponseField>

### Errors

- **400** — missing `user_content`, `assistant_content`, or `session_key`

---

## POST /search/memories

L1 structured memory search. Maps to `TdaiCore.searchMemories`. Default `limit` inside core is **5** when omitted. Strategy is typically `hybrid`, `embedding`, `fts`, or `none` depending on store/embedding availability.

### Request body

<ParamField body="query" type="string" required>
Search query.
</ParamField>

<ParamField body="limit" type="number">
Max results (core default 5).
</ParamField>

<ParamField body="type" type="string">
Optional L1 type filter.
</ParamField>

<ParamField body="scene" type="string">
Optional scene filter.
</ParamField>

### Response body

| Field | Type | Meaning |
|-------|------|---------|
| `results` | string | Formatted search text (`formatSearchResponse`) |
| `total` | number | Hit count |
| `strategy` | string | Effective retrieval strategy |

### Errors

- **400** — missing `query`

---

## POST /search/conversations

L0 raw conversation search. Maps to `TdaiCore.searchConversations`. Default `limit` is **5** when omitted.

### Request body

<ParamField body="query" type="string" required>
Search query.
</ParamField>

<ParamField body="limit" type="number">
Max results (core default 5).
</ParamField>

<ParamField body="session_key" type="string">
Optional session scope.
</ParamField>

### Response body

| Field | Type | Meaning |
|-------|------|---------|
| `results` | string | Formatted conversation hits |
| `total` | number | Hit count |

### Errors

- **400** — missing `query`

---

## POST /session/end

Flush buffered pipeline work for a session. Maps to `TdaiCore.handleSessionEnd` → `scheduler.flushSession`. Unknown session keys are tolerated (no-op).

### Request body

<ParamField body="session_key" type="string" required>
Session to flush.
</ParamField>

<ParamField body="user_id" type="string">
Accepted by type/Hermes client; not consumed by the current handler.
</ParamField>

### Response body

```json
{ "flushed": true }
```

`flushed` is always `true` on the success path after `handleSessionEnd` returns (even if the session was unknown or the scheduler was absent).

### Errors

- **400** — missing `session_key`

---

## POST /seed

Batch-import historical conversations through the same validation and pipeline as the CLI seed command. **Blocking** — large payloads can take minutes. Hermes client default timeout for this call is **300s**.

### Request body

<ParamField body="data" type="unknown" required>
Seed payload: Format A or Format B (see below).
</ParamField>

<ParamField body="session_key" type="string">
Fallback session key when an input session omits one.
</ParamField>

<ParamField body="strict_round_role" type="boolean">
When true, each round must include both user and assistant roles.
</ParamField>

<ParamField body="auto_fill_timestamps" type="boolean">
Auto-fill missing timestamps. Default **true**.
</ParamField>

<ParamField body="config_override" type="object">
Deep-merged plugin config overrides on top of gateway `memory` config (plus injected `llm` from gateway LLM settings).
</ParamField>

### Seed input formats (`data`)

**Format A** — object wrapper:

```json
{
  "sessions": [
    {
      "sessionKey": "import:history-1",
      "sessionId": "optional",
      "conversations": [
        [
          { "role": "user", "content": "Hello", "timestamp": 1710000000000 },
          { "role": "assistant", "content": "Hi" }
        ]
      ]
    }
  ]
}
```

**Format B** — top-level array of the same session objects:

```json
[
  {
    "sessionKey": "import:history-1",
    "conversations": [
      [
        { "role": "user", "content": "Hello" },
        { "role": "assistant", "content": "Hi" }
      ]
    ]
  }
]
```

Message `timestamp` may be epoch ms or ISO-8601 string. Output is written under `{data.baseDir}/seed-YYYYMMDD-HHMMSS`.

### Response body

| Field | Type | Meaning |
|-------|------|---------|
| `sessions_processed` | number | Sessions in the run |
| `rounds_processed` | number | Conversation rounds processed |
| `messages_processed` | number | Messages processed |
| `l0_recorded` | number | L0 rows written |
| `duration_ms` | number | Wall time |
| `output_dir` | string | Seed output directory |

### Errors

- **400** — missing `data`
- **400** — validation failure with `error` + `validation_errors`

<RequestExample>
```bash
curl -sS -X POST http://127.0.0.1:8420/seed \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_KEY' \
  -d '{
    "data": {
      "sessions": [{
        "sessionKey": "seed:demo",
        "conversations": [[
          {"role":"user","content":"Project uses SQLite by default"},
          {"role":"assistant","content":"Noted."}
        ]]
      }]
    },
    "auto_fill_timestamps": true
  }'
```
</RequestExample>

---

## Hermes client mapping

| Client method | HTTP | Default timeout |
|---------------|------|-----------------|
| `health()` | `GET /health` | 3s |
| `recall(...)` | `POST /recall` | 10s |
| `capture(...)` | `POST /capture` | 10s |
| `search_memories(...)` | `POST /search/memories` | 10s |
| `search_conversations(...)` | `POST /search/conversations` | 10s |
| `end_session(...)` | `POST /session/end` | 10s |
| `seed(...)` | `POST /seed` | 300s |

When `api_key` is set on the client, every request (including health) sends `Authorization: Bearer ...`. The Gateway still does not require auth for `/health` even if a key is configured server-side.

## Operational notes

- **Stack**: Node `http` only; no framework middleware chain.
- **Concurrency**: Multiple `/capture` calls can hit the gateway while the pipeline scheduler starts once under a shared promise gate inside `TdaiCore`.
- **Security posture**: Prefer loopback bind or set `TDAI_GATEWAY_API_KEY` before exposing the port. Align Hermes `MEMORY_TENCENTDB_GATEWAY_API_KEY` with the Gateway key.
- **Lifecycle**: Start/stop/status/logs via `memory-tencentdb-ctl` (see Gateway lifecycle docs). Config file resolution: `TDAI_GATEWAY_CONFIG` → `./tdai-gateway.yaml|json` → `<dataDir>/tdai-gateway.yaml|json` → env-only.
- **LLM on seed**: Seed injects gateway `llm` (`baseUrl`, `apiKey`, `model`, …) into the seed plugin config so extraction can run without OpenClaw.

## Quick verification

```bash
# Probe (no auth)
curl -sS http://127.0.0.1:8420/health

# Auth check (expect 401 if key configured and header wrong)
curl -sS -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8420/recall \
  -H 'Content-Type: application/json' \
  -d '{"query":"x","session_key":"s"}'
```

## Related pages

<CardGroup>
  <Card title="Secure the Gateway" href="/secure-gateway">
    Bearer auth, CORS allow-list, non-loopback warnings, Hermes key alignment.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Install memory_tencentdb, Gateway discovery, and health checks.
  </Card>
  <Card title="Seed historical conversations" href="/seed-history">
    Format A/B details, CLI flags, config overrides, L0→L1→L2→L3 path.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    memory-tencentdb-ctl start/stop/status/health/logs and path layout.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    TDAI_* and MEMORY_TENCENTDB_* keys, config resolution order.
  </Card>
  <Card title="TdaiCore and host adapters" href="/tdai-core-adapters">
    How HTTP handlers map to TdaiCore vs in-process OpenClaw hooks.
  </Card>
  <Card title="Agent tools" href="/agent-tools">
    OpenClaw and Hermes tool schemas for memory/conversation search.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth 401, circuit breaker, embedding failures, probe checklist.
  </Card>
</CardGroup>

---

## 18. CLI reference

> Package bin commands and OpenClaw memory-tdai namespace: migrate-sqlite-to-tcvdb, export-tencent-vdb, read-local-memory, seed flags, and memory-tencentdb-ctl subcommands.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/18-cli-reference.md
- Generated: 2026-08-04T18:34:23.924Z

### Source Files

- `package.json`
- `src/cli/README.md`
- `src/cli/commands/seed.ts`
- `scripts/migrate-sqlite-to-tcvdb/cli-entry.ts`
- `scripts/export-tencent-vdb/export-tencent-vdb.ts`
- `scripts/read-local-memory/read-local-memory.ts`
- `scripts/README.memory-tencentdb-ctl.md`
- `bin/migrate-sqlite-to-tcvdb.mjs`

---
title: "CLI reference"
description: "Package bin commands and OpenClaw memory-tdai namespace: migrate-sqlite-to-tcvdb, export-tencent-vdb, read-local-memory, seed flags, and memory-tencentdb-ctl subcommands."
---

`@tencentdb-agent-memory/memory-tencentdb` ships three npm `bin` entry points, one OpenClaw CLI namespace (`openclaw memory-tdai`), and a non-bin ops shell script (`scripts/memory-tencentdb-ctl.sh`). Bins load prebuilt JS under `scripts/*/dist/`; the OpenClaw namespace is registered from the plugin via `api.registerCli()` and currently exposes only `seed`.

## Command map

| Surface | How to invoke | Role |
|---|---|---|
| `migrate-sqlite-to-tcvdb` | `npx migrate-sqlite-to-tcvdb …` or `npm run migrate-sqlite-to-tcvdb -- …` | Offline SQLite → Tencent VectorDB (TCVDB) migration |
| `export-tencent-vdb` | `npx export-tencent-vdb …` or `npm run export-tencent-vdb -- …` | Export TCVDB collections to JSONL |
| `read-local-memory` | `npx read-local-memory …` or `npm run read-local-memory -- …` | Inspect local L0–L3 data under a memory data dir |
| `openclaw memory-tdai seed` | OpenClaw CLI after plugin install | Seed historical conversation JSON through L0→L1→L2→L3 |
| `memory-tencentdb-ctl` | `scripts/memory-tencentdb-ctl.sh` (optional PATH symlink) | Gateway lifecycle + `tdai-gateway.json` config |

```text
Package bins (package.json "bin")
  bin/migrate-sqlite-to-tcvdb.mjs  → scripts/migrate-sqlite-to-tcvdb/dist/.../cli-entry.js
  bin/export-tencent-vdb.mjs       → scripts/export-tencent-vdb/dist/export-tencent-vdb.js
  bin/read-local-memory.mjs        → scripts/read-local-memory/dist/read-local-memory.js

OpenClaw plugin CLI
  index.ts → api.registerCli("memory-tdai") → registerMemoryTdaiCli() → seed

Ops script (shipped, not a bin)
  scripts/memory-tencentdb-ctl.sh
```

<Note>
Bin launchers require a prior build (`npm run build` / per-script `build:*`). If the dist file is missing, `export-tencent-vdb` and `read-local-memory` exit with a clear “precompiled artifact missing” message.
</Note>

## Prerequisites

| Tool | Requirement |
|---|---|
| Node | `>=22.16.0` (package `engines`) |
| OpenClaw | Plugin installed for `openclaw memory-tdai`; peer `openclaw >=2026.3.7`, compat `>=2026.3.13` |
| `memory-tencentdb-ctl` | `bash`, `python3`, `node`, `npx`, `lsof` or `ss` |

Build scripts before first bin use from a source checkout:

```bash
npm run build:scripts
# or individually:
npm run build:migrate-sqlite-to-vdb
npm run build:export-tencent-vdb
npm run build:read-local-memory
```

---

## `openclaw memory-tdai seed`

Registered under the `memory-tdai` Commander namespace (`commandAliases: ["memory-tdai"]` in `openclaw.plugin.json`). Description in code: seed historical conversation data into the memory pipeline (L0 → L1); the CLI README documents full L0→L1→L2→L3 execution via the seed runtime.

```bash
openclaw memory-tdai seed --input <file> [options]
```

### Flags

<ParamField body="--input" type="string" required>
Path to input JSON file (Format A object wrapper or Format B top-level array).
</ParamField>

<ParamField body="--output-dir" type="string">
Output directory for pipeline data. Default: `<stateDir>/memory-tdai-seed-<YYYYMMDD-HHmmss>` where `stateDir` is the OpenClaw state root (for example `~/.openclaw`).
</ParamField>

<ParamField body="--session-key" type="string">
Fallback session key when the input lacks one.
</ParamField>

<ParamField body="--config" type="string">
JSON config override file, two-level deep-merged on top of the current plugin config from `openclaw.json`.
</ParamField>

<ParamField body="--strict-round-role" type="boolean">
Require each conversation round to include both `user` and `assistant` messages. Default: `false`.
</ParamField>

<ParamField body="--yes" type="boolean">
Skip interactive confirmations (for example timestamp auto-fill). Default: `false`.
</ParamField>

### Behavior notes

- Missing timestamps: without `--yes`, prompts to fill with current time; with `--yes`, auto-fills.
- Existing non-empty `--output-dir`: exits with error. Checkpoint resume is **not** implemented; a directory with `.metadata/checkpoint.json` fails with a “use a new output directory” message.
- Progress is printed per round; a seed summary box reports sessions, rounds, messages, L0 count, and duration.

### Input shapes (summary)

| Format | Shape |
|---|---|
| A | `{ "sessions": [ { "sessionKey", "sessionId?", "conversations": message[][] } ] }` |
| B | Top-level array of the same session objects |

Message fields: `role` (`user` \| `assistant`), `content`, optional `timestamp` (epoch ms or ISO string).

### Config override merge

`--config` merges plain objects one level deep (sibling keys under each top-level group are shallow-merged; other values replace). Useful for aggressive pipeline timing or a dedicated TCVDB database during seed.

### Output layout

```text
<output-dir>/
├── conversations/     # L0 JSONL
├── records/           # L1 JSONL
├── scene_blocks/      # L2
├── vectors.db         # SQLite backend only
├── .metadata/
│   ├── manifest.json
│   └── checkpoint.json
└── .backup/
```

<RequestExample>
```bash title="Seed examples"
openclaw memory-tdai seed --input conversations.json
openclaw memory-tdai seed --input data.json --output-dir ./seed-output --strict-round-role
openclaw memory-tdai seed --input data.json --config ./seed-config.json --yes
```
</RequestExample>

For full formats and pipeline behavior, see [Seed historical conversations](/seed-history).

---

## `migrate-sqlite-to-tcvdb`

Offline migration of local SQLite (`vectors.db`) L0/L1/profile data into Tencent VectorDB, with optional rewrite of `openclaw.json` plugin config and data-dir `manifest.json`.

```bash
migrate-sqlite-to-tcvdb [options]
# or
npm run migrate-sqlite-to-tcvdb -- [options]
```

Stdout prints the migration summary as pretty-printed JSON. Logs go to stderr with tag `[memory-tdai][migrate]`.

### Required options

| Flag | Description |
|---|---|
| `--plugin-data-dir <path>` | Plugin data directory (for example `~/.openclaw/memory-tdai`) |
| `--openclaw-config-path <path>` | Path to `openclaw.json` |
| `--tcvdb-url <url>` | TCVDB HTTP base URL |
| `--tcvdb-username <name>` | TCVDB username |
| `--tcvdb-database <name>` | Target database name |
| `--tcvdb-embedding-model <name>` | Server-side embedding model |
| `--tcvdb-api-key <key>` **or** `--tcvdb-api-key-env <var>` | API key (mutually exclusive) |

### Optional options

| Flag | Default | Description |
|---|---|---|
| `--sqlite-path <path>` | `<plugin-data-dir>/vectors.db` | Source SQLite path |
| `--plugin-id <id>` | `memory-tencentdb` | Plugin ID written into config |
| `--layers <list>` | `l0,l1,l2,l3` | Comma-separated layers |
| `--tcvdb-alias <name>` | `""` | Optional alias |
| `--tcvdb-timeout-ms <ms>` | `10000` | Request timeout |
| `--tcvdb-ca-pem <path>` | — | CA PEM for HTTPS |
| `--bm25-language <zh\|en>` | `zh` | BM25 tokenization language |
| `--summary-json-path <path>` | — | Write summary JSON to file |
| `--job-id <id>` | — | Job id for tracking |

### Boolean flags (`node:util` allowNegative)

Defaults are all **true** for safety options unless noted:

| Flag | Default | Description |
|---|---|---|
| `--dry-run` | `false` | Preflight only; no writes |
| `--yes` | `false` | Skip interactive confirmation |
| `--no-apply-config` | apply = true | Do not update `openclaw.json` |
| `--no-config-backup` | backup = true | Skip config backup before write |
| `--no-rewrite-manifest` | rewrite = true | Do not update `manifest.json` |
| `--no-fail-if-target-nonempty` | fail if nonempty = true | Allow non-empty target |
| `--no-verify-counts` | verify = true | Skip post-migration count checks |
| `--no-bm25-enabled` | bm25 = true | Disable BM25 sparse vectors |

### Layer semantics

| Layer token | Migrated content |
|---|---|
| `l0` | L0 conversation rows from SQLite |
| `l1` | L1 memory records from SQLite |
| `l2` or `l3` | Local profiles via `listLocalProfiles` / `syncProfiles` (either token enables profile migration) |

Empty source (missing data dir or `vectors.db`, or zero counts) skips data migration and still can apply config/manifest depending on flags.

### Example

```bash
export TCVDB_API_KEY='...'

migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --dry-run

migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --layers l0,l1 \
  --yes
```

Workflow depth: [Migrate SQLite to TCVDB](/migrate-to-tcvdb).

---

## `export-tencent-vdb`

Connects to a Tencent VectorDB instance over HTTP and exports collection documents. Connection parameters are CLI-only (no `.env`).

```bash
export-tencent-vdb \
  --url <addr> --username <user> --api-key <key> --database <name> [options]
```

### Connection (required)

| Flag | Description |
|---|---|
| `--url` | VDB HTTP address (for example `http://host:8100`) |
| `--username` | Auth username (often `root`) |
| `--api-key` | Auth key |
| `--database` | Database name |

Auth header form: `Bearer account=<username>&api_key=<apiKey>`.

### Options

| Flag | Default | Description |
|---|---|---|
| `--timeout <ms>` | `30000` | Per-request timeout |
| `-o, --output <dir>` | `./vdb-export-YYYY-MM-DD` | Output directory |
| `-c, --collection <name>` | all | Exact collection name filter |
| `-f, --filter <expr>` | — | VDB filter expression |
| `-l, --limit <n>` | all | Max documents (`>= 1`) |
| `--offset <n>` | `0` | Start offset (`>= 0`) |
| `--include-vectors` | off | Include dense `vector` fields (default strips them) |
| `--probe` | off | Connectivity check + list collections, no export |
| `-h, --help` | — | Help |

`sparse_vector` is always exported. Page size is 100 documents.

### Output layout

```text
<outputDir>/
├── <collection>.jsonl
├── schemas.json
└── export-meta.json
```

```bash
export-tencent-vdb \
  --url "http://gz-vdb-xxx:8100" --username root --api-key "xxx" --database mydb \
  --probe

export-tencent-vdb \
  --url "http://gz-vdb-xxx:8100" --username root --api-key "xxx" --database mydb \
  -c mydb_l0_conversations -o /tmp/backup
```

---

## `read-local-memory`

Read-only inspection of a local memory data directory. L0/L1 come from `vectors.db` (`node:sqlite`, `PRAGMA query_only = ON`). L2 is `scene_blocks/*.md`; L3 is `persona.md`.

```bash
read-local-memory -d <data-dir> [options]
```

### Options

| Flag | Default | Description |
|---|---|---|
| `-d, --data-dir <path>` | required | Memory data dir (must resolve; overview needs `vectors.db`) |
| `-L, --level <L0\|L1\|L2\|L3>` | all (overview) | Single layer query |
| `--since <time>` | — | ISO or relative (`7d`, `24h`, `30m`, `60s`) |
| `--until <time>` | — | Same formats as `--since` |
| `-l, --limit <n>` | `50` | Page size (`>= 1`) |
| `--offset <n>` | `0` | Offset (`>= 0`) |
| `--sort <asc\|desc>` | `desc` | Sort direction |
| `-f, --filter <expr>` | — | Column filters, comma-separated (`type=persona`, `priority>=80`) |
| `--format <table\|json\|jsonl>` | `table` | Output format |
| `--file <name>` | — | L2 single-file detail (full body) |
| `-h, --help` | — | Help |

### Filter columns

| Level | Allowed columns (snake or camel aliases) |
|---|---|
| L0 | `record_id`, `session_key`, `session_id`, `role`, `message_text`, `recorded_at`, `timestamp` |
| L1 | `record_id`, `content`, `type`, `priority`, `scene_name`, `session_key`, `session_id`, `timestamp_str`, `timestamp_start`, `timestamp_end`, `created_time`, `updated_time`, `metadata_json` |

Time filters: L0 uses integer epoch `timestamp`; L1 uses ISO `updated_time`.

### Missing data behavior

| Situation | Behavior |
|---|---|
| No `vectors.db`, level L0/L1 | Empty result (JSON/table), exit 0 |
| No `vectors.db`, overview | Error exit |
| Missing `scene_blocks/` or `persona.md` | Empty / “not generated yet”, exit 0 |

```bash
read-local-memory -d ~/.openclaw/memory-tdai
read-local-memory -d ~/.openclaw/memory-tdai -L L0 --since 7d
read-local-memory -d ~/.openclaw/memory-tdai -L L1 -f 'type=persona' --format json
read-local-memory -d ~/.openclaw/memory-tdai -L L2 --file my-scene.md
```

Deeper layout notes: [Inspect local memory](/inspect-local-memory).

---

## `memory-tencentdb-ctl`

Bash ops controller for the standalone/Hermes Node Gateway. Published under `scripts/` but **not** registered as an npm `bin` (explicit PATH install is intentional).

```bash
# One-shot from install
"$(npm root)/@tencentdb-agent-memory/memory-tencentdb/scripts/memory-tencentdb-ctl.sh" --help

# Optional symlink
sudo ln -sf "$SCRIPT" /usr/local/bin/memory-tencentdb-ctl
```

### Modes

| Mode | Activation | Behavior |
|---|---|---|
| `standalone` (default) | no flag | Start/stop Gateway; write `$TDAI_DATA_DIR/tdai-gateway.json`; logs under `$TDAI_DATA_DIR/logs/` |
| `hermes` | `--hermes` or `MEMORY_TENCENTDB_MODE=hermes` | Same + LLM env under `$HERMES_HOME/env.d/`; logs under `$HERMES_HOME/logs/memory_tencentdb/`; enables `enable-hermes-memory` |

Default paths (overridable):

| Variable | Default |
|---|---|
| `MEMORY_TENCENTDB_ROOT` | `~/.memory-tencentdb` |
| `TDAI_INSTALL_DIR` | `$MEMORY_TENCENTDB_ROOT/tdai-memory-openclaw-plugin` |
| `TDAI_DATA_DIR` | `$MEMORY_TENCENTDB_ROOT/memory-tdai` |
| Gateway listen | `127.0.0.1:8420` (`MEMORY_TENCENTDB_GATEWAY_HOST` / `_PORT`) |

### Subcommands

| Command | Description |
|---|---|
| `start` | Background spawn; no-op if port already in use; waits for `/health` |
| `stop` | SIGTERM then SIGKILL after 5s |
| `restart` | stop + start |
| `status` | Mode, port, data/log paths, process state |
| `health` | GET `/health` via `python3` (no curl required) |
| `logs [out\|err\|all] [N=200]` | Tail logs |
| `config llm …` | Write `$.llm` (and hermes env file when `--hermes`) |
| `config embedding …` | Write `$.memory.embedding` |
| `config vdb …` | Write `$.memory.tcvdb` and usually `storeBackend=tcvdb` |
| `config vdb-off …` | Set `storeBackend=sqlite`; optional `--purge-creds` |
| `config show` | Print config with secrets redacted |
| `enable-hermes-memory` | Set Hermes `memory.provider` to `memory_tencentdb` (**hermes mode only**) |

### Global options

| Flag | Description |
|---|---|
| `--hermes` / `--standalone` | Select mode |
| `--dry-run` | Print planned writes; do not persist |
| `-h, --help` | Usage |

### Config write examples

```bash
memory-tencentdb-ctl config llm \
  --api-key "sk-..." --base-url "https://api.openai.com/v1" --model "gpt-4o" --restart

memory-tencentdb-ctl config embedding \
  --provider openai --api-key "sk-..." --base-url "https://api.openai.com/v1" \
  --model "text-embedding-3-small" --dimensions 1536 --restart

memory-tencentdb-ctl config embedding --provider none --restart

memory-tencentdb-ctl config vdb \
  --url "http://xxx:8100" --username root --api-key "..." --database "openclaw_memory" \
  --alias "primary" --embedding-model "bge-large-zh" --ca-pem "/etc/ssl/vdb-ca.pem" --restart

memory-tencentdb-ctl config vdb --url "..." --api-key "..." --database "..." --no-set-backend
memory-tencentdb-ctl config vdb-off --restart
memory-tencentdb-ctl config vdb-off --purge-creds --restart
memory-tencentdb-ctl config show
```

`config llm` JSON path: `$.llm.{baseUrl, apiKey, model}`.  
`config embedding` path: `$.memory.embedding.{provider, baseUrl, apiKey, model, dimensions, enabled, proxyUrl?}`.  
`config vdb` path: `$.memory.tcvdb.{url, username, apiKey, database, alias?, caPemPath?, embeddingModel?}` and typically `$.memory.storeBackend = "tcvdb"`.

### Exit codes

| Code | Meaning |
|---|---|
| `0` | Success |
| `1` | Bad args / validation (for example hermes-only command in standalone) |
| `2` | Write failure (disk/permissions) |
| `127` | Missing dependency (`python3` / `node` / `npx`) |

Ops guide: [Gateway lifecycle](/gateway-ops).

---

## Related npm scripts

| `package.json` script | Action |
|---|---|
| `build:scripts` | Compile migrate + export + read-local-memory |
| `migrate-sqlite-to-tcvdb` | `node ./bin/migrate-sqlite-to-tcvdb.mjs` |
| `export-tencent-vdb` | `node ./bin/export-tencent-vdb.mjs` |
| `read-local-memory` | `node ./bin/read-local-memory.mjs` |

Pass flags after `--` when using `npm run`:

```bash
npm run read-local-memory -- -d ~/.openclaw/memory-tdai -L L1 --format json
```

---

## Failure signals

| Symptom | Likely cause |
|---|---|
| Bin: “precompiled artifact missing” | Run the matching `npm run build:*` script |
| Seed: “Resume from checkpoint is not implemented” | Pick a fresh `--output-dir` |
| Seed: output dir not empty | Clean or choose another directory |
| Migrate: missing required option / both API key flags | Supply required flags; use only one of `--tcvdb-api-key` / `--tcvdb-api-key-env` |
| Export: list collection HTTP/code error | Wrong URL/credentials/database |
| `read-local-memory` overview exits 1 | `vectors.db` absent under `--data-dir` |
| `ctl enable-hermes-memory` fails | Not in hermes mode |
| `ctl` exit 127 | Install `node`/`npx`/`python3` |

---

## Related pages

<CardGroup>
  <Card title="Seed historical conversations" href="/seed-history">
    Input formats A/B, config overrides, and L0→L1→L2→L3 seed path.
  </Card>
  <Card title="Migrate SQLite to TCVDB" href="/migrate-to-tcvdb">
    Dry-run, layer selection, config rewrite, and verification.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    standalone vs hermes modes, start/stop, and path layout.
  </Card>
  <Card title="Inspect local memory" href="/inspect-local-memory">
    On-disk layout and diagnostic export workflows.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    `TDAI_*`, `MEMORY_TENCENTDB_*`, and gateway config resolution.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    `/health`, `/seed`, and other Gateway routes used by ctl health checks.
  </Card>
</CardGroup>

---

## 19. Environment variables

> Gateway, LLM, Hermes supervisor, install root, and data-dir env keys: TDAI_*, MEMORY_TENCENTDB_*, MODEL_*, resolution order for tdai-gateway.json, and client vs server API-key names.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/19-environment-variables.md
- Generated: 2026-08-04T18:35:09.697Z

### Source Files

- `src/gateway/config.ts`
- `src/utils/env.ts`
- `README.md`
- `hermes-plugin/memory/memory_tencentdb/README.md`
- `hermes-plugin/memory/memory_tencentdb/supervisor.py`
- `scripts/README.memory-tencentdb-ctl.md`
- `docker/opensource/README-hermes.md`

---
title: "Environment variables"
description: "Gateway, LLM, Hermes supervisor, install root, and data-dir env keys: TDAI_*, MEMORY_TENCENTDB_*, MODEL_*, resolution order for tdai-gateway.json, and client vs server API-key names."
---

Standalone Gateway and Hermes integrations read environment variables in distinct namespaces. The Node Gateway binds and authenticates with `TDAI_*`; the Hermes Python provider dials and authenticates with `MEMORY_TENCENTDB_*`; Docker greenfield images unify model credentials under `MODEL_*` and export them into `TDAI_LLM_*` at container start. Env access in the compiled OpenClaw bundle goes through an indirect `getEnv` helper so the host security scanner does not treat direct `process.env` + network code as credential harvesting.

## Namespaces at a glance

| Prefix | Consumer | Role |
| --- | --- | --- |
| `TDAI_*` | Node Gateway (`loadGatewayConfig`) | Bind host/port, server Bearer secret, CORS, data dir, LLM, config file path |
| `MEMORY_TENCENTDB_*` | Hermes provider, supervisor, `memory-tencentdb-ctl` | Client dial host/port, client Bearer secret, spawn command, logs, install root, ctl mode |
| `MODEL_*` | `Dockerfile.hermes` CMD only | Single operator-facing model surface; mapped to Hermes config + `TDAI_LLM_*` |
| `HERMES_HOME` | Hermes / ctl / install scripts | Hermes home (default `~/.hermes`) |
| `OPENCLAW_*` | OpenClaw plugin paths | State dir / config path when the host does not inject `resolveStateDir()` |

<Warning>
The Gateway never reads `MEMORY_TENCENTDB_GATEWAY_API_KEY` or `MEMORY_TENCENTDB_LLM_*`. The Hermes client never enforces Gateway auth by itself. Matching secrets on both ends is an operator responsibility.
</Warning>

## Config file resolution (Gateway)

`loadGatewayConfig()` builds a full `GatewayConfig` from an optional file plus env overrides.

### File path order

1. `TDAI_GATEWAY_CONFIG` — absolute or relative path; used only if the file exists
2. `./tdai-gateway.yaml` or `./tdai-gateway.json` in the process CWD
3. `<defaultDataDir>/tdai-gateway.yaml` or `<defaultDataDir>/tdai-gateway.json`
4. No file — pure env + defaults

`<defaultDataDir>` is the same path as the data-dir fallback (see [Install root and data directories](#install-root-and-data-directories)).

`memory-tencentdb-ctl config *` writes `$TDAI_DATA_DIR/tdai-gateway.json` (mode `0600`). That file is found via step 3 when CWD is not the data dir and `TDAI_GATEWAY_CONFIG` is unset.

### Field merge rules

| Concern | Precedence | Notes |
| --- | --- | --- |
| Most server / data / LLM scalars | **Env wins** over file | e.g. `TDAI_GATEWAY_PORT` over `server.port` |
| CORS allow-list | **File wins** over env | Explicit `server.corsOrigins: []` disables CORS even if `TDAI_CORS_ORIGINS` is set |
| Memory plugin block | File `memory` only | Parsed by `parseConfig()`; not mirrored field-by-field to env |
| Missing / malformed file | Silent fall-through | Treated as env-only config |

YAML/JSON string leaves that are exactly `${VAR_NAME}` expand to `process.env[VAR_NAME]` (missing → empty string). Only whole-string placeholders expand; partial interpolation is not supported. Leading `~/` in `data.baseDir` / `TDAI_DATA_DIR` expands with `HOME` or `USERPROFILE`.

```json title="Minimal ~/.memory-tencentdb/memory-tdai/tdai-gateway.json"
{
  "llm": {
    "baseUrl": "https://api.openai.com/v1",
    "apiKey": "sk-...",
    "model": "gpt-4o"
  }
}
```

## Gateway server (`TDAI_*`)

Read by `src/gateway/config.ts` when the Node sidecar starts.

### Server bind, auth, CORS

<ParamField body="TDAI_GATEWAY_PORT" type="integer">
Listen port. Default `8420`. Invalid non-integers are ignored (file/default used).
</ParamField>

<ParamField body="TDAI_GATEWAY_HOST" type="string">
Bind address. Default `127.0.0.1`. Docker image defaults to `0.0.0.0`.
</ParamField>

<ParamField body="TDAI_GATEWAY_API_KEY" type="string">
Optional Bearer secret. When non-empty, every route except `GET /health` and CORS `OPTIONS` requires `Authorization: Bearer <key>`. Maps to `server.apiKey`. Unset = open (legacy).
</ParamField>

<ParamField body="TDAI_CORS_ORIGINS" type="string">
Comma-separated CORS allow-list. Empty / unset → no `Access-Control-*` headers. Use `*` only for local development. File `server.corsOrigins` overrides this env.
</ParamField>

<ParamField body="TDAI_GATEWAY_CONFIG" type="string">
Explicit path to `tdai-gateway.yaml` or `.json`.
</ParamField>

### Data directory

<ParamField body="TDAI_DATA_DIR" type="string">
L0–L3 storage root for standalone/Hermes Gateway. Overrides `data.baseDir`. Supports `~/...`.
</ParamField>

Default when unset:

1. `$MEMORY_TENCENTDB_ROOT/memory-tdai` (root defaults to `~/.memory-tencentdb`)
2. Legacy `~/memory-tdai` if the new path does not exist but the legacy path does (stderr deprecation warning)

OpenClaw in-process mode does **not** use `TDAI_DATA_DIR`; plugin data lives under the OpenClaw state dir (typically `~/.openclaw/memory-tdai/`).

### LLM (extraction pipeline)

| Variable | File field | Default |
| --- | --- | --- |
| `TDAI_LLM_BASE_URL` | `llm.baseUrl` | `https://api.openai.com/v1` |
| `TDAI_LLM_API_KEY` | `llm.apiKey` | `""` |
| `TDAI_LLM_MODEL` | `llm.model` | `gpt-4o` |
| `TDAI_LLM_MAX_TOKENS` | `llm.maxTokens` | `4096` |
| `TDAI_LLM_TIMEOUT_MS` | `llm.timeoutMs` | `120000` |
| `TDAI_LLM_DISABLE_THINKING` | `llm.disableThinking` | unset |

`TDAI_LLM_DISABLE_THINKING` accepts booleans (`true`/`false`/`1`/`0`) or a strategy name (e.g. `deepseek`, `anthropic`); values are lowercased then normalized.

<Note>
BYOK/BYOC: any OpenAI-compatible base URL works. No provider is hard-coded beyond defaults.
</Note>

## Hermes client and supervisor (`MEMORY_TENCENTDB_*`)

### Dial and spawn

| Variable | Default | Used by |
| --- | --- | --- |
| `MEMORY_TENCENTDB_GATEWAY_HOST` | `127.0.0.1` | Provider client base URL; supervisor spawn label |
| `MEMORY_TENCENTDB_GATEWAY_PORT` | `8420` | Same; invalid ports fall back to `8420` with a warning |
| `MEMORY_TENCENTDB_GATEWAY_CMD` | _(unset)_ | Shell command the supervisor `Popen`s; if unset, auto-discover `src/gateway/server.ts` |
| `MEMORY_TENCENTDB_LOG_DIR` | see below | Supervisor stdout/stderr log directory |
| `MEMORY_TENCENTDB_GATEWAY_API_KEY` | _(unset)_ | **Client** Bearer header only |

Log directory priority in `GatewaySupervisor._resolve_log_dir()`:

1. `MEMORY_TENCENTDB_LOG_DIR`
2. `~/.hermes/logs/memory_tencentdb` (uses `HOME` / `USERPROFILE`)
3. `<cwd>/.memory-tencentdb-logs`

`memory-tencentdb-ctl` in standalone mode prefers `$TDAI_DATA_DIR/logs/` when `MEMORY_TENCENTDB_LOG_DIR` is unset.

### Client vs server API keys

```text
┌─────────────────────────────┐         HTTP Bearer          ┌──────────────────────────┐
│ Hermes memory_tencentdb     │ ───────────────────────────▶ │ Node Gateway             │
│ MEMORY_TENCENTDB_GATEWAY_   │   Authorization: Bearer …    │ TDAI_GATEWAY_API_KEY     │
│   API_KEY                   │                              │   or server.apiKey       │
│ fallback: TDAI_GATEWAY_     │                              │ (never reads MEMORY_*    │
│   API_KEY                   │                              │  GATEWAY_API_KEY)        │
└─────────────────────────────┘                              └──────────────────────────┘
```

- **Server enforcement:** `TDAI_GATEWAY_API_KEY` / `server.apiKey`. Unset → open routes.
- **Client attachment:** `MEMORY_TENCENTDB_GATEWAY_API_KEY`, then fallback `TDAI_GATEWAY_API_KEY`. Unset → no `Authorization` header.
- Supervisor **does not** inject `TDAI_GATEWAY_API_KEY` into the child env when spawning; configure the Gateway the same way you configure its port and data dir (env, `tdai-gateway.json`, Docker, systemd).

Shared single-name setup (both processes inherit one env file):

```bash
export TDAI_GATEWAY_API_KEY="shared-secret"
# Hermes client falls back to the same name; Gateway reads it for enforcement.
```

Split names (recommended when only Hermes should carry the client secret):

```bash
export TDAI_GATEWAY_API_KEY="shared-secret"                 # Gateway process
export MEMORY_TENCENTDB_GATEWAY_API_KEY="shared-secret"     # Hermes process
```

### LLM schema names vs Gateway names

Hermes `get_config_schema()` advertises:

- `MEMORY_TENCENTDB_LLM_API_KEY` (required in schema)
- `MEMORY_TENCENTDB_LLM_BASE_URL` (default `https://api.openai.com/v1`)
- `MEMORY_TENCENTDB_LLM_MODEL` (default `gpt-4o`)

The **Node Gateway only consumes `TDAI_LLM_*`** (or `llm.*` in the config file). `memory-tencentdb-ctl --hermes config llm` dual-writes:

- `$TDAI_DATA_DIR/tdai-gateway.json` → `$.llm.{baseUrl,apiKey,model}`
- `$HERMES_HOME/env.d/memory-tencentdb-llm.sh` → `TDAI_LLM_*` plus `MEMORY_TENCENTDB_LLM_*=$TDAI_LLM_*` aliases for Hermes inheritance

For a supervised Gateway, set `TDAI_LLM_*` (or write `tdai-gateway.json`) in the environment Hermes copies with `os.environ.copy()`. Setting only `MEMORY_TENCENTDB_LLM_*` without `TDAI_LLM_*` or a config file does not configure the Node sidecar.

### Gateway auto-discovery paths

When `MEMORY_TENCENTDB_GATEWAY_CMD` is unset, the provider searches (in order):

1. In-tree: `<plugin-root>/src/gateway/server.ts`
2. `~/.memory-tencentdb/tdai-memory-openclaw-plugin/src/gateway/server.ts`
3. `~/tdai-memory-openclaw-plugin/src/gateway/server.ts` (legacy)
4. `~/.hermes/plugins/tdai-memory-openclaw-plugin/src/gateway/server.ts` (legacy)

Discovered launch form: `sh -c 'cd <plugin-root> && exec pnpm exec tsx src/gateway/server.ts'`.

### Host / port alignment

The supervisor sets `MEMORY_TENCENTDB_GATEWAY_HOST` / `_PORT` on the child for bookkeeping, but the **bind** address is still `TDAI_GATEWAY_HOST` / `TDAI_GATEWAY_PORT` (or file defaults). Changing only the client port without the server port breaks `/health` and all API calls.

```bash
# Both ends on 18420
export TDAI_GATEWAY_PORT=18420
export MEMORY_TENCENTDB_GATEWAY_PORT=18420
```

## Install root and data directories

Used by install scripts, ctl, and Gateway default data-dir resolution.

| Variable | Default | Purpose |
| --- | --- | --- |
| `MEMORY_TENCENTDB_ROOT` | `~/.memory-tencentdb` | Unified root for install + data |
| `TDAI_INSTALL_DIR` | `$MEMORY_TENCENTDB_ROOT/tdai-memory-openclaw-plugin` | Gateway source + `node_modules` |
| `TDAI_DATA_DIR` | `$MEMORY_TENCENTDB_ROOT/memory-tdai` | Gateway data + `tdai-gateway.json` |
| `HERMES_HOME` | `~/.hermes` | Hermes config, `env.d/`, hermes-mode logs |
| `MEMORY_TENCENTDB_MODE` | `standalone` | ctl mode: `standalone` \| `hermes` (same as `--hermes`) |

Legacy paths `~/tdai-memory-openclaw-plugin` and `~/memory-tdai` are detected with warnings; `install_hermes_memory_tencentdb.sh` migrates them into the new root.

:::files
~/.memory-tencentdb/
├── tdai-memory-openclaw-plugin/   # TDAI_INSTALL_DIR
│   └── src/gateway/server.ts
└── memory-tdai/                   # TDAI_DATA_DIR
    ├── tdai-gateway.json
    ├── conversations/
    ├── records/
    ├── scene_blocks/
    ├── persona/
    └── logs/                      # standalone ctl logs (default)
:::

## Docker greenfield (`MODEL_*`)

`docker/opensource/Dockerfile.hermes` exposes one operator-facing model surface. At `CMD` start it exports Gateway LLM vars and writes Hermes `config.yaml` + `.env`.

| Variable | Default in image | Maps to |
| --- | --- | --- |
| `MODEL_API_KEY` | _(required at run)_ | `TDAI_LLM_API_KEY`, Hermes `model.api_key`, `OPENAI_API_KEY` in `.env` |
| `MODEL_BASE_URL` | `https://api.lkeap.cloud.tencent.com/v1` | `TDAI_LLM_BASE_URL`, Hermes `model.base_url` |
| `MODEL_NAME` | `deepseek-v3.2` | `TDAI_LLM_MODEL`, Hermes `model.default` |
| `MODEL_PROVIDER` | `custom` | Hermes `model.provider` only |

Also set in the image:

| Variable | Image default |
| --- | --- |
| `TDAI_GATEWAY_PORT` | `8420` |
| `TDAI_GATEWAY_HOST` | `0.0.0.0` |
| `TDAI_DATA_DIR` | `/opt/data/tdai-memory` |
| `HERMES_HOME` | `/opt/data` |
| `MEMORY_TENCENTDB_GATEWAY_HOST` | `127.0.0.1` |
| `MEMORY_TENCENTDB_GATEWAY_PORT` | `8420` |

```bash
docker run -d --name hermes-memory -p 8420:8420 \
  -e MODEL_API_KEY="your-api-key" \
  -v hermes_data:/opt/data \
  hermes-memory
```

## OpenClaw-related env (plugin host)

| Variable | Role |
| --- | --- |
| `OPENCLAW_STATE_DIR` | Fallback state dir when the host does not inject `runtime.state.resolveStateDir()` (default `~/.openclaw`) |
| `OPENCLAW_CONFIG_PATH` | Optional explicit path to `openclaw.json` (hook policy helpers) |
| `OPENCLAW_ROOT` | Optional root override for clean-context tooling |

Memory plugin behavior (capture, recall, embedding, TCVDB) is configured in `openclaw.json` under `plugins.entries.memory-tencentdb`, not via `TDAI_*` env vars.

## Ops modes and env sourcing

### ctl standalone vs hermes

| | standalone (default) | hermes (`--hermes` or `MEMORY_TENCENTDB_MODE=hermes`) |
| --- | --- | --- |
| Config write | `$TDAI_DATA_DIR/tdai-gateway.json` | Same + `$HERMES_HOME/env.d/memory-tencentdb-llm.sh` for `config llm` |
| Logs | `$TDAI_DATA_DIR/logs/` | `$HERMES_HOME/logs/memory_tencentdb/` (unless `MEMORY_TENCENTDB_LOG_DIR`) |
| Hermes config | Untouched | `enable-hermes-memory` edits `memory.provider` |

Start command resolution for ctl:

1. `MEMORY_TENCENTDB_GATEWAY_CMD`
2. `sh -c 'cd $TDAI_INSTALL_DIR && exec npx tsx src/gateway/server.ts'`

Sourced before start when present: `/etc/profile.d/memory-tencentdb-env.sh`; in hermes mode also `/etc/profile.d/hermes-env.sh` and `$HERMES_HOME/env.d/*.sh`.

### Supervisor spawn env

On `Popen`, the supervisor copies `os.environ` and sets:

- `MEMORY_TENCENTDB_GATEWAY_PORT`
- `MEMORY_TENCENTDB_GATEWAY_HOST`

It does **not** set `TDAI_GATEWAY_API_KEY` or rewrite LLM credentials. Credentials must already be in Hermes’s environment, `env.d`, or `tdai-gateway.json` under the resolved data dir.

## Quick reference tables

### Gateway process (must bind / extract)

| Env | Default |
| --- | --- |
| `TDAI_GATEWAY_HOST` | `127.0.0.1` |
| `TDAI_GATEWAY_PORT` | `8420` |
| `TDAI_GATEWAY_API_KEY` | unset (open) |
| `TDAI_CORS_ORIGINS` | empty (no CORS headers) |
| `TDAI_GATEWAY_CONFIG` | unset (search order) |
| `TDAI_DATA_DIR` | `$MEMORY_TENCENTDB_ROOT/memory-tdai` |
| `TDAI_LLM_BASE_URL` | `https://api.openai.com/v1` |
| `TDAI_LLM_API_KEY` | `""` |
| `TDAI_LLM_MODEL` | `gpt-4o` |
| `TDAI_LLM_MAX_TOKENS` | `4096` |
| `TDAI_LLM_TIMEOUT_MS` | `120000` |
| `TDAI_LLM_DISABLE_THINKING` | unset |
| `MEMORY_TENCENTDB_ROOT` | `~/.memory-tencentdb` |

### Hermes process (must dial)

| Env | Default |
| --- | --- |
| `MEMORY_TENCENTDB_GATEWAY_HOST` | `127.0.0.1` |
| `MEMORY_TENCENTDB_GATEWAY_PORT` | `8420` |
| `MEMORY_TENCENTDB_GATEWAY_CMD` | auto-discover |
| `MEMORY_TENCENTDB_GATEWAY_API_KEY` | unset (no auth header) |
| `MEMORY_TENCENTDB_LOG_DIR` | `~/.hermes/logs/memory_tencentdb` |
| `MEMORY_TENCENTDB_LLM_*` | schema / dual-write aliases; Gateway needs `TDAI_LLM_*` or JSON |

### Removed / no-ops

| Name | Status |
| --- | --- |
| `MEMORY_TENCENTDB_DATA_DIR` | **Not read** (never matched Gateway). Use `TDAI_DATA_DIR`. |

## Verification

```bash
# Gateway bind + health (no auth on /health)
curl -s http://127.0.0.1:8420/health

# Authenticated route when TDAI_GATEWAY_API_KEY is set
curl -s -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"test","session_key":"debug"}' \
  http://127.0.0.1:8420/recall

# Hermes-mode: confirm dual env after ctl config llm
tr '\0' '\n' < /proc/$(pgrep -n hermes-agent)/environ | grep -E 'TDAI_|MEMORY_TENCENTDB_'

# Docker image model sync
docker exec hermes-memory env | grep -E '(MODEL_|TDAI_LLM_)'
```

## Related pages

<CardGroup cols={2}>
  <Card title="Secure the Gateway" href="/secure-gateway">
    Bearer auth, CORS allow-list, non-loopback warnings, Hermes client alignment.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    `memory-tencentdb-ctl` modes, start/stop, config llm/embedding/vdb, path layout.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Docker greenfield, attach to existing Hermes, auto-discovery, health checks.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Routes, auth exceptions for `/health`, error envelope.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full `memory` schema inside `tdai-gateway.json` / OpenClaw config.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth 401, circuit breaker, missing Gateway command, log probes.
  </Card>
</CardGroup>

---

## 20. Migrate SQLite to TCVDB

> Offline migrate-sqlite-to-tcvdb workflow: dry-run, layer selection (l0/l1/profile), TCVDB connection flags, config rewrite, manifest updates, and rollback-safe verification.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/20-migrate-sqlite-to-tcvdb.md
- Generated: 2026-08-04T18:35:05.543Z

### Source Files

- `scripts/migrate-sqlite-to-tcvdb/README.md`
- `scripts/migrate-sqlite-to-tcvdb/cli-entry.ts`
- `scripts/migrate-sqlite-to-tcvdb/sqlite-to-tcvdb.ts`
- `scripts/migrate-sqlite-to-tcvdb/config-write.ts`
- `scripts/migrate-sqlite-to-tcvdb/manifest-write.ts`
- `SKILL-MIGRATION.md`
- `bin/migrate-sqlite-to-tcvdb.mjs`

---
title: "Migrate SQLite to TCVDB"
description: "Offline migrate-sqlite-to-tcvdb workflow: dry-run, layer selection (l0/l1/profile), TCVDB connection flags, config rewrite, manifest updates, and rollback-safe verification."
---

`migrate-sqlite-to-tcvdb` is an offline CLI that copies L0/L1 rows from local `vectors.db` and filesystem profiles (L2 scene blocks + L3 `persona.md`) into Tencent VectorDB (TCVDB), then rewrites OpenClaw plugin config and the data-dir manifest so runtime uses `storeBackend: "tcvdb"`. It does not delete the SQLite source.

## What migrates

| Layer flag | Source | Target operation |
|---|---|---|
| `l0` | SQLite L0 message rows (`VectorStore`) | `upsertL0` / `upsertL0Batch` |
| `l1` | SQLite L1 memory rows | `upsertL1` / `upsertL1Batch` |
| `l2` or `l3` | Local files via `listLocalProfiles`: `scene_blocks/*.md` (type `l2`) and `persona.md` (type `l3`) | `syncProfiles` |

Default `--layers` is `l0,l1,l2,l3`. Selecting either `l2` or `l3` runs the full local profile sync (both scene blocks and persona when present). TCVDB uses **server-side** dense embedding (`embeddingModel`); local SQLite vectors are not copied as client embedding arrays. BM25 sparse encoding can be enabled on the target with `--bm25-language zh|en` (default `zh`).

```text
plugin-data-dir/
  vectors.db              → L0 + L1 source (default path)
  scene_blocks/*.md       → L2 profiles
  persona.md              → L3 profile
  .metadata/manifest.json → rewritten to store.type = tcvdb

openclaw.json
  plugins.entries.<pluginId>.config
    storeBackend, tcvdb{...}, bm25{...}
```

## Prerequisites

- Node.js ≥ 22.16
- Plugin installed (package bin available), or a local checkout with scripts built
- Readable plugin data dir and `openclaw.json`
- Reachable TCVDB endpoint, credentials, database name, and embedding model name
- Prefer a **stopped** gateway during cutover so nothing writes to SQLite mid-migrate

Build the TypeScript migrator before running the bin (required for source checkouts; published packages ship prebuilt `scripts/migrate-sqlite-to-tcvdb/dist/`):

```bash
npm run build:migrate-sqlite-to-vdb
```

## Invoke the CLI

Package bin and npm script both resolve to `bin/migrate-sqlite-to-tcvdb.mjs` → compiled `cli-entry.js`.

```bash
# Via package bin (after install)
migrate-sqlite-to-tcvdb --help

# Via npm script in a checkout
npm run migrate-sqlite-to-tcvdb -- --help
```

Stdout prints a JSON preflight/summary. Progress and errors go to stderr with tag `[memory-tdai][migrate]` / `[memory-tdai][migrate-cli]`.

## Workflow

<Steps>
<Step title="Dry-run preflight">
Inspect source counts and target connection params without writing TCVDB, config, or manifest.

```bash
export TCVDB_API_KEY='...'

migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --dry-run
```

Expected summary fields (among others):

- `source.l0Count` / `l1Count` / `profileCount`
- `source.manifestExists` / `manifestStoreType`
- `target.*` connection and BM25 flags
- `options.*` apply/verify toggles
- `dryRun: true` and **no** `migration` block

If the data dir or `vectors.db` is missing, the tool returns an empty source summary and does not fail (fresh install / nothing to migrate).
</Step>

<Step title="Backup config (recommended)">
Copy `openclaw.json` before a live run. The migrator rewrites `plugins.entries.<pluginId>.config` when `--apply-config` is on (default). Manifest backups are automatic (see below); treat OpenClaw config backup as operator-owned.
</Step>

<Step title="Live migrate">
Omit `--dry-run`. Use `--yes` only if your automation expects that flag; **writes are gated by the absence of `--dry-run`**, not by an interactive prompt in the current implementation.

```bash
migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --yes
```

Order of work when source has data:

1. Open SQLite source; init `TcvdbMemoryStore` (BM25 encoder attached when enabled)
2. Abort if target already has L0/L1/profiles and `--fail-if-target-nonempty` is true (default)
3. Page migrate L1 then L0 (`DEFAULT_MIGRATION_PAGE_SIZE` = 50)
4. Sync profiles when `l2` or `l3` is selected
5. Optionally verify counts (default: wait ~10s for remote settle, then exact match)
6. Write OpenClaw config patch (`storeBackend: "tcvdb"`, `tcvdb`, `bm25`)
7. Rewrite `<dataDir>/.metadata/manifest.json` to `store.type: "tcvdb"` (backup `manifest.json.migrate.bak` when updating an existing file)
</Step>

<Step title="Restart and verify runtime">
Restart OpenClaw gateway (or Hermes-attached gateway) so the plugin reloads with `storeBackend: "tcvdb"`. Smoke-check recall/search and confirm no degraded store init in logs.
</Step>
</Steps>

## CLI flags

### Required connection and paths

<ParamField body="plugin-data-dir" type="string" required>
Plugin data directory (conversations/records/scene_blocks/vectors.db layout).
</ParamField>

<ParamField body="openclaw-config-path" type="string" required>
Path to `openclaw.json` (JSON/JSON5). Used for apply-config writes and preflight readability checks.
</ParamField>

<ParamField body="tcvdb-url" type="string" required>
TCVDB service URL.
</ParamField>

<ParamField body="tcvdb-username" type="string" required>
TCVDB username (examples commonly use `root`).
</ParamField>

<ParamField body="tcvdb-database" type="string" required>
Target TCVDB database name.
</ParamField>

<ParamField body="tcvdb-embedding-model" type="string" required>
Server-side embedding model name written into the target store and config (e.g. `bge-large-zh`).
</ParamField>

<ParamField body="tcvdb-api-key" type="string">
Plaintext API key. Mutually exclusive with `--tcvdb-api-key-env`.
</ParamField>

<ParamField body="tcvdb-api-key-env" type="string">
Environment variable name that holds the API key. Mutually exclusive with `--tcvdb-api-key`.
</ParamField>

### Optional controls

| Flag | Default | Behavior |
|---|---|---|
| `--sqlite-path` | `<plugin-data-dir>/vectors.db` | Alternate SQLite file |
| `--plugin-id` | `memory-tencentdb` | Config entry key under `plugins.entries` |
| `--layers` | `l0,l1,l2,l3` | Comma-separated subset of `l0`,`l1`,`l2`,`l3` |
| `--tcvdb-alias` | `""` | Alias stored in config/manifest |
| `--tcvdb-timeout-ms` | `10000` | Request timeout (must be > 0) |
| `--tcvdb-ca-pem` | — | CA PEM path for HTTPS TCVDB during **migration client** init |
| `--bm25-language` | `zh` | `zh` or `en` |
| `--summary-json-path` | — | Write full summary JSON to this path |
| `--job-id` | — | Accepted tracking id (parsed into options) |
| `--dry-run` | `false` | Preflight only; no TCVDB/config/manifest writes |
| `--yes` | `false` | Documented skip-confirm; parsed into summary |
| `--no-apply-config` | apply on | Skip openclaw.json rewrite |
| `--no-config-backup` | backup flag on | Parsed into summary; operator should still backup config manually |
| `--no-rewrite-manifest` | rewrite on | Skip `.metadata/manifest.json` update |
| `--no-fail-if-target-nonempty` | fail on | Allow non-empty target (append-style) |
| `--no-verify-counts` | verify on | Skip exact L0/L1/profile count match |
| `--no-bm25-enabled` | BM25 on | Disable BM25 sparse vectors on target |

Boolean flags use Node `parseArgs` with `allowNegative: true` (`--no-*` form).

## Config rewrite

When `applyConfig` is true, the tool merges into:

`plugins.entries[<pluginId>].config`

```json
{
  "storeBackend": "tcvdb",
  "tcvdb": {
    "url": "...",
    "username": "...",
    "apiKey": "...",
    "database": "...",
    "alias": "...",
    "embeddingModel": "...",
    "timeout": 10000
  },
  "bm25": {
    "enabled": true,
    "language": "zh"
  }
}
```

Notes:

- Existing sibling plugin config keys are preserved; `tcvdb` / `bm25` objects are deep-merged.
- Output is rewritten as pretty-printed JSON (JSON5 input is accepted).
- `caPemPath` is used for the migrator client when `--tcvdb-ca-pem` is set; it is **not** included in the config patch. Set `tcvdb.caPemPath` in OpenClaw config separately if the runtime needs a custom CA.

## Manifest rewrite

Path: `<plugin-data-dir>/.metadata/manifest.json`

- Missing manifest → create `version: 1` with `store.type: "tcvdb"` and TCVDB url/database/alias.
- Existing manifest → copy to `manifest.json.migrate.bak` in the same `.metadata/` directory, then update `store` while keeping other fields (e.g. `seed`).

## Verification and safety

| Guard | Default | Failure mode |
|---|---|---|
| Empty / missing source | soft skip | Log and finish without data copy |
| Degraded SQLite open | hard fail | Throws on preflight or reopen |
| Degraded TCVDB init | hard fail | Throws before migrate |
| Non-empty target | hard fail | `Target store is not empty (L1=…, L0=…, profiles=…)` |
| Count verify | hard fail | `L1/L0/Profile count verification failed: source=…, target=…` after ~10s settle |
| Batch upsert zero rows | hard fail | Failed batch migrate for L0/L1 |

**Rollback-safe properties**

- Source `vectors.db` and on-disk profiles are not removed by the migrator.
- Manifest previous content is recoverable from `manifest.json.migrate.bak` when an existing manifest was updated.
- To roll back runtime: restore prior `openclaw.json` (`storeBackend: "sqlite"`), restore or rewrite manifest `store` to sqlite if needed, restart gateway. TCVDB data left behind is harmless if unused.

## Common recipes

<CodeGroup>
```bash title="L1 only"
migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --layers l1 \
  --yes
```

```bash title="L0+L1 without profiles"
migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --layers l0,l1 \
  --yes
```

```bash title="English BM25, no auto config/manifest"
migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-en-v1.5 \
  --bm25-language en \
  --no-apply-config \
  --no-rewrite-manifest \
  --yes
```

```bash title="Append to non-empty target (CI-style)"
migrate-sqlite-to-tcvdb \
  --plugin-data-dir ~/.openclaw/memory-tdai \
  --openclaw-config-path ~/.openclaw/openclaw.json \
  --tcvdb-url http://127.0.0.1:80 \
  --tcvdb-username root \
  --tcvdb-api-key-env TCVDB_API_KEY \
  --tcvdb-database agent_memory_prod \
  --tcvdb-embedding-model bge-large-zh \
  --no-fail-if-target-nonempty \
  --no-verify-counts \
  --summary-json-path ./migration-report.json \
  --yes
```
</CodeGroup>

## Summary JSON shape

On success, CLI stdout is a `MigrationPreflightSummary`. After a live run, `migration` includes:

| Field | Meaning |
|---|---|
| `l0Migrated` / `l1Migrated` / `profileMigrated` | Rows/profiles written this run |
| `targetL0Count` / `targetL1Count` / `targetProfileCount` | Post-run target counts |
| `configWritten` | Whether openclaw config was patched |
| `manifestWritten` | Whether manifest was created/updated |
| `manifestBackupPath` | Path to `manifest.json.migrate.bak` when applicable |

Use `--summary-json-path` to persist the same object for automation.

## Troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `Missing required option --…` | Incomplete argv | Pass all required flags |
| `Provide either --tcvdb-api-key or --tcvdb-api-key-env, not both` | Dual key sources | Use one |
| `Environment variable … is empty or not set` | Bad env name / empty value | Export the key |
| `Unsupported layer(s): …` | Typo in `--layers` | Use only `l0,l1,l2,l3` |
| `Target store is not empty` | Prior data in DB | Use empty DB or `--no-fail-if-target-nonempty` |
| Count verification failed | Incomplete write / timing / partial layers | Re-check target; for partial layer runs disable verify or migrate all layers |
| `Target store entered degraded mode` | Bad URL/credentials/network/CA | Fix TCVDB connectivity; try `--tcvdb-ca-pem` for HTTPS |
| `Failed to open sqlite store` | Corrupt or locked `vectors.db` | Validate path; stop writers |
| Config not switched after migrate | `--no-apply-config` or wrong `--plugin-id` | Confirm `plugins.entries.memory-tencentdb.config.storeBackend` |
| Runtime still on SQLite | Gateway not restarted | Restart after config rewrite |

<Warning>
This page covers **storage backend** migration (SQLite → TCVDB). Package rename migration (`@tdai/memory-tdai` → `@tencentdb-agent-memory/memory-tencentdb`) is a separate workflow and is not performed by this CLI.
</Warning>

## Related pages

<CardGroup>
<Card title="Use Tencent VectorDB" href="/use-tcvdb">
Runtime `storeBackend: tcvdb` fields, BM25 language, embedding model, and CA PEM for live traffic.
</Card>
<Card title="Storage backends" href="/storage-backends">
SQLite vs TCVDB factory selection, hybrid retrieval, and embedding roles.
</Card>
<Card title="CLI reference" href="/cli-reference">
All package bins including `migrate-sqlite-to-tcvdb`, export, and inspect tools.
</Card>
<Card title="Plugin configuration reference" href="/plugin-config-reference">
Full schema for `storeBackend`, `tcvdb`, and `bm25` after config rewrite.
</Card>
<Card title="Inspect local memory" href="/inspect-local-memory">
`read-local-memory` / `export-tencent-vdb` for post-migrate validation.
</Card>
</CardGroup>

---

## 21. Gateway lifecycle

> Operate the Node Gateway with memory-tencentdb-ctl: standalone vs hermes modes, start/stop/status/health/logs, config llm/embedding/vdb writes, and path layout under ~/.memory-tencentdb.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/21-gateway-lifecycle.md
- Generated: 2026-08-04T18:37:27.859Z

### Source Files

- `scripts/README.memory-tencentdb-ctl.md`
- `scripts/memory-tencentdb-ctl.sh`
- `scripts/install_hermes_memory_tencentdb.sh`
- `src/gateway/server.ts`
- `src/gateway/config.ts`
- `hermes-plugin/memory/memory_tencentdb/supervisor.py`

---
title: "Gateway lifecycle"
description: "Operate the Node Gateway with memory-tencentdb-ctl: standalone vs hermes modes, start/stop/status/health/logs, config llm/embedding/vdb writes, and path layout under ~/.memory-tencentdb."
---

`memory-tencentdb-ctl` (`scripts/memory-tencentdb-ctl.sh`) is the operator entry for the standalone Node Gateway (`src/gateway/server.ts`). It starts and stops the HTTP sidecar on `127.0.0.1:8420` by default, writes credentials into `$TDAI_DATA_DIR/tdai-gateway.json` (mode `0600`), and optionally wires Hermes via `$HERMES_HOME` env fragments and `config.yaml`. The script is **not** registered as an npm `bin`; invoke it from the package path or symlink it to your `PATH`.

## Modes

| Mode | Activate | Writes | Does not touch |
|------|----------|--------|----------------|
| `standalone` (default) | no flag, or `--standalone` / `MEMORY_TENCENTDB_MODE=standalone` | Gateway lifecycle; `$TDAI_DATA_DIR/tdai-gateway.json`; logs under `$TDAI_DATA_DIR/logs/` | `$HERMES_HOME/env.d/`, `$HERMES_HOME/config.yaml` |
| `hermes` | `--hermes` or `MEMORY_TENCENTDB_MODE=hermes` | Everything standalone does, plus `config llm` → `$HERMES_HOME/env.d/memory-tencentdb-llm.sh`; logs under `$HERMES_HOME/logs/memory_tencentdb/`; `enable-hermes-memory` | — |

Hermes mode exists because Hermes spawns the Gateway with `os.environ.copy()`. LLM secrets must be present in the parent shell (via `$HERMES_HOME/env.d/*.sh`), not only in `tdai-gateway.json`. Embedding and VDB config are written only to JSON in both modes.

```mermaid
flowchart TB
  subgraph ctl ["memory-tencentdb-ctl"]
    start["start / stop / restart"]
    status["status / health / logs"]
    cfg["config llm | embedding | vdb | vdb-off | show"]
  end
  subgraph root ["~/.memory-tencentdb"]
    install["tdai-memory-openclaw-plugin/"]
    data["memory-tdai/"]
    json["memory-tdai/tdai-gateway.json"]
    slog["memory-tdai/logs/  (standalone)"]
  end
  subgraph hermes_home ["~/.hermes  (hermes mode only)"]
    envd["env.d/memory-tencentdb-llm.sh"]
    hlogs["logs/memory_tencentdb/"]
    hyaml["config.yaml memory.provider"]
  end
  gw["Node TdaiGateway :8420"]
  sup["GatewaySupervisor ensure_running"]
  start --> gw
  cfg --> json
  cfg -->|config llm + hermes| envd
  gw --> data
  sup -->|spawn MEMORY_TENCENTDB_GATEWAY_CMD| gw
  envd -->|source into hermes env| sup
```

## Path layout

Canonical root since 0.4.x: `$MEMORY_TENCENTDB_ROOT` → `~/.memory-tencentdb`.

| Path | Role |
|------|------|
| `$MEMORY_TENCENTDB_ROOT` | Unified root (default `~/.memory-tencentdb`) |
| `$TDAI_INSTALL_DIR` | Plugin checkout + `node_modules` + `src/gateway/server.ts` (default `$MEMORY_TENCENTDB_ROOT/tdai-memory-openclaw-plugin`) |
| `$TDAI_DATA_DIR` | Gateway data `baseDir` (default `$MEMORY_TENCENTDB_ROOT/memory-tdai`) |
| `$TDAI_DATA_DIR/tdai-gateway.json` | Primary config: top-level `llm`, nested `memory.embedding` / `memory.tcvdb` / `memory.storeBackend`; written at `0600` |
| `$TDAI_DATA_DIR/logs/` | standalone: `gateway.pid`, `gateway.stdout.log`, `gateway.stderr.log` |
| `$HERMES_HOME/logs/memory_tencentdb/` | hermes mode log/pid location |
| `$HERMES_HOME/env.d/memory-tencentdb-llm.sh` | hermes-only LLM env for supervisor inheritance |
| `$HERMES_HOME/config.yaml` | `enable-hermes-memory` sets `memory.provider: memory_tencentdb` |

Override with: `MEMORY_TENCENTDB_ROOT`, `TDAI_INSTALL_DIR`, `TDAI_DATA_DIR`, `HERMES_HOME`, `MEMORY_TENCENTDB_LOG_DIR`, `MEMORY_TENCENTDB_GATEWAY_HOST`, `MEMORY_TENCENTDB_GATEWAY_PORT`.

Legacy top-level `~/tdai-memory-openclaw-plugin` and `~/memory-tdai` are still detected. `install_hermes_memory_tencentdb.sh` migrates them; `memory-tencentdb-ctl` only warns. The Gateway process falls back to legacy `~/memory-tdai` if the new data dir is missing.

:::files
~/.memory-tencentdb/
├── tdai-memory-openclaw-plugin/   # TDAI_INSTALL_DIR
│   └── src/gateway/server.ts
└── memory-tdai/                   # TDAI_DATA_DIR
    ├── tdai-gateway.json          # 0600
    ├── logs/                      # standalone pid + stdout/stderr
    ├── conversations/             # L0 runtime data
    ├── records/                   # L1
    └── ...
~/.hermes/                         # hermes mode only
├── config.yaml
├── env.d/memory-tencentdb-llm.sh
└── logs/memory_tencentdb/
:::

## Install and invoke

Dependencies: `bash`, `python3`, `node` ≥ 22, `npx`, and `lsof` or `ss`.

Deploy the install tree first (Hermes greenfield path):

```bash
bash scripts/install_hermes_memory_tencentdb.sh
```

That script packs `@tencentdb-agent-memory/memory-tencentdb` into `$TDAI_INSTALL_DIR`, runs `npm install`, and writes `MEMORY_TENCENTDB_GATEWAY_CMD` (absolute `node --import tsx/esm src/gateway/server.ts`) plus host/port into `/etc/profile.d/memory-tencentdb-env.sh` and `$HERMES_HOME/.env`.

Call the ctl without a global bin:

```bash
"$(npm root -g)/@tencentdb-agent-memory/memory-tencentdb/scripts/memory-tencentdb-ctl.sh" --help
# or from a clone:
./scripts/memory-tencentdb-ctl.sh --help
```

Optional symlink for ops:

```bash
SCRIPT="$(npm root -g)/@tencentdb-agent-memory/memory-tencentdb/scripts/memory-tencentdb-ctl.sh"
sudo ln -sf "$SCRIPT" /usr/local/bin/memory-tencentdb-ctl
```

## Lifecycle commands

```bash
memory-tencentdb-ctl start
memory-tencentdb-ctl stop
memory-tencentdb-ctl restart
memory-tencentdb-ctl status
memory-tencentdb-ctl health
memory-tencentdb-ctl logs            # tail -f stdout + stderr
memory-tencentdb-ctl logs err 500    # stderr only, last 500 lines then follow
```

### start

1. `source` `/etc/profile.d/memory-tencentdb-env.sh` (both modes). Hermes mode also sources `/etc/profile.d/hermes-env.sh` and `$HERMES_HOME/env.d/*.sh`.
2. If something already listens on `$GATEWAY_PORT`, exit success with a warning.
3. Resolve command: `MEMORY_TENCENTDB_GATEWAY_CMD` if set, else  
   `sh -c 'cd $TDAI_INSTALL_DIR && exec npx tsx src/gateway/server.ts'`.
4. Spawn with `setsid nohup` (or `nohup`), append to stdout/stderr logs, write wrapper PID to `gateway.pid`.
5. Poll up to ~15s for listen + `GET /health`.

### stop / restart

Stop finds the listener PID(s) on the configured port, sends `SIGTERM`, waits ~5s, then `SIGKILL` if needed, and removes `gateway.pid`. Restart is stop + short sleep + start.

### status

Prints mode, host:port, install/data/log/config paths, `RUNNING`/`STOPPED`, and health. Hermes mode also shows `memory.provider` from `config.yaml` and `env.d/*.sh` listing.

### health

`GET http://$GATEWAY_HOST:$GATEWAY_PORT/health` via `python3` + `urllib` (no `curl` required). Gateway response shape:

```json
{
  "status": "ok",
  "version": "0.1.0",
  "uptime": 12,
  "stores": {
    "vectorStore": true,
    "embeddingService": true
  }
}
```

`status` is `"ok"` when a vector store is present, otherwise `"degraded"`. Hermes `GatewaySupervisor` treats both `"ok"` and `"degraded"` as ready.

### logs

`tail -n N -f` on stdout, stderr, or both (`all` default, `N` default 200).

## Config writes

All config subcommands merge into `$TDAI_DATA_DIR/tdai-gateway.json` with atomic temp-file + rename and mode `0600`. Use `--restart` to recycle the process after a write. Prefix with `--dry-run` to print intended content without writing.

### LLM

```bash
memory-tencentdb-ctl config llm \
  --api-key "sk-..." \
  --base-url "https://api.openai.com/v1" \
  --model "gpt-4o" \
  --restart
```

| Flag | Required | Notes |
|------|----------|-------|
| `--api-key` | yes | |
| `--base-url` | yes | must start with `http://` or `https://` |
| `--model` | yes | |
| `--restart` | no | restart Gateway after write |

JSON: top-level `llm.{baseUrl,apiKey,model}`.

Hermes mode **also** writes `$HERMES_HOME/env.d/memory-tencentdb-llm.sh`:

```bash
export TDAI_LLM_BASE_URL=...
export TDAI_LLM_API_KEY=...
export TDAI_LLM_MODEL=...
export MEMORY_TENCENTDB_LLM_BASE_URL="$TDAI_LLM_BASE_URL"
export MEMORY_TENCENTDB_LLM_API_KEY="$TDAI_LLM_API_KEY"
export MEMORY_TENCENTDB_LLM_MODEL="$TDAI_LLM_MODEL"
```

### Embedding

Default in plugin config is effectively off (`provider=none` / keyword-only). Enable an OpenAI-compatible provider:

```bash
memory-tencentdb-ctl config embedding \
  --provider openai \
  --api-key "sk-..." \
  --base-url "https://api.openai.com/v1" \
  --model "text-embedding-3-small" \
  --dimensions 1536 \
  --restart

memory-tencentdb-ctl config embedding --provider none --restart
```

| Flag | Required | Notes |
|------|----------|-------|
| `--provider` | yes | `none` disables; otherwise e.g. `openai`, `deepseek`, `qclaw` |
| `--api-key` / `--base-url` / `--model` / `--dimensions` | yes if not `none` | dimensions must be a positive integer |
| `--proxy-url` | for `qclaw` | required when provider is `qclaw` |
| `--restart` | no | |

JSON: `memory.embedding.{provider,baseUrl,apiKey,model,dimensions,enabled,proxyUrl?}`.

### VectorDB (tcvdb)

```bash
memory-tencentdb-ctl config vdb \
  --url "http://xxx-vdb.example:8100" \
  --username root \
  --api-key "YOUR-VDB-API-KEY" \
  --database "openclaw_memory" \
  --alias "primary" \
  --embedding-model "bge-large-zh" \
  --ca-pem "/etc/ssl/vdb-ca.pem" \
  --restart
```

| Flag | Required | Notes |
|------|----------|-------|
| `--url` | yes | `http(s)://…` |
| `--api-key` | yes | |
| `--database` | yes | |
| `--username` | no | default `root` |
| `--alias` | no | |
| `--embedding-model` | no | server-side embedding model name |
| `--ca-pem` | no | path only; file must be readable; not copied |
| `--no-set-backend` | no | keep current `storeBackend`; default sets `memory.storeBackend` to `tcvdb` |
| `--restart` | no | |

JSON: `memory.tcvdb.{url,username,apiKey,database,alias?,caPemPath?,embeddingModel?}`.

### Fall back to SQLite

```bash
memory-tencentdb-ctl config vdb-off --restart
memory-tencentdb-ctl config vdb-off --purge-creds --restart
```

Sets `memory.storeBackend` to `sqlite`. Default keeps `memory.tcvdb` credentials for later re-enable; `--purge-creds` deletes the whole `tcvdb` object. Does **not** change Hermes `memory.provider`.

### Inspect config

```bash
memory-tencentdb-ctl config show
```

Prints redacted JSON (`apiKey` / `password` / `token` → `<redacted:N chars>`). Hermes mode also dumps `memory-tencentdb-*.sh` with keys redacted.

## Hermes wiring

```bash
memory-tencentdb-ctl --hermes enable-hermes-memory
```

Idempotently sets `$HERMES_HOME/config.yaml` → `memory.provider: memory_tencentdb` without rewriting the whole YAML:

1. Prefer `ruamel.yaml` round-trip (comments/order preserved).
2. Else minimal in-place `provider:` line edit.
3. Else append a minimal `memory:` block.

Fails in standalone mode. After enable:

```bash
source "$HERMES_HOME/env.d/memory-tencentdb-llm.sh"
# restart hermes so supervisor re-reads env and config
```

`GatewaySupervisor` (Hermes plugin) calls `ensure_running()`: health-check first; if down, spawn `MEMORY_TENCENTDB_GATEWAY_CMD`, wait up to 30s for `/health` `ok`|`degraded`, log under `MEMORY_TENCENTDB_LOG_DIR` or `~/.hermes/logs/memory_tencentdb/`. On shutdown it `SIGTERM`s its own child (10s then `SIGKILL`). It does **not** inject `TDAI_GATEWAY_API_KEY` into the child; server auth is operator-owned.

## Gateway process config resolution

`loadGatewayConfig()` resolution order for the config file:

1. `TDAI_GATEWAY_CONFIG` explicit path  
2. `./tdai-gateway.yaml` or `./tdai-gateway.json` in CWD  
3. `<dataDir>/tdai-gateway.yaml` or `tdai-gateway.json`  
4. env-only defaults  

| Concern | Env (Node Gateway) | Default |
|---------|--------------------|---------|
| Bind host/port | `TDAI_GATEWAY_HOST` / `TDAI_GATEWAY_PORT` | `127.0.0.1` / `8420` |
| API key | `TDAI_GATEWAY_API_KEY` | unset (auth off; `/health` always open) |
| CORS | `TDAI_CORS_ORIGINS` | empty (no CORS headers) |
| Data dir | `TDAI_DATA_DIR` | `$MEMORY_TENCENTDB_ROOT/memory-tdai` |
| LLM | `TDAI_LLM_BASE_URL` / `TDAI_LLM_API_KEY` / `TDAI_LLM_MODEL` | OpenAI-compatible defaults; empty key until configured |

Ctl / Hermes client address the same port via `MEMORY_TENCENTDB_GATEWAY_HOST` / `MEMORY_TENCENTDB_GATEWAY_PORT` (defaults also `127.0.0.1:8420`). Keep both sides aligned if you change the port.

## Typical workflows

### Standalone Gateway

```bash
bash scripts/install_hermes_memory_tencentdb.sh   # or install tree already present
memory-tencentdb-ctl config llm --api-key "sk-..." --base-url "https://api.openai.com/v1" --model gpt-4o
memory-tencentdb-ctl config embedding --provider openai --api-key "sk-..." \
  --base-url "https://api.openai.com/v1" --model text-embedding-3-small --dimensions 1536
# optional:
# memory-tencentdb-ctl config vdb --url "http://..." --api-key "..." --database openclaw_memory
memory-tencentdb-ctl start
memory-tencentdb-ctl status
memory-tencentdb-ctl health
```

### Hermes-integrated

```bash
export MEMORY_TENCENTDB_MODE=hermes   # optional sticky mode
memory-tencentdb-ctl --hermes config llm --api-key "sk-..." --base-url "https://api.openai.com/v1" --model gpt-4o --restart
memory-tencentdb-ctl --hermes enable-hermes-memory
memory-tencentdb-ctl --hermes start   # manual fallback; supervisor usually owns spawn
memory-tencentdb-ctl --hermes status
```

### Temporary SQLite while Hermes stays on `memory_tencentdb`

```bash
memory-tencentdb-ctl config vdb-off --restart
# later re-declare vdb (required fields must be supplied again):
memory-tencentdb-ctl config vdb --url "http://..." --api-key "..." --database openclaw_memory --restart
```

Do not change Hermes `memory.provider` only to switch storage; backend is Gateway-internal.

## Exit codes and ops notes

| Code | Meaning |
|------|---------|
| `0` | success |
| `1` | bad args / validation (e.g. non-http base URL; hermes-only cmd in standalone) |
| `2` | write failure |
| `127` | missing dependency (`python3` / `node` / `npx`) |

- Sensitive files are `0600`. Never commit `env.d/memory-tencentdb-llm.sh`.
- Startup failures: `memory-tencentdb-ctl logs err 200`, or foreground:  
  `cd "$TDAI_INSTALL_DIR" && npx tsx src/gateway/server.ts`  
  (or the absolute `node --import tsx/esm` command from install).
- Port conflict: `MEMORY_TENCENTDB_GATEWAY_PORT=18420 memory-tencentdb-ctl restart` and set matching `TDAI_GATEWAY_PORT` for the Node process.
- systemd: wrap `start`/`stop` as `Type=forking` if needed; the Gateway is a stateless HTTP sidecar without a custom readiness protocol beyond `/health`.

## Related pages

<CardGroup>
  <Card title="Hermes setup" href="/hermes-setup">
    Install path, Docker greenfield, auto-discovery, and health verification with Hermes.
  </Card>
  <Card title="Secure the Gateway" href="/secure-gateway">
    Bearer auth, CORS allow-list, non-loopback warnings, client key alignment.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Routes: /health, /recall, /capture, search, /session/end, /seed.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full TDAI_* / MEMORY_TENCENTDB_* resolution and client vs server key names.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Package bins plus memory-tencentdb-ctl subcommand inventory.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth 401, circuit breaker, embedding failures, and log/probe checklist.
  </Card>
</CardGroup>

---

## 22. Inspect local memory

> Query L0–L3 artifacts with read-local-memory, export TCVDB collections with export-tencent-vdb, diagnostic export script, and on-disk layout (conversations, records, scene_blocks, persona, vectors.db).

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/22-inspect-local-memory.md
- Generated: 2026-08-04T18:35:27.770Z

### Source Files

- `scripts/read-local-memory/read-local-memory.ts`
- `scripts/export-tencent-vdb/export-tencent-vdb.ts`
- `scripts/export-diagnostic.sh`
- `SKILL-DIAGNOSTIC-EXPORT.md`
- `src/utils/pipeline-factory.ts`
- `src/utils/manifest.ts`
- `bin/read-local-memory.mjs`

---
title: "Inspect local memory"
description: "Query L0–L3 artifacts with read-local-memory, export TCVDB collections with export-tencent-vdb, diagnostic export script, and on-disk layout (conversations, records, scene_blocks, persona, vectors.db)."
---

The package ships three inspection surfaces: the `read-local-memory` CLI (read-only L0–L3 query over a local data directory), `export-tencent-vdb` (HTTP export of Tencent VectorDB collections), and `scripts/export-diagnostic.sh` (full OpenClaw + memory-tdai field dump with redacted config). Runtime data always lives under a directory named **`memory-tdai`** (historical path; the plugin package is `memory-tencentdb`).

## Data directory locations

| Host | Default data dir | Override |
|------|------------------|----------|
| OpenClaw plugin | `<stateDir>/memory-tdai` (usually `~/.openclaw/memory-tdai`) | `OPENCLAW_STATE_DIR` / host `resolveStateDir()` |
| Standalone Gateway / Hermes | `~/.memory-tencentdb/memory-tdai` | `TDAI_DATA_DIR`, `MEMORY_TENCENTDB_ROOT` |
| Legacy Gateway | `~/memory-tdai` if present and the new path does not exist | migrate or set env overrides |

`initDataDirectories()` creates `conversations/`, `records/`, `scene_blocks/`, `.metadata/`, and `.backup/` under the plugin data dir on startup.

## On-disk layout

```text
<memory-tdai data dir>/
├── conversations/          # L0 raw dialogue (daily JSONL shards)
├── records/                # L1 structured atoms (daily JSONL shards)
├── scene_blocks/           # L2 scene Markdown (*.md + META header)
├── persona.md              # L3 user persona
├── vectors.db              # SQLite: l0_conversations, l1_records, vectors, FTS
├── .metadata/
│   ├── manifest.json           # store binding (sqlite | tcvdb) + optional seed info
│   ├── recall_checkpoint.json  # capture/pipeline cursors and counters
│   └── scene_index.json        # L2 scene index
└── .backup/                # rolling backups before destructive LLM writes
```

| Path | Layer | Role |
|------|-------|------|
| `conversations/` | L0 | Append-only JSONL message capture |
| `records/` | L1 | Extracted memory atoms (JSONL) |
| `scene_blocks/*.md` | L2 | Scene profiles with `-----META-START-----` / `-----META-END-----` headers (`created`, `updated`, `summary`, `heat`) |
| `persona.md` | L3 | Aggregated persona body |
| `vectors.db` | L0/L1 index | Primary source for `read-local-memory` L0/L1 queries (`l0_conversations`, `l1_records`) |
| `.metadata/manifest.json` | ops | Store type (`sqlite` path or `tcvdb` url/database); written once |
| `.metadata/recall_checkpoint.json` | ops | Pipeline/runner progress |
| `.metadata/scene_index.json` | L2 | Scene index cache |

<Note>
JSONL under `conversations/` and `records/` is the file-layer write path. For inspection of indexed L0/L1 rows (filters, sort, pagination), use `vectors.db` via `read-local-memory`. L2 and L3 remain filesystem Markdown.
</Note>

## Query with `read-local-memory`

Package bin: `read-local-memory` → `bin/read-local-memory.mjs` → compiled `scripts/read-local-memory/dist/read-local-memory.js`.

Build once if the dist artifact is missing:

```bash
npm run build:read-local-memory
```

### Invocation

```bash
# After install of @tencentdb-agent-memory/memory-tencentdb
read-local-memory -d ~/.openclaw/memory-tdai

# From repo / package scripts
npm run read-local-memory -- -d ~/.openclaw/memory-tdai -L L0 --since 7d
npx tsx scripts/read-local-memory/read-local-memory.ts -d ./path/to/memory-tdai
```

### Flags

<ParamField body="data-dir" type="string" required>
Local `memory-tdai` directory (`-d`). Overview and L0/L1 modes expect `vectors.db` under this path.
</ParamField>

<ParamField body="level" type="L0 | L1 | L2 | L3">
`-L`. Omit for multi-layer overview (counts, role/type distributions, scene heat, persona size).
</ParamField>

<ParamField body="since" type="string">
Start time: ISO string or relative `Nd` / `Nh` / `Nm` / `Ns` (for example `7d`, `24h`).
</ParamField>

<ParamField body="until" type="string">
End time (same formats as `since`).
</ParamField>

<ParamField body="limit" type="number">
Page size (`-l`). Default `50`.
</ParamField>

<ParamField body="offset" type="number">
Pagination offset. Default `0`.
</ParamField>

<ParamField body="sort" type="asc | desc">
Time sort. Default `desc`. L0 sorts on `timestamp` (epoch ms); L1 on `updated_time` (ISO text).
</ParamField>

<ParamField body="filter" type="string">
`-f`. Comma-separated column predicates. Operators: `=`, `!=`, `>=`, `<=`, `>`, `<`. CamelCase or snake_case column names. Whitelisted columns only (parameterized SQL).
</ParamField>

<ParamField body="format" type="table | json | jsonl">
Default `table`.
</ParamField>

<ParamField body="file" type="string">
L2 only: return full body of one scene file by basename (for example `work.md`).
</ParamField>

### L0 / L1 filter columns

| Level | Filterable columns |
|-------|--------------------|
| L0 | `record_id`, `session_key`, `session_id`, `role`, `message_text`, `recorded_at`, `timestamp` |
| L1 | `record_id`, `content`, `type`, `priority`, `scene_name`, `session_key`, `session_id`, `timestamp_str`, `timestamp_start`, `timestamp_end`, `created_time`, `updated_time`, `metadata_json` |

### Behavior by layer

| Mode | Source | Notes |
|------|--------|-------|
| Overview (no `-L`) | `vectors.db` + L2 dir + `persona.md` | Requires `vectors.db`; exits with error if missing |
| L0 | table `l0_conversations` | Time filter on epoch `timestamp`; maps to camelCase fields (`id`, `sessionKey`, `content`, …) |
| L1 | table `l1_records` | Time filter on `updated_time`; parses `metadata_json` |
| L2 | `scene_blocks/*.md` | No SQLite. List mode omits body in JSON (meta only). `--file` returns full body |
| L3 | `persona.md` | Missing file returns empty content (not an error) |

Missing `vectors.db` with `-L L0` or `-L L1` returns empty data (fresh install). Missing L2 directory or L3 file is treated as “not generated yet.”

### Examples

```bash
# Layer overview
read-local-memory -d ~/.openclaw/memory-tdai

# Recent L0 dialogue
read-local-memory -d ~/.openclaw/memory-tdai -L L0 --since 7d

# L1 persona-type atoms
read-local-memory -d ~/.openclaw/memory-tdai -L L1 -f 'type=persona'

# Paginated L0 page 2 (20 per page)
read-local-memory -d ~/.openclaw/memory-tdai -L L0 -l 20 --offset 20

# Machine-readable export
read-local-memory -d ~/.openclaw/memory-tdai -L L1 --format json

# Single L2 scene body
read-local-memory -d ~/.openclaw/memory-tdai -L L2 --file work.md
```

<RequestExample>
```bash
read-local-memory -d ~/.openclaw/memory-tdai -L L0 --since 7d --format json
```
</RequestExample>

<ResponseExample>
```json
{
  "level": "L0",
  "total": 128,
  "offset": 0,
  "limit": 50,
  "sort": "desc",
  "filter": null,
  "data": [
    {
      "id": "…",
      "sessionKey": "agent:main:main",
      "sessionId": "…",
      "role": "user",
      "content": "…",
      "recordedAt": "…",
      "timestamp": 1710000000000
    }
  ]
}
```
</ResponseExample>

SQLite is opened with `PRAGMA query_only = ON` (read-only inspection).

## Export TCVDB with `export-tencent-vdb`

Package bin: `export-tencent-vdb` → `bin/export-tencent-vdb.mjs`. Connects only to **Tencent VectorDB** over HTTP (built-in `fetch`; Node ≥ 18). Connection parameters are CLI flags only (no `.env` file).

```bash
npm run build:export-tencent-vdb   # if dist missing
export-tencent-vdb \
  --url "http://your-vdb-host:8100" \
  --username root \
  --api-key "$TCVDB_API_KEY" \
  --database mydb
```

### Connection parameters (required)

<ParamField body="url" type="string" required>
VDB HTTP base URL (trailing slash stripped).
</ParamField>

<ParamField body="username" type="string" required>
Auth account (for example `root`). Sent as `Authorization: Bearer account=…&api_key=…`.
</ParamField>

<ParamField body="api-key" type="string" required>
API key.
</ParamField>

<ParamField body="database" type="string" required>
Database name.
</ParamField>

### Export options

| Flag | Default | Purpose |
|------|---------|---------|
| `--timeout` | `30000` | Per-request timeout (ms) |
| `-o` / `--output` | `./vdb-export-YYYY-MM-DD` | Output directory |
| `-c` / `--collection` | all collections | Exact collection name |
| `-f` / `--filter` | none | Server-side document filter (VDB filter expression) |
| `-l` / `--limit` | all | Max documents |
| `--offset` | `0` | Start offset (page size 100) |
| `--include-vectors` | off | Include dense `vector` field (skipped by default) |
| `--probe` | off | Connectivity check + collection list; no export |
| `-h` / `--help` | | Help text |

`sparse_vector` (BM25) is always exported. Dense `vector` is omitted unless `--include-vectors` is set.

### Output layout

```text
<outputDir>/
├── <collection>.jsonl    # one JSON document per line
├── schemas.json          # /collection/describe per collection
└── export-meta.json      # exportedAt, url, database, filter, counts
```

### Probe first

```bash
export-tencent-vdb \
  --url "http://gz-vdb-xxx:8100" \
  --username root \
  --api-key "$TCVDB_API_KEY" \
  --database mydb \
  --probe
```

Success prints collection names and document counts, then exits. Export uses `/collection/list`, `/collection/describe`, and paged `/document/query` with `readConsistency: "strongConsistency"`.

## Diagnostic field export

For support / debugging of OpenClaw + memory-tencentdb:

```bash
bash scripts/export-diagnostic.sh
# optional output base (default: ~/Downloads)
bash scripts/export-diagnostic.sh /tmp
```

### State dir resolution

1. `OPENCLAW_STATE_DIR` if set  
2. else `~/.openclaw` if it exists  
3. else `~/.clawdbot`  
4. else exit with error  

### Archive contents

Output: `<base>/openclaw-diagnostic-<timestamp>.tar.gz` (and an unpacked sibling directory).

| Entry | Contents | Privacy |
|-------|----------|---------|
| `env-info.txt` | OS, Node, OpenClaw version, tree listing, `du` for `memory-tdai` | Low |
| `logs/` | Gateway logs from `<stateDir>/logs/`; last 3 rolling `/tmp/openclaw/openclaw-*.log` files (tail 5000 lines each) | Low–medium |
| `memory-tdai/` | Full L0–L3 tree, `.metadata/`, `vectors.db`, optional `.backup/` | **High** (conversation text) |
| `openclaw-config-redacted.json` | Redacted `openclaw.json` | Low |
| `plugins-info.txt` | Extension package names/versions | Low |

### Config redaction rules

| Match | Replacement |
|-------|-------------|
| Keys matching `apiKey` / `token` / `password` / `secret` / `credential` (string values) | `***REDACTED(Nchars)***` |
| SecretRef objects (`source` / `provider` / `id`) | `id` redacted |
| Top-level `models`, `secrets`, `channels`, `env` | Whole section placeholder |
| `gateway.auth` token/password | Redacted |
| Remaining fields (including `plugins`) | Kept for diagnosis |

The archive is **local only** — nothing is uploaded. Review memory contents before sharing.

### Manual fallback

If the script cannot run, copy `logs/`, `memory-tdai/`, and a carefully redacted config into a temp dir and `tar -czf` yourself. Never send raw API keys.

## Inspection workflows

<Steps>
  <Step title="Confirm the data directory">
    ```bash
    ls -la "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/memory-tdai/"
    # Gateway / Hermes default
    ls -la "${TDAI_DATA_DIR:-$HOME/.memory-tencentdb/memory-tdai}/"
    ```
    Expect `conversations/`, `records/`, `scene_blocks/`, and optionally `vectors.db` / `persona.md`.
  </Step>
  <Step title="Overview and layer drill-down">
    ```bash
    read-local-memory -d ~/.openclaw/memory-tdai
    read-local-memory -d ~/.openclaw/memory-tdai -L L1 -f 'priority>=80' --format json
    read-local-memory -d ~/.openclaw/memory-tdai -L L2
    read-local-memory -d ~/.openclaw/memory-tdai -L L3
    ```
  </Step>
  <Step title="Check pipeline progress">
    Inspect `.metadata/recall_checkpoint.json` for capture cursors, L1/L2 session state, and persona request flags. Inspect `.metadata/manifest.json` for store backend (`sqlite` vs `tcvdb`).
  </Step>
  <Step title="TCVDB backend">
    If `manifest.store.type` is `tcvdb` (or config uses `storeBackend: tcvdb`), probe and export collections with `export-tencent-vdb` using the same url/database credentials as the runtime.
  </Step>
  <Step title="Bundle for support">
    ```bash
    bash scripts/export-diagnostic.sh
    ```
    Verify the printed `.tar.gz` path and size; confirm memory data may leave the machine before sending.
  </Step>
</Steps>

## Troubleshooting signals

| Symptom | Check |
|---------|--------|
| `SQLite 数据库不存在` on overview | No `vectors.db` yet — capture/index never ran, or wrong `-d` |
| Empty L0/L1 with `-L` | Fresh install or filters too tight; try overview without filters |
| L2 “尚未生成场景数据” | Pipeline has not produced scenes; check logs for `[pipeline]` |
| L3 “画像文件尚未生成” | Persona trigger/generator not completed; see checkpoint `last_persona_*` |
| `export-tencent-vdb` list failure | Wrong url/api-key/database; use `--probe` |
| Diagnostic script “未找到 OpenClaw 工作目录” | Set `OPENCLAW_STATE_DIR` or ensure `~/.openclaw` exists |
| Log search for runtime issues | `[memory-tdai]`, `[recall]`, `[pipeline]` (log tag remains `memory-tdai`) |

## Related pages

<CardGroup>
  <Card title="Memory layers" href="/memory-layers">
    What L0–L3 store and how L1→L2→L3 is scheduled.
  </Card>
  <Card title="Storage backends" href="/storage-backends">
    sqlite vs tcvdb, hybrid retrieval, and store factory rules.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full package bins and `openclaw memory-tdai` commands.
  </Card>
  <Card title="Use Tencent VectorDB" href="/use-tcvdb">
    Runtime `storeBackend: tcvdb` connection fields.
  </Card>
  <Card title="Migrate SQLite to TCVDB" href="/migrate-to-tcvdb">
    Offline migrate-sqlite-to-tcvdb workflow.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Plugin disabled, no recall, embedding errors, and probe checklist.
  </Card>
</CardGroup>

---

## 23. Troubleshooting

> Source-backed failure modes: plugin disabled, no recall, embedding 400/matryoshka, retention cleanup, offload patch missing, Gateway circuit breaker, auth 401, and log/probe checklist.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/23-troubleshooting.md
- Generated: 2026-08-04T18:36:04.476Z

### Source Files

- `SKILL.md`
- `README.md`
- `hermes-plugin/memory/memory_tencentdb/README.md`
- `hermes-plugin/memory/memory_tencentdb/tests/test_memory_tencentdb_recovery.py`
- `scripts/bugfix-20260423/BUGFIX-20260423-SOP.md`
- `src/core/store/embedding.ts`
- `src/gateway/server.ts`
- `SKILL-DIAGNOSTIC-EXPORT.md`

---
title: "Troubleshooting"
description: "Source-backed failure modes: plugin disabled, no recall, embedding 400/matryoshka, retention cleanup, offload patch missing, Gateway circuit breaker, auth 401, and log/probe checklist."
---

Runtime failures in `@tencentdb-agent-memory/memory-tencentdb` cluster around host enablement (OpenClaw plugin or Hermes Gateway sidecar), embedding degradation, retention cleanup, context-offload patches, and Hermes client reliability (circuit breaker + Bearer auth). Log tags still use the historical prefix `[memory-tdai]`; the on-disk data directory remains `memory-tdai` under the OpenClaw state dir (or `~/.memory-tencentdb/memory-tdai` for standalone Hermes Gateway).

## Quick triage

| Symptom | First check | Likely fix |
| --- | --- | --- |
| No `[memory-tdai]` logs after restart | Plugin enable flag + Gateway restart | Set `memory-tencentdb.enabled: true`, `openclaw gateway restart` |
| Capture works, no injection | `recall.enabled`, `scoreThreshold`, timeout | Lower threshold; raise `recall.timeoutMs` (default 5000) |
| Vector search empty / keyword-only | `embedding.provider` + apiKey/baseUrl/model/dimensions | Complete remote embedding quadruple, or accept keyword-only |
| HTTP 400 on embed (matryoshka) | `embedding.sendDimensions` | Set `sendDimensions: false` for BGE-M3-style backends |
| History disappears overnight | `capture.l0l1RetentionDays`, `allowAggressiveCleanup` | Use `0` (never clean) or `>= 3`; avoid 1–2 without aggressive flag |
| Offload tools/results not recovered | `offload.enabled`, `plugins.slots.contextEngine`, after-tool-call patch | Register slot + re-run patch after OpenClaw upgrades |
| Hermes: “Gateway not available” / empty tools | Port 8420, auto-discovery, stderr log | Start Gateway, set `MEMORY_TENCENTDB_GATEWAY_CMD`, inspect logs |
| Hermes: “circuit breaker tripped” | 5 consecutive Gateway failures | Fix Gateway health; wait 60s or let watchdog recover |
| HTTP 401 on `/recall` (etc.) | Bearer key mismatch | Align `TDAI_GATEWAY_API_KEY` with client `MEMORY_TENCENTDB_GATEWAY_API_KEY` |

## Plugin disabled or not loading (OpenClaw)

### Signals

- No `[memory-tdai]` lines in Gateway logs after restart
- Missing data dir: `$OPENCLAW_STATE_DIR/memory-tdai/` (default `~/.openclaw/memory-tdai/`)
- Expected subdirs never appear: `conversations/`, `records/`, `scene_blocks/`, `vectors.db`

### Causes and fixes

1. **Plugin not enabled** — zero-config enable is:

```json
{
  "memory-tencentdb": {
    "enabled": true
  }
}
```

in `~/.openclaw/openclaw.json`. Config changes require `openclaw gateway restart`.

2. **Wrong upgrade path** — use `openclaw plugins update @tencentdb-agent-memory/memory-tencentdb` (native OpenClaw commands). Semantic version range installs can leave the plugin effectively disabled.

3. **Host version gate** — OpenClaw `>= 2026.3.13`, Node `>= 22.16.0`. Confirm with `openclaw --version` and `node -v`.

4. **OpenClaw 2026.4.23 hook schema** — Zod `.strict()` can reject `hooks.allowConversationAccess`, blocking non-bundled session hooks. Apply the version-scoped fix:

```bash
openclaw gateway stop
bash scripts/bugfix-20260423/bugfix-20260423.sh
# verify allowConversationAccess under plugins.entries.memory-tencentdb.hooks
openclaw gateway run
```

Later hosts (`>= 2026.4.24`) auto-handle the policy path in plugin registration.

### Verify

```bash
openclaw gateway restart
# logs should show [memory-tdai] Registering plugin ... and Config parsed: ...
ls -la "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/memory-tdai/"
```

## No recall / empty memory injection

Auto-recall runs in `performAutoRecall` with tag `[memory-tdai] [recall]`. It searches L1 (`keyword` / `embedding` / `hybrid`), injects L3 persona and L2 scene navigation, and races against `recall.timeoutMs` (default **5000** ms). On timeout it **skips injection** without blocking the user turn.

### Config knobs

| Key | Default | Failure mode if wrong |
| --- | --- | --- |
| `recall.enabled` | `true` | Auto-recall hooks not registered |
| `recall.scoreThreshold` | `0.3` | Hits filtered out as “below threshold” |
| `recall.strategy` | `hybrid` | `embedding`/`hybrid` fall back to keyword when embedding unavailable |
| `recall.timeoutMs` | `5000` | Slow embedding → timeout skip |
| `recall.maxResults` | `5` | Caps injected items |
| `capture.enabled` / `extraction.enabled` | `true` | No L0/L1 data to recall |

### Checklist

<Steps>
  <Step title="Confirm L0/L1 data exists">
    Inspect `memory-tdai/conversations/` and `records/`, or use `read-local-memory` / OpenClaw `memory-tdai` CLI. No shards means nothing to inject.
  </Step>
  <Step title="Check recall flags">
    Ensure `recall.enabled` is true and `scoreThreshold` is not overly high for sparse early data.
  </Step>
  <Step title="Watch strategy and embedding">
    Logs show `strategy=…`, `embeddingAvailable=…`, FTS/embedding hit counts. Missing embedding forces keyword-only; FTS unavailable yields empty keyword path.
  </Step>
  <Step title="Exercise tools">
    Call `tdai_memory_search` / `tdai_conversation_search` (OpenClaw) or Hermes `memory_tencentdb_*` tools. Combined tool budget is **3 calls per turn**.
  </Step>
</Steps>

<Note>
Default `embedding.provider` is `"none"`: vector search is off and hybrid degrades to keyword. Incomplete remote embedding config disables embedding with a stored `configError` and continues without vectors — the plugin does not throw.
</Note>

## Embedding HTTP 400 / Matryoshka `dimensions`

Remote OpenAI-compatible embedding posts to `{baseUrl}/embeddings`. By default `sendDimensions` is **true**, so the request body includes `dimensions` (Matryoshka-style truncation for models like `text-embedding-3-*`).

Some self-hosted / OSS models (e.g. **BGE-M3**) reject unknown `dimensions` with HTTP **400** (`does not support matryoshka representation`). Client errors in the 4xx range (except 429) are **not retried**.

### Fix

```json
{
  "embedding": {
    "enabled": true,
    "provider": "openai",
    "baseUrl": "http://your-host:port/v1",
    "apiKey": "<KEY>",
    "model": "bge-m3",
    "dimensions": 1024,
    "sendDimensions": false
  }
}
```

### Other embedding failure modes

| Condition | Behavior |
| --- | --- |
| `provider: "none"` (default) | Embedding disabled; keyword path only |
| Missing any of `apiKey` / `baseUrl` / `model` / `dimensions` | Embedding disabled; error message logged via `configError` |
| `provider: "local"` in user config | Treated as disabled at config parse (not exposed) |
| `provider: "qclaw"` without `proxyUrl` (+ full remote fields) | Embedding disabled |
| ZeroEntropy (`provider: "zeroentropy"`) | Uses `/models/embed`; Matryoshka dims must match accepted set when `sendDimensions` is true |
| Local model not ready | `EmbeddingNotReadyError`; callers fall back to keyword-only |

Timeouts default to **10000** ms per call with up to **3** retries on 5xx/429/network errors. Separate `recallTimeoutMs` / `captureTimeoutMs` can shorten or lengthen recall vs capture paths.

## Retention cleanup too aggressive

Daily cleaner (`LocalMemoryCleaner`, tag `[memory-tdai][cleaner]`) deletes aged L0 (`conversations/`) and L1 (`records/`) shards when cleanup is enabled.

| Setting | Rule |
| --- | --- |
| `capture.l0l1RetentionDays: 0` | Cleanup **disabled** (default) |
| `>= 3` | Retention honored |
| `1` or `2` | Honored only if `capture.allowAggressiveCleanup: true`; otherwise cleaned retention is ignored |
| `cleanTime` | Daily run time (default `03:00` local) |

Safety floors: skip deletion if total L0 records `<= 50` or L1 records `<= 20`. Cleanup uses **local calendar days**, not rolling 24h windows.

Offload artifacts use separate `offload.offloadRetentionDays` (reclaim scheduler only when `>= 3`).

## Context offload patch missing or slot not owned

Offload requires three independent conditions:

1. **`offload.enabled: true`** under the plugin config  
2. **`plugins.slots.contextEngine: "memory-tencentdb"`** so OpenClaw routes the context-engine slot here  
3. **Runtime patch** so `after-tool-call` hooks receive the full messages list:

```bash
bash scripts/openclaw-after-tool-call-messages.patch.sh
# or one-shot enable:
bash scripts/setup-offload.sh --enable --user-id <id> --backend-url <url> [--backend-api-key <key>]
```

The patch is **idempotent** (skips already-patched files; backups as `*.pre-offload-patch.bak`). Re-run after every OpenClaw upgrade.

### Log signals when offload is dead

| Log fragment | Meaning |
| --- | --- |
| `slots.contextEngine=… (expected "memory-tencentdb")` | Slot not assigned — **all** offload functions disabled |
| `registerContextEngine returned { ok: false, existingOwner: … }` | Another plugin owns the slot |
| `backendUrl not configured` | Backend mode cannot run L1/L1.5/L2/L4 |
| `No model resolved` / `LLM client not available` | Local offload LLM path disabled |

## Hermes Gateway not available

The Hermes provider is an HTTP client + supervisor for the Node Gateway (default `127.0.0.1:8420`).

### Startup paths

1. **Auto-discovery** of `src/gateway/server.ts` (in-tree → `~/.memory-tencentdb/tdai-memory-openclaw-plugin/` → legacy paths)  
2. **`MEMORY_TENCENTDB_GATEWAY_CMD`** explicit command (wins over discovery)  
3. **Pre-started** Gateway already healthy on `/health`

### Failures

- Directory name must be exactly `memory_tencentdb` (underscore) under Hermes plugins  
- Config aliases `memory-tencentdb` / `tdai` are valid **config** values, not directory names  
- Tools return empty schemas until Gateway is reachable **or** gateway env/port is set optimistically  
- Crash diagnostics: `~/.hermes/logs/memory_tencentdb/gateway.stderr.log` (override `MEMORY_TENCENTDB_LOG_DIR`)  
- LLM for L1/L2/L3 is Gateway-side: `MEMORY_TENCENTDB_LLM_API_KEY` (and optional base URL / model)

```bash
curl -s http://127.0.0.1:8420/health
# or: memory-tencentdb-ctl health / status / logs
```

## Circuit breaker and recovery (Hermes client)

Constants in the Hermes provider:

| Constant | Value | Role |
| --- | --- | --- |
| `_BREAKER_THRESHOLD` | **5** consecutive failures | Opens breaker |
| `_BREAKER_COOLDOWN_SECS` | **60** s | Pause API calls |
| `_RECOVER_COOLDOWN_SECS` | **15** s | Throttle request-path `ensure_running` |
| Watchdog interval | **10** s (production) | Resurrects dead Gateway even with no traffic |
| Capture back-pressure | max **4** in-flight `sync_turn` | 5th waits up to **5** s |

When open, tool calls may return:

```json
{"error": "memory-tencentdb Gateway temporarily unavailable (circuit breaker open)."}
```

Logs: `memory-tencentdb circuit breaker tripped after N failures. Pausing for 60s.`

Recovery paths:

- Request-path `_try_recover_gateway` after failures  
- Lazy probe `_ensure_alive_for_request` before short-circuit guards (when breaker closed)  
- Watchdog thread (resets breaker on successful revive)

Inspect Gateway health and LLM timeouts; hung L1 extractions also produce capture backlog warnings.

## Auth 401 on Gateway routes

Auth is **opt-in**. When `TDAI_GATEWAY_API_KEY` (or `server.apiKey` in `tdai-gateway.yaml` / JSON) is set:

| Route | Auth |
| --- | --- |
| `GET /health` | Always open |
| All other routes (`POST /recall`, `/capture`, `/search/*`, `/session/end`, `/seed`) | Require `Authorization: Bearer <key>` |

### Error messages

- `Unauthorized: missing Bearer token` — header absent or not `Bearer …`  
- `Unauthorized: invalid token` — constant-time compare failed  

### Client alignment (Hermes)

```bash
# Gateway process
export TDAI_GATEWAY_API_KEY="shared-secret"

# Hermes client (preferred name); falls back to TDAI_GATEWAY_API_KEY
export MEMORY_TENCENTDB_GATEWAY_API_KEY="shared-secret"
```

The Gateway **never** reads `MEMORY_TENCENTDB_GATEWAY_API_KEY`. Whitespace in env values is stripped client-side; mismatched secrets still 401.

When auth is unset, startup logs warn; binding non-loopback without a key triggers a louder warning. CORS `*` also warns at startup.

```bash
curl -s -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"test","session_key":"s1"}' \
  http://127.0.0.1:8420/recall
```

## Log and probe checklist

### OpenClaw

| Probe | Command / path |
| --- | --- |
| Host versions | `openclaw --version`, `node -v` |
| Plugin load | Gateway logs: `[memory-tdai]` |
| Config parse | `[memory-tdai] Config parsed: capture=… recall=…` |
| Recall path | `[memory-tdai] [recall]` — strategy, hits, timeout |
| Pipeline | `[pipeline]` / extraction scheduling |
| Cleaner | `[memory-tdai][cleaner]` |
| Offload | `[context-offload]` |
| Embedding | `[memory-tdai][embedding]` |
| Data dir | `${OPENCLAW_STATE_DIR:-~/.openclaw}/memory-tdai/` |
| Gateway logs | `~/.openclaw/logs/gateway.log`, `gateway.err.log` |
| Rolling logs | `/tmp/openclaw/openclaw-YYYY-MM-DD.log` |

### Hermes / standalone Gateway

| Probe | Path / command |
| --- | --- |
| Health | `GET http://127.0.0.1:8420/health` |
| Supervisor logs | `~/.hermes/logs/memory_tencentdb/gateway.stdout.log` / `.stderr.log` |
| Agent log | `~/.hermes/logs/agent.log` (auto-discovery lines) |
| Data dir | `TDAI_DATA_DIR` or `~/.memory-tencentdb/memory-tdai` |
| Ops CLI | `memory-tencentdb-ctl status|health|logs` |

### Diagnostic export package

```bash
bash scripts/export-diagnostic.sh
# optional: bash scripts/export-diagnostic.sh /tmp
```

Produces `~/Downloads/openclaw-diagnostic-<timestamp>.tar.gz` with redacted config, logs, and full `memory-tdai/` data. **Memory data contains raw conversations** — review before sharing. Export stays local; nothing is uploaded automatically.

Log search guide after export:

| Question | Search |
| --- | --- |
| Plugin loaded? | `[memory-tdai]` |
| Recall working? | `[recall]` |
| L1/L2/L3 scheduled? | `[pipeline]` |
| Embedding config | redacted config `plugins.entries` / embedding block |
| Checkpoint | `memory-tdai/.metadata/recall_checkpoint.json` |

## Smoke tests after a fix

<Tabs>
  <Tab title="OpenClaw">
```bash
openclaw gateway restart
# chat 2–3 turns with memorable facts, then start a new turn
# call tdai_memory_search / tdai_conversation_search
ls "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/memory-tdai/"{conversations,records,scene_blocks}
```
  </Tab>
  <Tab title="Hermes">
```bash
curl -s http://127.0.0.1:8420/health
# confirm memory.provider: memory_tencentdb in ~/.hermes/config.yaml
# watch agent.log for Gateway auto-discovery or breaker warnings
```
  </Tab>
</Tabs>

## Related pages

<CardGroup>
  <Card title="Configure OpenClaw" href="/configure-openclaw">
    Enable flag, capture/pipeline/recall groups, and post-restart verification.
  </Card>
  <Card title="Configure embedding" href="/configure-embedding">
    Remote providers, required fields, sendDimensions, and keyword-only degradation.
  </Card>
  <Card title="Enable context offload" href="/enable-context-offload">
    Slot registration, after-tool-call patch, and compression ratios.
  </Card>
  <Card title="Secure the Gateway" href="/secure-gateway">
    TDAI_GATEWAY_API_KEY, CORS, and Hermes client key alignment.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    memory-tencentdb-ctl start/stop/status/health/logs.
  </Card>
  <Card title="Inspect local memory" href="/inspect-local-memory">
    read-local-memory, diagnostic export, and on-disk layout.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Routes, auth exceptions, and error envelope.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    TDAI_* and MEMORY_TENCENTDB_* resolution order.
  </Card>
</CardGroup>

---

## 24. Contributing

> Local development with openclaw plugins install --link, build and test scripts, project layout, CI expectations, and contribution workflow from CONTRIBUTING.md.

- Page Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/pages/24-contributing.md
- Generated: 2026-08-04T18:36:32.433Z

### Source Files

- `CONTRIBUTING.md`
- `package.json`
- `vitest.config.ts`
- `tsdown.config.ts`
- `.github/workflows/pr-ci.yml`
- `CHANGELOG.md`
- `LICENSE`

---
title: "Contributing"
description: "Local development with openclaw plugins install --link, build and test scripts, project layout, CI expectations, and contribution workflow from CONTRIBUTING.md."
---

Contributions to **TencentDB Agent Memory** (`@tencentdb-agent-memory/memory-tencentdb`) land as GitHub Issues/PRs against `main`, with DCO sign-off required on every commit. Local OpenClaw development uses `openclaw plugins install --link .` so the gateway loads TypeScript sources in place; packaging uses `tsdown` + script builds, and PR CI validates install, pack, plugin manifest metadata, and a 2 MB tarball size limit (it does not run Vitest).

## Prerequisites

| Requirement | Constraint | Source of truth |
| --- | --- | --- |
| Node.js | `>=22.16.0` | `package.json` `engines.node` |
| Package manager | npm or pnpm | `CONTRIBUTING.md` |
| OpenClaw (for plugin host work) | Plugin API / min gateway `>=2026.3.13` | `package.json` `openclaw.compat` |
| Peer `openclaw` (optional) | `>=2026.3.7` | `peerDependencies` |
| Peer `node-llama-cpp` (optional) | `^3.16.2` | `peerDependenciesMeta` (optional) |

OpenClaw is optional as a peer when you only build the published package or run the standalone Gateway / Hermes path. Linked plugin development and hook registration need a compatible OpenClaw install.

## Ways to contribute

| Kind | Where | Notes |
| --- | --- | --- |
| Bug reports | GitHub Issues (`bug_report.yml`) | Title prefix `[Bug]`, label `bug` |
| Feature requests | Issues (`feature_request.yml`) | Title prefix `[Feature]`, label `enhancement` |
| Questions | Issues (`question.yml`) | Title prefix `[Question]`, label `question` |
| Docs / examples | PR against `main` | Prefer README + `CHANGELOG.md` when behavior changes |
| Code | PR against `main` | Focused commits; run local tests before opening |

Security vulnerabilities: email **agentmemory@tencent.com** (do not open a public Issue for exploits).

## Local development (OpenClaw link)

OpenClaw loads the plugin from source when linked. Node 22.16+ type-stripping means **no build step is required** for edit → restart → verify on a linked checkout.

<Steps>
  <Step title="Clone and install">
```bash
git clone https://github.com/Tencent/TencentDB-Agent-Memory.git
cd TencentDB-Agent-Memory
npm install
```
  </Step>
  <Step title="Link into OpenClaw">
```bash
openclaw plugins install --link .
```
`install --link` registers the current directory as a local plugin. Edit TypeScript under the repo root; **restart the OpenClaw gateway** for changes to take effect.
  </Step>
  <Step title="Enable and exercise">
Enable `memory-tencentdb` in OpenClaw config, restart the gateway, and verify capture/recall or tools as needed. See related install and config pages for production-shaped setup.
  </Step>
</Steps>

<Note>
`npm install` runs `postinstall`: `scripts/openclaw-after-tool-call-messages.patch.sh` (errors ignored with `|| true`). CI installs with `npm install --ignore-scripts`, so postinstall is **not** exercised in PR CI.
</Note>

## Build scripts

Development against a linked tree does not need `dist/`. Publishing and `npm pack` do: `prepack` runs `npm run build`.

| Script | Command | Purpose |
| --- | --- | --- |
| `build` | `npm run build` | `build:plugin` then `build:scripts` |
| `build:plugin` | `tsdown` | Bundle plugin entry `./index.ts` → `./dist` (ESM) |
| `build:scripts` | three `tsc` projects | Compile migrate / export / read-local-memory CLIs |
| `prepack` | `npm run build` | Ensures pack/publish includes built artifacts |
| `migrate-sqlite-to-tcvdb` | `node ./bin/migrate-sqlite-to-tcvdb.mjs` | Offline SQLite → TCVDB migration CLI |
| `export-tencent-vdb` | `node ./bin/export-tencent-vdb.mjs` | Export TCVDB collections |
| `read-local-memory` | `node ./bin/read-local-memory.mjs` | Inspect local L0–L3 artifacts |

### Plugin bundle (`tsdown.config.ts`)

- **Entry:** `./index.ts`
- **Output:** `./dist`, format `esm`, platform `node`, `clean: true`, `fixedExtension: true`
- **Not bundled:** `openclaw` / `openclaw/*`, `node:` builtins, and all declared `dependencies`, `peerDependencies`, and `optionalDependencies`
- **DTS / sourcemaps:** off (`dts: false`, `sourcemap: false`)

Published package name: `@tencentdb-agent-memory/memory-tencentdb`. Plugin id in `openclaw.plugin.json`: `memory-tencentdb` (CLI aliases include `memory-tdai`).

## Tests

### Node (Vitest)

| Script | Behavior |
| --- | --- |
| `npm test` | `vitest run` (unit/integration config) |
| `npm run test:watch` | Vitest watch mode |
| `npm run test:coverage` | `vitest run --coverage` (v8; reporters `text`, `html`, `lcov`) |

Default config (`vitest.config.ts`):

- **Environment:** `node`, pool `forks`
- **Include:** `src/**/*.test.ts`, `__tests__/**/*.test.ts`
- **Exclude:** `dist/**`, `node_modules/**`, `**/*.e2e.test.ts`
- **Timeouts:** test and hook `120_000` ms
- **Isolation:** `clearMocks`, `restoreMocks`, `unstubEnvs`, `unstubGlobals`
- **Coverage include:** `src/**/*.ts`, `index.ts` (excludes `*.test.ts`)

E2E config (`vitest.e2e.config.ts`) includes only `**/*.e2e.test.ts` (separate from default `npm test`).

Existing co-located unit tests include (non-exhaustive):

- `src/utils/time.test.ts`
- `src/utils/sanitize.test.ts`
- `src/utils/no-think-fetch.test.ts`
- `src/offload/auth-profile-key.test.ts`

### Hermes (Python)

Provider recovery and gateway lifecycle tests live under:

`hermes-plugin/memory/memory_tencentdb/tests/`

Examples: `test_memory_tencentdb_recovery.py`, `test_gateway_shutdown_leak.py` (pytest + mocks; no real Node gateway required).

<Warning>
PR CI (`.github/workflows/pr-ci.yml`) does **not** invoke `vitest` or pytest. Run `npm test` (and Hermes tests when touching `hermes-plugin/`) locally before opening a PR.
</Warning>

## Project layout

`CONTRIBUTING.md` still shows a flat `src/conversation` tree. The implemented layout nests host-neutral memory under `src/core/`, with separate adapters, gateway, offload, and CLI surfaces:

:::files
.
├── index.ts                 # OpenClaw plugin shell (tools, hooks → TdaiCore)
├── openclaw.plugin.json     # Plugin id, configSchema, tool contracts
├── package.json             # Scripts, engines, openclaw.compat, exports
├── tsdown.config.ts         # Plugin ESM bundle
├── vitest.config.ts         # Unit tests
├── vitest.e2e.config.ts     # E2E Vitest include set
├── bin/                     # Packaged CLI entrypoints (.mjs)
├── src/
│   ├── config.ts            # Config parse / schema
│   ├── adapters/            # openclaw + standalone host adapters
│   ├── cli/                 # memory-tdai CLI (e.g. seed)
│   ├── core/
│   │   ├── tdai-core.ts     # Host-neutral facade
│   │   ├── conversation/    # L0 capture
│   │   ├── record/          # L1 extract / write / dedup
│   │   ├── scene/           # L2 scene blocks
│   │   ├── persona/         # L3 persona
│   │   ├── store/           # sqlite / tcvdb / embedding / BM25
│   │   ├── hooks/           # auto-capture / auto-recall
│   │   ├── prompts/         # LLM prompt templates
│   │   ├── tools/           # memory + conversation search
│   │   ├── seed/            # Historical seed path
│   │   └── report/          # Metrics / health reporting
│   ├── gateway/             # Standalone HTTP TdaiGateway
│   ├── offload/             # Context engine / compression
│   └── utils/               # Pipeline, cleaner, time, sanitize, …
├── hermes-plugin/           # Hermes memory_tencentdb provider (Python)
├── scripts/                 # ctl, install, patches, migrate/export sources
├── docker/opensource/       # Hermes Dockerfile + docs
├── CHANGELOG.md
├── CONTRIBUTING.md
└── LICENSE
:::

Useful scope tags for commits (from `CONTRIBUTING.md`): `store`, `hooks`, `persona`, `scene`, `record`, `conversation`, `gateway`, `hermes`. Additional practical scopes that match the tree: `offload`, `config`, `cli`, `adapters`.

## CI expectations

Workflow: `.github/workflows/pr-ci.yml`  
**Trigger:** `pull_request` → `main`  
**Concurrency:** `ci-${{ github.ref }}` with `cancel-in-progress: true`  
**Node:** `22` (`actions/setup-node@v4`)

| Job | Depends on | What it checks |
| --- | --- | --- |
| **Install** | — | Cache `node_modules` keyed by OS + `package.json` hash; on miss, `npm install --ignore-scripts` |
| **Pack** | Install | `npm pack --dry-run`, then `npm pack`; upload `*.tgz` artifact (7-day retention) |
| **Manifest** | — (independent) | `openclaw.plugin.json` exists, valid JSON, has string `id`; if `configSchema` present it must be an object; `package.json` `openclaw.extensions`, `openclaw.compat.pluginApi`, `openclaw.build.openclawVersion` required |
| **Size Guard** | Pack | Tarball size ≤ **2048 KB** |

Because `prepack` runs `npm run build`, `npm pack` in CI exercises the tsdown + script TypeScript builds. Failures common on PRs:

- Broken `openclaw.plugin.json` or missing `package.json` `openclaw.*` metadata
- Accidental large files bloating the tarball over 2 MB
- Build breakage under `prepack` / `tsdown` / script `tsc` projects

What CI does **not** currently enforce: unit coverage, e2e, Hermes pytest, lint/format gates, or DCO bot automation (DCO is still a merge policy in `CONTRIBUTING.md`).

## Pull request workflow

1. **Fork** and branch from `main` (default PR target).
2. **Change** in focused, atomic commits.
3. **Test** locally (`npm test`; Hermes tests if applicable).
4. **Docs** — update README / `CHANGELOG.md` when user-facing behavior changes (Keep a Changelog + SemVer in `CHANGELOG.md`).
5. **Open a PR** using `.github/PULL_REQUEST_TEMPLATE.md`: description, related issue, change type, self-test checklist.

### PR change types (template)

- Bug fix  
- New feature  
- Documentation update  
- Code optimization  

### Commit messages

```
<type>(<scope>): <short summary>

<optional body>

Closes #123
Signed-off-by: Your Name <your-email@example.com>
```

| Type | Meaning | Maps to PR checkbox |
| --- | --- | --- |
| `fix` | Bug fix | Bug fix |
| `feat` | New feature | New feature |
| `docs` | Documentation | Documentation update |
| `perf` | Performance | Code optimization |
| `refactor` | No behavior change | Code optimization |
| `test` | Tests | — |
| `chore` | Build / tooling / deps | — |

### Developer Certificate of Origin (DCO)

Every commit must include a valid `Signed-off-by` line (Developer Certificate of Origin). Commits without it will not be merged.

```bash
git commit -s -m "feat(store): add batch insert support"
```

## Code style

- Match existing TypeScript style in the tree.
- Prefer clear English identifiers.
- Comment **why** at non-obvious logic, not restatements of **what**.
- Import order: Node built-ins → third-party → internal modules.

## License

Contributions are accepted under the **MIT** license (`LICENSE`). Copyright notice: Tencent, 2026.

## Local self-check before PR

| Check | Command / action |
| --- | --- |
| Unit tests | `npm test` |
| Coverage (optional) | `npm run test:coverage` |
| Full package build | `npm run build` |
| Pack dry-run (mirrors CI) | `npm pack --dry-run` |
| Manifest sanity | Confirm `openclaw.plugin.json` `id` and `package.json` `openclaw` block still valid |
| Linked plugin smoke | `openclaw plugins install --link .`, restart gateway, exercise change |
| Hermes changes | Run pytest under `hermes-plugin/memory/memory_tencentdb/tests/` |
| DCO | `git commit -s` on each commit |
| Changelog | Update `[Unreleased]` / version notes when behavior ships |

## Next

<CardGroup>
  <Card title="Installation" href="/installation">
    Node/OpenClaw prerequisites, npm and plugin install, postinstall patch behavior.
  </Card>
  <Card title="Overview" href="/overview">
    Host surfaces (OpenClaw plugin, Hermes Gateway) and first routes through the system.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Bin commands and memory-tdai namespace used while developing tooling changes.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    memory-tencentdb-ctl start/stop/status for standalone and Hermes gateway work.
  </Card>
  <Card title="Plugin configuration reference" href="/plugin-config-reference">
    Full memory-tencentdb config schema when extending config or openclaw.plugin.json.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Source-backed failure modes useful when local link or gateway smoke checks fail.
  </Card>
</CardGroup>

---
