How to test, develop, and debug the migration flow.
# Full cycle: reset → migrate → Claude finishes
bash migrate-v2-reset.sh && bash migrate-v2.shTwo-part migration:
-
migrate-v2.sh— deterministic bash script. Handles prerequisites, DB seeding, file copies, channel install, container build, service switchover. Writeslogs/setup-migration/handoff.jsonthenexecs into Claude. -
/migrate-from-v1skill — Claude-driven. Reads the handoff, seeds owner/roles, cleans up CLAUDE.local.md, validates container configs, ports fork customizations.
migrate-v2.sh # Entry point
migrate-v2-reset.sh # Wipe v2 state for re-testing
setup/migrate-v2/
env.ts # Phase 1a: merge .env
db.ts # Phase 1b: seed v2 DB
groups.ts # Phase 1c: copy group folders + container.json
sessions.ts # Phase 1d: copy sessions + set continuation
tasks.ts # Phase 1e: port scheduled tasks
channel-auth.ts # Phase 2b: copy channel auth state
select-channels.ts # Phase 2a: clack multiselect
switchover-prompt.ts # Service switch prompts
setup/migrate-v2/shared.ts # Shared helpers (JID parsing, trigger mapping, etc.)
.claude/skills/migrate-from-v1/ # The Claude skill
logs/setup-migration/handoff.json # Written by migrate-v2.sh, read by skill
logs/migrate-steps/*.log # Per-step raw output
# Reset v2 to clean state (keeps node_modules)
bash migrate-v2-reset.sh
# Run migration with non-interactive channel selection
NANOCLAW_CHANNELS="telegram" bash migrate-v2.sh
# Or run interactively (clack multiselect)
bash migrate-v2.shmigrate-v2-reset.sh wipes: data/, logs/, .env, groups/ (restores git-tracked), container/skills/ (restores git-tracked), src/channels/ (restores git-tracked).
It does NOT wipe node_modules/ (expensive to reinstall).
Each step is a standalone TypeScript file:
# Run a single step (after pnpm install)
pnpm exec tsx setup/migrate-v2/env.ts /path/to/v1
pnpm exec tsx setup/migrate-v2/db.ts /path/to/v1
pnpm exec tsx setup/migrate-v2/groups.ts /path/to/v1
pnpm exec tsx setup/migrate-v2/sessions.ts /path/to/v1
pnpm exec tsx setup/migrate-v2/tasks.ts /path/to/v1
pnpm exec tsx setup/migrate-v2/channel-auth.ts /path/to/v1 telegram discordEach prints OK:<details>, SKIPPED:<reason>, or errors to stdout. Exit 0 on success/skip, non-zero on failure.
# Agent groups
sqlite3 data/v2.db "SELECT * FROM agent_groups"
# Messaging groups + wiring
sqlite3 data/v2.db "SELECT mg.id, mg.channel_type, mg.platform_id, mg.unknown_sender_policy, mga.engage_mode, mga.engage_pattern FROM messaging_groups mg JOIN messaging_group_agents mga ON mga.messaging_group_id = mg.id"
# Sessions
sqlite3 data/v2.db "SELECT * FROM sessions"
# Users and roles
sqlite3 data/v2.db "SELECT * FROM users"
sqlite3 data/v2.db "SELECT * FROM user_roles"
# Session continuation (which Claude Code session will be resumed)
AG_ID=$(sqlite3 data/v2.db "SELECT id FROM agent_groups LIMIT 1")
SESS_ID=$(sqlite3 data/v2.db "SELECT id FROM sessions LIMIT 1")
sqlite3 data/v2-sessions/$AG_ID/$SESS_ID/outbound.db "SELECT * FROM session_state"
# Scheduled tasks
sqlite3 data/v2-sessions/$AG_ID/$SESS_ID/inbound.db "SELECT id, kind, recurrence, status FROM messages_in WHERE kind='task'"python3 -m json.tool logs/setup-migration/handoff.jsonBot doesn't respond after switchover:
- Check both services aren't running:
systemctl --user list-units 'nanoclaw*' - Check error log:
tail logs/nanoclaw.error.log - Check sender policy:
sqlite3 data/v2.db "SELECT unknown_sender_policy FROM messaging_groups"— must bepublicbefore owner is seeded - Check engage pattern:
sqlite3 data/v2.db "SELECT engage_mode, engage_pattern FROM messaging_group_agents"— should bepattern/.for respond-to-everything
Session not continuing from v1:
- Check continuation is set: see "Session continuation" query above
- Check JSONL exists at the right path:
ls data/v2-sessions/<ag_id>/.claude-shared/projects/-workspace-agent/ - The v1 session JSONL should be copied from
-workspace-group/to-workspace-agent/(v2 container CWD is/workspace/agent)
Service switchover revert didn't work:
- The v2 service name is
nanoclaw-v2-<hash>— find it:systemctl --user list-units 'nanoclaw*' - Manually stop:
systemctl --user stop <unit> && systemctl --user disable <unit> - Restart v1:
systemctl --user start nanoclaw
Each step writes raw output to logs/migrate-steps/<step>.log. Read these when a step fails:
cat logs/migrate-steps/1b-db.log
cat logs/migrate-steps/1d-sessions.logunknown_sender_policyis set topublicduring migration so the bot responds immediately. The/migrate-from-v1skill tightens it after seeding the owner.requires_trigger=0in v1 takes priority over a non-emptytrigger_pattern— it means "respond to everything."- v1
container_config.additionalMountsis written directly to v2container.json(same shape). - v1 Claude Code sessions are copied from
-workspace-group/to-workspace-agent/and the session ID is written tooutbound.dbascontinuation:claudeso the agent-runner resumes the same conversation. exec claude "/migrate-from-v1"at the end replaces the bash process —write_handoffis called explicitly beforeexecsince EXIT traps don't fire onexec.