From 61252432f45ebe2a0188d0fac8b2e29b5a965da3 Mon Sep 17 00:00:00 2001 From: San Date: Fri, 24 Jul 2026 03:38:57 -0700 Subject: [PATCH] feat: bundle conversation framing in get_conversation; fix list_inbox description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_conversation now hoists a top-level conversation descriptor (type/group_name/member_count) from the server's per-message context, so the model sees the room without digging; per-message context (identity, mentions) is preserved. list_inbox description corrected — it promised a 'last message preview' the payload never returned; now accurately lists group name/member count/participants+display names/last-activity/mute and points to get_conversation for identity + mentions. --- src/tools/get-conversation.ts | 29 ++++++++++++++++++++++++++ src/tools/list-inbox.ts | 2 +- tests/tools/handlers.test.ts | 38 +++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/tools/get-conversation.ts b/src/tools/get-conversation.ts index 91a32f7..60f1451 100644 --- a/src/tools/get-conversation.ts +++ b/src/tools/get-conversation.ts @@ -55,10 +55,39 @@ export function createHandler(ctx: ToolContext) { limit, ...(before_seq !== undefined ? { beforeSeq: before_seq } : {}), }) + // Each message carries the server's trusted `context` block (resolved + // sender identity, the conversation descriptor, and the parsed mention + // list). Hoist a single conversation descriptor to the top level so the + // model sees the room — DM vs group and the group's NAME — without + // digging into per-message context. Per-message context (who each sender + // is, who was @-mentioned) stays on each message. + const conv = messages + .map( + (m) => + ( + m as { + context?: { + conversation?: { + type?: string + group_name?: string | null + member_count?: number | null + } + } + } + ).context?.conversation, + ) + .find((c) => c != null) return { type: 'json', value: { conversation_id, + conversation: conv + ? { + type: conv.type ?? null, + group_name: conv.group_name ?? null, + member_count: conv.member_count ?? null, + } + : null, count: messages.length, messages, }, diff --git a/src/tools/list-inbox.ts b/src/tools/list-inbox.ts index 4601463..83cc1cf 100644 --- a/src/tools/list-inbox.ts +++ b/src/tools/list-inbox.ts @@ -17,7 +17,7 @@ export const INPUT_SHAPE = { export const DESCRIPTION = [ "List the agent's conversations, most-recent first. Use this as the polling tool to discover new messages — call it at the start of a turn before deciding whether to engage.", '', - 'Each row carries the conversation_id, type (direct or group), the other participant(s), the last message preview, and the timestamp. Pass any conversation_id to agentchat_get_conversation to read the full thread.', + 'Each row carries the conversation_id, type (direct or group), the group name and member count (for groups), the other participant(s) with their display names, the last-activity timestamp, and mute state — but NOT the message text. Pass any conversation_id to agentchat_get_conversation to read the actual messages (which include full sender identity and who was @-mentioned).', '', "This is a snapshot, not a subscription. New messages arriving between calls only appear on the next invocation. If you're on a real-time runtime (OpenClaw), prefer the native plugin's WebSocket-driven inbox instead.", ].join('\n') diff --git a/tests/tools/handlers.test.ts b/tests/tools/handlers.test.ts index 941b419..e7f9027 100644 --- a/tests/tools/handlers.test.ts +++ b/tests/tools/handlers.test.ts @@ -201,6 +201,44 @@ describe('agentchat_get_conversation', () => { await handler({ conversation_id: 'conv_x', limit: 25 }) expect(getMessagesMock).toHaveBeenCalledWith('conv_x', { limit: 25 }) }) + + it('hoists a top-level conversation descriptor from message context', async () => { + const getMessagesMock = vi.fn().mockResolvedValue([ + { + id: 'msg_1', + seq: 50, + context: { + conversation: { type: 'group', group_name: 'Ops', member_count: 5 }, + sender: { handle: 'bob', display_name: 'Bob', kind: 'agent' }, + mentions: ['me'], + }, + }, + ]) + const handler = getConversation.createHandler( + makeCtx({ getMessages: getMessagesMock }), + ) + const result = await handler({ conversation_id: 'grp_ops', limit: 50 }) + const value = parseJsonContent(result) as { + conversation: unknown + messages: Array<{ context?: { mentions?: string[] } }> + } + expect(value.conversation).toEqual({ + type: 'group', + group_name: 'Ops', + member_count: 5, + }) + // Per-message context (identity, mentions) is preserved on each message. + expect(value.messages[0]?.context?.mentions).toEqual(['me']) + }) + + it('returns a null descriptor when no message carries context', async () => { + const getMessagesMock = vi.fn().mockResolvedValue([{ id: 'msg_1', seq: 1 }]) + const handler = getConversation.createHandler( + makeCtx({ getMessages: getMessagesMock }), + ) + const result = await handler({ conversation_id: 'conv_x', limit: 50 }) + expect((parseJsonContent(result) as { conversation: unknown }).conversation).toBeNull() + }) }) describe('agentchat_mark_read', () => {