diff --git a/docs/features/F055-a2a-mcp-structured-routing.md b/docs/features/F055-a2a-mcp-structured-routing.md index 987a27312..37e40c995 100644 --- a/docs/features/F055-a2a-mcp-structured-routing.md +++ b/docs/features/F055-a2a-mcp-structured-routing.md @@ -61,6 +61,12 @@ created: 2026-03-04 - 不改 thread participation 逻辑 - 不做 7B 意图识别(成本高、CJK 准确率不够、双通道已够兜底) +### 相关改进(#417) + +- 协议明确区分:inline `@xx` = semantic only,line-start `@xx` = actionable handoff +- `callback-tools.ts` post_message 描述已统一为"行首 @猫名",避免误导 +- `detectInlineActionMentions()` 补上 write-side feedback,句中 action-like @ 不再静默丢弃 + --- ## Acceptance Criteria diff --git a/docs/features/F064-a2a-exit-check.md b/docs/features/F064-a2a-exit-check.md index 3685b6b72..a2f895797 100644 --- a/docs/features/F064-a2a-exit-check.md +++ b/docs/features/F064-a2a-exit-check.md @@ -33,7 +33,7 @@ F064 的核心动机是修复 A2A 协作中的“链条终止盲区”:该 @ ### Phase B(运行时注入) - [x] AC-B1: 非 parallel 且 a2aEnabled 时注入出口检查提示。 - [x] AC-B2: `mentionRoutingFeedback` read-side 注入与测试覆盖完成。 -- [ ] AC-B3: write-side 自动回写尚未接入(列为已知债务并保留后续方案)。 +- [x] AC-B3: write-side 自动回写已部分接入(#417: serial response path via route-serial; callback/post_message path 尚未覆盖)。 ## Dependencies @@ -127,30 +127,25 @@ Maine Coon(GPT-5.2) 在协作场景中反复出现两种极端: - 历史事件:Maine Coon mention spam 事件(Anti-Mention-Spam 规则起源) - 相关 Feature:F046 Anti-Drift Protocol、F055 A2A MCP Structured Routing -## Known Debt: `mentionRoutingFeedback` write-side 未接入 +## Partially Resolved Debt: `mentionRoutingFeedback` write-side (#417) -**状态**:read-side 已完成(F064 PR #227),write-side 未实现 +**状态**:serial response path 已接入;callback/post_message path 尚未覆盖 -**现状**: -- `buildInvocationContext()` 已能渲染 `mentionRoutingFeedback`(如果有值的话)→ 提醒猫"上次 @ 没生效" -- `ThreadStore.setMentionRoutingFeedback()` 接口已存在(in-memory + Redis 两个实现都有) -- **但没有任何代码在检测到"句中 @ 未路由"时调用 `setMentionRoutingFeedback()`** +**已接入路径(route-serial)**: +- `a2a-mentions.ts` 新增 `detectInlineActionMentions()`:邻近性检测句中 `@pattern` + 紧邻动作词 +- `route-serial.ts` 在 `parseA2AMentions()` 之后调用检测,命中时写入 `setMentionRoutingFeedback()` +- 下次该猫被唤起时,read-side 渲染为 `[路由提醒]`,消费后自动清除(one-shot) +- `callback-tools.ts` post_message 描述已统一为"行首 @猫名" -**为什么没一起修**: -1. write-side 需要在 `routeSerial` 完成后分析猫的回复——检测"句中有 `@xxx` 但不在行首"→ 写入 feedback。当前 `a2a-mentions.ts` 只解析行首 @,不检测句中 @,需要扩展解析逻辑 -2. **误报风险高**:叙述性提及(如"Ragdoll已经完成了 @opus 的建议")不应触发反馈,但简单的正则很难区分"想 @ 但格式错"和"单纯的叙述性提及"。如果误报频繁,反而会引发 mention spam(猫收到"你上次 @ 没生效"→ 补一个行首 @ → 实际上不需要对方行动) -3. F064 的核心目标是**主动预防**(出口检查),write-side feedback 是**被动纠正**,优先级低 +**未覆盖路径**: +- `callbacks.ts` 的 post_message callback 路径仍只做行首 mention 解析,未调用 write-side feedback +- 后续如需完整覆盖,需在 callback 写路径也接入 `detectInlineActionMentions` -**未来接入建议**: -- 在 `routeSerial` 的猫回复完成后,用 `a2a-mentions.ts` 的扩展版本检测"句中有 @pattern 但不在行首"的情况 -- 只有同时满足以下条件才写入 feedback:(a) 句中有 @pattern (b) 同段有动作词(请/帮/review/确认等)(c) 不在代码块或引用块中 -- 写入后设 TTL(如 1 次调用后过期),避免反复提醒 - -**相关代码位置**: -- Read-side: `SystemPromptBuilder.ts:384-388` -- Store interface: `ThreadStore.ts:129-137` -- 路由入口: `route-serial.ts:120-149` -- 解析器: `a2a-mentions.ts`(需扩展) +**误报控制(邻近性检测)**: +- 动作词必须紧邻 @mention 前后(BEFORE: ready for/请/帮/交接给/转给;AFTER: review/check/确认/处理 等) +- 整行有动作词但不紧邻 @mention 不触发(如"请按 @codex 之前的建议继续处理") +- 纯叙述性提及(如"之前 @codex 提出的方案不错")不会触发 +- feedback 是 one-shot(consumeMentionRoutingFeedback 读后即删),不会反复提醒 ## 愿景守护签收表 diff --git a/packages/api/src/domains/cats/services/agents/routing/a2a-mentions.ts b/packages/api/src/domains/cats/services/agents/routing/a2a-mentions.ts index 4b81e3f35..7f0701e87 100644 --- a/packages/api/src/domains/cats/services/agents/routing/a2a-mentions.ts +++ b/packages/api/src/domains/cats/services/agents/routing/a2a-mentions.ts @@ -47,6 +47,12 @@ export interface A2AMentionAnalysis { readonly suppressed: SuppressedA2AMention[]; } +/** #417: Inline @mention paired with action words — missed handoff candidate. */ +export interface InlineActionMention { + readonly catId: CatId; + readonly lineText: string; +} + /** @deprecated Mode is ignored — line-start mentions always route regardless of mode. */ export type MentionActionabilityMode = 'strict' | 'relaxed'; @@ -118,3 +124,91 @@ export function analyzeA2AMentions( return { mentions: found, suppressed: [] }; } + +/** + * #417: Detect inline @mentions paired with action words — missed handoff candidates. + * Used for write-side feedback only, NOT for routing. + * + * Conditions (all must hold): + * 1. @pattern appears mid-line (not at line start) + * 2. Action keyword immediately adjacent to @mention (proximity-based, not whole-line) + * 3. Not inside a fenced code block or blockquote + * 4. Target cat was not already routed via line-start mention + * 5. Not a self-mention + */ + +/** + * Action patterns that appear immediately BEFORE @mention (e.g. "Ready for @xxx"). + * Chinese 请 uses negative lookbehind to exclude compounds (邀请 = invite, 申请 = apply). + */ +const BEFORE_HANDOFF_RE = /(?:ready\s+for|交接给?|转给|(? 0 ? catRegistry.getAllConfigs() : CAT_CONFIGS; + + const entries: MentionPatternEntry[] = []; + for (const [id, config] of Object.entries(allConfigs)) { + if (currentCatId && id === currentCatId) continue; + if (!isCatAvailable(id)) continue; + for (const pattern of config.mentionPatterns) { + entries.push({ catId: id as CatId, pattern: pattern.toLowerCase() }); + } + } + entries.sort((a, b) => b.pattern.length - a.pattern.length); + + const routedSet = new Set(routedMentions ?? []); + const found: InlineActionMention[] = []; + const seen = new Set(); + + for (const rawLine of stripped.split(/\r?\n/)) { + const trimmed = rawLine.trimStart(); + const normalized = trimmed.toLowerCase(); + if (normalized.startsWith('@') || normalized.startsWith('>')) continue; + + let lineMatched = false; + for (const entry of entries) { + if (lineMatched) break; + // Scan ALL occurrences of this pattern in the line (not just first indexOf hit). + // Fixes: "之前 @codex 提过意见,现在 Ready for @codex review" must find the second one. + let searchFrom = 0; + while (searchFrom < normalized.length) { + const idx = normalized.indexOf(entry.pattern, searchFrom); + if (idx < 0) break; + searchFrom = idx + 1; + // Left boundary: @ must not be preceded by word-like chars (avoids "foo@codex") + if (idx > 0 && HANDLE_CONTINUATION_RE.test(normalized[idx - 1]!)) continue; + const charAfter = normalized[idx + entry.pattern.length]; + const isBoundary = !charAfter || TOKEN_BOUNDARY_RE.test(charAfter) || !HANDLE_CONTINUATION_RE.test(charAfter); + if (!isBoundary) continue; + // Already routed via line-start: skip this entry but keep scanning other cats on same line. + if (routedSet.has(entry.catId)) break; + const before = normalized.slice(0, idx); + const after = normalized.slice(idx + entry.pattern.length); + if (!BEFORE_HANDOFF_RE.test(before) && !AFTER_HANDOFF_RE.test(after)) continue; + if (!seen.has(entry.catId)) { + seen.add(entry.catId); + found.push({ catId: entry.catId, lineText: rawLine.trim() }); + lineMatched = true; + } + // Already-seen cat: don't claim the line — let other cats still be scanned. + break; + } + } + } + + return found; +} diff --git a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts index 233aac399..5511a5d71 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts @@ -36,7 +36,7 @@ import { invokeSingleCat } from '../invocation/invoke-single-cat.js'; import { buildMcpCallbackInstructions, needsMcpInjection } from '../invocation/McpPromptInjector.js'; import { getRichBlockBuffer } from '../invocation/RichBlockBuffer.js'; import { resolveDefaultClaudeMcpServerPath } from '../providers/ClaudeAgentService.js'; -import { getMaxA2ADepth, parseA2AMentions } from '../routing/a2a-mentions.js'; +import { detectInlineActionMentions, getMaxA2ADepth, parseA2AMentions } from '../routing/a2a-mentions.js'; import { registerWorklist, unregisterWorklist } from '../routing/WorklistRegistry.js'; import { extractContextEvalSignals } from './context-eval.js'; import { buildBriefingMessage } from './format-briefing.js'; @@ -667,6 +667,25 @@ export async function* routeSerial( // Line-start @mention = always actionable (no keyword gate) a2aMentions = parseA2AMentions(storedContent, catId); + // #417 / F064 AC-B3: Write-side feedback for inline action-like @mentions + if (deps.invocationDeps.threadStore) { + const inlineHits = detectInlineActionMentions(storedContent, catId, a2aMentions); + if (inlineHits.length > 0) { + try { + await deps.invocationDeps.threadStore.setMentionRoutingFeedback(threadId, catId, { + sourceTimestamp: Date.now(), + items: inlineHits.map((m) => ({ targetCatId: m.catId, reason: 'inline_action' as const })), + }); + log.info( + { catId: catId as string, threadId, targets: inlineHits.map((h) => h.catId) }, + 'Inline action @mention detected — wrote routing feedback', + ); + } catch { + /* best-effort */ + } + } + } + // F079 Phase 2: Vote interception — extract [VOTE:xxx] from cat response const votedOption = extractVoteFromText(storedContent); if (votedOption && deps.invocationDeps.threadStore) { diff --git a/packages/api/src/domains/cats/services/stores/ports/ThreadStore.ts b/packages/api/src/domains/cats/services/stores/ports/ThreadStore.ts index ae77d2093..34eabc3b1 100644 --- a/packages/api/src/domains/cats/services/stores/ports/ThreadStore.ts +++ b/packages/api/src/domains/cats/services/stores/ports/ThreadStore.ts @@ -68,7 +68,7 @@ export interface ThreadMemoryV1 { artifacts?: string[]; } -export type MentionRoutingSuppressionReason = 'no_action' | 'cross_paragraph'; +export type MentionRoutingSuppressionReason = 'no_action' | 'cross_paragraph' | 'inline_action'; export type MentionActionabilityMode = 'strict' | 'relaxed'; export interface ThreadMentionRoutingFeedbackItem { diff --git a/packages/api/test/a2a-mentions.test.js b/packages/api/test/a2a-mentions.test.js index fbbe07eb9..668ea0fef 100644 --- a/packages/api/test/a2a-mentions.test.js +++ b/packages/api/test/a2a-mentions.test.js @@ -257,6 +257,215 @@ describe('F052: cross-thread self-reference exemption', () => { }); }); +describe('#417: detectInlineActionMentions', () => { + it('detects inline @mention with action word (Ready for @codex review)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '217 tests pass. Ready for @codex review on lifecycle completeness.'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.equal(result.length, 1); + assert.equal(result[0].catId, 'codex'); + }); + + it('detects Chinese action word with inline @mention', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '这个方案 @codex 请帮忙看一下'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.equal(result.length, 1); + assert.equal(result[0].catId, 'codex'); + }); + + it('ignores inline @mention WITHOUT action word (pure narrative)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '之前 @codex 提出的方案不错'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, []); + }); + + it('skips cats already routed via line-start mention', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = 'Ready for @codex review'; + const result = detectInlineActionMentions(text, 'opus', ['codex']); + assert.deepEqual(result, []); + }); + + it('ignores line-start @mention (those are handled by parseA2AMentions)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '@codex 请 review'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, []); + }); + + it('ignores @mention inside code blocks', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '看看代码:\n```\nReady for @codex review\n```'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, []); + }); + + it('ignores @mention inside blockquotes', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '> Ready for @codex review'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, []); + }); + + it('filters self-mention', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = 'Ready for @opus review'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, []); + }); + + it('returns empty for empty text', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('', 'opus', []), []); + }); + + // --- P1 regression: gpt52 review repro cases (proximity-based matching) --- + + it('P1-repro: "请按 @codex 之前的建议" is narrative, not handoff', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '请按 @codex 之前的建议继续处理这个问题。'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, [], 'should not trigger: "请按" is not a handoff directive to @codex'); + }); + + it('P1-repro: multi-mention line targets the right cat', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '之前 @codex 讨论过,Ready for @gemini review on this patch.'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.equal(result.length, 1, 'should detect exactly one inline action mention'); + assert.equal(result[0].catId, 'gemini', 'should target gemini (Ready for), not codex (narrative)'); + }); + + it('proximity: "帮 @codex review" triggers (adjacent action)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '这个问题帮 @codex review 一下'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.equal(result.length, 1); + assert.equal(result[0].catId, 'codex'); + }); + + it('proximity: distant action word does not trigger', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '请参考 @codex 提出的方案并继续处理'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.deepEqual(result, [], 'action words "请" and "处理" are not adjacent to @codex'); + }); + + // --- R2 P1 regression: completion suffixes turn commands into narrative --- + + it('R2-P1: "@codex 处理过" is narrative (completion suffix 过)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('之前 @codex 处理过这个问题', 'opus', []), []); + }); + + it('R2-P1: "@codex 确认了" is narrative (completion suffix 了)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('之前 @codex 确认了这个方案', 'opus', []), []); + }); + + it('R2-P1: "@codex 来看过" is narrative (completion suffix 过)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('之前 @codex 来看过一次', 'opus', []), []); + }); + + it('R2-P1: "@codex 处理一下" is still handoff (no completion suffix)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const result = detectInlineActionMentions('这个问题 @codex 处理一下', 'opus', []); + assert.equal(result.length, 1); + assert.equal(result[0].catId, 'codex'); + }); + + // --- R2 P2 regression: same cat appears twice, second occurrence is handoff --- + + it('R2-P2: same cat twice — first narrative, second handoff', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = '之前 @codex 提过意见,现在 Ready for @codex review'; + const result = detectInlineActionMentions(text, 'opus', []); + assert.equal(result.length, 1, 'should detect the second occurrence as handoff'); + assert.equal(result[0].catId, 'codex'); + }); + + // --- Codex review: routed cat on same line must not block other cats --- + + it('routed cat does not block other cats on same line', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const text = 'Ready for @codex and @gemini review'; + // codex already routed via line-start, gemini is not + const result = detectInlineActionMentions(text, 'opus', ['codex']); + assert.equal(result.length, 1, 'should still detect gemini'); + assert.equal(result[0].catId, 'gemini'); + }); + + // --- Codex R3: 请 compound exclusion + left boundary --- + + it('"@codex 请教过" is narrative (请教 = consult, not imperative)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('之前 @codex 请教过这个问题', 'opus', []), []); + }); + + it('"@codex 请看" is still handoff (请 + action verb)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const result = detectInlineActionMentions('这个 @codex 请看一下', 'opus', []); + assert.equal(result.length, 1); + }); + + it('ignores embedded @mention without left boundary (foo@codex)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('contact foo@codex review', 'opus', []), []); + }); + + it('detects @mention with left boundary (space before @)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const result = detectInlineActionMentions('contact @codex review', 'opus', []); + assert.equal(result.length, 1); + }); + + // --- Codex R4: English verb boundary --- + + it('"@codex reviewed" is narrative (past tense, not imperative)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('之前 @codex reviewed this', 'opus', []), []); + }); + + it('"@codex checklist" is narrative (compound word)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('@codex checklist 已更新', 'opus', []), []); + }); + + // --- Codex R5: 请 as compound suffix (邀请/申请) --- + + it('"邀请 @codex 参加评审" is narrative (邀请 = invite, not imperative 请)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('邀请 @codex 参加评审', 'opus', []), []); + }); + + it('"申请 @codex 权限" is narrative (申请 = apply, not imperative 请)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + assert.deepEqual(detectInlineActionMentions('申请 @codex 权限', 'opus', []), []); + }); + + it('"请 @codex review" still triggers (standalone 请 is imperative)', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + const result = detectInlineActionMentions('这个问题请 @codex review 一下', 'opus', []); + assert.equal(result.length, 1); + }); + + // --- Codex R6: already-seen cat must not block fresh cats on later lines --- + + it('already-seen cat (longer pattern, scanned first) does not block fresh cat on same line', async () => { + const { detectInlineActionMentions } = await import('../dist/domains/cats/services/agents/routing/a2a-mentions.js'); + // @gemini (7 chars) is longer than @codex (6 chars), so entries sort puts gemini first. + // Line 1 adds gemini to seen. Line 2: gemini scanned first → already seen → must NOT set lineMatched. + const text = 'Ready for @gemini review\nReady for @gemini and @codex review'; + const result = detectInlineActionMentions(text, 'opus', []); + const catIds = result.map((r) => r.catId); + assert.ok(catIds.includes('gemini'), 'gemini should be detected from line 1'); + assert.ok(catIds.includes('codex'), 'codex should be detected from line 2 even though gemini already seen'); + }); +}); + describe('SystemPromptBuilder A2A injection', () => { it('includes A2A section when a2aEnabled and serial mode', async () => { const { buildSystemPrompt } = await import('../dist/domains/cats/services/context/SystemPromptBuilder.js'); diff --git a/packages/mcp-server/src/tools/callback-tools.ts b/packages/mcp-server/src/tools/callback-tools.ts index fa5a553ed..8eb72aaba 100644 --- a/packages/mcp-server/src/tools/callback-tools.ts +++ b/packages/mcp-server/src/tools/callback-tools.ts @@ -720,8 +720,8 @@ export const callbackTools = [ description: 'Post a proactive async message to YOUR CURRENT thread mid-task (e.g. progress updates, sharing results). ' + 'Always posts to the thread your invocation belongs to. To post to a DIFFERENT thread, use cat_cafe_cross_post_message instead. ' + - 'To simply @mention another cat at the end of your response, use @猫名 in your reply text instead — it is free and never expires. ' + - 'GOTCHA: This tool uses callback credentials that expire — if it fails with 401, fall back to inline @mention in your response text. ' + + 'To hand off to another cat, write @猫名 on its own line at the START of the line (sentence-internal @mention does NOT route — it is treated as narrative only). ' + + 'GOTCHA: This tool uses callback credentials that expire — if it fails with 401, fall back to line-start @mention in your response text. ' + 'GOTCHA: Do NOT use this for routine replies — only for mid-task proactive messages when you need to share something before your response completes.', inputSchema: postMessageInputSchema, handler: handlePostMessage,