diff --git a/CHANGELOG.md b/CHANGELOG.md index 9367fc943cb..e7f7d226178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,7 @@ Docs: https://docs.openclaw.ai - TTS: Restore 3.28 schema compatibility and fallback observability. (#57953) Thanks @joshavant. - Telegram/forum topics: restore reply routing to the active topic and keep ACP `sessions_spawn(..., thread=true, mode="session")` bound to that same topic instead of falling back to root chat or losing follow-up routing. (#56060) Thanks @one27001. - Config/SecretRef + Control UI: harden SecretRef redaction round-trip restore, block unsafe raw fallback (force Form mode when raw is unavailable), and preflight submitted-config SecretRefs before config write RPC persistence. (#58044) Thanks @joshavant. +- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan. ## 2026.3.28 diff --git a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts index c61f4d334ee..02bfbad507e 100644 --- a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts +++ b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts @@ -214,6 +214,25 @@ describe("legacy config detection", () => { expect(res.changes).toEqual([]); expect(res.config).toBeNull(); }); + + it("flags channels.telegram.groupMentionsOnly as legacy in snapshot", async () => { + await withSnapshotForConfig( + { channels: { telegram: { groupMentionsOnly: true } } }, + async (ctx) => { + expect(ctx.snapshot.valid).toBe(true); + expect( + ctx.snapshot.legacyIssues.some( + (issue) => issue.path === "channels.telegram.groupMentionsOnly", + ), + ).toBe(true); + const parsed = ctx.parsed as { + channels?: { telegram?: { groupMentionsOnly?: boolean } }; + }; + expect(parsed.channels?.telegram?.groupMentionsOnly).toBe(true); + }, + ); + }); + it("does not rewrite removed messages.tts.enabled migrations", async () => { const res = migrateLegacyConfig({ messages: { tts: { enabled: true } }, diff --git a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts index 887f8dec131..f9002fc49da 100644 --- a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts +++ b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts @@ -242,6 +242,17 @@ describe("legacy config detection", () => { expect(res.issues[0]?.message).toContain('"telegram"'); } }); + it("rejects channels.telegram.groupMentionsOnly", async () => { + const res = validateConfigObject({ + channels: { telegram: { groupMentionsOnly: true } }, + }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues.some((issue) => issue.path === "channels.telegram.groupMentionsOnly")).toBe( + true, + ); + } + }); it("rejects gateway.token", async () => { const res = validateConfigObject({ gateway: { token: "legacy-token" }, diff --git a/src/config/legacy-migrate.test.ts b/src/config/legacy-migrate.test.ts index 4810ad91dc8..7e039bec875 100644 --- a/src/config/legacy-migrate.test.ts +++ b/src/config/legacy-migrate.test.ts @@ -77,6 +77,89 @@ describe("legacy migrate mention routing", () => { expect(res.changes).toEqual([]); expect(res.config).toBeNull(); }); + + it("moves channels.telegram.groupMentionsOnly into groups.*.requireMention", () => { + const res = migrateLegacyConfig({ + channels: { + telegram: { + groupMentionsOnly: true, + }, + }, + }); + + expect(res.changes).toContain( + 'Moved channels.telegram.groupMentionsOnly → channels.telegram.groups."*".requireMention.', + ); + expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(true); + expect( + (res.config?.channels?.telegram as { groupMentionsOnly?: unknown } | undefined) + ?.groupMentionsOnly, + ).toBeUndefined(); + }); + + it('keeps explicit channels.telegram.groups."*".requireMention when migrating groupMentionsOnly', () => { + const res = migrateLegacyConfig({ + channels: { + telegram: { + groupMentionsOnly: true, + groups: { + "*": { + requireMention: false, + }, + }, + }, + }, + }); + + expect(res.changes).toContain( + 'Removed channels.telegram.groupMentionsOnly (channels.telegram.groups."*" already set).', + ); + expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false); + expect( + (res.config?.channels?.telegram as { groupMentionsOnly?: unknown } | undefined) + ?.groupMentionsOnly, + ).toBeUndefined(); + }); + + it("does not overwrite invalid channels.telegram.groups when migrating groupMentionsOnly", () => { + const res = migrateLegacyConfig({ + channels: { + telegram: { + groupMentionsOnly: true, + groups: [], + }, + }, + }); + + expect(res.config).toBeNull(); + expect(res.changes).toContain( + "Skipped channels.telegram.groupMentionsOnly migration because channels.telegram.groups already has an incompatible shape; fix remaining issues manually.", + ); + expect(res.changes).toContain( + "Migration applied, but config still invalid; fix remaining issues manually.", + ); + }); + + it('does not overwrite invalid channels.telegram.groups."*" when migrating groupMentionsOnly', () => { + const res = migrateLegacyConfig({ + channels: { + telegram: { + groupMentionsOnly: true, + groups: { + "*": false, + }, + }, + }, + }); + + expect(res.config).toBeNull(); + expect(res.changes).toContain( + "Skipped channels.telegram.groupMentionsOnly migration because channels.telegram.groups already has an incompatible shape; fix remaining issues manually.", + ); + expect(res.changes).toContain( + "Migration applied, but config still invalid; fix remaining issues manually.", + ); + }); }); describe("legacy migrate tts provider shape", () => { diff --git a/src/config/legacy.migrations.runtime.ts b/src/config/legacy.migrations.runtime.ts index eb8ecdc2485..4cc5b34e1a1 100644 --- a/src/config/legacy.migrations.runtime.ts +++ b/src/config/legacy.migrations.runtime.ts @@ -215,12 +215,36 @@ function migrateLegacyTtsConfig( } } +function resolveCompatibleDefaultGroupEntry(section: Record): { + groups: Record; + entry: Record; +} | null { + const existingGroups = section.groups; + if (existingGroups !== undefined && !getRecord(existingGroups)) { + return null; + } + const groups = getRecord(existingGroups) ?? {}; + const defaultKey = "*"; + const existingEntry = groups[defaultKey]; + if (existingEntry !== undefined && !getRecord(existingEntry)) { + return null; + } + const entry = getRecord(existingEntry) ?? {}; + return { groups, entry }; +} + const MEMORY_SEARCH_RULE: LegacyConfigRule = { path: ["memorySearch"], message: "top-level memorySearch was moved; use agents.defaults.memorySearch instead (auto-migrated on load).", }; +const GROUP_MENTIONS_ONLY_RULE: LegacyConfigRule = { + path: ["channels", "telegram", "groupMentionsOnly"], + message: + 'channels.telegram.groupMentionsOnly was removed; use channels.telegram.groups."*".requireMention instead (auto-migrated on load).', +}; + const GATEWAY_BIND_RULE: LegacyConfigRule = { path: ["gateway", "bind"], message: @@ -307,6 +331,53 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [ ); }, }), + defineLegacyConfigMigration({ + // v2026.2.23 replaced channels.telegram.groupMentionsOnly with + // channels.telegram.groups."*".requireMention. Existing configs crash on + // startup because gateway auto-migration only runs for registered legacy + // keys, and this removed key previously fell through as an unknown field. + id: "channels.telegram.groupMentionsOnly->channels.telegram.groups.*.requireMention", + describe: + "Move channels.telegram.groupMentionsOnly to channels.telegram.groups.*.requireMention", + legacyRules: [GROUP_MENTIONS_ONLY_RULE], + apply: (raw, changes) => { + const channels = ensureRecord(raw, "channels"); + const telegram = getRecord(channels.telegram); + if (!telegram || telegram.groupMentionsOnly === undefined) { + return; + } + + const groupMentionsOnly = telegram.groupMentionsOnly; + const defaultGroupEntry = resolveCompatibleDefaultGroupEntry(telegram); + const defaultKey = "*"; + + if (!defaultGroupEntry) { + changes.push( + "Skipped channels.telegram.groupMentionsOnly migration because channels.telegram.groups already has an incompatible shape; fix remaining issues manually.", + ); + return; + } + + const { groups, entry } = defaultGroupEntry; + + if (entry.requireMention === undefined) { + entry.requireMention = groupMentionsOnly; + groups[defaultKey] = entry; + telegram.groups = groups; + changes.push( + 'Moved channels.telegram.groupMentionsOnly → channels.telegram.groups."*".requireMention.', + ); + } else { + changes.push( + 'Removed channels.telegram.groupMentionsOnly (channels.telegram.groups."*" already set).', + ); + } + + delete telegram.groupMentionsOnly; + channels.telegram = telegram; + raw.channels = channels; + }, + }), defineLegacyConfigMigration({ id: "memorySearch->agents.defaults.memorySearch", describe: "Move top-level memorySearch to agents.defaults.memorySearch",