Background

I run an AI assistant called Hachimi on my server — built on the OpenClaw platform, connected to Telegram, handling various daily tasks and controlling a Windows Chrome browser remotely. One afternoon she started behaving strangely: every time she called a tool, the raw tool prompt was printed verbatim into the chat.

Two kinds of symptoms. First, raw tool call JSON printed directly into the chat:

{"type":"tool_use","id":"toolu_01...","name":"exec","input":{"command":"..."}}
{"type":"tool_result","tool_use_id":"toolu_01...","content":"..."}
...

Second, and stranger — </s> tokens started appearing in responses:

Something went wrong — the response contained a large number of </s> tokens, which indicates a model anomaly.

Let me try calling a tool normally now:
</s>
</s>
</s>

</s> is an end-of-sequence token that should never appear in output text. Its presence means the context contains something that doesn't belong to the current conversation — the model is trying to "end" a sequence it thinks is still open.

The chat filled up with JSON and garbage tokens. Context was polluted and kept growing, leaving almost no usable token window for actual responses. I tried /new to reset the session — no effect. Restarted the gateway — still broken.

Eventually I made the call: reinstall.

System Architecture

Telegram
    ↕
Hachimi (Claude Sonnet)
    ↕  OpenClaw Gateway (AWS EC2)
    ↕  WSL2 Node Host (local Windows machine)
    ↕  Windows Chrome CDP
         ↕  reverse SSH tunnel
    local port on server

The system spans three places: the gateway on the server, the node host inside WSL2, and the Chrome CDP interface on Windows. A reinstall doesn't mean running one command — every one of these connections has to be re-established.

02 Symptom and Root Cause

The symptom was clear: every time the AI invoked a tool (exec, web_search, etc.), Telegram received the raw tool_use / tool_result JSON instead of a processed response.

This kind of issue usually means the context assembly went wrong — tool results got inserted into the message sequence in the wrong format, so subsequent prompt rounds carried raw structured data from the previous turn. Essentially, the session history had a tool_use / tool_result mismatch, the provider rejected it, and the gateway bubbled the error back as-is.

Session reset can't fix this because the underlying history format is already corrupted. The cleanest solution is to start fresh.

Interestingly, OpenClaw's gateway already has a defensive layer for exactly this kind of leakage — before sending anything to Telegram, it strips <function_calls> and <function_response> tags from the output:

async sendMessage(chatId, text, options = {}) {
  text = text
    .replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "")
    .replace(/<function_response>[\s\S]*?<\/function_response>/g, "")
    .trim();
  // ...
}

Which tells you this isn't the first time someone's hit this problem — the gateway already anticipated it. But when the context is corrupted enough, filtering isn't sufficient. Bare tokens like </s> fall outside the filter and pass through unchanged.

03 Reinstall Process

Step 1 — Pull the workspace

All of Hachimi's config files, memory, and custom skills live in a private GitHub repository. This is the most important step — getting the "memory" back first.

cd /home/ubuntu/.openclaw/workspace
git init
git remote add origin https://github.com/<user>/<repo>.git
git fetch origin
git reset --hard origin/master
git branch --set-upstream-to=origin/master master

Once pulled, the workspace has MEMORY.md, SOUL.md, the memory/ journal directory, and several custom skills (self-commit, self-followup, session-resume).

Step 2 — Configure openclaw.json

OpenClaw's core config file is ~/.openclaw/openclaw.json. After a fresh install, it's blank. The main things to fill in:

  • Model configuration

  • Gateway port and auth token

  • Telegram bot token and channel allowlist

  • API keys (LLM, search, speech-to-text, integrations)

  • memory-lancedb plugin (vector memory)

Most keys were already stored in pass. This time I also backed everything up to AWS SSM Parameter Store for easier recovery next time:

aws ssm put-parameter --name "/mybot/<key-name>" \
  --value "..." --type SecureString --overwrite

Step 3 — Restart gateway and verify plugins

systemctl --user restart openclaw-gateway
openclaw status

Checking for:

Memory   │ enabled (plugin memory-lancedb)
Telegram │ ON  │ OK

One gotcha here: the memory-lancedb config fields autoCapture and autoRecall need to go inside entries.memory-lancedb.config, not directly under entries. The gateway's config validator caught it, but it took a couple of attempts to get the nesting right.

