# Seed historical conversations

> Run openclaw memory-tdai seed with Format A/B JSON input, config overrides, output directory layout, and L0→L1→L2→L3 verification signals.

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

## Source Files

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

---

---
title: "Seed historical conversations"
description: "Run openclaw memory-tdai seed with Format A/B JSON input, config overrides, output directory layout, and L0→L1→L2→L3 verification signals."
---

`openclaw memory-tdai seed` imports historical conversation JSON into a dedicated pipeline data directory: it validates Format A/B input, runs synchronous L0 capture via `performAutoCapture`, drains L1 on the `everyNConversations` boundary, wires L2/L3 runners from the shared pipeline factory, and writes a seed section into `.metadata/manifest.json`. The same `executeSeed` runtime powers Gateway `POST /seed`.

## Prerequisites

- OpenClaw plugin installed so the `memory-tdai` CLI namespace is registered (`api.registerCli` → `registerMemoryTdaiCli` → `registerSeedCommand`).
- LLM credentials available for L1 extraction (and L2/L3 if those stages run): either OpenClaw host config, or plugin `llm` settings used by `StandaloneLLMRunnerFactory` when `cfg.llm.enabled` and `cfg.llm.apiKey` are set.
- Optional embedding/store backend config if you need vector-backed L1 (sqlite-vec or tcvdb); defaults come from plugin config / `parseConfig`.
- Input file is non-empty JSON (Format A or B).

<Note>
Seed writes to an **isolated output directory**, not necessarily the live `~/.openclaw/memory-tdai` tree. Point live runtime at that directory only if you intentionally want seeded data as the active store.
</Note>

## Command

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

### Flags

| Flag | Required | Default | Behavior |
|------|----------|---------|----------|
| `--input <file>` | yes | — | Path to Format A/B JSON |
| `--output-dir <dir>` | no | `<stateDir>/memory-tdai-seed-<YYYYMMDD-HHmmss>` | Pipeline data root (`stateDir` is typically `~/.openclaw`) |
| `--session-key <key>` | no | from input, else `"seed-user"` | Fallback when a session omits `sessionKey` during normalize |
| `--config <file>` | no | plugin config only | JSON deep-merged on top of current plugin config |
| `--strict-round-role` | no | `false` | Each round must include at least one `user` and one `assistant` message |
| `--yes` | no | `false` | Skip interactive timestamp auto-fill confirmation |

### Examples

```bash
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
openclaw memory-tdai seed --input data.json --yes
openclaw memory-tdai seed --input data.json --config seed-config.json --strict-round-role --yes
```

## Input formats

Both formats are detected in `loadAndValidateInput` / `extractSessions`.

### Format A — object wrapper

```json
{
  "sessions": [
    {
      "sessionKey": "user-alice",
      "sessionId": "conv-001",
      "conversations": [
        [
          { "role": "user", "content": "Hello", "timestamp": 1711929600000 },
          { "role": "assistant", "content": "Hi — how can I help?", "timestamp": 1711929601000 }
        ],
        [
          { "role": "user", "content": "What is the weather today?" },
          { "role": "assistant", "content": "Clear and sunny." }
        ]
      ]
    }
  ]
}
```

### Format B — top-level array

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

### Session and message fields

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `sessionKey` | string | yes (non-empty) | User/channel identity; multiple sessions may share a key with different `sessionId` |
| `sessionId` | string | no | Auto `crypto.randomUUID()` if omitted |
| `conversations` | `message[][]` | yes | Outer array = rounds; each inner array = messages in that round |
| `role` | string | yes | Non-empty string; typically `user` / `assistant` |
| `content` | string | yes | Non-empty after trim |
| `timestamp` | number \| string | no | Epoch **milliseconds** (integer) or ISO 8601 string |

## Validation layers

Seed runs a six-layer check before the pipeline starts:

| Layer | Stage id | What fails |
|-------|----------|------------|
| 1 | `file` | Missing file, empty file, JSON parse error |
| 2 | `top_level` | Neither Format A (`sessions` array) nor Format B (top-level array) |
| 3 | `session` | Empty sessions list; missing/empty `sessionKey`; `conversations` not an array |
| 4 | `round` | Round not an array; empty round; optional `--strict-round-role` role gaps |
| 5 | `message` | Missing `role`/`content`; bad timestamp type/format |
| 6 | `timestamp_consistency` | **Mixed** timestamps (some present, some missing) |

<Warning>
Timestamps are **all-or-none**. Mixed presence is a hard error. If **all** messages omit timestamps, CLI prompts to fill with current time (or auto-fills with `--yes`). Gateway `POST /seed` auto-fills by default (`auto_fill_timestamps`, default `true`).
</Warning>

