Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 8 additions & 32 deletions src/core/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<available_bots>` block inlines the full
* roster (name + open_id). Above it the block collapses to a one-line pointer
Expand Down Expand Up @@ -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
? `<botmux_routing>\n${hints.join('\n')}\n</botmux_routing>`
Expand Down Expand Up @@ -836,13 +812,13 @@ export function buildFollowUpContent(
if (!skipSessionId) parts.push(`<session_id>${xmlEscape(sessionId)}</session_id>`);
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(`<botmux_reminder>${reminder}</botmux_reminder>`);
}
if (whiteboardBlock) parts.push(whiteboardBlock);
Expand Down
42 changes: 33 additions & 9 deletions test/prompt-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,16 @@ describe('buildNewTopicPrompt', () => {
expect(routing).not.toContain('&amp;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 <session_id> for CLIs with injectsSessionContext (claude-code)', () => {
Expand Down Expand Up @@ -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_reminder>需要回复时必须 botmux send;无需回复时不要解释沉默,final 只输出 BOTMUX_NO_REPLY</botmux_reminder>');
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>[^<]*别因「无输出」提示重发[^<]*<\/botmux_reminder>/);
} finally {
delete (config as { noVisibleOutputHint?: boolean }).noVisibleOutputHint;
}
});

it('places the short whiteboard hint before follow-up user content', () => {
Expand Down