Migrating from openclaw to gini
This guide moves an existing openclaw install onto gini. Read Per-Agent Memory Isolation , Connector Secret Storage , and Openclaw Migration for the architectural reasoning.
Prerequisites
- A working openclaw install you no longer want as your primary agent. The state must live on the same machine you are running gini on; the migrator does not transfer state across hosts.
- A gini install (
bun run gini install). The migrator does not bootstrap gini; it only adds to an existing instance. - Stop openclaw’s gateway before migrating so the migrator reads a consistent snapshot.
openclaw stop(or its daemon equivalent) is enough — the migrator never writes back into openclaw’s state root. - Stop the gini gateway for the target instance before running
apply.gini import apply openclawrefuses to run while a gateway is alive for the same instance because the in-processmutateStatelock cannot serialize writes across separate OS processes. Rungini stop --instance <name>first, apply, thengini start --instance <name>.
Resolving the openclaw state root
The migrator looks for openclaw’s state root in this order, matching openclaw’s own resolution:
- An explicit path argument to
gini import. OPENCLAW_STATE_DIRenvironment variable.~/.openclaw/.- The legacy
~/.clawdbot/.
If none of those exist, you’ll see “no openclaw.json found” in the planner output. The apply step does not migrate anything in that case, but it DOES record a failed ImportReport row (status: "failed", mode: "applied", with an error field naming the missing config path) so the activity feed reflects the attempt. You can ignore that row safely — it exists so a later “where did my migration go?” question has an answer in gini import.
Two-step flow
The migrator separates inspection from mutation. Always run plan first so you can read the summary before any state changes.
# Dry-run: print a redacted summary of what would happen.
bun run gini import plan openclaw
# Apply: actually mutate gini state.
bun run gini import apply openclawBoth commands accept an optional path argument when your openclaw state lives somewhere other than the default. The CLI prints JSON; pipe it to jq for readable filtering.
bun run gini import plan openclaw /Volumes/backup/old-openclaw | jqWhat gets migrated
The migrator walks the openclaw state and synthesizes equivalent gini records for the subsystems gini implements today:
| Openclaw artifact | Gini destination | Notes |
|---|---|---|
Entire <state>/ directory | <instance>/imports/openclaw-<timestamp>.zip | Written before any other step. The migration never deletes openclaw data, but the archive is your insurance policy in case you later wipe ~/.openclaw thinking the migration moved it. The archive failing to write aborts the migration before any state mutation lands; the safety net is non-optional. |
cfg.agents.list[] | New AgentRecord per agent | Agent name carries the openclaw id. The default openclaw agent maps to a new gini agent — the seeded agent_default is left alone so your existing defaults aren’t disturbed. |
agents/<id>/agent/auth-profiles.json (api_key / token) | ~/.gini/secrets.env line <canonical-env-var>=… | Env var names are taken from the provider layer, not hand-rolled — openai → OPENAI_API_KEY, openrouter → OPENROUTER_API_KEY, local → GINI_LOCAL_API_KEY (the GINI_ prefix is what normalizeProvider in src/provider.ts reads). codex is a no-op because gini reads OAuth from ~/.codex/auth.json; you’ll see a provider:codex note pointing at codex --login. Anthropic, Google, and similar are listed in unsupported so you can wire them manually. |
channels.telegram + state-dir .env TELEGRAM_BOT_TOKEN + credentials/telegram-allowFrom.json | MessagingBridgeRecord (kind telegram) with encrypted bot token and per-chat allowlist | Allowlist string ids are coerced to numbers. See Telegram Messaging Bridge . |
channels.discord + DISCORD_BOT_TOKEN | MessagingBridgeRecord (kind discord) with encrypted bot token | See Discord Messaging Bridge . The supervisor won’t poll the bridge until you add at least one delivery channel. Openclaw stores a per-sender allowlist while gini stores per-channel snowflakes, so the migrator cannot derive the channel list. The migration warning instructs the operator to disable the migrated bridge with gini messaging disable <id> and re-create it via gini messaging add <name> discord <channel-id>... --bot-token <token> (re-supplying the original openclaw bot token). An in-place edit verb is a known follow-up. |
<state>/skills/<name>/SKILL.md | <instance>/skills/<name>/SKILL.md | Top-level openclaw: frontmatter block is rewritten to metadata:\n gini:. Sibling files in the skill dir (scripts, references) are copied verbatim. |
<state>/workspace/{AGENTS,SOUL,TOOLS,IDENTITY,USER,HEARTBEAT,BOOTSTRAP,MEMORY}.md | <instance>/workspace/<file> | Same-named files are skipped unless --force is passed. The migrator resolves the workspace dir in this order: OPENCLAW_WORKSPACE_DIR (explicit override, taken verbatim), then OPENCLAW_PROFILE-derived <state>/workspace-<profile>/ (and ~/.openclaw/workspace-<profile>/ when no explicit state path was given), then <state>/workspace/, then ~/.openclaw/workspace/ as the final fallback (only when no explicit state path was given). |
<state>/agents/<id>/sessions/*.jsonl | One ChatSessionRecord per JSONL plus one ChatMessageRecord per type: "message" line | Tool_use and tool_result blocks are dropped from migrated message content (ChatMessageRecord.content is a flat string). The full verbatim transcript stays in the archive zip. Session createdAt/updatedAt are rebased to the openclaw timestamps so recent-chats sort reflects the original transcript date, not migration day. |
<state>/memory/<id>.sqlite (Hindsight schema: memory_banks + memory_units) | One memory_units row per source row in <instance>/memory.db, routed into the per-agent bank (bank_<agentId>) of the gini agent whose name matches the source SQLite filename | Migrated with embedding NULL; run gini embedding reembed --all-banks after migration to populate vectors across every per-agent bank (the default gini embedding reembed only walks the default bank). Unknown statuses/networks are coerced to active/experience so a schema drift can’t poison recall. The legacy file-chunk RAG schema (chunks + files + embedding_cache) has no direct gini target and lands on the unsupported list with a Re-index via /api/memory/retain hint. |
Provider keys land in ~/.gini/secrets.env because the installed gini wrapper sources that file with set -a on every invocation. Connector tokens go through the per-instance encrypted secret store described in Connector Secret Storage — they are never logged or echoed.
What is NOT migrated
The migrator surfaces every unmigrated subsystem in the unsupported field so you know what is left on the openclaw side:
- Tasks and cron registries, plugin installs, device-pair tokens. Either the feature doesn’t exist on the gini side yet or the state is safer to re-establish (devices in particular — openclaw device tokens cannot be reused under gini; re-pair via
gini paironce you’re on gini). - Non-Telegram, non-Discord channels (WhatsApp, Signal, Slack, etc.). Gini has no bridge implementation for those yet; the migrator lists each unsupported channel by name.
- Openclaw file-chunk RAG memory (
<state>/memory/*.sqlitewith thechunks+files+embedding_cacheschema). The chunk shape doesn’t map cleanly to gini’sMemoryUnitmodel; re-index relevant files via/api/memory/retainif you still need them.
Idempotency and re-runs
gini import apply openclaw is safe to re-run. The default behavior skips anything that already exists by name:
- Agents with a matching
name: behavior depends on provenance. If the existing agent was created by an earlier run of this migrator (audited via anopenclaw.agent.taggedrow tying the agent id to the openclaw agent id), re-importing reuses it and idempotently dedupes sessions and memory. If it’s a native gini agent the operator created themselves, the migrator refuses to attach openclaw sessions and memory to it — anagent:<name>:name-collisionentry lands on theunsupportedlist and the operator must either delete the native agent (gini agent delete <name>) and re-migrate so a fresh tagged agent gets created, or pass--forceto acknowledge the merge of openclaw history into the existing agent. - Workspace files that already exist on disk are skipped.
- Skills with the same directory name are skipped.
- Messaging bridges: at most one per kind exists per instance. If the bridge was created by an earlier run of this migrator (audited via a
messaging.configuredrow carryingevidence.source: "openclaw-migration"),--forcerotates its token and merges the allow-list. If it’s a native bridge the operator created themselves, the migrator refuses to touch it even with--force(the bot token and allow-list would be silently overwritten); amessaging:<kind>:native-collisionentry lands on theunsupportedlist with remediation pointing atgini messaging disable <bridge-id>followed by a re-migration.
Use --force to rotate values: a fresh openclaw config with a new TELEGRAM_BOT_TOKEN re-applied with --force against a migrator-created bridge rewrites the encrypted secret file and updates metadata.allowedChatIds. Workspace files and skills also overwrite under --force.
Audit trail
Every apply invocation — whether the migration found data or not — writes:
- An
ImportReportrow withsource: "openclaw"andmode: "applied". On success the row carriesstatus: "completed"plus acountsmap summarizing each subsystem. On a no-config apply (the state path exists butopenclaw.jsonis missing), the row carriesstatus: "failed"with anerrorfield and empty counts — written deliberately so the activity feed always reflects the attempt instead of silently producing no record. - Per-creation audit rows in
state.audit(agent created, messaging configured, etc.) emitted by the sameaddAuditpath the live CRUD endpoints use.
You can read the report via gini import (the default subcommand lists reports) and the audit rows via gini audit.
Verifying after migration
The verification commands below are API-backed, so they need a running gateway. The migration prerequisite required stopping the gateway before apply, so the first step is gini smoke — it self-starts the runtime so every command after it has a live /api/* to call.
# Smoke test the runtime end-to-end. This self-starts the runtime,
# so the API-backed commands below have a live gateway to query.
bun run gini smoke
# Inspect agent state.
bun run gini agents list
# Confirm the bridge is configured and healthy.
bun run gini messaging list
# Inspect migrated chat history.
bun run gini chat listAfter the smoke passes, populate the migrated memory unit embeddings with the active embedding provider, then start gini:
# Re-embed every migrated memory unit so semantic recall returns them.
# The migrator stores units with embedding NULL and routes them into
# the matching per-agent bank (`bank_<agentId>`); --all-banks
# enumerates every bank in the instance so per-agent units aren't
# missed. Plain `gini embedding reembed` only walks the default bank
# and would leave per-agent units invisible to semantic recall.
bun run gini embedding reembed --all-banks
bun run gini startThe newly-imported provider keys are picked up automatically because the installed gini wrapper sources ~/.gini/secrets.env on every invocation.
Where the openclaw archive lives
Every applied migration writes a verbatim zip of your openclaw state root to:
<instance>/imports/openclaw-<timestamp>.zipYou can find the instance root with gini status (it prints the active instance dir). Restore from the archive by unzipping into a fresh path and pointing gini import apply openclaw <unzipped-dir> at it (the path is a positional argument, not a --path flag). The archive is intentionally kept on disk indefinitely — delete it manually only after you’ve confirmed the migration result is what you want.
Common questions
Can I run this without stopping openclaw?
The migrator only reads from the openclaw state, never writes. But openclaw’s gateway can rewrite files mid-read (a sessions.json flush, an offset bump). Stopping openclaw first guarantees a consistent snapshot.
Where do I find my openclaw state if I customized it?
Run openclaw doctor — it prints the active state root. Or check OPENCLAW_STATE_DIR in your shell environment.
My openclaw agents use Anthropic. What happens?
The migrator creates the agent record but skips the API key (Anthropic isn’t in gini’s native provider list yet). The unsupported array in the report lists provider:anthropic. Add the key manually via gini provider set once gini supports Anthropic, or point the agent at OpenRouter as an interim alternative.
Will my chat history come over?
Yes. Each <state>/agents/<id>/sessions/<sessionId>.jsonl becomes one ChatSessionRecord plus one ChatMessageRecord per type: "message" line under the matching gini agent. Tool_use and tool_result blocks are dropped from the migrated message text (ChatMessageRecord.content is a single string), but the full verbatim transcript stays in <instance>/imports/openclaw-<timestamp>.zip for anyone who needs the original tool-call detail. Session timestamps are rebased to the openclaw values so recent-chats sort matches what you remember from openclaw.
Will my Hindsight memory come over?
If your <state>/memory/<id>.sqlite carries the Hindsight schema (memory_banks + memory_units), yes — each unit lands in <instance>/memory.db and is routed into the per-agent bank (bank_<agentId>) of the gini agent whose name matches the source SQLite filename. Run gini embedding reembed --all-banks after migration to populate vectors across every per-agent bank — the plain gini embedding reembed only walks bank_default and would leave the per-agent units invisible to semantic recall. If your memory store instead carries the legacy file-chunk RAG schema (chunks + files + embedding_cache), it lands on the unsupported list — there’s no clean target for that shape in gini today; re-index the underlying files via /api/memory/retain if you still need them.