Auto-fill uses a single monotonically increasing counter across **all** sessions (`Date.now()` then `+100` ms per message) so L0 capture cursors do not drop later sessions that share a `sessionKey`.

Validation failures throw `SeedValidationError` and exit the CLI with a multi-line summary (`[stage] session[i] round[j] msg[k] ...`).

## Config overrides

`--config` loads a JSON object and **two-level deep-merges** it onto the plugin config from OpenClaw:

- Top-level key is a plain object on both sides → shallow-merge children (`{ ...base, ...override }`).
- Otherwise → override wins.

Missing override file, invalid JSON, or non-object root → CLI exits with an error.

### Accelerate pipeline for bulk seed

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

Defaults without override (from `parseConfig`): `everyNConversations=5`, `enableWarmup=true`, `l1IdleTimeoutSeconds=600`, `l2DelayAfterL1Seconds=10`, `l2MinIntervalSeconds=900`, `l2MaxIntervalSeconds=3600`.

### Seed into an isolated TCVDB database

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

## Pipeline behavior

```text
JSON input
   │
   ▼
loadAndValidateInput / validateAndNormalizeRaw
   │
   ▼
createSeedPipeline (createPipeline + L2/L3 runners)
   │
   ▼
per session → per round
   performAutoCapture (L0)     pluginStartTimestamp = 0
   every everyN rounds → waitForL1Idle
   end of session → waitForL1Idle
   final → waitForL1Idle (all session keys)
   │
   ▼
pipeline.destroy()
manifest.seed written
SeedSummary printed / returned
```

### L0 capture

- Each round is mapped to `{ role, content, timestamp }` and passed to `performAutoCapture`.
- `pluginStartTimestamp` is forced to **`0`** so the live cold-start guard does not drop historical messages.
- L0 failures per round are logged; the loop continues.

### L1 batching and idle wait

- After every `everyNConversations` rounds **within a session**, seed pauses and polls `waitForL1Idle` so L1 batches align with the every-N boundary (mid-batch: poll 500 ms, 2 stable rounds, max 120 s; session tail / final: poll 1 s, 3 stable rounds, max 300 s).
- Idle means: scheduler reports `l1Idle`, buffered message count is 0, and session `conversation_count` is 0.
- If max wait elapses, seed logs a warning and proceeds.

### L2 / L3 (important constraint)

L2 (scene) and L3 (persona) runners are **wired** the same way as live runtime (`createL2Runner` / `createL3Runner`), but seed **only waits for L1 idle** before `pipeline.destroy()`. L2/L3 may still be in flight and can be interrupted. Summary counters report L0/L1-oriented stats (`l0RecordedCount`); they do not claim complete L2/L3 materialization.

For denser L2/L3 artifacts after seed, lower L2 interval delays via `--config`, keep the process alive longer after L1 drain when possible, and inspect `scene_blocks/` and `persona.md` rather than relying on the summary box alone.

### Interrupt handling

- First `SIGINT`: finish the current round, stop feeding new rounds, then destroy the pipeline.
- Second `SIGINT`: force `process.exit(1)`.

### Output directory guards

| Condition | Result |
|-----------|--------|
| `--output-dir` set | `path.resolve` that path |
| unset | `<stateDir>/memory-tdai-seed-<YYYYMMDD-HHmmss>` |
| dir exists + `.metadata/checkpoint.json` | Error: resume not implemented in P0 — use a new directory |
| dir exists, non-empty, no checkpoint | Error: clean or choose another directory |
| dir empty or missing | Proceed |

## Output directory layout

Created via `initDataDirectories` plus store init:

:::files
&lt;output-dir&gt;/
├── conversations/     # L0 JSONL (by day)
├── records/           # L1 JSONL
├── scene_blocks/      # L2 scene markdown (if L2 completes)
├── persona.md         # L3 persona (if L3 completes; dataDir root)
├── vectors.db         # sqlite backend only
├── .metadata/
│   ├── manifest.json  # store binding + seed run record
│   └── checkpoint.json
└── .backup/
:::

### `manifest.json` seed section

On success, `executeSeed` appends:

```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 stored as basename only. Manifest update failures are non-fatal.

## CLI progress and summary

During run:

```text
[12/42] 29% session=user-alice stage=l0_captured
[15/42] 36% session=user-alice stage=l1_waiting
```

On completion:

```text
╔══════════════════════════════════════════╗
║               Seed Summary               ║
╠══════════════════════════════════════════╣
║  Sessions:              3                ║
║  Rounds:               42                ║
║  Messages:            128                ║
║  L0 recorded:         128                ║
║  Duration:           45.2s               ║
╚══════════════════════════════════════════╝

