I tried to do something that sounds trivial:

  • Add a second Telegram bot
  • Map it to a second isolated agent (separate workspace, SOUL, identity, memory)
  • Keep everything deterministic: Bot A → Agent A, Bot B → Agent B

And yet I managed to hit a surprisingly nasty failure mode:

After adding the second bot, my original bot stopped replying — or worse, it replied using the wrong agent’s workspace and “forgot” all our previous context.

This post is a practical write-up of what went wrong, why it’s so easy to mess up, and the clean configuration that fixes it.

What I wanted

I wanted the “two brains” setup:

  • Bot AAgent A (default)
  • Bot BAgent B (separate persona)

Each agent should have:

  • its own workspace
  • its own SOUL.md, IDENTITY.md, USER.md
  • its own session store and memory

The symptom that revealed the bug

The first symptom was obvious:

  • The new bot replied with a pairing code
  • The old bot went completely silent

The second symptom was more subtle but more important:

  • Sometimes I could still chat, but the assistant didn’t remember previous context
  • The “current workspace” clearly wasn’t the one I expected

That’s when it clicked: this wasn’t a Telegram problem — it was a routing problem.

Two concepts you must not mix up

OpenClaw has two parallel ideas:

  1. Multi-account channels (e.g., Telegram with multiple bot tokens)
  2. Multi-agent routing (multiple isolated agents in one gateway)

They only work cleanly if you commit to the canonical representations.

1) Multi-account: don’t keep one token at the root

When you go multi-account, you should put all accounts under accounts, including the original one as default.

If you keep the old bot token at channels.telegram.botToken while adding a new one under channels.telegram.accounts.secondary, you’ve created a “two-level representation” for the same concept.

Even if it looks harmless, it’s a trap: runtime behavior becomes confusing, and you can end up with only the accounts.* bot actually polling.

The canonical pattern is:

channels: {
  telegram: {
    accounts: {
      default: { botToken: "<OLD>" },
      secondary: { botToken: "<NEW>" }
    }
  }
}

This is exactly how the configuration reference describes multi-account for all channels.

2) Default agent: routing fallback is deterministic

When a message doesn’t match any explicit binding, OpenClaw falls back to a default agent in a very specific order:

  1. agents.list[].default == true
  2. else the first agent in agents.list
  3. else main

If you accidentally end up with only one agent in agents.list (or you forget to mark your intended agent as default), you can route messages into the wrong brain — and it will look like the assistant lost memory.

It didn’t lose memory. You just talked to the wrong agent.

The clean fix (canonical config)

Here’s the minimal, stable configuration that matches the mental model “two bots, two agents”.

Telegram: two bot tokens, both inside accounts

channels: {
  telegram: {
    enabled: true,
    dmPolicy: "pairing",
    groupPolicy: "allowlist",
    streaming: "off",

    accounts: {
      default: {
        name: "Primary Bot",
        enabled: true,
        botToken: "<OLD_BOT_TOKEN>"
      },
      secondary: {
        name: "Secondary Bot",
        enabled: true,
        botToken: "<SECONDARY_BOT_TOKEN>"
      }
    }
  }
}

Agents: explicitly declare both brains, mark one default

agents: {
  defaults: {
    workspace: "~/.openclaw/workspace"
  },

  list: [
    {
      id: "main",
      name: "Primary Agent",
      workspace: "~/.openclaw/workspace",
      default: true
    },
    {
      id: "secondary",
      name: "Secondary Agent",
      workspace: "~/.openclaw/workspace-secondary"
    }
  ]
}

Bindings: bind both accounts explicitly

Yes, you can bind only secondary and rely on fallback for default.

But I strongly recommend binding both. It makes the configuration self-documenting and immune to list ordering mistakes.

bindings: [
  { agentId: "main", match: { channel: "telegram", accountId: "default" } },
  { agentId: "secondary", match: { channel: "telegram", accountId: "secondary" } }
]

Pairing: why the bot sometimes stops repeating the pairing code

If you use dmPolicy: "pairing", the bot may show a pairing code once, then go quiet.

That’s not a bug. It’s a workflow.

To approve pairing for a specific Telegram account, you must include the account id:

openclaw pairing list telegram --account secondary
openclaw pairing approve telegram <CODE> --account secondary --notify

If you approve without --account in a multi-account setup, you can end up looking at the wrong queue.

Verification checklist (don’t skip this)

After any multi-agent / multi-account change:

openclaw gateway restart
openclaw agents list --bindings
openclaw channels status --probe

If these three commands look correct, your setup is almost always correct.

Takeaway

The lesson wasn’t “OpenClaw is flaky.”

The lesson was: once you cross into multi-agent + multi-account territory, you must respect:

  • canonical config shapes (accounts.default is not optional)
  • default-agent routing rules (make it explicit)
  • account-aware pairing approval (--account matters)

Do that, and the whole system becomes boringly deterministic — which is exactly what you want.