diff --git a/services/backend/src/modules/messages/messages.service.ts b/services/backend/src/modules/messages/messages.service.ts
index 41628417..945aee10 100644
--- a/services/backend/src/modules/messages/messages.service.ts
+++ b/services/backend/src/modules/messages/messages.service.ts
@@ -132,6 +132,8 @@ function mapSearchHit(hit: {
channel_id?: string;
guild_id?: string;
thread_id?: string | null;
+ channel_name?: string | null;
+ thread_name?: string | null;
created_at?: number;
};
}) {
@@ -146,6 +148,8 @@ function mapSearchHit(hit: {
channel_id: hit.payload.channel_id ?? null,
guild_id: hit.payload.guild_id ?? null,
thread_id: hit.payload.thread_id ?? null,
+ channel_name: hit.payload.channel_name ?? null,
+ thread_name: hit.payload.thread_name ?? null,
};
}
diff --git a/services/backend/src/modules/messages/qdrant.ts b/services/backend/src/modules/messages/qdrant.ts
index d995cd78..e3467753 100644
--- a/services/backend/src/modules/messages/qdrant.ts
+++ b/services/backend/src/modules/messages/qdrant.ts
@@ -14,6 +14,8 @@ export interface ArchiveHit {
channel_id?: string;
guild_id?: string;
thread_id?: string | null;
+ channel_name?: string | null;
+ thread_name?: string | null;
created_at?: number;
};
}
diff --git a/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts b/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts
index 8f718b0e..018b7de7 100644
--- a/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts
+++ b/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts
@@ -44,6 +44,8 @@ export interface QdrantVerdictPayload {
channel_id?: string;
guild_id?: string;
thread_id?: string | null;
+ channel_name?: string | null;
+ thread_name?: string | null;
created_at?: number;
}
diff --git a/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts b/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts
index fbbbce91..d22807cb 100644
--- a/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts
+++ b/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts
@@ -20,12 +20,42 @@ export interface ArchiveMessage {
guild_id: string;
thread_id: string | null;
created_at: number;
+ /** JSON string of RichMessageMetadata (parsed for channel/thread names). */
+ metadata?: string | null;
/** True when the message came from an age-restricted (NSFW) channel. NSFW
* content is deliberately NOT embedded into the public archive so it can't
* be found via public semantic search. Defaults to false. */
isAgeRestricted?: boolean;
}
+/**
+ * Extract a human-readable channel label from the message's metadata JSON.
+ * The gateway captures `metadata.channel.{channelName,threadName}` per message;
+ * prefer the thread name (thread messages read better by their thread title),
+ * then the channel name. Returns null when unavailable (old messages without
+ * the metadata field, or malformed JSON).
+ */
+export function extractChannelLabel(metadata: string | null | undefined): {
+ channel_name: string | null;
+ thread_name: string | null;
+} {
+ if (!metadata) return { channel_name: null, thread_name: null };
+ try {
+ const m = JSON.parse(metadata) as {
+ channel?: {
+ channelName?: string | null;
+ threadName?: string | null;
+ };
+ };
+ return {
+ channel_name: m?.channel?.channelName ?? null,
+ thread_name: m?.channel?.threadName ?? null,
+ };
+ } catch {
+ return { channel_name: null, thread_name: null };
+ }
+}
+
/**
* Fire-and-forget: embed a captured message and upsert it into the persistent
* archive collection so the public web can semantic-search the corpus.
@@ -52,6 +82,9 @@ export function archiveMessageEmbedded(message: ArchiveMessage): void {
if (!normalized) return; // nothing meaningful left after cleanup
const vector = await embedText(normalized);
if (!vector) return;
+ const { channel_name, thread_name } = extractChannelLabel(
+ message.metadata,
+ );
const ok = await upsertQdrantPointV2(
ARCHIVE_COLLECTION,
qdrantPointId(`archive:${message.id}`),
@@ -65,6 +98,8 @@ export function archiveMessageEmbedded(message: ArchiveMessage): void {
channel_id: message.channel_id ?? "",
guild_id: message.guild_id ?? "",
thread_id: message.thread_id ?? null,
+ channel_name: channel_name ?? null,
+ thread_name: thread_name ?? null,
created_at: message.created_at ?? Date.now(),
analyzed_at: Date.now(),
// 5-year persistent window (archive is NOT a TTL cache).
diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx
index 0fc7ee2e..c792d761 100644
--- a/services/frontend/src/app/(dashboard)/messages/view.tsx
+++ b/services/frontend/src/app/(dashboard)/messages/view.tsx
@@ -366,8 +366,18 @@ export function MessagesView({
{r.username}
)}
- {r.channel_id && (
+ {r.thread_name && (
+ ▶ {r.thread_name}
+
+ )}
+ {!r.thread_name && r.channel_name && (
+
+ #{r.channel_name}
+
+ )}
+ {!r.thread_name && !r.channel_name && r.channel_id && (
+
#{r.channel_id}
)}
diff --git a/services/frontend/src/lib/types/message.ts b/services/frontend/src/lib/types/message.ts
index 17b7924e..79e71b8d 100644
--- a/services/frontend/src/lib/types/message.ts
+++ b/services/frontend/src/lib/types/message.ts
@@ -177,6 +177,8 @@ export interface SemanticSearchResult {
channel_id: string | null;
guild_id: string | null;
thread_id: string | null;
+ channel_name: string | null;
+ thread_name: string | null;
}
export interface MessageActivityBucket {