fix(migrate-v1): splice guild_id into Discord platform_id during seed

v2's Chat SDK Discord adapter emits `platform_id` as
`discord:<guild_id>:<channel_id>` at runtime, but v1 only stored
`dc:<channel_id>` (no guild). Before this fix `migrate-db` wrote
`discord:<channel_id>` into `messaging_groups.platform_id`, which didn't
match what v2 saw on incoming messages — v2 treated every message as a
new channel and fired its channel-registration approval flow instead of
routing to the migrated agent_group.

Now `migrate-db` fetches the bot's guilds once per channel_type via
`GET /users/@me/guilds`. When the bot is in exactly one guild (the
common case), the guild id is spliced into every Discord platform_id at
seed time — matching v2's runtime format. Multi-guild bots fall back to
the v1-format id; v2's channel-registration flow repairs on first
message.

Cost: one extra Discord API call per migration run (not per channel).
No new failure modes — network/auth issues return null, fall through to
the existing behavior.

## Surface

- `v2PlatformId(channelType, jid, { guildId })` — new optional `extra`
  parameter. Back-compat with existing callers.
- `fetchBotGuilds(channelType, lookup)` — new helper in `shared.ts`,
  same pattern as `autoResolveV2Keys`. Handles Discord today; extending
  to other channels is a case-by-case API check.
- `migrate-db` pre-loop: builds `v1EnvMap`, fetches guilds per channel
  type, caches single-guild IDs for the row loop.

## Testing

Verified on a 300-channel Discord v1 install:
- Fresh run produced `discord:<guild>:<channel>` platform_ids from the
  start
- Incoming messages now route to the migrated agent_group instead of
  firing the unwire approval flow

Rate-limit note: `/users/@me/guilds` is a single call. Per-channel
`/guilds/<id>/channels` lookups for multi-guild bots would need proper
rate-limit handling — deferred.
This commit is contained in:
gabi-simons
2026-04-23 12:41:33 +00:00
parent e1c8876a72
commit 9faa8a9a2c
2 changed files with 74 additions and 3 deletions

View File

@@ -36,6 +36,7 @@ import { runMigrations } from '../../src/db/migrations/index.js';
import { log } from '../../src/log.js';
import { emitStatus } from '../status.js';
import {
fetchBotGuilds,
generateId,
inferChannelType,
readHandoff,
@@ -158,6 +159,29 @@ export async function run(args: string[]): Promise<void> {
}));
writeHandoff(h);
// For channels where v2's platform_id includes a component v1 didn't record
// (Discord's guild id), fetch the bot's guilds up-front. If the bot is in
// a single guild we can splice that id into every platform_id; otherwise
// fall back to the v1-format id (v2's channel-registration flow will repair
// on first message). Done ONCE per channel_type, not per-row, so this is
// cheap regardless of group count.
const v1EnvText = fs.existsSync(paths.env) ? fs.readFileSync(paths.env, 'utf-8') : '';
const v1EnvMap = new Map<string, string>();
for (const line of v1EnvText.split('\n')) {
const t = line.trim();
if (!t || t.startsWith('#')) continue;
const eq = t.indexOf('=');
if (eq <= 0) continue;
v1EnvMap.set(t.slice(0, eq).trim(), t.slice(eq + 1));
}
const singleGuildByChannel = new Map<string, string>();
for (const channelType of detectedChannels.keys()) {
const info = await fetchBotGuilds(channelType, (k) => v1EnvMap.get(k));
if (info && info.guildIds.length === 1) {
singleGuildByChannel.set(channelType, info.guildIds[0]);
}
}
// Initialize v2.db (creates schema if not present — runMigrations is no-op
// when the schema is already current, so this is safe on a live v2 install).
fs.mkdirSync(path.join(process.cwd(), 'data'), { recursive: true });
@@ -181,7 +205,8 @@ export async function run(args: string[]): Promise<void> {
continue;
}
const platformId = v2PlatformId(channelType, g.jid);
const guildId = singleGuildByChannel.get(channelType);
const platformId = v2PlatformId(channelType, g.jid, { guildId });
const createdAt = new Date().toISOString();
try {