11#! /usr/bin/env bash
22# Fix: responsePrefix {model} not interpolating in heartbeat replies (#43064)
3- # Applies changes from upstream PR #46858 (unmerged as of 2026-04-09).
3+ # Based on upstream PR #46858 (unmerged as of 2026-04-09), adapted to current main .
44#
5- # Root cause: heartbeat runner calls resolveEffectiveMessagesConfig() to get
6- # responsePrefix but never interpolates template variables like {model} and
7- # {provider}. The prefix is used as-is, so "{model}" appears as literal text.
5+ # Root cause: heartbeat runner gets responsePrefix from resolveEffectiveMessagesConfig()
6+ # but never interpolates template variables like {model}/{provider}.
87#
9- # Fix: After the LLM responds, interpolate the prefix template using
10- # resolveResponsePrefixTemplate() with model/provider context captured via
11- # an onModelSelected callback passed to getReplyFromConfig().
8+ # Fix: Capture model/provider via onModelSelected callback, then interpolate after LLM responds.
129set -euo pipefail
1310
1411echo " [patch] Applying responsePrefix interpolation fix (#43064, PR #46858)..."
2219node -e '
2320const fs = require("fs");
2421let code = fs.readFileSync(process.argv[1], "utf8");
22+ let changes = 0;
2523
26- // 1. Add import for resolveIdentityName (already importing resolveEffectiveMessagesConfig)
27- code = code.replace(
28- /import \{ resolveEffectiveMessagesConfig \} from "\.\.\/agents\/identity\.js";/,
29- `import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js";`
30- );
24+ // 1. Add resolveIdentityNamePrefix to identity import
25+ const oldIdentityImport = `import { resolveEffectiveMessagesConfig } from "../agents/identity.js";`;
26+ const newIdentityImport = `import { resolveEffectiveMessagesConfig, resolveIdentityNamePrefix } from "../agents/identity.js";`;
27+ if (code.includes(oldIdentityImport)) {
28+ code = code.replace(oldIdentityImport, newIdentityImport);
29+ changes++;
30+ console.log("[patch] 1. Added resolveIdentityNamePrefix import");
31+ } else {
32+ console.error("[patch] ERROR: Could not find identity import");
33+ process.exit(1);
34+ }
3135
32- // 2. Add imports for response-prefix-template
33- const prefixImport = `import {
36+ // 2. Add response-prefix-template imports after HEARTBEAT_TOKEN import
37+ const tokenImport = `import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";`;
38+ const newImports = `import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
39+ import {
3440 extractShortModelName,
3541 resolveResponsePrefixTemplate,
3642 type ResponsePrefixContext,
3743} from "../auto-reply/reply/response-prefix-template.js";`;
44+ if (code.includes(tokenImport)) {
45+ code = code.replace(tokenImport, newImports);
46+ changes++;
47+ console.log("[patch] 2. Added response-prefix-template imports");
48+ } else {
49+ console.error("[patch] ERROR: Could not find HEARTBEAT_TOKEN import");
50+ process.exit(1);
51+ }
3852
39- // Insert after the getReplyFromConfig import
40- code = code.replace(
41- /import \{ getReplyFromConfig \} from "\.\.\/auto-reply\/reply\.js";/,
42- `import { getReplyFromConfig } from "../auto-reply/reply.js";\n${prefixImport}`
43- );
44-
45- // 3. Replace responsePrefix resolution with template-aware version
46- code = code.replace(
47- /const responsePrefix = resolveEffectiveMessagesConfig\(cfg, agentId, \{\n\s+channel: delivery\.channel !== "none" \? delivery\.channel : undefined,\n\s+accountId: delivery\.accountId,\n\s+\}\)\.responsePrefix;/,
48- `const responsePrefixTemplate = resolveEffectiveMessagesConfig(cfg, agentId, {
53+ // 3. Replace responsePrefix with responsePrefixTemplate + context
54+ const oldPrefixBlock = ` const responsePrefix = resolveEffectiveMessagesConfig(cfg, agentId, {
55+ channel: delivery.channel !== "none" ? delivery.channel : undefined,
56+ accountId: delivery.accountId,
57+ }).responsePrefix;`;
58+ const newPrefixBlock = ` const responsePrefixTemplate = resolveEffectiveMessagesConfig(cfg, agentId, {
4959 channel: delivery.channel !== "none" ? delivery.channel : undefined,
5060 accountId: delivery.accountId,
5161 }).responsePrefix;
5262 const responsePrefixContext: ResponsePrefixContext = {
53- identityName: resolveIdentityName (cfg, agentId),
63+ identityName: resolveIdentityNamePrefix (cfg, agentId),
5464 };
55- let responsePrefix = responsePrefixTemplate;`
56- );
57-
58- // 4. Move heartbeatOkText inside the sendOk closure (before LLM runs, model unknown)
59- code = code.replace(
60- /const heartbeatOkText = responsePrefix \? `\$\{responsePrefix\} \$\{HEARTBEAT_TOKEN\}` : HEARTBEAT_TOKEN;\n\s+const outboundSession/,
61- `const outboundSession`
62- );
63-
64- // Insert heartbeatOkText inside the sendOk function, before the heartbeatPlugin check
65- code = code.replace(
66- /if \(!canAttemptHeartbeatOk \|\| delivery\.channel === "none" \|\| !delivery\.to\) \{\n\s+return false;\n\s+\}/,
67- `if (!canAttemptHeartbeatOk || delivery.channel === "none" || !delivery.to) {
68- return false;
69- }
70- const heartbeatOkText = responsePrefix
71- ? \`\${responsePrefix} \${HEARTBEAT_TOKEN}\`
72- : HEARTBEAT_TOKEN;`
73- );
65+ let responsePrefix = responsePrefixTemplate;`;
66+ if (code.includes(oldPrefixBlock)) {
67+ code = code.replace(oldPrefixBlock, newPrefixBlock);
68+ changes++;
69+ console.log("[patch] 3. Replaced responsePrefix with template + context");
70+ } else {
71+ console.error("[patch] ERROR: Could not find responsePrefix block");
72+ process.exit(1);
73+ }
7474
75- // Remove the duplicate heartbeatOkText declaration if the original pattern was slightly different
76- // (safety — no-op if already removed above)
77-
78- // 5. Add onModelSelected callback before replyOpts
79- code = code.replace(
80- /const replyOpts = heartbeatModelOverride\n?\s+\?/,
81- `const onModelSelected = (selection: {
75+ // 4. Add onModelSelected callback and pass it to replyOpts
76+ const oldReplyOpts = ` const replyOpts = heartbeatModelOverride
77+ ? {
78+ isHeartbeat: true,
79+ heartbeatModelOverride,
80+ suppressToolErrorWarnings,
81+ bootstrapContextMode,
82+ }
83+ : { isHeartbeat: true, suppressToolErrorWarnings, bootstrapContextMode };`;
84+ const newReplyOpts = ` const onModelSelected = (selection: {
8285 provider: string;
8386 model: string;
8487 thinkLevel?: string;
@@ -89,29 +92,41 @@ code = code.replace(
8992 responsePrefixContext.thinkingLevel = selection.thinkLevel ?? "off";
9093 };
9194 const replyOpts = heartbeatModelOverride
92- ?`
93- );
94-
95- // 6. Add onModelSelected to both branches of replyOpts
96- code = code.replace(
97- /suppressToolErrorWarnings,\n\s+bootstrapContextMode,\n\s+\}\n\s+: \{ isHeartbeat: true, suppressToolErrorWarnings, bootstrapContextMode \};/,
98- `suppressToolErrorWarnings,
95+ ? {
96+ isHeartbeat: true,
97+ heartbeatModelOverride,
98+ suppressToolErrorWarnings,
9999 bootstrapContextMode,
100100 onModelSelected,
101101 }
102- : { isHeartbeat: true, suppressToolErrorWarnings, bootstrapContextMode, onModelSelected };`
103- );
102+ : { isHeartbeat: true, suppressToolErrorWarnings, bootstrapContextMode, onModelSelected };`;
103+ if (code.includes(oldReplyOpts)) {
104+ code = code.replace(oldReplyOpts, newReplyOpts);
105+ changes++;
106+ console.log("[patch] 4. Added onModelSelected + updated replyOpts");
107+ } else {
108+ console.error("[patch] ERROR: Could not find replyOpts block");
109+ process.exit(1);
110+ }
104111
105- // 7. Add responsePrefix interpolation after getReplyFromConfig
106- code = code.replace(
107- /const replyResult = await getReplyFromConfig\(ctx, replyOpts, cfg\);\n\s+const replyPayload = resolveHeartbeatReplyPayload\(replyResult\);/,
108- `const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
112+ // 5. Interpolate prefix after LLM responds (before normalizeHeartbeatReply)
113+ const oldAckMaxChars = ` const ackMaxChars = resolveHeartbeatAckMaxChars(cfg, heartbeat);
114+ const normalized = normalizeHeartbeatReply(replyPayload, responsePrefix, ackMaxChars);`;
115+ const newAckMaxChars = ` const ackMaxChars = resolveHeartbeatAckMaxChars(cfg, heartbeat);
116+ // Interpolate template variables (e.g. {model}, {provider}) now that the LLM has responded.
109117 responsePrefix = resolveResponsePrefixTemplate(responsePrefixTemplate, responsePrefixContext);
110- const replyPayload = resolveHeartbeatReplyPayload(replyResult);`
111- );
118+ const normalized = normalizeHeartbeatReply(replyPayload, responsePrefix, ackMaxChars);`;
119+ if (code.includes(oldAckMaxChars)) {
120+ code = code.replace(oldAckMaxChars, newAckMaxChars);
121+ changes++;
122+ console.log("[patch] 5. Added prefix interpolation before normalizeHeartbeatReply");
123+ } else {
124+ console.error("[patch] ERROR: Could not find ackMaxChars + normalizeHeartbeatReply block");
125+ process.exit(1);
126+ }
112127
113128fs.writeFileSync(process.argv[1], code, "utf8");
129+ console.log(`[patch] All ${changes} changes applied successfully.`);
114130' " $FILE "
115131
116- echo " [patch] Fixed $FILE "
117132echo " [patch] responsePrefix interpolation fix applied."
0 commit comments