From d0a4cce41e6d61f0670da461ddd71cef65bc3d0a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 14 Jan 2026 23:05:08 -0500 Subject: [PATCH 1/6] feat: add dynamic template variables to messages.responsePrefix (#923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for template variables in `messages.responsePrefix` that resolve dynamically at runtime with the actual model used (including after fallback). Supported variables (case-insensitive): - {model} - short model name (e.g., "claude-opus-4-5", "gpt-4o") - {modelFull} - full model identifier (e.g., "anthropic/claude-opus-4-5") - {provider} - provider name (e.g., "anthropic", "openai") - {thinkingLevel} or {think} - thinking level ("high", "low", "off") - {identity.name} or {identityName} - agent identity name Example: "[{model} | think:{thinkingLevel}]" → "[claude-opus-4-5 | think:high]" Variables show the actual model used after fallback, not the intended model. Unresolved variables remain as literal text. Implementation: - New module: src/auto-reply/reply/response-prefix-template.ts - Template interpolation in normalize-reply.ts via context provider - onModelSelected callback in agent-runner-execution.ts - Updated all 6 provider message handlers (web, signal, discord, telegram, slack, imessage) - 27 unit tests covering all variables and edge cases - Documentation in docs/gateway/configuration.md and JSDoc Fixes #923 --- docs/gateway/configuration.md | 25 +++ src/agents/identity.ts | 5 + .../reply/agent-runner-execution.ts | 8 + src/auto-reply/reply/normalize-reply.ts | 17 +- src/auto-reply/reply/reply-dispatcher.ts | 15 +- .../reply/response-prefix-template.test.ts | 181 ++++++++++++++++++ .../reply/response-prefix-template.ts | 97 ++++++++++ src/auto-reply/types.ts | 10 + src/config/types.messages.ts | 15 +- .../monitor/message-handler.process.ts | 21 ++ src/imessage/monitor/monitor-provider.ts | 26 ++- src/signal/monitor/event-handler.ts | 26 ++- src/slack/monitor/message-handler/dispatch.ts | 21 ++ src/telegram/bot-message-dispatch.ts | 21 +- src/web/auto-reply/monitor/process-message.ts | 21 +- 15 files changed, 500 insertions(+), 9 deletions(-) create mode 100644 src/auto-reply/reply/response-prefix-template.test.ts create mode 100644 src/auto-reply/reply/response-prefix-template.ts diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 012bec824..9906e94b1 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -1184,6 +1184,31 @@ streaming, final replies) across channels unless already present. If `messages.responsePrefix` is unset, no prefix is applied by default. Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set). +#### Template variables + +The `responsePrefix` string can include template variables that resolve dynamically: + +| Variable | Description | Example | +|----------|-------------|---------| +| `{model}` | Short model name | `claude-opus-4-5`, `gpt-4o` | +| `{modelFull}` | Full model identifier | `anthropic/claude-opus-4-5` | +| `{provider}` | Provider name | `anthropic`, `openai` | +| `{thinkingLevel}` | Current thinking level | `high`, `low`, `off` | +| `{identity.name}` | Agent identity name | (same as `"auto"` mode) | + +Variables are case-insensitive (`{MODEL}` = `{model}`). `{think}` is an alias for `{thinkingLevel}`. +Unresolved variables remain as literal text. + +```json5 +{ + messages: { + responsePrefix: "[{model} | think:{thinkingLevel}]" + } +} +``` + +Example output: `[claude-opus-4-5 | think:high] Here's my response...` + WhatsApp inbound prefix is configured via `channels.whatsapp.messagePrefix` (deprecated: `messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when `channels.whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using diff --git a/src/agents/identity.ts b/src/agents/identity.ts index 6ac80da2f..b78e2746d 100644 --- a/src/agents/identity.ts +++ b/src/agents/identity.ts @@ -26,6 +26,11 @@ export function resolveIdentityNamePrefix( return `[${name}]`; } +/** Returns just the identity name (without brackets) for template context. */ +export function resolveIdentityName(cfg: ClawdbotConfig, agentId: string): string | undefined { + return resolveAgentIdentity(cfg, agentId)?.name?.trim() || undefined; +} + export function resolveMessagePrefix( cfg: ClawdbotConfig, agentId: string, diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index fbc7a7451..d1f0e276d 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -125,6 +125,14 @@ export async function runAgentTurnWithFallback(params: { resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey), ), run: (provider, model) => { + // Notify that model selection is complete (including after fallback). + // This allows responsePrefix template interpolation with the actual model. + params.opts?.onModelSelected?.({ + provider, + model, + thinkLevel: params.followupRun.run.thinkLevel, + }); + if (isCliProvider(provider, params.followupRun.run.config)) { const startedAt = Date.now(); emitAgentEvent({ diff --git a/src/auto-reply/reply/normalize-reply.ts b/src/auto-reply/reply/normalize-reply.ts index f46a49b9f..ac8e85f2e 100644 --- a/src/auto-reply/reply/normalize-reply.ts +++ b/src/auto-reply/reply/normalize-reply.ts @@ -1,9 +1,15 @@ import { stripHeartbeatToken } from "../heartbeat.js"; import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import type { ReplyPayload } from "../types.js"; +import { + resolveResponsePrefixTemplate, + type ResponsePrefixContext, +} from "./response-prefix-template.js"; export type NormalizeReplyOptions = { responsePrefix?: string; + /** Context for template variable interpolation in responsePrefix */ + responsePrefixContext?: ResponsePrefixContext; onHeartbeatStrip?: () => void; stripHeartbeat?: boolean; silentToken?: string; @@ -36,13 +42,18 @@ export function normalizeReplyPayload( text = stripped.text; } + // Resolve template variables in responsePrefix if context is provided + const effectivePrefix = opts.responsePrefixContext + ? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext) + : opts.responsePrefix; + if ( - opts.responsePrefix && + effectivePrefix && text && text.trim() !== HEARTBEAT_TOKEN && - !text.startsWith(opts.responsePrefix) + !text.startsWith(effectivePrefix) ) { - text = `${opts.responsePrefix} ${text}`; + text = `${effectivePrefix} ${text}`; } return { ...payload, text }; diff --git a/src/auto-reply/reply/reply-dispatcher.ts b/src/auto-reply/reply/reply-dispatcher.ts index 5cc3a97fa..b8eeca477 100644 --- a/src/auto-reply/reply/reply-dispatcher.ts +++ b/src/auto-reply/reply/reply-dispatcher.ts @@ -1,6 +1,7 @@ import type { HumanDelayConfig } from "../../config/types.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { normalizeReplyPayload } from "./normalize-reply.js"; +import type { ResponsePrefixContext } from "./response-prefix-template.js"; import type { TypingController } from "./typing.js"; export type ReplyDispatchKind = "tool" | "block" | "final"; @@ -33,6 +34,11 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export type ReplyDispatcherOptions = { deliver: ReplyDispatchDeliverer; responsePrefix?: string; + /** Static context for response prefix template interpolation. */ + responsePrefixContext?: ResponsePrefixContext; + /** Dynamic context provider for response prefix template interpolation. + * Called at normalization time, after model selection is complete. */ + responsePrefixContextProvider?: () => ResponsePrefixContext; onHeartbeatStrip?: () => void; onIdle?: () => void; onError?: ReplyDispatchErrorHandler; @@ -61,10 +67,17 @@ export type ReplyDispatcher = { function normalizeReplyPayloadInternal( payload: ReplyPayload, - opts: Pick, + opts: Pick< + ReplyDispatcherOptions, + "responsePrefix" | "responsePrefixContext" | "responsePrefixContextProvider" | "onHeartbeatStrip" + >, ): ReplyPayload | null { + // Prefer dynamic context provider over static context + const prefixContext = opts.responsePrefixContextProvider?.() ?? opts.responsePrefixContext; + return normalizeReplyPayload(payload, { responsePrefix: opts.responsePrefix, + responsePrefixContext: prefixContext, onHeartbeatStrip: opts.onHeartbeatStrip, }); } diff --git a/src/auto-reply/reply/response-prefix-template.test.ts b/src/auto-reply/reply/response-prefix-template.test.ts new file mode 100644 index 000000000..db899f44d --- /dev/null +++ b/src/auto-reply/reply/response-prefix-template.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, it } from "vitest"; + +import { + extractShortModelName, + hasTemplateVariables, + resolveResponsePrefixTemplate, +} from "./response-prefix-template.js"; + +describe("resolveResponsePrefixTemplate", () => { + it("returns undefined for undefined template", () => { + expect(resolveResponsePrefixTemplate(undefined, {})).toBeUndefined(); + }); + + it("returns template as-is when no variables present", () => { + expect(resolveResponsePrefixTemplate("[Claude]", {})).toBe("[Claude]"); + }); + + it("resolves {model} variable", () => { + const result = resolveResponsePrefixTemplate("[{model}]", { + model: "gpt-5.2", + }); + expect(result).toBe("[gpt-5.2]"); + }); + + it("resolves {modelFull} variable", () => { + const result = resolveResponsePrefixTemplate("[{modelFull}]", { + modelFull: "openai-codex/gpt-5.2", + }); + expect(result).toBe("[openai-codex/gpt-5.2]"); + }); + + it("resolves {provider} variable", () => { + const result = resolveResponsePrefixTemplate("[{provider}]", { + provider: "anthropic", + }); + expect(result).toBe("[anthropic]"); + }); + + it("resolves {thinkingLevel} variable", () => { + const result = resolveResponsePrefixTemplate("think:{thinkingLevel}", { + thinkingLevel: "high", + }); + expect(result).toBe("think:high"); + }); + + it("resolves {think} as alias for thinkingLevel", () => { + const result = resolveResponsePrefixTemplate("think:{think}", { + thinkingLevel: "low", + }); + expect(result).toBe("think:low"); + }); + + it("resolves {identity.name} variable", () => { + const result = resolveResponsePrefixTemplate("[{identity.name}]", { + identityName: "Clawdbot", + }); + expect(result).toBe("[Clawdbot]"); + }); + + it("resolves {identityName} as alias", () => { + const result = resolveResponsePrefixTemplate("[{identityName}]", { + identityName: "Clawdbot", + }); + expect(result).toBe("[Clawdbot]"); + }); + + it("resolves multiple variables", () => { + const result = resolveResponsePrefixTemplate("[{model} | think:{thinkingLevel}]", { + model: "claude-opus-4-5", + thinkingLevel: "high", + }); + expect(result).toBe("[claude-opus-4-5 | think:high]"); + }); + + it("leaves unresolved variables as-is", () => { + const result = resolveResponsePrefixTemplate("[{model}]", {}); + expect(result).toBe("[{model}]"); + }); + + it("leaves unrecognized variables as-is", () => { + const result = resolveResponsePrefixTemplate("[{unknownVar}]", { + model: "gpt-5.2", + }); + expect(result).toBe("[{unknownVar}]"); + }); + + it("handles case insensitivity", () => { + const result = resolveResponsePrefixTemplate("[{MODEL} | {ThinkingLevel}]", { + model: "gpt-5.2", + thinkingLevel: "low", + }); + expect(result).toBe("[gpt-5.2 | low]"); + }); + + it("handles mixed resolved and unresolved variables", () => { + const result = resolveResponsePrefixTemplate("[{model} | {provider}]", { + model: "gpt-5.2", + // provider not provided + }); + expect(result).toBe("[gpt-5.2 | {provider}]"); + }); + + it("handles complex template with all variables", () => { + const result = resolveResponsePrefixTemplate( + "[{identity.name}] {provider}/{model} (think:{thinkingLevel})", + { + identityName: "Clawdbot", + provider: "anthropic", + model: "claude-opus-4-5", + thinkingLevel: "high", + }, + ); + expect(result).toBe("[Clawdbot] anthropic/claude-opus-4-5 (think:high)"); + }); +}); + +describe("extractShortModelName", () => { + it("strips provider prefix", () => { + expect(extractShortModelName("openai/gpt-5.2")).toBe("gpt-5.2"); + expect(extractShortModelName("anthropic/claude-opus-4-5")).toBe("claude-opus-4-5"); + expect(extractShortModelName("openai-codex/gpt-5.2-codex")).toBe("gpt-5.2-codex"); + }); + + it("strips date suffix", () => { + expect(extractShortModelName("claude-opus-4-5-20251101")).toBe("claude-opus-4-5"); + expect(extractShortModelName("gpt-5.2-20250115")).toBe("gpt-5.2"); + }); + + it("strips -latest suffix", () => { + expect(extractShortModelName("gpt-5.2-latest")).toBe("gpt-5.2"); + expect(extractShortModelName("claude-sonnet-latest")).toBe("claude-sonnet"); + }); + + it("handles model without provider", () => { + expect(extractShortModelName("gpt-5.2")).toBe("gpt-5.2"); + expect(extractShortModelName("claude-opus-4-5")).toBe("claude-opus-4-5"); + }); + + it("handles full path with provider and date suffix", () => { + expect(extractShortModelName("anthropic/claude-opus-4-5-20251101")).toBe("claude-opus-4-5"); + }); + + it("preserves version numbers that look like dates but are not", () => { + // Date suffix must be exactly 8 digits at the end + expect(extractShortModelName("model-v1234567")).toBe("model-v1234567"); + expect(extractShortModelName("model-123456789")).toBe("model-123456789"); + }); +}); + +describe("hasTemplateVariables", () => { + it("returns false for undefined", () => { + expect(hasTemplateVariables(undefined)).toBe(false); + }); + + it("returns false for empty string", () => { + expect(hasTemplateVariables("")).toBe(false); + }); + + it("returns false for static prefix", () => { + expect(hasTemplateVariables("[Claude]")).toBe(false); + }); + + it("returns true when template variables present", () => { + expect(hasTemplateVariables("[{model}]")).toBe(true); + expect(hasTemplateVariables("{provider}")).toBe(true); + expect(hasTemplateVariables("prefix {thinkingLevel} suffix")).toBe(true); + }); + + it("returns true for multiple variables", () => { + expect(hasTemplateVariables("[{model} | {provider}]")).toBe(true); + }); + + it("handles consecutive calls correctly (regex lastIndex reset)", () => { + // First call + expect(hasTemplateVariables("[{model}]")).toBe(true); + // Second call should still work + expect(hasTemplateVariables("[{model}]")).toBe(true); + // Static string should return false + expect(hasTemplateVariables("[Claude]")).toBe(false); + }); +}); diff --git a/src/auto-reply/reply/response-prefix-template.ts b/src/auto-reply/reply/response-prefix-template.ts new file mode 100644 index 000000000..788531ca5 --- /dev/null +++ b/src/auto-reply/reply/response-prefix-template.ts @@ -0,0 +1,97 @@ +/** + * Template interpolation for response prefix. + * + * Supports variables like `{model}`, `{provider}`, `{thinkingLevel}`, etc. + * Variables are case-insensitive and unresolved ones remain as literal text. + */ + +export type ResponsePrefixContext = { + /** Short model name (e.g., "gpt-5.2", "claude-opus-4-5") */ + model?: string; + /** Full model ID including provider (e.g., "openai-codex/gpt-5.2") */ + modelFull?: string; + /** Provider name (e.g., "openai-codex", "anthropic") */ + provider?: string; + /** Current thinking level (e.g., "high", "low", "off") */ + thinkingLevel?: string; + /** Agent identity name */ + identityName?: string; +}; + +// Regex pattern for template variables: {variableName} or {variable.name} +const TEMPLATE_VAR_PATTERN = /\{([a-zA-Z][a-zA-Z0-9.]*)\}/g; + +/** + * Interpolate template variables in a response prefix string. + * + * @param template - The template string with `{variable}` placeholders + * @param context - Context object with values for interpolation + * @returns The interpolated string, or undefined if template is undefined + * + * @example + * resolveResponsePrefixTemplate("[{model} | think:{thinkingLevel}]", { + * model: "gpt-5.2", + * thinkingLevel: "high" + * }) + * // Returns: "[gpt-5.2 | think:high]" + */ +export function resolveResponsePrefixTemplate( + template: string | undefined, + context: ResponsePrefixContext, +): string | undefined { + if (!template) return undefined; + + return template.replace(TEMPLATE_VAR_PATTERN, (match, varName: string) => { + const normalizedVar = varName.toLowerCase(); + + switch (normalizedVar) { + case "model": + return context.model ?? match; + case "modelfull": + return context.modelFull ?? match; + case "provider": + return context.provider ?? match; + case "thinkinglevel": + case "think": + return context.thinkingLevel ?? match; + case "identity.name": + case "identityname": + return context.identityName ?? match; + default: + // Leave unrecognized variables as-is + return match; + } + }); +} + +/** + * Extract short model name from a full model string. + * + * Strips: + * - Provider prefix (e.g., "openai/" from "openai/gpt-5.2") + * - Date suffixes (e.g., "-20251101" from "claude-opus-4-5-20251101") + * - Common version suffixes (e.g., "-latest") + * + * @example + * extractShortModelName("openai-codex/gpt-5.2") // "gpt-5.2" + * extractShortModelName("claude-opus-4-5-20251101") // "claude-opus-4-5" + * extractShortModelName("gpt-5.2-latest") // "gpt-5.2" + */ +export function extractShortModelName(fullModel: string): string { + // Strip provider prefix + const slash = fullModel.lastIndexOf("/"); + const modelPart = slash >= 0 ? fullModel.slice(slash + 1) : fullModel; + + // Strip date suffixes (YYYYMMDD format) + return modelPart.replace(/-\d{8}$/, "").replace(/-latest$/, ""); +} + +/** + * Check if a template string contains any template variables. + */ +export function hasTemplateVariables(template: string | undefined): boolean { + if (!template) return false; + // Reset lastIndex since we're using a global regex + TEMPLATE_VAR_PATTERN.lastIndex = 0; + return TEMPLATE_VAR_PATTERN.test(template); +} diff --git a/src/auto-reply/types.ts b/src/auto-reply/types.ts index bf6e765e1..e1bf611db 100644 --- a/src/auto-reply/types.ts +++ b/src/auto-reply/types.ts @@ -5,6 +5,13 @@ export type BlockReplyContext = { timeoutMs?: number; }; +/** Context passed to onModelSelected callback with actual model used. */ +export type ModelSelectedContext = { + provider: string; + model: string; + thinkLevel: string | undefined; +}; + export type GetReplyOptions = { onReplyStart?: () => Promise | void; onTypingController?: (typing: TypingController) => void; @@ -13,6 +20,9 @@ export type GetReplyOptions = { onReasoningStream?: (payload: ReplyPayload) => Promise | void; onBlockReply?: (payload: ReplyPayload, context?: BlockReplyContext) => Promise | void; onToolResult?: (payload: ReplyPayload) => Promise | void; + /** Called when the actual model is selected (including after fallback). + * Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */ + onModelSelected?: (ctx: ModelSelectedContext) => void; disableBlockStreaming?: boolean; /** Timeout for block reply delivery (ms). */ blockReplyTimeoutMs?: number; diff --git a/src/config/types.messages.ts b/src/config/types.messages.ts index 76754cb80..d137b56d8 100644 --- a/src/config/types.messages.ts +++ b/src/config/types.messages.ts @@ -44,8 +44,21 @@ export type MessagesConfig = { messagePrefix?: string; /** * Prefix auto-added to all outbound replies. - * - string: explicit prefix + * + * - string: explicit prefix (may include template variables) * - special value: `"auto"` derives `[{agents.list[].identity.name}]` for the routed agent (when set) + * + * Supported template variables (case-insensitive): + * - `{model}` - short model name (e.g., `claude-opus-4-5`, `gpt-4o`) + * - `{modelFull}` - full model identifier (e.g., `anthropic/claude-opus-4-5`) + * - `{provider}` - provider name (e.g., `anthropic`, `openai`) + * - `{thinkingLevel}` or `{think}` - current thinking level (`high`, `low`, `off`) + * - `{identity.name}` or `{identityName}` - agent identity name + * + * Example: `"[{model} | think:{thinkingLevel}]"` → `"[claude-opus-4-5 | think:high]"` + * + * Unresolved variables remain as literal text (e.g., `{model}` if context unavailable). + * * Default: none */ responsePrefix?: string; diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index b4862edba..4758feb09 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -2,7 +2,12 @@ import { resolveAckReaction, resolveEffectiveMessagesConfig, resolveHumanDelayConfig, + resolveIdentityName, } from "../../agents/identity.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../../auto-reply/reply/response-prefix-template.js"; import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../auto-reply/envelope.js"; import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js"; import { buildHistoryContextFromMap, clearHistoryEntries } from "../../auto-reply/reply/history.js"; @@ -280,8 +285,15 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) const typingChannelId = deliverTarget.startsWith("channel:") ? deliverTarget.slice("channel:".length) : message.channelId; + + // Create mutable context for response prefix template interpolation + let prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(cfg, route.agentId), + }; + const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, + responsePrefixContextProvider: () => prefixContext, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload: ReplyPayload) => { const replyToId = replyReference.use(); @@ -316,6 +328,15 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) typeof discordConfig?.blockStreaming === "boolean" ? !discordConfig.blockStreaming : undefined, + onModelSelected: (ctx) => { + prefixContext = { + ...prefixContext, + provider: ctx.provider, + model: extractShortModelName(ctx.model), + modelFull: `${ctx.provider}/${ctx.model}`, + thinkingLevel: ctx.thinkLevel ?? "off", + }; + }, }, }); markDispatchIdle(); diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 831219c04..b8a408419 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -1,4 +1,12 @@ -import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js"; +import { + resolveEffectiveMessagesConfig, + resolveHumanDelayConfig, + resolveIdentityName, +} from "../../agents/identity.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../../auto-reply/reply/response-prefix-template.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { formatAgentEnvelope } from "../../auto-reply/envelope.js"; @@ -341,8 +349,15 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P } let didSendReply = false; + + // Create mutable context for response prefix template interpolation + let prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(cfg, route.agentId), + }; + const dispatcher = createReplyDispatcher({ responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, + responsePrefixContextProvider: () => prefixContext, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { await deliverReplies({ @@ -370,6 +385,15 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P typeof accountInfo.config.blockStreaming === "boolean" ? !accountInfo.config.blockStreaming : undefined, + onModelSelected: (ctx) => { + prefixContext = { + ...prefixContext, + provider: ctx.provider, + model: extractShortModelName(ctx.model), + modelFull: `${ctx.provider}/${ctx.model}`, + thinkingLevel: ctx.thinkLevel ?? "off", + }; + }, }, }); if (!queuedFinal) { diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index bcc77ec9d..7f093a3c1 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -1,4 +1,12 @@ -import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js"; +import { + resolveEffectiveMessagesConfig, + resolveHumanDelayConfig, + resolveIdentityName, +} from "../../agents/identity.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../../auto-reply/reply/response-prefix-template.js"; import { formatAgentEnvelope } from "../../auto-reply/envelope.js"; import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js"; import { buildHistoryContextFromMap, clearHistoryEntries } from "../../auto-reply/reply/history.js"; @@ -310,8 +318,15 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { } let didSendReply = false; + + // Create mutable context for response prefix template interpolation + let prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(deps.cfg, route.agentId), + }; + const dispatcher = createReplyDispatcher({ responsePrefix: resolveEffectiveMessagesConfig(deps.cfg, route.agentId).responsePrefix, + responsePrefixContextProvider: () => prefixContext, humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId), deliver: async (payload) => { await deps.deliverReplies({ @@ -338,6 +353,15 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { replyOptions: { disableBlockStreaming: typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined, + onModelSelected: (ctx) => { + prefixContext = { + ...prefixContext, + provider: ctx.provider, + model: extractShortModelName(ctx.model), + modelFull: `${ctx.provider}/${ctx.model}`, + thinkingLevel: ctx.thinkLevel ?? "off", + }; + }, }, }); if (!queuedFinal) { diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index dc740d43c..836e85d46 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -1,7 +1,12 @@ import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig, + resolveIdentityName, } from "../../../agents/identity.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../../../auto-reply/reply/response-prefix-template.js"; import { dispatchReplyFromConfig } from "../../../auto-reply/reply/dispatch-from-config.js"; import { clearHistoryEntries } from "../../../auto-reply/reply/history.js"; import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js"; @@ -62,8 +67,15 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }; let didSendReply = false; + + // Create mutable context for response prefix template interpolation + let prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(cfg, route.agentId), + }; + const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, + responsePrefixContextProvider: () => prefixContext, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { const replyThreadTs = replyPlan.nextThreadTs(); @@ -104,6 +116,15 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : undefined, + onModelSelected: (ctx) => { + prefixContext = { + ...prefixContext, + provider: ctx.provider, + model: extractShortModelName(ctx.model), + modelFull: `${ctx.provider}/${ctx.model}`, + thinkingLevel: ctx.thinkLevel ?? "off", + }; + }, }, }); markDispatchIdle(); diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 195f2872c..bae515f4b 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -1,5 +1,9 @@ // @ts-nocheck -import { resolveEffectiveMessagesConfig } from "../agents/identity.js"; +import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../auto-reply/reply/response-prefix-template.js"; import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { clearHistoryEntries } from "../auto-reply/reply/history.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; @@ -114,12 +118,18 @@ export const dispatchTelegramMessage = async ({ Boolean(draftStream) || (typeof telegramCfg.blockStreaming === "boolean" ? !telegramCfg.blockStreaming : undefined); + // Create mutable context for response prefix template interpolation + let prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(cfg, route.agentId), + }; + let didSendReply = false; const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg, dispatcherOptions: { responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, + responsePrefixContextProvider: () => prefixContext, deliver: async (payload, info) => { if (info.kind === "final") { await flushDraft(); @@ -151,6 +161,15 @@ export const dispatchTelegramMessage = async ({ } : undefined, disableBlockStreaming, + onModelSelected: (ctx) => { + prefixContext = { + ...prefixContext, + provider: ctx.provider, + model: extractShortModelName(ctx.model), + modelFull: `${ctx.provider}/${ctx.model}`, + thinkingLevel: ctx.thinkLevel ?? "off", + }; + }, }, }); draftStream?.stop(); diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 4ae3b3fc0..0a57e6877 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -1,4 +1,8 @@ -import { resolveEffectiveMessagesConfig } from "../../../agents/identity.js"; +import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../../agents/identity.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../../../auto-reply/reply/response-prefix-template.js"; import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; import { formatAgentEnvelope } from "../../../auto-reply/envelope.js"; import { buildHistoryContext } from "../../../auto-reply/reply/history.js"; @@ -173,6 +177,11 @@ export async function processMessage(params: { params.route.agentId, ).responsePrefix; + // Create mutable context for response prefix template interpolation + let prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(params.cfg, params.route.agentId), + }; + const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({ ctx: { Body: combinedBody, @@ -210,6 +219,7 @@ export async function processMessage(params: { replyResolver: params.replyResolver, dispatcherOptions: { responsePrefix, + responsePrefixContextProvider: () => prefixContext, onHeartbeatStrip: () => { if (!didLogHeartbeatStrip) { didLogHeartbeatStrip = true; @@ -267,6 +277,15 @@ export async function processMessage(params: { typeof params.cfg.channels?.whatsapp?.blockStreaming === "boolean" ? !params.cfg.channels.whatsapp.blockStreaming : undefined, + onModelSelected: (ctx) => { + prefixContext = { + ...prefixContext, + provider: ctx.provider, + model: extractShortModelName(ctx.model), + modelFull: `${ctx.provider}/${ctx.model}`, + thinkingLevel: ctx.thinkLevel ?? "off", + }; + }, }, }); From 7b04e6ac42ac62963ce033325a652e1bfc65689e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 14 Jan 2026 23:15:46 -0500 Subject: [PATCH 2/6] debug: add prefix template resolution logging --- src/auto-reply/reply/agent-runner-execution.ts | 3 +++ src/auto-reply/reply/reply-dispatcher.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index d1f0e276d..b580dcc4b 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -127,6 +127,9 @@ export async function runAgentTurnWithFallback(params: { run: (provider, model) => { // Notify that model selection is complete (including after fallback). // This allows responsePrefix template interpolation with the actual model. + logVerbose( + `[prefix-debug] onModelSelected firing: provider=${provider} model=${model} thinkLevel=${params.followupRun.run.thinkLevel}`, + ); params.opts?.onModelSelected?.({ provider, model, diff --git a/src/auto-reply/reply/reply-dispatcher.ts b/src/auto-reply/reply/reply-dispatcher.ts index b8eeca477..4a45161d8 100644 --- a/src/auto-reply/reply/reply-dispatcher.ts +++ b/src/auto-reply/reply/reply-dispatcher.ts @@ -75,6 +75,14 @@ function normalizeReplyPayloadInternal( // Prefer dynamic context provider over static context const prefixContext = opts.responsePrefixContextProvider?.() ?? opts.responsePrefixContext; + // Debug logging for prefix template resolution + if (opts.responsePrefix?.includes("{")) { + // eslint-disable-next-line no-console + console.log( + `[prefix-debug] normalizing with context: ${JSON.stringify(prefixContext)} prefix: ${opts.responsePrefix}`, + ); + } + return normalizeReplyPayload(payload, { responsePrefix: opts.responsePrefix, responsePrefixContext: prefixContext, From 113eea5047c49e11b5d014fafcb9c2c7bab5c2c2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 14 Jan 2026 23:20:19 -0500 Subject: [PATCH 3/6] fix: mutate prefixContext object instead of reassigning for closure correctness --- src/auto-reply/reply/agent-runner-execution.ts | 3 --- src/auto-reply/reply/reply-dispatcher.ts | 8 -------- src/discord/monitor/message-handler.process.ts | 12 +++++------- src/imessage/monitor/monitor-provider.ts | 12 +++++------- src/signal/monitor/event-handler.ts | 12 +++++------- src/slack/monitor/message-handler/dispatch.ts | 12 +++++------- src/telegram/bot-message-dispatch.ts | 12 +++++------- src/web/auto-reply/monitor/process-message.ts | 12 +++++------- 8 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index b580dcc4b..d1f0e276d 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -127,9 +127,6 @@ export async function runAgentTurnWithFallback(params: { run: (provider, model) => { // Notify that model selection is complete (including after fallback). // This allows responsePrefix template interpolation with the actual model. - logVerbose( - `[prefix-debug] onModelSelected firing: provider=${provider} model=${model} thinkLevel=${params.followupRun.run.thinkLevel}`, - ); params.opts?.onModelSelected?.({ provider, model, diff --git a/src/auto-reply/reply/reply-dispatcher.ts b/src/auto-reply/reply/reply-dispatcher.ts index 4a45161d8..b8eeca477 100644 --- a/src/auto-reply/reply/reply-dispatcher.ts +++ b/src/auto-reply/reply/reply-dispatcher.ts @@ -75,14 +75,6 @@ function normalizeReplyPayloadInternal( // Prefer dynamic context provider over static context const prefixContext = opts.responsePrefixContextProvider?.() ?? opts.responsePrefixContext; - // Debug logging for prefix template resolution - if (opts.responsePrefix?.includes("{")) { - // eslint-disable-next-line no-console - console.log( - `[prefix-debug] normalizing with context: ${JSON.stringify(prefixContext)} prefix: ${opts.responsePrefix}`, - ); - } - return normalizeReplyPayload(payload, { responsePrefix: opts.responsePrefix, responsePrefixContext: prefixContext, diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index 4758feb09..dae301d04 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -329,13 +329,11 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) ? !discordConfig.blockStreaming : undefined, onModelSelected: (ctx) => { - prefixContext = { - ...prefixContext, - provider: ctx.provider, - model: extractShortModelName(ctx.model), - modelFull: `${ctx.provider}/${ctx.model}`, - thinkingLevel: ctx.thinkLevel ?? "off", - }; + // Mutate the object directly instead of reassigning to ensure the closure sees updates + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; }, }, }); diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index b8a408419..1d86ec993 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -386,13 +386,11 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P ? !accountInfo.config.blockStreaming : undefined, onModelSelected: (ctx) => { - prefixContext = { - ...prefixContext, - provider: ctx.provider, - model: extractShortModelName(ctx.model), - modelFull: `${ctx.provider}/${ctx.model}`, - thinkingLevel: ctx.thinkLevel ?? "off", - }; + // Mutate the object directly instead of reassigning to ensure the closure sees updates + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; }, }, }); diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 7f093a3c1..0aac7c97b 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -354,13 +354,11 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { disableBlockStreaming: typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined, onModelSelected: (ctx) => { - prefixContext = { - ...prefixContext, - provider: ctx.provider, - model: extractShortModelName(ctx.model), - modelFull: `${ctx.provider}/${ctx.model}`, - thinkingLevel: ctx.thinkLevel ?? "off", - }; + // Mutate the object directly instead of reassigning to ensure the closure sees updates + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; }, }, }); diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index 836e85d46..fb0eb66b1 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -117,13 +117,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag ? !account.config.blockStreaming : undefined, onModelSelected: (ctx) => { - prefixContext = { - ...prefixContext, - provider: ctx.provider, - model: extractShortModelName(ctx.model), - modelFull: `${ctx.provider}/${ctx.model}`, - thinkingLevel: ctx.thinkLevel ?? "off", - }; + // Mutate the object directly instead of reassigning to ensure the closure sees updates + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; }, }, }); diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index bae515f4b..c609a07c2 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -162,13 +162,11 @@ export const dispatchTelegramMessage = async ({ : undefined, disableBlockStreaming, onModelSelected: (ctx) => { - prefixContext = { - ...prefixContext, - provider: ctx.provider, - model: extractShortModelName(ctx.model), - modelFull: `${ctx.provider}/${ctx.model}`, - thinkingLevel: ctx.thinkLevel ?? "off", - }; + // Mutate the object directly instead of reassigning to ensure the closure sees updates + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; }, }, }); diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 0a57e6877..205fd5699 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -278,13 +278,11 @@ export async function processMessage(params: { ? !params.cfg.channels.whatsapp.blockStreaming : undefined, onModelSelected: (ctx) => { - prefixContext = { - ...prefixContext, - provider: ctx.provider, - model: extractShortModelName(ctx.model), - modelFull: `${ctx.provider}/${ctx.model}`, - thinkingLevel: ctx.thinkLevel ?? "off", - }; + // Mutate the object directly instead of reassigning to ensure the closure sees updates + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; }, }, }); From 56b3b44342d79793ce34a9caf0ca1ab5069f0863 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 14 Jan 2026 23:23:21 -0500 Subject: [PATCH 4/6] debug: add responsePrefix template logging --- src/auto-reply/reply/agent-runner-execution.ts | 3 +++ src/auto-reply/reply/normalize-reply.ts | 9 +++++++++ src/telegram/bot-message-dispatch.ts | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index d1f0e276d..49a50cd29 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -127,6 +127,9 @@ export async function runAgentTurnWithFallback(params: { run: (provider, model) => { // Notify that model selection is complete (including after fallback). // This allows responsePrefix template interpolation with the actual model. + logVerbose( + `[responsePrefix] onModelSelected callback exists: ${!!params.opts?.onModelSelected}, provider=${provider}, model=${model}`, + ); params.opts?.onModelSelected?.({ provider, model, diff --git a/src/auto-reply/reply/normalize-reply.ts b/src/auto-reply/reply/normalize-reply.ts index ac8e85f2e..a9d0d0060 100644 --- a/src/auto-reply/reply/normalize-reply.ts +++ b/src/auto-reply/reply/normalize-reply.ts @@ -1,3 +1,4 @@ +import { logVerbose } from "../../globals.js"; import { stripHeartbeatToken } from "../heartbeat.js"; import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import type { ReplyPayload } from "../types.js"; @@ -43,9 +44,17 @@ export function normalizeReplyPayload( } // Resolve template variables in responsePrefix if context is provided + if (opts.responsePrefix?.includes("{")) { + logVerbose( + `[responsePrefix] normalizing: prefix="${opts.responsePrefix}", context=${JSON.stringify(opts.responsePrefixContext)}`, + ); + } const effectivePrefix = opts.responsePrefixContext ? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext) : opts.responsePrefix; + if (opts.responsePrefix?.includes("{") && effectivePrefix !== opts.responsePrefix) { + logVerbose(`[responsePrefix] resolved to: "${effectivePrefix}"`); + } if ( effectivePrefix && diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index c609a07c2..22dd9a420 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -163,10 +163,14 @@ export const dispatchTelegramMessage = async ({ disableBlockStreaming, onModelSelected: (ctx) => { // Mutate the object directly instead of reassigning to ensure the closure sees updates + logVerbose( + `[responsePrefix] telegram onModelSelected fired: provider=${ctx.provider}, model=${ctx.model}, thinkLevel=${ctx.thinkLevel}`, + ); prefixContext.provider = ctx.provider; prefixContext.model = extractShortModelName(ctx.model); prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; + logVerbose(`[responsePrefix] telegram prefixContext updated: ${JSON.stringify(prefixContext)}`); }, }, }); From e7167e35ed9f9420ff038f69cef075e06449fdff Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 14 Jan 2026 23:29:17 -0500 Subject: [PATCH 5/6] debug: use console.log instead of logVerbose for always-visible logging --- src/auto-reply/reply/agent-runner-execution.ts | 3 ++- src/auto-reply/reply/normalize-reply.ts | 9 +++++---- src/telegram/bot-message-dispatch.ts | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 49a50cd29..8a3033005 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -127,7 +127,8 @@ export async function runAgentTurnWithFallback(params: { run: (provider, model) => { // Notify that model selection is complete (including after fallback). // This allows responsePrefix template interpolation with the actual model. - logVerbose( + // eslint-disable-next-line no-console + console.log( `[responsePrefix] onModelSelected callback exists: ${!!params.opts?.onModelSelected}, provider=${provider}, model=${model}`, ); params.opts?.onModelSelected?.({ diff --git a/src/auto-reply/reply/normalize-reply.ts b/src/auto-reply/reply/normalize-reply.ts index a9d0d0060..455deb998 100644 --- a/src/auto-reply/reply/normalize-reply.ts +++ b/src/auto-reply/reply/normalize-reply.ts @@ -1,4 +1,3 @@ -import { logVerbose } from "../../globals.js"; import { stripHeartbeatToken } from "../heartbeat.js"; import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import type { ReplyPayload } from "../types.js"; @@ -45,15 +44,17 @@ export function normalizeReplyPayload( // Resolve template variables in responsePrefix if context is provided if (opts.responsePrefix?.includes("{")) { - logVerbose( + // eslint-disable-next-line no-console + console.log( `[responsePrefix] normalizing: prefix="${opts.responsePrefix}", context=${JSON.stringify(opts.responsePrefixContext)}`, ); } const effectivePrefix = opts.responsePrefixContext ? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext) : opts.responsePrefix; - if (opts.responsePrefix?.includes("{") && effectivePrefix !== opts.responsePrefix) { - logVerbose(`[responsePrefix] resolved to: "${effectivePrefix}"`); + if (opts.responsePrefix?.includes("{")) { + // eslint-disable-next-line no-console + console.log(`[responsePrefix] resolved to: "${effectivePrefix}"`); } if ( diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 22dd9a420..d005d97ae 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -163,14 +163,16 @@ export const dispatchTelegramMessage = async ({ disableBlockStreaming, onModelSelected: (ctx) => { // Mutate the object directly instead of reassigning to ensure the closure sees updates - logVerbose( + // eslint-disable-next-line no-console + console.log( `[responsePrefix] telegram onModelSelected fired: provider=${ctx.provider}, model=${ctx.model}, thinkLevel=${ctx.thinkLevel}`, ); prefixContext.provider = ctx.provider; prefixContext.model = extractShortModelName(ctx.model); prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; - logVerbose(`[responsePrefix] telegram prefixContext updated: ${JSON.stringify(prefixContext)}`); + // eslint-disable-next-line no-console + console.log(`[responsePrefix] telegram prefixContext updated: ${JSON.stringify(prefixContext)}`); }, }, }); From 6ef3837e731ce95cbec907a400a0f585a4dedeef Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 14 Jan 2026 23:36:47 -0500 Subject: [PATCH 6/6] Remove debug logging for responsePrefix template resolution --- src/auto-reply/reply/agent-runner-execution.ts | 4 ---- src/auto-reply/reply/normalize-reply.ts | 10 ---------- src/telegram/bot-message-dispatch.ts | 6 ------ 3 files changed, 20 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 8a3033005..d1f0e276d 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -127,10 +127,6 @@ export async function runAgentTurnWithFallback(params: { run: (provider, model) => { // Notify that model selection is complete (including after fallback). // This allows responsePrefix template interpolation with the actual model. - // eslint-disable-next-line no-console - console.log( - `[responsePrefix] onModelSelected callback exists: ${!!params.opts?.onModelSelected}, provider=${provider}, model=${model}`, - ); params.opts?.onModelSelected?.({ provider, model, diff --git a/src/auto-reply/reply/normalize-reply.ts b/src/auto-reply/reply/normalize-reply.ts index 455deb998..ac8e85f2e 100644 --- a/src/auto-reply/reply/normalize-reply.ts +++ b/src/auto-reply/reply/normalize-reply.ts @@ -43,19 +43,9 @@ export function normalizeReplyPayload( } // Resolve template variables in responsePrefix if context is provided - if (opts.responsePrefix?.includes("{")) { - // eslint-disable-next-line no-console - console.log( - `[responsePrefix] normalizing: prefix="${opts.responsePrefix}", context=${JSON.stringify(opts.responsePrefixContext)}`, - ); - } const effectivePrefix = opts.responsePrefixContext ? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext) : opts.responsePrefix; - if (opts.responsePrefix?.includes("{")) { - // eslint-disable-next-line no-console - console.log(`[responsePrefix] resolved to: "${effectivePrefix}"`); - } if ( effectivePrefix && diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index d005d97ae..c609a07c2 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -163,16 +163,10 @@ export const dispatchTelegramMessage = async ({ disableBlockStreaming, onModelSelected: (ctx) => { // Mutate the object directly instead of reassigning to ensure the closure sees updates - // eslint-disable-next-line no-console - console.log( - `[responsePrefix] telegram onModelSelected fired: provider=${ctx.provider}, model=${ctx.model}, thinkLevel=${ctx.thinkLevel}`, - ); prefixContext.provider = ctx.provider; prefixContext.model = extractShortModelName(ctx.model); prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; - // eslint-disable-next-line no-console - console.log(`[responsePrefix] telegram prefixContext updated: ${JSON.stringify(prefixContext)}`); }, }, });