Step 4 — Re-import memory into LanceDB

LanceDB uses lazy init — the database is empty after gateway startup. All the previous memory files need to be re-embedded and re-ingested.

NODE_PATH=/home/ubuntu/.npm-global/lib/node_modules \
OAI_KEY='sk-proj-...' \
node /tmp/import_memory.js

The script walks all memory/*.md and MEMORY.md, splits them into ~800-character chunks, calls OpenAI's text-embedding-3-small, and writes to LanceDB. Ended up with 150 records ingested.

Step 5 — Rebuild cron jobs

Cron jobs weren't stored in the workspace (lesson learned), so they were all gone after reinstall:

openclaw cron add --name "my-task" \
  --cron "0 9 * * *" --tz "America/New_York" \
  --session isolated --announce --channel telegram \
  --timeout-seconds 300 \
  --message "run task..."

Practical tip: keep cron job definitions in a file in your workspace, so reinstall means running a script, not typing from memory.

04 Reconnecting the WSL2 Node

How the connection works

Hachimi controls Windows Chrome through an OpenClaw node host running inside WSL2, combined with a reverse SSH tunnel that maps the Chrome CDP port back to the server:

# WSL2: SSH tunnel + node host
ssh -fN -L <local-port>:127.0.0.1:<gateway-port> -i ~/.ssh/key.pem user@server
OPENCLAW_GATEWAY_TOKEN=xxx openclaw node run \
  --host 127.0.0.1 --port <local-port> --display-name "Windows WSL2"

# Verify Chrome CDP is reachable from server
curl -s http://127.0.0.1:<cdp-port>/json/version

The token changed during reinstall, so the old hardcoded token in the WSL2 systemd service caused the connection to fail silently. Updated the token, reconnected.

The exec approval problem

Node exec requires approval — but Telegram doesn't have an approval UI. By default, triggering node exec from Telegram just fails:

Exec approval is required, but chat exec approvals are not enabled on Telegram.

The fix requires config on both sides:

WSL2 side (~/.openclaw/exec-approvals.json):

{
  "version": 1,
  "defaults": { "security": "full", "ask": "off", "askFallback": "full" },
  "agents": { "main": { "security": "full", "ask": "off", "askFallback": "full" } }
}

Server side (openclaw.json):

"tools": {
  "exec": { "security": "full", "ask": "off" }
}

Another trap: if you set tools.exec.host = "node" globally in openclaw.json, every exec call gets routed to the node — including server-local commands like gateway restart. This creates a circular deadlock where restarting the gateway also runs on the node. The right approach is to pass host=node per-call, not as a global default.

05 Takeaways

Write a reinstall playbook

The biggest problem during this reinstall was relying on memory — Hachimi's memory, and my own. Several steps had to be pieced together from old memory files and Telegram history.

Afterward I added a SETUP.md to the workspace root with every step, every field, every command, and every mistake documented. Next reinstall: follow the doc.

Centralize secrets in AWS SSM

Keys were previously scattered across pass, openclaw.json, and even Telegram message history. Painful to track down during reinstall.

Everything is now in AWS SSM Parameter Store under a consistent namespace:

aws ssm get-parameter \
  --name "/mybot/<key-name>" \
  --with-decryption \
  --query Parameter.Value --output text

One remaining item: the EC2 instance is still using root credentials to access SSM. That should be swapped out for a proper IAM Role.

Keep MEMORY.md lean

After the reinstall I found MEMORY.md had ballooned to 300 lines, mixing runtime reference material with setup instructions that only matter during reinstall.

Did a cleanup pass: setup content moved to SETUP.md, MEMORY.md trimmed to runtime essentials only. Twenty-odd small journal fragments archived to memory/archive/, with the relevant parts folded into MEMORY.md.

06 Summary

The whole reinstall took about two hours, with Hachimi actively involved throughout — reading her own memory files, hunting for old config values, debugging connections, writing the recovery playbook. In some sense, she was reinstalling herself.

The more complex the system, the more "reinstallability" matters. The lesson here: any state that only exists in memory — human or AI — is fragile. Documentation, version-controlled config, and proper secret management aren't just good engineering hygiene. For an AI assistant, they're how she continues to exist.