chore: format + regenerate protocol

This commit is contained in:
Peter Steinberger
2026-01-17 03:40:49 +00:00
parent 09bed2ccde
commit a82217a5f3
20 changed files with 79 additions and 74 deletions

View File

@@ -357,7 +357,7 @@ public struct SendParams: Codable, Sendable {
gifplayback: Bool?, gifplayback: Bool?,
channel: String?, channel: String?,
accountid: String?, accountid: String?,
sessionkey: String? = nil, sessionkey: String?,
idempotencykey: String idempotencykey: String
) { ) {
self.to = to self.to = to
@@ -431,6 +431,7 @@ public struct AgentParams: Codable, Sendable {
public let deliver: Bool? public let deliver: Bool?
public let attachments: [AnyCodable]? public let attachments: [AnyCodable]?
public let channel: String? public let channel: String?
public let accountid: String?
public let timeout: Int? public let timeout: Int?
public let lane: String? public let lane: String?
public let extrasystemprompt: String? public let extrasystemprompt: String?
@@ -447,6 +448,7 @@ public struct AgentParams: Codable, Sendable {
deliver: Bool?, deliver: Bool?,
attachments: [AnyCodable]?, attachments: [AnyCodable]?,
channel: String?, channel: String?,
accountid: String?,
timeout: Int?, timeout: Int?,
lane: String?, lane: String?,
extrasystemprompt: String?, extrasystemprompt: String?,
@@ -462,6 +464,7 @@ public struct AgentParams: Codable, Sendable {
self.deliver = deliver self.deliver = deliver
self.attachments = attachments self.attachments = attachments
self.channel = channel self.channel = channel
self.accountid = accountid
self.timeout = timeout self.timeout = timeout
self.lane = lane self.lane = lane
self.extrasystemprompt = extrasystemprompt self.extrasystemprompt = extrasystemprompt
@@ -478,6 +481,7 @@ public struct AgentParams: Codable, Sendable {
case deliver case deliver
case attachments case attachments
case channel case channel
case accountid = "accountId"
case timeout case timeout
case lane case lane
case extrasystemprompt = "extraSystemPrompt" case extrasystemprompt = "extraSystemPrompt"

View File

@@ -37,5 +37,7 @@ export function channelTargetSchema(options?: { description?: string }) {
} }
export function channelTargetsSchema(options?: { description?: string }) { export function channelTargetsSchema(options?: { description?: string }) {
return Type.Array(channelTargetSchema({ description: options?.description ?? CHANNEL_TARGETS_DESCRIPTION })); return Type.Array(
channelTargetSchema({ description: options?.description ?? CHANNEL_TARGETS_DESCRIPTION }),
);
} }

View File

@@ -16,10 +16,7 @@ import {
} from "../auto-reply/reply/queue.js"; } from "../auto-reply/reply/queue.js";
import { callGateway } from "../gateway/call.js"; import { callGateway } from "../gateway/call.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
type DeliveryContext,
normalizeDeliveryContext,
} from "../utils/delivery-context.js";
import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js"; import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";
import { readLatestAssistantReply } from "./tools/agent-step.js"; import { readLatestAssistantReply } from "./tools/agent-step.js";

View File

@@ -11,7 +11,9 @@ describe("formatInboundBodyWithSenderMeta", () => {
it("appends a sender meta line for non-direct messages", () => { it("appends a sender meta line for non-direct messages", () => {
const ctx: MsgContext = { ChatType: "group", SenderName: "Alice", SenderId: "A1" }; const ctx: MsgContext = { ChatType: "group", SenderName: "Alice", SenderId: "A1" };
expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe("[X] hi\n[from: Alice (A1)]"); expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe(
"[X] hi\n[from: Alice (A1)]",
);
}); });
it("prefers SenderE164 in the label when present", () => { it("prefers SenderE164 in the label when present", () => {
@@ -21,7 +23,9 @@ describe("formatInboundBodyWithSenderMeta", () => {
SenderId: "bob@s.whatsapp.net", SenderId: "bob@s.whatsapp.net",
SenderE164: "+222", SenderE164: "+222",
}; };
expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe("[X] hi\n[from: Bob (+222)]"); expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe(
"[X] hi\n[from: Bob (+222)]",
);
}); });
it("preserves escaped newline style when body uses literal \\\\n", () => { it("preserves escaped newline style when body uses literal \\\\n", () => {
@@ -38,4 +42,3 @@ describe("formatInboundBodyWithSenderMeta", () => {
); );
}); });
}); });

