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
29 changes: 29 additions & 0 deletions src/tools/get-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion src/tools/list-inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
38 changes: 38 additions & 0 deletions tests/tools/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading