diff --git a/src/core/session-manager.ts b/src/core/session-manager.ts index 724ccd980..ddf0f2996 100644 --- a/src/core/session-manager.ts +++ b/src/core/session-manager.ts @@ -533,30 +533,6 @@ function renderWhiteboardBlock(opts?: { whiteboardId?: string }): string { ].join('\n'); } -function buildHermesBotmuxHints(locale?: Locale): string[] { - if (locale === 'en') { - return [ - 'You are running in a Feishu/Lark chat through botmux. For ordinary text replies, write the user-facing answer as your final assistant message; botmux automatically forwards that final output to Feishu/Lark.', - 'Do not call `botmux send` for normal text answers. Use `botmux send` only for special delivery needs: files/images/videos, voice, cross-chat/top-level sends, or explicit mention routing to another person/bot.', - '`botmux send` / `botmux history` / `botmux quoted` / `botmux bots` are shell commands installed in $PATH; run them via Bash/terminal tools when needed.', - 'If you already used `botmux send` for special delivery in this turn, do not put a second copy of the answer, messageId, or send-success receipt in the final assistant message.', - ]; - } - return [ - '你运行在飞书(Lark)聊天中。普通文字回复请直接写在 assistant final 里,botmux 会自动把 final_output 转发到飞书。', - '普通文本答案不要调用 `botmux send`。只有需要图片/文件/视频/语音、跨群或顶层发送、显式 @ 某人/某 bot 等特殊投递能力时,才使用 `botmux send`。', - '`botmux send` / `botmux history` / `botmux quoted` / `botmux bots` 是已安装在 $PATH 的 shell 命令;需要时通过 Bash/terminal 工具执行。', - '如果本轮已经为了特殊投递调用过 `botmux send`,final 里不要再写第二份正文、messageId 或“发送成功/已处理”回执。', - ]; -} - -function hermesFollowupReminder(locale?: Locale): string { - if (locale === 'en') { - return 'For ordinary text replies, do not call `botmux send`; put the user-facing answer in final and botmux will forward it to Feishu/Lark. Use `botmux send` only for special delivery such as files/images/videos, voice, cross-chat/top-level sends, or explicit mention routing. If already used, do not add a second answer or send-success receipt in final.'; - } - return '普通文字回复不要调用 `botmux send`;直接把给用户看的答案写在 final,botmux 会自动转发到飞书。只有图片/文件/视频/语音、跨群/顶层发送、特殊 @ 路由等特殊投递才用 `botmux send`;如果本轮已经用过,不要在 final 里再写第二份答案或发送成功回执。'; -} - /** * Peer count at/below which the `` block inlines the full * roster (name + open_id). Above it the block collapses to a one-line pointer @@ -651,7 +627,7 @@ export function buildNewTopicPrompt( // (Claude Code builds its own via --append-system-prompt). Source hints // freshly from i18n so they respect the resolved locale instead of the // static `adapter.systemHints` array that was baked at module load. - const hints = adapter.injectsSessionContext ? [] : (cliId === 'hermes' ? buildHermesBotmuxHints(locale) : buildBotmuxShellHints(locale)); + const hints = adapter.injectsSessionContext ? [] : buildBotmuxShellHints(locale); const routingBlock = hints.length > 0 ? `\n${hints.join('\n')}\n` @@ -836,13 +812,13 @@ export function buildFollowUpContent( if (!skipSessionId) parts.push(`${xmlEscape(sessionId)}`); if (roleBlock) parts.push(roleBlock); if (opts?.cliId !== 'mira') { - // Non-hermes CLIs get the anti-resend variant only when the experimental - // dashboard toggle is on (config.noVisibleOutputHint, default OFF); otherwise - // the reminder is byte-for-byte the pre-feature baseline. Live-read so a - // Settings flip applies to the next follow-up turn without a daemon restart. - const reminder = opts?.cliId === 'hermes' - ? hermesFollowupReminder(opts?.locale) - : t(config.noVisibleOutputHint ? 'ai.followup.reminder_no_resend' : 'ai.followup.reminder', undefined, opts?.locale); + // All non-Mira CLIs — including Hermes, which no longer gets reverse + // send-first guidance (#653) and now shares this standard path — get the + // anti-resend variant only when the experimental dashboard toggle is on + // (config.noVisibleOutputHint, default OFF); otherwise the reminder is + // byte-for-byte the pre-feature baseline. Live-read so a Settings flip + // applies to the next follow-up turn without a daemon restart. + const reminder = t(config.noVisibleOutputHint ? 'ai.followup.reminder_no_resend' : 'ai.followup.reminder', undefined, opts?.locale); parts.push(`${reminder}`); } if (whiteboardBlock) parts.push(whiteboardBlock); diff --git a/test/prompt-builder.test.ts b/test/prompt-builder.test.ts index e07e37373..7219529fc 100644 --- a/test/prompt-builder.test.ts +++ b/test/prompt-builder.test.ts @@ -130,12 +130,16 @@ describe('buildNewTopicPrompt', () => { expect(routing).not.toContain('&lt;'); }); - it('uses final-output routing hints for Hermes instead of normal botmux send guidance', () => { + it('gives Hermes the standard botmux-send routing hints like other structured-bridge CLIs', () => { + // #365 previously steered Hermes AWAY from `botmux send` (reverse guidance) + // as a redundant belt-and-braces on top of the real dedup fix + // (preserveMarkTimeMs). That reverse hint weakened multi-agent collaboration + // (bridge-forwarded finals can't carry an @mention), so Hermes now uses the + // same send-first hints as codex/traex/grok. const prompt = buildNewTopicPrompt('hello', SESSION_ID, 'hermes'); - expect(prompt).toContain('普通文字回复请直接写在 assistant final'); - expect(prompt).toContain('普通文本答案不要调用 `botmux send`'); - expect(prompt).not.toContain("botmux send <<'EOF'"); - expect(prompt).not.toContain('回复必须 botmux send'); + expect(prompt).toContain('把消息发给用户(唯一方式)'); + expect(prompt).not.toContain('普通文本答案不要调用 `botmux send`'); + expect(prompt).not.toContain('botmux 会自动把 final_output 转发到飞书'); }); it('should NOT embed for CLIs with injectsSessionContext (claude-code)', () => { @@ -423,12 +427,32 @@ describe('buildFollowUpContent', () => { } }); - it('uses final-output reminder for Hermes follow-ups', () => { + it('gives Hermes the standard follow-up reminder like other CLIs (no reverse send guidance)', () => { + // #365 previously steered Hermes AWAY from `botmux send` (reverse guidance) + // as redundant belt-and-braces on top of the real dedup fix + // (preserveMarkTimeMs). That reverse hint weakened multi-agent collaboration + // (bridge-forwarded finals can't carry an @mention), so Hermes now shares + // the standard path. With the anti-resend toggle OFF (default) that is + // exactly #554's BOTMUX_NO_REPLY sentinel baseline — same as codex/traex. const content = buildFollowUpContent('hello', SESSION_ID, { cliId: 'hermes' }); - expect(content).toContain('普通文字回复不要调用 `botmux send`'); - expect(content).toContain('直接把给用户看的答案写在 final'); - expect(content).not.toContain('回复必须 botmux send'); + expect(content).toContain('需要回复时必须 botmux send;无需回复时不要解释沉默,final 只输出 BOTMUX_NO_REPLY'); + expect(content).not.toContain('普通文字回复不要调用 `botmux send`'); + expect(content).not.toContain('直接把给用户看的答案写在 final'); + }); + + it('routes Hermes through the shared anti-resend branch when noVisibleOutputHint is ON', () => { + // Codex-review guard for #653: prove Hermes really lands in the shared + // noVisibleOutputHint branch (not a special-cased bypass), so the toggle + // reaches it just like every other non-Mira CLI. + (config as { noVisibleOutputHint?: boolean }).noVisibleOutputHint = true; + try { + const content = buildFollowUpContent('hello', SESSION_ID, { cliId: 'hermes' }); + expect(content).toContain('final 只输出 BOTMUX_NO_REPLY'); + expect(content).toMatch(/[^<]*别因「无输出」提示重发[^<]*<\/botmux_reminder>/); + } finally { + delete (config as { noVisibleOutputHint?: boolean }).noVisibleOutputHint; + } }); it('places the short whiteboard hint before follow-up user content', () => {