Skip to content
Merged
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
6 changes: 3 additions & 3 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ export const messages: Record<string, string> = {
'ai.routing.intro': 'You are connected to a Lark (Feishu) topic group. The user is reading on Lark and CANNOT see your terminal output.',
'ai.routing.must_use_botmux': 'To make the user see something, you MUST send it via the `botmux send` command. Terminal output does NOT reach the chat.',
'ai.routing.usage_heading': 'How to use it:',
'ai.routing.usage_send_when': '- Use `botmux send` for: key conclusions, plans (wait for user approval before acting), final results, progress updates.',
'ai.routing.usage_send_when': '- Use `botmux send` for: key conclusions, plans (wait for user approval before acting), final results, progress updates. If no reply is needed, do not explain the silence and do not call `botmux send`; the final assistant message must be exactly `BOTMUX_NO_REPLY`.',
'ai.routing.usage_send_text': '- Plain text is fine: `botmux send "your message"`. Formatting is auto-handled.',
'ai.routing.usage_heredoc': '- Multi-line body text MUST use a quoted heredoc / stdin (or a UTF-8 `--content-file`). Never write `botmux send "line1\\nline2"` or pass `JSON.stringify` / JSON-escaped text as a positional argument; shell / botmux do not turn literal `\\n` back into newlines.',
'ai.routing.heredoc_example': " Correct multi-line example:\n```bash\nbotmux send <<'EOF'\nline 1\nline 2\nEOF\n```",
Expand Down Expand Up @@ -596,7 +596,7 @@ export const messages: Record<string, string> = {
'ai.shell.multiline_heredoc': 'Multi-line body text MUST use a quoted heredoc / stdin (or a UTF-8 `--content-file`). Never write `botmux send "line1\\nline2"` or pass `JSON.stringify` / JSON-escaped text as a positional argument; shell / botmux do not turn literal `\\n` back into newlines.',
'ai.shell.heredoc_example': "Correct multi-line example:\n```bash\nbotmux send <<'EOF'\nline 1\nline 2\nEOF\n```",
'ai.shell.helpers': 'Helpers: `botmux history` (read this session\'s history — thread/topic sessions are topic-scoped; regular-group chat-scope sessions are group-wide), `botmux quoted <message_id>` (fetch a quoted message — only use it when the prompt header shows `[user quoted message ...]`), `botmux bots list` (list other bots in the group).',
'ai.shell.when_to_send': 'When to send: key conclusions, plans (wait for user approval before acting), final results, progress updates. A bare `print`/`echo` does NOT count as a reply.',
'ai.shell.when_to_send': 'When to send: key conclusions, plans (wait for user approval before acting), final results, progress updates. A bare `print`/`echo` does NOT count as a reply. If no reply is needed, do not explain the silence and do not call `botmux send`; the final assistant message must be exactly `BOTMUX_NO_REPLY`.',
'ai.shell.mention_gate': '@ decision (mandatory): every `botmux send` MUST explicitly pick one or it errors — `--mention <open_id:name>` (name a person/bot; REQUIRED to communicate or collaborate with another bot) / `--mention-back` (@ the sender of the message you are replying to) / `--no-mention` (none). Choose by VALUE: substantive conclusion the other party should read/confirm/decide → --mention-back; pure record / low-priority / short ack → --no-mention; a contentless "got it" is better not sent. Do not default to --no-mention, and do not @ people for nothing.',

// ─── AI prompt blocks (session-manager) ──────────────────────────────────
Expand All @@ -605,7 +605,7 @@ export const messages: Record<string, string> = {
'ai.available_bots.hint': 'To communicate or collaborate with a bot listed here you MUST --mention its open_id (botmux send --mention ou_xxx ...). Without --mention the other bot receives nothing.',
'ai.available_bots.hint_collapsed': 'To communicate or collaborate with another bot, first run `botmux bots list` to get its open_id, then --mention it. Without --mention the other bot receives nothing.',
'ai.available_bots.collapsed_line': 'There are {count} collaborator bots in this chat: {names}.',
'ai.followup.reminder': 'Replies must go via `botmux send` — terminal output does not reach the user.',
'ai.followup.reminder': 'When a reply is needed, use `botmux send`; when none is needed, do not explain the silence and make the final exactly BOTMUX_NO_REPLY.',
'ai.cursor.sender_note': 'The sender tag is metadata identifying the current speaker — never copy its open_id or name (e.g. ou_xxx:Alice) into your botmux send body or opening line; to @ the triggerer use botmux send --mention-back.',
'ai.bridge.attachments_label': '[Attachments]',
'ai.bridge.mentions_label': '[@Mentions]',
Expand Down
6 changes: 3 additions & 3 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ export const messages: Record<string, string> = {
'ai.routing.intro': '你连接到了飞书(Lark)话题群。用户在飞书上阅读,看不到你的终端输出。',
'ai.routing.must_use_botmux': '想让用户看到的内容必须通过 `botmux send` 命令发送,终端输出不会到达聊天。',
'ai.routing.usage_heading': '使用指南:',
'ai.routing.usage_send_when': '- 用 `botmux send` 发送:关键结论、方案(等用户确认再执行)、最终结果、进度更新。',
'ai.routing.usage_send_when': '- 用 `botmux send` 发送:关键结论、方案(等用户确认再执行)、最终结果、进度更新。若无需回复,不要解释沉默,也不要调用 `botmux send`;最终 assistant message 必须只输出 `BOTMUX_NO_REPLY`。',
'ai.routing.usage_send_text': '- 发送纯文本即可:`botmux send "消息"`。格式自动处理。',
'ai.routing.usage_heredoc': '- 多行正文必须走 quoted heredoc / stdin(或 UTF-8 `--content-file`);禁止写成 `botmux send "第一行\\n第二行"`,也不要先 `JSON.stringify` / JSON 转义再传位置参数,shell / botmux 不会把字面量 `\\n` 还原成换行。',
'ai.routing.heredoc_example': " 正确多行示例:\n```bash\nbotmux send <<'EOF'\n第一行\n第二行\nEOF\n```",
Expand Down Expand Up @@ -599,7 +599,7 @@ export const messages: Record<string, string> = {
'ai.shell.multiline_heredoc': '多行正文必须走 quoted heredoc / stdin(或 UTF-8 `--content-file`);禁止写成 `botmux send "第一行\\n第二行"`,也不要先 `JSON.stringify` / JSON 转义再传位置参数,shell / botmux 不会把字面量 `\\n` 还原成换行。',
'ai.shell.heredoc_example': "正确多行示例:\n```bash\nbotmux send <<'EOF'\n第一行\n第二行\nEOF\n```",
'ai.shell.helpers': '辅助命令:`botmux history`(读此会话历史;thread/话题会话拉话题内,普通群 chat-scope 会话拉整群)、`botmux quoted <message_id>`(按需读取被引用的消息,仅在 prompt 头部出现 `[用户引用了消息 ...]` 提示时使用)、`botmux bots list`(查群内其他机器人)。',
'ai.shell.when_to_send': '发送时机:关键结论、方案(等用户确认再动手)、最终结果、进度更新。只 print/echo 不算回复。',
'ai.shell.when_to_send': '发送时机:关键结论、方案(等用户确认再动手)、最终结果、进度更新。只 print/echo 不算回复。若无需回复,不要解释沉默,也不要调用 `botmux send`;最终 assistant message 必须只输出 `BOTMUX_NO_REPLY`。',
'ai.shell.mention_gate': '@ 决策(硬性):每条 `botmux send` 必须显式三选一否则报错——`--mention <open_id:名字>`(点名某人/bot,跟别的 bot 沟通/协作必须用它)/ `--mention-back`(@回触发你的那条消息的发送者)/ `--no-mention`(不@)。按内容价值选:有实质结论要对方看/确认/决策→--mention-back;纯记录/低优先级/简短确认→--no-mention;没信息量的"收到"不如不发。别把 --no-mention 当默认,也别无意义 @ 打扰。',

// ─── AI prompt blocks (session-manager) ──────────────────────────────────
Expand All @@ -608,7 +608,7 @@ export const messages: Record<string, string> = {
'ai.available_bots.hint': '要跟这里的某个 bot 沟通或协作必须 --mention 它的 open_id(botmux send --mention ou_xxx ...),不 --mention 对方 bot 完全收不到消息',
'ai.available_bots.hint_collapsed': '要跟别的 bot 沟通或协作先 `botmux bots list` 查 open_id 再 --mention,不 --mention 对方收不到',
'ai.available_bots.collapsed_line': '群里有 {count} 个可协作 bot:{names}。',
'ai.followup.reminder': '回复必须 botmux send,终端输出用户看不到',
'ai.followup.reminder': '需要回复时必须 botmux send;无需回复时不要解释沉默,final 只输出 BOTMUX_NO_REPLY',
'ai.cursor.sender_note': 'sender 标签只是元信息(标识当前发言人),不要把其中的 open_id 或名字(例如 ou_xxx:高鹏)抄进 botmux send 的正文或开头;要 @ 回触发者请用 botmux send --mention-back。',
'ai.bridge.attachments_label': '[附件]',
'ai.bridge.mentions_label': '[@提及]',
Expand Down
11 changes: 11 additions & 0 deletions src/services/bridge-fallback-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
* disk and threads them through here.
*
* Rules:
* - Non-adopt + exact no-reply sentinel: suppress. Botmux-aware models use
* this explicit protocol when a turn genuinely needs no chat response.
* Exact matching is intentional: prose such as "I should stay silent"
* is still a normal final answer and must not be guessed away.
* - Adopt mode never suppresses: in /adopt the model in the adopted
* session is unaware of botmux, so transcript drain is the ONLY
* channel from model to Lark. There's no `botmux send` to compete
Expand Down Expand Up @@ -35,6 +39,12 @@ import { normaliseForFingerprint } from './bridge-turn-queue.js';
const MATERIAL_FINAL_LENGTH_RATIO = 2;
const MATERIAL_FINAL_MIN_EXTRA_CHARS = 120;

export const BRIDGE_NO_REPLY_SENTINEL = 'BOTMUX_NO_REPLY';

export function isBridgeNoReplyFinal(finalText: string | undefined): boolean {
return finalText?.trim() === BRIDGE_NO_REPLY_SENTINEL;
}

export interface BridgeSendMarker {
sentAtMs: number;
messageId?: string;
Expand Down Expand Up @@ -98,6 +108,7 @@ export function shouldSuppressBridgeEmit(
adoptMode: boolean,
): boolean {
if (adoptMode) return false;
if (isBridgeNoReplyFinal(turn.finalText)) return true;
if (turn.isLocal) return true;
if (turn.markTimeMs === undefined) return false;
const lower = turn.markTimeMs;
Expand Down
28 changes: 28 additions & 0 deletions test/bridge-fallback-gate.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import {
BRIDGE_NO_REPLY_SENTINEL,
shouldEmitEmptyCompletedBridgeFallback,
shouldSuppressBridgeEmit,
type BridgeSendMarker,
Expand All @@ -18,6 +19,33 @@ const markerForContent = (sentAtMs: number, content: string): BridgeSendMarker =
};

describe('shouldSuppressBridgeEmit', () => {
it('non-adopt: exact no-reply sentinel suppresses without a send marker', () => {
expect(shouldSuppressBridgeEmit(
{ ...turn(100), finalText: ` ${BRIDGE_NO_REPLY_SENTINEL}\n` },
undefined,
[],
false,
)).toBe(true);
});

it('non-adopt: prose about staying silent is not guessed away', () => {
expect(shouldSuppressBridgeEmit(
{ ...turn(100), finalText: `I will stay silent instead of replying. ${BRIDGE_NO_REPLY_SENTINEL}` },
undefined,
[],
false,
)).toBe(false);
});

it('adopt mode does not interpret the no-reply sentinel', () => {
expect(shouldSuppressBridgeEmit(
{ ...turn(100), finalText: BRIDGE_NO_REPLY_SENTINEL },
undefined,
[],
true,
)).toBe(false);
});

it('adopt mode never suppresses, even with markers in window', () => {
const markers: BridgeSendMarker[] = [{ sentAtMs: 150 }];
expect(shouldSuppressBridgeEmit(turn(100), 200, markers, true)).toBe(false);
Expand Down
4 changes: 4 additions & 0 deletions test/cli-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,10 @@ describe('systemHints', () => {
expect(hints.length).toBeGreaterThan(0);
expect(hints.some(h => h.includes('botmux send'))).toBe(true);
});

it('traex systemHints declare the exact no-reply protocol', () => {
expect(createTraexAdapter('/bin/traex').systemHints.join('\n')).toContain('BOTMUX_NO_REPLY');
});
});

// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion test/prompt-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ describe('buildFollowUpContent', () => {
expect(content.indexOf('<mentions>')).toBeGreaterThan(content.indexOf('</user_message>'));
// Complex send guidance is discoverable once in the opening catalog; keep
// every follow-up reminder intentionally tiny.
expect(content).toContain('<botmux_reminder>回复必须 botmux send,终端输出用户看不到</botmux_reminder>');
expect(content).toContain('<botmux_reminder>需要回复时必须 botmux send;无需回复时不要解释沉默,final 只输出 BOTMUX_NO_REPLY</botmux_reminder>');
expect(content).not.toContain('JSON.stringify');
expect(content).not.toContain('botmux skill show botmux-send');
});
Expand Down