📁 Output: /path/to/output-dir
```

## Verification signals (L0 → L1 → L2 → L3)

Use the seed **output directory** as the data root.

| Layer | Signal | How to check |
|-------|--------|--------------|
| **Run ok** | Summary box + logs | `Sessions` / `Rounds` / `L0 recorded` non-zero; logs contain `[memory-tdai] [seed] Seed complete` |
| **Manifest** | `seed` block present | Read `<output-dir>/.metadata/manifest.json` — `seed.sessions`, `seed.rounds`, `seed.messages`, timestamps |
| **L0** | Conversation JSONL | Files under `conversations/` (typically `YYYY-MM-DD.jsonl`) grow with seeded messages |
| **L1** | Memory records | Files under `records/`; vector rows in `vectors.db` when `storeBackend=sqlite` and embedding is enabled |
| **L2** | Scene blocks | Non-empty `scene_blocks/*.md` (may be incomplete if destroy cut L2 short) |
| **L3** | Persona | `persona.md` at output root (may be absent if L3 never finished or persona trigger threshold not reached) |
| **Search smoke** | Agent tools / Gateway | Point tools or Gateway data dir at the seed output, then call `tdai_memory_search` / `tdai_conversation_search` or Gateway search endpoints |

<Check>
Minimum healthy seed: CLI exits 0, summary shows expected session/round counts, `conversations/` has content, `manifest.json` has a filled `seed` object, and `records/` (or vector store) shows L1 output after L1 idle wait.
</Check>

## Gateway alternative: `POST /seed`

Standalone/Hermes Gateway reuses `validateAndNormalizeRaw` + `executeSeed`.

| Item | Value |
|------|--------|
| Method / path | `POST /seed` |
| Body | `SeedRequest`: `data` (required Format A/B), optional `session_key`, `strict_round_role`, `auto_fill_timestamps` (default `true`), `config_override` |
| Output dir | `<gateway data.baseDir>/seed-<YYYYMMDD-HHmmss>` (not CLI `memory-tdai-seed-…`) |
| LLM | Gateway injects its `llm` block (`enabled: true` + baseUrl/apiKey/model/…) into plugin config before merge |
| Response | `sessions_processed`, `rounds_processed`, `messages_processed`, `l0_recorded`, `duration_ms`, `output_dir` |
| Validation errors | HTTP 400 with `error` + `validation_errors` |

Request is **blocking** and can take minutes for large inputs.

## Failure modes

| Symptom | Likely cause | Recovery |
|---------|--------------|----------|
| `Input file not found` / parse error | Bad `--input` path or JSON | Fix file path/content |
| `Timestamp consistency check failed` | Mixed timestamps | Supply timestamps for all messages or remove all |
| Prompt / abort on missing timestamps | Interactive confirm declined | Re-run with timestamps or `--yes` |
| `Resume from checkpoint is not implemented` | Output dir has `.metadata/checkpoint.json` | New `--output-dir` or remove old dir intentionally |
| `Output directory already exists and is not empty` | Stale files without checkpoint | Clean dir or choose another path |
| Config override not found / not object | Bad `--config` | Fix path; root must be a JSON object |
| L0 recorded but empty `records/` | L1 LLM failure, capture disabled, or interrupt | Check `[memory-tdai] [seed]` / L1 logs; verify `extraction.enabled` and LLM credentials |
| L1 present, no `scene_blocks/` / `persona.md` | L2/L3 not awaited or thresholds not met | Tune `pipeline` / `persona` via `--config`; re-seed or re-run live pipeline on the data dir |
| Second Ctrl+C force exit | User force quit | Treat as partial; re-seed into a **new** directory |

## Related pages

<CardGroup>
  <Card title="Memory layers" href="/memory-layers">
    L0 conversation, L1 atom, L2 scene, and L3 persona model and storage shapes.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full `openclaw memory-tdai seed` flag inventory and package bins.
  </Card>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    `POST /seed` envelope, auth, and response fields for the Hermes sidecar.
  </Card>
  <Card title="Data directories" href="/data-directories">
    On-disk trees for OpenClaw, context-offload, and Hermes roots.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Full plugin schema including `pipeline`, `llm`, embedding, and store backends used by seed overrides.
  </Card>
  <Card title="Configure storage backends" href="/configure-storage">
    sqlite vs tcvdb and embedding settings for seed output stores.
  </Card>
  <Card title="Agent tools" href="/agent-tools">
    `tdai_memory_search` and `tdai_conversation_search` for post-seed smoke checks.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Broader failure modes: silent plugin, embedding degrade, Gateway circuit breaker.
  </Card>
</CardGroup>
