77import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload" ;
88import { stripPlainTextToolCallBlocks } from "../../../packages/tool-call-repair/src/index.js" ;
99import { resolveSessionAgentId } from "../../agents/agent-scope.js" ;
10- import { resolveResponsePrefix } from "../../agents/identity.js" ;
10+ import { resolveAgentIdentity , resolveResponsePrefix } from "../../agents/identity.js" ;
1111import type { AgentToolResult } from "../../agents/runtime/index.js" ;
1212import {
1313 readPositiveIntegerParam ,
@@ -16,6 +16,7 @@ import {
1616} from "../../agents/tools/common.js" ;
1717import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js" ;
1818import type { ReplyPayload } from "../../auto-reply/reply-payload.js" ;
19+ import { resolveResponsePrefixTemplate } from "../../auto-reply/reply/response-prefix-template.js" ;
1920import { normalizeChatType , type ChatType } from "../../channels/chat-type.js" ;
2021import type { InboundEventKind } from "../../channels/inbound-event/kind.js" ;
2122import { getChannelPlugin } from "../../channels/plugins/index.js" ;
@@ -1025,6 +1026,10 @@ async function buildSendPayloadParts(params: {
10251026 } ;
10261027}
10271028
1029+ // Detects leftover `{variable}` placeholders after prefix interpolation. Non-global so
1030+ // `.test()` stays stateless; mirrors the variable shape in response-prefix-template.ts.
1031+ const UNRESOLVED_PREFIX_VAR_PATTERN = / \{ [ a - z A - Z ] [ a - z A - Z 0 - 9 . ] * \} / ;
1032+
10281033async function handleSendAction ( ctx : ResolvedActionContext ) : Promise < MessageActionRunResult > {
10291034 const {
10301035 cfg,
@@ -1053,13 +1058,26 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
10531058
10541059 // `message(action=send)` crosses into other conversations, so mirror the direct-reply
10551060 // egress and prepend messages.responsePrefix here too; otherwise the disambiguation
1056- // prefix is silently dropped on tool sends while replies keep it. The startsWith guard
1057- // matches normalize-reply.ts and keeps re-runs idempotent.
1058- const responsePrefix = resolveResponsePrefix ( cfg , agentId ?? "" , {
1059- channel,
1060- accountId : accountId ?? undefined ,
1061- } ) ;
1062- if ( responsePrefix && sendPayload . message && ! sendPayload . message . startsWith ( responsePrefix ) ) {
1061+ // prefix is silently dropped on tool sends while replies keep it. Interpolate the
1062+ // template like normalize-reply.ts so identity tokens render. model/provider/thinking
1063+ // tokens need the live model selection that a tool send never performs, so when any
1064+ // placeholder stays unresolved we skip prefixing instead of leaking a literal `{model}`.
1065+ // The startsWith guard matches normalize-reply.ts and keeps re-runs idempotent.
1066+ const responsePrefix = resolveResponsePrefixTemplate (
1067+ resolveResponsePrefix ( cfg , agentId ?? "" , {
1068+ channel,
1069+ accountId : accountId ?? undefined ,
1070+ } ) ,
1071+ { identityName : normalizeOptionalString ( resolveAgentIdentity ( cfg , agentId ?? "" ) ?. name ) } ,
1072+ ) ;
1073+ const prefixHasUnresolvedVar =
1074+ responsePrefix !== undefined && UNRESOLVED_PREFIX_VAR_PATTERN . test ( responsePrefix ) ;
1075+ if (
1076+ responsePrefix &&
1077+ ! prefixHasUnresolvedVar &&
1078+ sendPayload . message &&
1079+ ! sendPayload . message . startsWith ( responsePrefix )
1080+ ) {
10631081 const prefixedMessage = `${ responsePrefix } ${ sendPayload . message } ` ;
10641082 sendPayload = {
10651083 ...sendPayload ,
0 commit comments