From bbb71c919893315716ac582b74d287789ca6f9f2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 09:01:36 +0000 Subject: [PATCH] refactor: prune legacy group targets --- src/auto-reply/reply/history.test.ts | 20 +++++++++---------- src/auto-reply/reply/session.ts | 10 +++++----- src/config/sessions.test.ts | 2 +- src/config/sessions/group.ts | 12 +++++------ src/config/sessions/store.ts | 8 ++++++++ src/config/sessions/types.ts | 2 +- src/gateway/server-bridge-methods-sessions.ts | 2 +- src/gateway/session-utils.ts | 6 +++--- src/gateway/session-utils.types.ts | 2 +- src/infra/outbound/target-resolver.ts | 10 +++++----- src/infra/state-migrations.ts | 8 +++++++- src/telegram/inline-buttons.test.ts | 1 - src/telegram/targets.test.ts | 4 ++++ src/telegram/targets.ts | 13 +++++++++++- src/tui/gateway-chat.ts | 2 +- 15 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/auto-reply/reply/history.test.ts b/src/auto-reply/reply/history.test.ts index 8641fa86d..addbf5860 100644 --- a/src/auto-reply/reply/history.test.ts +++ b/src/auto-reply/reply/history.test.ts @@ -40,43 +40,43 @@ describe("history helpers", () => { appendHistoryEntry({ historyMap, - historyKey: "room", + historyKey: "group", limit: 2, entry: { sender: "A", body: "one" }, }); appendHistoryEntry({ historyMap, - historyKey: "room", + historyKey: "group", limit: 2, entry: { sender: "B", body: "two" }, }); appendHistoryEntry({ historyMap, - historyKey: "room", + historyKey: "group", limit: 2, entry: { sender: "C", body: "three" }, }); - expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual(["two", "three"]); + expect(historyMap.get("group")?.map((entry) => entry.body)).toEqual(["two", "three"]); }); it("builds context from map and appends entry", () => { const historyMap = new Map(); - historyMap.set("room", [ + historyMap.set("group", [ { sender: "A", body: "one" }, { sender: "B", body: "two" }, ]); const result = buildHistoryContextFromMap({ historyMap, - historyKey: "room", + historyKey: "group", limit: 3, entry: { sender: "C", body: "three" }, currentMessage: "current", formatEntry: (entry) => `${entry.sender}: ${entry.body}`, }); - expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual(["one", "two", "three"]); + expect(historyMap.get("group")?.map((entry) => entry.body)).toEqual(["one", "two", "three"]); expect(result).toContain(HISTORY_CONTEXT_MARKER); expect(result).toContain("A: one"); expect(result).toContain("B: two"); @@ -85,20 +85,20 @@ describe("history helpers", () => { it("builds context from pending map without appending", () => { const historyMap = new Map(); - historyMap.set("room", [ + historyMap.set("group", [ { sender: "A", body: "one" }, { sender: "B", body: "two" }, ]); const result = buildPendingHistoryContextFromMap({ historyMap, - historyKey: "room", + historyKey: "group", limit: 3, currentMessage: "current", formatEntry: (entry) => `${entry.sender}: ${entry.body}`, }); - expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual(["one", "two"]); + expect(historyMap.get("group")?.map((entry) => entry.body)).toEqual(["one", "two"]); expect(result).toContain(HISTORY_CONTEXT_MARKER); expect(result).toContain("A: one"); expect(result).toContain("B: two"); diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 12c08e4af..3dc75c283 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -229,7 +229,7 @@ export async function initSessionState(params: { channel: baseEntry?.channel, groupId: baseEntry?.groupId, subject: baseEntry?.subject, - room: baseEntry?.room, + groupChannel: baseEntry?.groupChannel, space: baseEntry?.space, deliveryContext: deliveryFields.deliveryContext, // Track originating channel for subagent announce routing. @@ -247,24 +247,24 @@ export async function initSessionState(params: { normalizedChannel && getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"), ); - const nextRoom = + const nextGroupChannel = explicitChannel ?? ((groupResolution.chatType === "channel" || isChannelProvider) && subject && subject.startsWith("#") ? subject : undefined); - const nextSubject = nextRoom ? undefined : subject; + const nextSubject = nextGroupChannel ? undefined : subject; sessionEntry.chatType = groupResolution.chatType ?? "group"; sessionEntry.channel = channel; sessionEntry.groupId = groupResolution.id; if (nextSubject) sessionEntry.subject = nextSubject; - if (nextRoom) sessionEntry.room = nextRoom; + if (nextGroupChannel) sessionEntry.groupChannel = nextGroupChannel; if (space) sessionEntry.space = space; sessionEntry.displayName = buildGroupDisplayName({ provider: sessionEntry.channel, subject: sessionEntry.subject, - room: sessionEntry.room, + groupChannel: sessionEntry.groupChannel, space: sessionEntry.space, id: groupResolution.id, key: sessionKey, diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index c78a9653c..f6b2bb217 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -55,7 +55,7 @@ describe("sessions", () => { expect( buildGroupDisplayName({ provider: "discord", - room: "#general", + groupChannel: "#general", space: "friends-of-clawd", id: "123", key: "discord:group:123", diff --git a/src/config/sessions/group.ts b/src/config/sessions/group.ts index 0896cfc67..857b1aeaf 100644 --- a/src/config/sessions/group.ts +++ b/src/config/sessions/group.ts @@ -22,26 +22,26 @@ function shortenGroupId(value?: string) { export function buildGroupDisplayName(params: { provider?: string; subject?: string; - room?: string; + groupChannel?: string; space?: string; id?: string; key: string; }) { const providerKey = (params.provider?.trim().toLowerCase() || "group").trim(); - const room = params.room?.trim(); + const groupChannel = params.groupChannel?.trim(); const space = params.space?.trim(); const subject = params.subject?.trim(); const detail = - (room && space - ? `${space}${room.startsWith("#") ? "" : "#"}${room}` - : room || subject || space || "") || ""; + (groupChannel && space + ? `${space}${groupChannel.startsWith("#") ? "" : "#"}${groupChannel}` + : groupChannel || subject || space || "") || ""; const fallbackId = params.id?.trim() || params.key; const rawLabel = detail || fallbackId; let token = normalizeGroupLabel(rawLabel); if (!token) { token = normalizeGroupLabel(shortenGroupId(rawLabel)); } - if (!params.room && token.startsWith("#")) { + if (!params.groupChannel && token.startsWith("#")) { token = token.replace(/^#+/, ""); } if (token && !/^[@#]/.test(token) && !token.startsWith("g-") && !token.includes("#")) { diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 3c8ead721..2d2793ffb 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -130,6 +130,14 @@ export function loadSessionStore( rec.lastChannel = rec.lastProvider; delete rec.lastProvider; } + + // Best-effort migration: legacy `room` field → `groupChannel` (keep value, prune old key). + if (typeof rec.groupChannel !== "string" && typeof rec.room === "string") { + rec.groupChannel = rec.room; + delete rec.room; + } else if ("room" in rec) { + delete rec.room; + } } // Cache the result if caching is enabled diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index bda7f561e..075d5b905 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -67,7 +67,7 @@ export type SessionEntry = { channel?: string; groupId?: string; subject?: string; - room?: string; + groupChannel?: string; space?: string; deliveryContext?: DeliveryContext; lastChannel?: SessionChannelId; diff --git a/src/gateway/server-bridge-methods-sessions.ts b/src/gateway/server-bridge-methods-sessions.ts index c89d0aab0..f106925ef 100644 --- a/src/gateway/server-bridge-methods-sessions.ts +++ b/src/gateway/server-bridge-methods-sessions.ts @@ -209,7 +209,7 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async ( chatType: entry?.chatType, channel: entry?.channel, subject: entry?.subject, - room: entry?.room, + groupChannel: entry?.groupChannel, space: entry?.space, lastChannel: entry?.lastChannel, lastTo: entry?.lastTo, diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index c579d4997..7fe2b5561 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -378,7 +378,7 @@ export function listSessionsFromStore(params: { const parsed = parseGroupKey(key); const channel = entry?.channel ?? parsed?.channel; const subject = entry?.subject; - const room = entry?.room; + const groupChannel = entry?.groupChannel; const space = entry?.space; const id = parsed?.id; const displayName = @@ -387,7 +387,7 @@ export function listSessionsFromStore(params: { ? buildGroupDisplayName({ provider: channel, subject, - room, + groupChannel, space, id, key, @@ -401,7 +401,7 @@ export function listSessionsFromStore(params: { displayName, channel, subject, - room, + groupChannel, space, chatType: entry?.chatType, updatedAt, diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index 09e133111..051e293ac 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -15,7 +15,7 @@ export type GatewaySessionRow = { displayName?: string; channel?: string; subject?: string; - room?: string; + groupChannel?: string; space?: string; chatType?: NormalizedChatType; updatedAt: number | null; diff --git a/src/infra/outbound/target-resolver.ts b/src/infra/outbound/target-resolver.ts index ae587d667..80688325d 100644 --- a/src/infra/outbound/target-resolver.ts +++ b/src/infra/outbound/target-resolver.ts @@ -61,7 +61,7 @@ function normalizeQuery(value: string): string { function stripTargetPrefixes(value: string): string { return value - .replace(/^(channel|group|user):/i, "") + .replace(/^(channel|user):/i, "") .replace(/^[@#]/, "") .trim(); } @@ -88,7 +88,7 @@ export function formatTargetDisplay(params: { params.kind ?? (lowered.startsWith("user:") ? "user" - : lowered.startsWith("channel:") || lowered.startsWith("group:") + : lowered.startsWith("channel:") ? "group" : undefined); @@ -103,8 +103,8 @@ export function formatTargetDisplay(params: { if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) return trimmedTarget; const withoutPrefix = trimmedTarget.replace(/^telegram:/i, ""); - if (/^(channel|group):/i.test(withoutPrefix)) { - return `#${withoutPrefix.replace(/^(channel|group):/i, "")}`; + if (/^channel:/i.test(withoutPrefix)) { + return `#${withoutPrefix.replace(/^channel:/i, "")}`; } if (/^user:/i.test(withoutPrefix)) { return `@${withoutPrefix.replace(/^user:/i, "")}`; @@ -126,7 +126,7 @@ function detectTargetKind(raw: string, preferred?: TargetResolveKind): TargetRes const trimmed = raw.trim(); if (!trimmed) return "group"; if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) return "user"; - if (trimmed.startsWith("#") || /^channel:/i.test(trimmed) || /^group:/i.test(trimmed)) { + if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) { return "group"; } return "group"; diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index 7270da3f7..76d167fb8 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -131,7 +131,13 @@ function normalizeSessionEntry(entry: SessionEntryLike): SessionEntry | null { typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(); - return { ...(entry as unknown as SessionEntry), sessionId, updatedAt }; + const normalized = { ...(entry as unknown as SessionEntry), sessionId, updatedAt }; + const rec = normalized as unknown as Record; + if (typeof rec.groupChannel !== "string" && typeof rec.room === "string") { + rec.groupChannel = rec.room; + } + delete rec.room; + return normalized; } function emptyDirOrMissing(dir: string): boolean { diff --git a/src/telegram/inline-buttons.test.ts b/src/telegram/inline-buttons.test.ts index bd060f2d8..687d29bdd 100644 --- a/src/telegram/inline-buttons.test.ts +++ b/src/telegram/inline-buttons.test.ts @@ -21,7 +21,6 @@ describe("resolveTelegramTargetChatType", () => { it("handles tg/group prefixes and topic suffixes", () => { expect(resolveTelegramTargetChatType("tg:5232990709")).toBe("direct"); - expect(resolveTelegramTargetChatType("group:-123456789")).toBe("group"); expect(resolveTelegramTargetChatType("telegram:group:-1001234567890")).toBe("group"); expect(resolveTelegramTargetChatType("telegram:group:-1001234567890:topic:456")).toBe("group"); expect(resolveTelegramTargetChatType("-1001234567890:456")).toBe("group"); diff --git a/src/telegram/targets.test.ts b/src/telegram/targets.test.ts index 334f48499..f0b28fef1 100644 --- a/src/telegram/targets.test.ts +++ b/src/telegram/targets.test.ts @@ -11,6 +11,10 @@ describe("stripTelegramInternalPrefixes", () => { expect(stripTelegramInternalPrefixes("telegram:group:-100123")).toBe("-100123"); }); + it("does not strip group prefix without telegram prefix", () => { + expect(stripTelegramInternalPrefixes("group:-100123")).toBe("group:-100123"); + }); + it("is idempotent", () => { expect(stripTelegramInternalPrefixes("@mychannel")).toBe("@mychannel"); }); diff --git a/src/telegram/targets.ts b/src/telegram/targets.ts index fd0f0e379..2bf139a14 100644 --- a/src/telegram/targets.ts +++ b/src/telegram/targets.ts @@ -5,8 +5,19 @@ export type TelegramTarget = { export function stripTelegramInternalPrefixes(to: string): string { let trimmed = to.trim(); + let strippedTelegramPrefix = false; while (true) { - const next = trimmed.replace(/^(telegram|tg|group):/i, "").trim(); + const next = (() => { + if (/^(telegram|tg):/i.test(trimmed)) { + strippedTelegramPrefix = true; + return trimmed.replace(/^(telegram|tg):/i, "").trim(); + } + // Legacy internal form: `telegram:group:` (still emitted by session keys). + if (strippedTelegramPrefix && /^group:/i.test(trimmed)) { + return trimmed.replace(/^group:/i, "").trim(); + } + return trimmed; + })(); if (next === trimmed) return trimmed; trimmed = next; } diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index 61d28aad3..3a23b013a 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -57,7 +57,7 @@ export type GatewaySessionList = { label?: string; displayName?: string; provider?: string; - room?: string; + groupChannel?: string; space?: string; subject?: string; chatType?: string;