View File

@@ -1,9 +1,6 @@
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
export function formatInboundBodyWithSenderMeta(params: { export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
body: string;
ctx: MsgContext;
}): string {
const body = params.body; const body = params.body;
if (!body.trim()) return body; if (!body.trim()) return body;
const chatType = params.ctx.ChatType?.trim().toLowerCase(); const chatType = params.ctx.ChatType?.trim().toLowerCase();

View File

@@ -49,4 +49,3 @@ describe("initSessionState sender meta", () => {
expect(result.sessionCtx.BodyStripped).toBe("[WhatsApp +1] ping"); expect(result.sessionCtx.BodyStripped).toBe("[WhatsApp +1] ping");
}); });
}); });

View File

@@ -234,8 +234,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
} }
} }
const filtered = q ? rows.filter((row) => row.name?.toLowerCase().includes(q)) : rows; const filtered = q ? rows.filter((row) => row.name?.toLowerCase().includes(q)) : rows;
const limited = const limited = typeof limit === "number" && limit > 0 ? filtered.slice(0, limit) : filtered;
typeof limit === "number" && limit > 0 ? filtered.slice(0, limit) : filtered;
return limited; return limited;
}, },
}, },

View File

@@ -25,15 +25,9 @@ export function createMessageCliHelpers(
.option("--verbose", "Verbose logging", false); .option("--verbose", "Verbose logging", false);
const withMessageTarget = (command: Command) => const withMessageTarget = (command: Command) =>
command.option( command.option("-t, --to <dest>", CHANNEL_TARGET_DESCRIPTION);
"-t, --to <dest>",
CHANNEL_TARGET_DESCRIPTION,
);
const withRequiredMessageTarget = (command: Command) => const withRequiredMessageTarget = (command: Command) =>
command.requiredOption( command.requiredOption("-t, --to <dest>", CHANNEL_TARGET_DESCRIPTION);
"-t, --to <dest>",
CHANNEL_TARGET_DESCRIPTION,
);
const runMessageAction = async (action: string, opts: Record<string, unknown>) => { const runMessageAction = async (action: string, opts: Record<string, unknown>) => {
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));

View File

@@ -7,10 +7,7 @@ export function registerMessageBroadcastCommand(message: Command, helpers: Messa
.withMessageBase( .withMessageBase(
message.command("broadcast").description("Broadcast a message to multiple targets"), message.command("broadcast").description("Broadcast a message to multiple targets"),
) )
.requiredOption( .requiredOption("--targets <target...>", CHANNEL_TARGETS_DESCRIPTION)
"--targets <target...>",
CHANNEL_TARGETS_DESCRIPTION,
)
.option("--message <text>", "Message to send") .option("--message <text>", "Message to send")
.option("--media <url>", "Media URL") .option("--media <url>", "Media URL")
.action(async (options: Record<string, unknown>) => { .action(async (options: Record<string, unknown>) => {

View File

@@ -275,8 +275,7 @@ const FIELD_HELP: Record<string, string> = {
'Text prefix for cross-context markers (supports "{channel}").', 'Text prefix for cross-context markers (supports "{channel}").',
"tools.message.crossContext.marker.suffix": "tools.message.crossContext.marker.suffix":
'Text suffix for cross-context markers (supports "{channel}").', 'Text suffix for cross-context markers (supports "{channel}").',
"tools.message.broadcast.enabled": "tools.message.broadcast.enabled": "Enable broadcast action (default: true).",
"Enable broadcast action (default: true).",
"tools.web.search.enabled": "Enable the web_search tool (requires Brave API key).", "tools.web.search.enabled": "Enable the web_search tool (requires Brave API key).",
"tools.web.search.provider": 'Search provider (only "brave" supported today).', "tools.web.search.provider": 'Search provider (only "brave" supported today).',
"tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).", "tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).",

View File

@@ -202,8 +202,7 @@ export const agentHandlers: GatewayRequestHandlers = {
const lastChannel = sessionEntry?.lastChannel; const lastChannel = sessionEntry?.lastChannel;
const lastTo = typeof sessionEntry?.lastTo === "string" ? sessionEntry.lastTo.trim() : ""; const lastTo = typeof sessionEntry?.lastTo === "string" ? sessionEntry.lastTo.trim() : "";
const resolvedAccountId = const resolvedAccountId =
normalizeAccountId(request.accountId) ?? normalizeAccountId(request.accountId) ?? normalizeAccountId(sessionEntry?.lastAccountId);
normalizeAccountId(sessionEntry?.lastAccountId);
const wantsDelivery = request.deliver === true; const wantsDelivery = request.deliver === true;

View File

@@ -157,7 +157,10 @@ describe("runMessageAction context isolation", () => {
to: "imessage:+15551230000", to: "imessage:+15551230000",
message: "hi", message: "hi",
}, },
toolContext: { currentChannelId: "imessage:+15551234567", currentChannelProvider: "imessage" }, toolContext: {
currentChannelId: "imessage:+15551234567",
currentChannelProvider: "imessage",
},
dryRun: true, dryRun: true,
}); });

View File

@@ -13,7 +13,10 @@ import type {
} from "../../channels/plugins/types.js"; } from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js"; import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
import { listConfiguredMessageChannels, resolveMessageChannelSelection } from "./channel-selection.js"; import {
listConfiguredMessageChannels,
resolveMessageChannelSelection,
} from "./channel-selection.js";
import type { OutboundSendDeps } from "./deliver.js"; import type { OutboundSendDeps } from "./deliver.js";
import type { MessagePollResult, MessageSendResult } from "./message.js"; import type { MessagePollResult, MessageSendResult } from "./message.js";
import { sendMessage, sendPoll } from "./message.js"; import { sendMessage, sendPoll } from "./message.js";

View File

@@ -77,7 +77,8 @@ export function enforceCrossContextPolicy(params: {
if (params.cfg.tools?.message?.allowCrossContextSend) return; if (params.cfg.tools?.message?.allowCrossContextSend) return;
const currentProvider = params.toolContext?.currentChannelProvider; const currentProvider = params.toolContext?.currentChannelProvider;
const allowWithinProvider = params.cfg.tools?.message?.crossContext?.allowWithinProvider !== false; const allowWithinProvider =
params.cfg.tools?.message?.crossContext?.allowWithinProvider !== false;
const allowAcrossProviders = const allowAcrossProviders =
params.cfg.tools?.message?.crossContext?.allowAcrossProviders === true; params.cfg.tools?.message?.crossContext?.allowAcrossProviders === true;
@@ -132,7 +133,7 @@ export async function buildCrossContextDecoration(params: {
const adapter = getChannelMessageAdapter(params.channel); const adapter = getChannelMessageAdapter(params.channel);
const embeds = adapter.supportsEmbeds const embeds = adapter.supportsEmbeds
? adapter.buildCrossContextEmbeds?.(originLabel) ?? undefined ? (adapter.buildCrossContextEmbeds?.(originLabel) ?? undefined)
: undefined; : undefined;
return { prefix, suffix, embeds }; return { prefix, suffix, embeds };

View File

@@ -31,7 +31,10 @@ function normalizeQuery(value: string): string {
} }
function stripTargetPrefixes(value: string): string { function stripTargetPrefixes(value: string): string {
return value.replace(/^(channel|group|user):/i, "").replace(/^[@#]/, "").trim(); return value
.replace(/^(channel|group|user):/i, "")
.replace(/^[@#]/, "")
.trim();
} }
function preserveTargetCase(channel: ChannelId, raw: string, normalized: string): string { function preserveTargetCase(channel: ChannelId, raw: string, normalized: string): string {
@@ -132,7 +135,7 @@ async function listDirectoryEntries(params: {
const runtime = params.runtime ?? defaultRuntime; const runtime = params.runtime ?? defaultRuntime;
const useLive = params.source === "live"; const useLive = params.source === "live";
if (params.kind === "user") { if (params.kind === "user") {
const fn = useLive ? directory.listPeersLive ?? directory.listPeers : directory.listPeers; const fn = useLive ? (directory.listPeersLive ?? directory.listPeers) : directory.listPeers;
if (!fn) return []; if (!fn) return [];
return await fn({ return await fn({
cfg: params.cfg, cfg: params.cfg,
@@ -142,7 +145,7 @@ async function listDirectoryEntries(params: {
runtime, runtime,
}); });
} }
const fn = useLive ? directory.listGroupsLive ?? directory.listGroups : directory.listGroups; const fn = useLive ? (directory.listGroupsLive ?? directory.listGroups) : directory.listGroups;
if (!fn) return []; if (!fn) return [];
return await fn({ return await fn({
cfg: params.cfg, cfg: params.cfg,
@@ -254,9 +257,7 @@ export async function resolveMessagingTarget(params: {
if (match.kind === "ambiguous") { if (match.kind === "ambiguous") {
return { return {
ok: false, ok: false,
error: new Error( error: new Error(`Ambiguous target "${raw}". Provide a unique name or an explicit id.`),
`Ambiguous target "${raw}". Provide a unique name or an explicit id.`,
),
candidates: match.entries, candidates: match.entries,
}; };
} }

View File

@@ -323,15 +323,13 @@ export const buildTelegramMessageContext = async ({
replyTarget.id ? ` id:${replyTarget.id}` : "" replyTarget.id ? ` id:${replyTarget.id}` : ""
}]\n${replyTarget.body}\n[/Replying]` }]\n${replyTarget.body}\n[/Replying]`
: ""; : "";
const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined; const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined;
const body = formatAgentEnvelope({ const body = formatAgentEnvelope({
channel: "Telegram", channel: "Telegram",
from: isGroup from: isGroup ? (groupLabel ?? `group:${chatId}`) : buildSenderLabel(msg, senderId || chatId),
? (groupLabel ?? `group:${chatId}`) timestamp: msg.date ? msg.date * 1000 : undefined,
: buildSenderLabel(msg, senderId || chatId), body: `${bodyText}${replySuffix}`,
timestamp: msg.date ? msg.date * 1000 : undefined, });
body: `${bodyText}${replySuffix}`,
});
let combinedBody = body; let combinedBody = body;
if (isGroup && historyKey && historyLimit > 0) { if (isGroup && historyKey && historyLimit > 0) {
combinedBody = buildPendingHistoryContextFromMap({ combinedBody = buildPendingHistoryContextFromMap({

View File

@@ -171,17 +171,17 @@ describe("createTelegramBot", () => {
getFile: async () => ({ download: async () => new Uint8Array() }), getFile: async () => ({ download: async () => new Uint8Array() }),
}); });
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expect(payload.WasMentioned).toBe(true); expect(payload.WasMentioned).toBe(true);
expect(payload.SenderName).toBe("Ada"); expect(payload.SenderName).toBe("Ada");
expect(payload.SenderId).toBe("9"); expect(payload.SenderId).toBe("9");
expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 2025-01-09T00:00Z\]/); expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 2025-01-09T00:00Z\]/);
}); });
it("keeps group envelope headers stable (sender identity is separate)", async () => { it("keeps group envelope headers stable (sender identity is separate)", async () => {
onSpy.mockReset(); onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>; const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
replySpy.mockReset(); replySpy.mockReset();
loadConfig.mockReturnValue({ loadConfig.mockReturnValue({
channels: { channels: {
@@ -212,13 +212,13 @@ describe("createTelegramBot", () => {
getFile: async () => ({ download: async () => new Uint8Array() }), getFile: async () => ({ download: async () => new Uint8Array() }),
}); });
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expect(payload.SenderName).toBe("Ada Lovelace"); expect(payload.SenderName).toBe("Ada Lovelace");
expect(payload.SenderId).toBe("99"); expect(payload.SenderId).toBe("99");
expect(payload.SenderUsername).toBe("ada"); expect(payload.SenderUsername).toBe("ada");
expect(payload.Body).toMatch(/^\[Telegram Ops id:42 2025-01-09T00:00Z\]/); expect(payload.Body).toMatch(/^\[Telegram Ops id:42 2025-01-09T00:00Z\]/);
}); });
it("reacts to mention-gated group messages when ackReaction is enabled", async () => { it("reacts to mention-gated group messages when ackReaction is enabled", async () => {
onSpy.mockReset(); onSpy.mockReset();
setMessageReactionSpy.mockReset(); setMessageReactionSpy.mockReset();

View File

@@ -8,8 +8,7 @@ export type DeliveryContext = {
export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryContext | undefined { export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryContext | undefined {
if (!context) return undefined; if (!context) return undefined;
const channel = const channel = typeof context.channel === "string" ? context.channel.trim() : undefined;
typeof context.channel === "string" ? context.channel.trim() : undefined;
const to = typeof context.to === "string" ? context.to.trim() : undefined; const to = typeof context.to === "string" ? context.to.trim() : undefined;
const accountId = normalizeAccountId(context.accountId); const accountId = normalizeAccountId(context.accountId);
if (!channel && !to && !accountId) return undefined; if (!channel && !to && !accountId) return undefined;

View File

@@ -216,7 +216,12 @@ describe("broadcast groups", () => {
expect(resolver).toHaveBeenCalledTimes(2); expect(resolver).toHaveBeenCalledTimes(2);
for (const call of resolver.mock.calls.slice(0, 2)) { for (const call of resolver.mock.calls.slice(0, 2)) {
const payload = call[0] as { Body: string; SenderName?: string; SenderE164?: string; SenderId?: string }; const payload = call[0] as {
Body: string;
SenderName?: string;
SenderE164?: string;
SenderId?: string;
};
expect(payload.Body).toContain("Chat messages since your last reply"); expect(payload.Body).toContain("Chat messages since your last reply");
expect(payload.Body).toContain("Alice (+111): hello group"); expect(payload.Body).toContain("Alice (+111): hello group");
expect(payload.Body).toContain("[message_id: g1]"); expect(payload.Body).toContain("[message_id: g1]");

View File

@@ -9,7 +9,10 @@ import {
} from "../../../auto-reply/reply/response-prefix-template.js"; } from "../../../auto-reply/reply/response-prefix-template.js";
import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../../../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../../../auto-reply/envelope.js";
import { buildHistoryContextFromEntries, type HistoryEntry } from "../../../auto-reply/reply/history.js"; import {
buildHistoryContextFromEntries,
type HistoryEntry,
} from "../../../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js";
import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; import type { getReplyFromConfig } from "../../../auto-reply/reply.js";
import type { ReplyPayload } from "../../../auto-reply/types.js"; import type { ReplyPayload } from "../../../auto-reply/types.js";
@@ -88,7 +91,9 @@ export async function processMessage(params: {
currentMessage: combinedBody, currentMessage: combinedBody,
excludeLast: false, excludeLast: false,
formatEntry: (entry) => { formatEntry: (entry) => {
const bodyWithId = entry.messageId ? `${entry.body}\n[message_id: ${entry.messageId}]` : entry.body; const bodyWithId = entry.messageId
? `${entry.body}\n[message_id: ${entry.messageId}]`
: entry.body;
return formatAgentEnvelope({ return formatAgentEnvelope({
channel: "WhatsApp", channel: "WhatsApp",
from: conversationId, from: conversationId,