fix: unify inbound sender labels

This commit is contained in:
Peter Steinberger
2026-01-17 05:21:02 +00:00
parent 572e04d5fb
commit f7089cde54
20 changed files with 587 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { formatAgentEnvelope } from "./envelope.js";
import { formatAgentEnvelope, formatInboundEnvelope } from "./envelope.js";
describe("formatAgentEnvelope", () => {
it("includes channel, from, ip, host, and timestamp", () => {
@@ -43,3 +43,38 @@ describe("formatAgentEnvelope", () => {
expect(body).toBe("[Telegram] hi");
});
});
describe("formatInboundEnvelope", () => {
it("prefixes sender for non-direct chats", () => {
const body = formatInboundEnvelope({
channel: "Discord",
from: "Guild #general",
body: "hi",
chatType: "channel",
senderLabel: "Alice",
});
expect(body).toBe("[Discord Guild #general] Alice: hi");
});
it("uses sender fields when senderLabel is missing", () => {
const body = formatInboundEnvelope({
channel: "Signal",
from: "Signal Group id:123",
body: "ping",
chatType: "group",
sender: { name: "Bob", id: "42" },
});
expect(body).toBe("[Signal Signal Group id:123] Bob (42): ping");
});
it("keeps direct messages unprefixed", () => {
const body = formatInboundEnvelope({
channel: "iMessage",
from: "+1555",
body: "hello",
chatType: "direct",
senderLabel: "Alice",
});
expect(body).toBe("[iMessage +1555] hello");
});
});

View File

@@ -1,3 +1,6 @@
import { normalizeChatType } from "../channels/chat-type.js";
import { resolveSenderLabel, type SenderLabelParams } from "../channels/sender-label.js";
export type AgentEnvelopeParams = {
channel: string;
from?: string;
@@ -35,6 +38,27 @@ export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
return `${header} ${params.body}`;
}
export function formatInboundEnvelope(params: {
channel: string;
from: string;
body: string;
timestamp?: number | Date;
chatType?: string;
senderLabel?: string;
sender?: SenderLabelParams;
}): string {
const chatType = normalizeChatType(params.chatType);
const isDirect = !chatType || chatType === "direct";
const resolvedSender = params.senderLabel?.trim() || resolveSenderLabel(params.sender ?? {});
const body = !isDirect && resolvedSender ? `${resolvedSender}: ${params.body}` : params.body;
return formatAgentEnvelope({
channel: params.channel,
from: params.from,
timestamp: params.timestamp,
body,
});
}
export function formatThreadStarterEnvelope(params: {
channel: string;
author?: string;

View File

@@ -41,4 +41,11 @@ describe("formatInboundBodyWithSenderMeta", () => {
"[X] hi\n[from: Alice (A1)]",
);
});
it("does not append when the body already includes a sender prefix", () => {
const ctx: MsgContext = { ChatType: "group", SenderName: "Alice", SenderId: "A1" };
expect(formatInboundBodyWithSenderMeta({ ctx, body: "Alice (A1): hi" })).toBe(
"Alice (A1): hi",
);
});
});

View File

@@ -1,28 +1,43 @@
import type { MsgContext } from "../templating.js";
import { normalizeChatType } from "../../channels/chat-type.js";
import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js";
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
const body = params.body;
if (!body.trim()) return body;
const chatType = normalizeChatType(params.ctx.ChatType);
if (!chatType || chatType === "direct") return body;
if (hasSenderMetaLine(body)) return body;
if (hasSenderMetaLine(body, params.ctx)) return body;
const senderLabel = formatSenderLabel(params.ctx);
const senderLabel = resolveSenderLabel({
name: params.ctx.SenderName,
username: params.ctx.SenderUsername,
tag: params.ctx.SenderTag,
e164: params.ctx.SenderE164,
id: params.ctx.SenderId,
});
if (!senderLabel) return body;
return `${body}\n[from: ${senderLabel}]`;
}
function hasSenderMetaLine(body: string): boolean {
return /(^|\n)\[from:/i.test(body);
function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
if (/(^|\n)\[from:/i.test(body)) return true;
const candidates = listSenderLabelCandidates({
name: ctx.SenderName,
username: ctx.SenderUsername,
tag: ctx.SenderTag,
e164: ctx.SenderE164,
id: ctx.SenderId,
});
if (candidates.length === 0) return false;
return candidates.some((candidate) => {
const escaped = escapeRegExp(candidate);
const pattern = new RegExp(`(^|\\n)${escaped}:\\s`, "i");
return pattern.test(body);
});
}
function formatSenderLabel(ctx: MsgContext): string | null {
const senderName = ctx.SenderName?.trim();
const senderId = (ctx.SenderE164?.trim() || ctx.SenderId?.trim()) ?? "";
if (senderName && senderId && senderName !== senderId) {
return `${senderName} (${senderId})`;
}
return senderName ?? (senderId || null);
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}