From f0f6055031a3630883fbf65df2212abbe4f99712 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Tue, 17 Feb 2026 03:00:06 +0800 Subject: [PATCH] feat(platform): align conversation semantics across surfaces --- apps/desktop/src/main/ipc/hub.ts | 41 +++++++++++++---------- apps/gateway/telegram/telegram.service.ts | 8 ++--- apps/server/app.controller.ts | 20 +++++++++++ docs/development.md | 4 ++- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/main/ipc/hub.ts b/apps/desktop/src/main/ipc/hub.ts index 0af0955d..2937bb08 100644 --- a/apps/desktop/src/main/ipc/hub.ts +++ b/apps/desktop/src/main/ipc/hub.ts @@ -107,12 +107,15 @@ export function registerHubIpcHandlers(): void { ipcMain.handle('hub:init', async () => { await initializeHub() const h = getHub() + const defaultConversationId = defaultAgentId + ? (h.getAgentMainConversationId(defaultAgentId) ?? defaultAgentId) + : null return { hubId: h.hubId, url: h.url, connectionState: h.connectionState, defaultAgentId, - defaultConversationId: defaultAgentId, + defaultConversationId, } }) @@ -144,7 +147,7 @@ export function registerHubIpcHandlers(): void { gatewayUrl: h.url, defaultAgent: agent ? { - agentId: agent.sessionId, + agentId: defaultAgentId ?? agent.sessionId, status: agent.closed ? 'closed' : 'idle', } : null, @@ -160,7 +163,7 @@ export function registerHubIpcHandlers(): void { return null } return { - agentId: agent.sessionId, + agentId: defaultAgentId ?? agent.sessionId, status: agent.closed ? 'closed' : 'idle', } }) @@ -301,16 +304,18 @@ export function registerHubIpcHandlers(): void { */ ipcMain.handle('localChat:subscribe', async (_event, agentId: string) => { const h = getHub() - const agent = h.getAgent(agentId) - if (!agent) { - return { error: `Agent not found: ${agentId}` } + const conversationId = agentId + const conversation = h.getConversation(conversationId) + if (!conversation) { + return { error: `Agent not found: ${conversationId}` } } - if (agent.closed) { - return { error: `Agent is closed: ${agentId}` } + if (conversation.closed) { + return { error: `Agent is closed: ${conversationId}` } } + const logicalAgentId = h.getConversationAgentId(conversationId) ?? conversationId // Already subscribed? - if (ipcAgentSubscriptions.has(agentId)) { + if (ipcAgentSubscriptions.has(conversationId)) { return { ok: true, alreadySubscribed: true } } @@ -318,7 +323,7 @@ export function registerHubIpcHandlers(): void { let currentStreamId: string | null = null // Subscribe to agent events using the multi-subscriber mechanism - const unsubscribe = agent.subscribe((event) => { + const unsubscribe = conversation.subscribe((event) => { if (!mainWindowRef || mainWindowRef.isDestroyed()) { return } @@ -329,8 +334,8 @@ export function registerHubIpcHandlers(): void { if (isPassthroughEvent) { safeLog(`[IPC] Sending ${event.type} event to renderer`) mainWindowRef.webContents.send('localChat:event', { - agentId, - conversationId: agentId, + agentId: logicalAgentId, + conversationId, streamId: null, event, }) @@ -358,8 +363,8 @@ export function registerHubIpcHandlers(): void { safeLog(`[IPC] Sending event to renderer: ${event.type}, streamId: ${currentStreamId}`) mainWindowRef.webContents.send('localChat:event', { - agentId, - conversationId: agentId, + agentId: logicalAgentId, + conversationId, streamId: currentStreamId, event, }) @@ -370,16 +375,16 @@ export function registerHubIpcHandlers(): void { } }) - ipcAgentSubscriptions.set(agentId, unsubscribe) + ipcAgentSubscriptions.set(conversationId, unsubscribe) // Register local approval handler so exec approval requests route via IPC - h.setLocalApprovalHandler(agentId, (payload) => { + h.setLocalApprovalHandler(conversationId, (payload) => { if (!mainWindowRef || mainWindowRef.isDestroyed()) return safeLog(`[IPC] Sending approval request to renderer: ${payload.approvalId}`) mainWindowRef.webContents.send('localChat:approval', payload) }) - safeLog(`[IPC] Local chat subscribed to agent: ${agentId}`) + safeLog(`[IPC] Local chat subscribed to conversation: ${conversationId}`) return { ok: true } }) @@ -457,7 +462,7 @@ export function registerHubIpcHandlers(): void { const source = { type: 'local' as const } // Broadcast as local source (for consistency, though UI already knows) h.broadcastInbound({ - agentId, + agentId: h.getConversationAgentId(resolvedConversationId) ?? agentId, conversationId: resolvedConversationId, content, source, diff --git a/apps/gateway/telegram/telegram.service.ts b/apps/gateway/telegram/telegram.service.ts index 22d8d330..b1090064 100644 --- a/apps/gateway/telegram/telegram.service.ts +++ b/apps/gateway/telegram/telegram.service.ts @@ -721,13 +721,13 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { return message.includes("METHOD_NOT_FOUND") || message.includes("Unknown RPC method"); } - private async createConversationViaRpc(deviceId: string, hubId: string): Promise<{ id: string }> { + private async createConversationViaRpc(deviceId: string, hubId: string, agentId?: string): Promise<{ id: string }> { try { - const created = await this.sendRpc, CreateConversationResult>( + const created = await this.sendRpc<{ agentId?: string }, CreateConversationResult>( deviceId, hubId, "createConversation", - {}, + agentId ? { agentId } : {}, VERIFY_TIMEOUT_MS, "Create session request timed out", ); @@ -792,7 +792,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { } try { - const created = await this.createConversationViaRpc(user.deviceId, user.hubId); + const created = await this.createConversationViaRpc(user.deviceId, user.hubId, user.agentId); await this.userStore.upsert({ telegramUserId: user.telegramUserId, diff --git a/apps/server/app.controller.ts b/apps/server/app.controller.ts index 0a453766..c1fcd287 100644 --- a/apps/server/app.controller.ts +++ b/apps/server/app.controller.ts @@ -52,4 +52,24 @@ export class AppController { const ok = this.hub.closeAgent(id); return { ok }; } + + @Get("conversations") + listConversations() { + return this.hub.listConversations().map((id) => { + const conversation = this.hub.getConversation(id); + return { id, closed: conversation?.closed ?? true }; + }); + } + + @Post("conversations") + createConversation(@Body() body?: { id?: string; agentId?: string }) { + const conversation = this.hub.createConversation(body?.id, { agentId: body?.agentId }); + return { id: conversation.sessionId }; + } + + @Delete("conversations/:id") + deleteConversation(@Param("id") id: string) { + const ok = this.hub.closeConversation(id); + return { ok }; + } } diff --git a/docs/development.md b/docs/development.md index 5fde43a4..e464c0fd 100644 --- a/docs/development.md +++ b/docs/development.md @@ -60,11 +60,13 @@ pnpm dev:local:archive Compatibility behavior: -- If only `agentId` is provided, the runtime resolves `conversationId = agentId`. +- If only `agentId` is provided, runtime resolves to that agent's `mainConversationId`. +- Legacy fallback is still supported: when no mapping exists, `conversationId = agentId`. - New integrations should pass `conversationId` explicitly. - Hub RPC supports both naming sets: - Legacy: `createAgent/listAgents/deleteAgent` - Conversation-first aliases: `createConversation/listConversations/deleteConversation` + - `createConversation` supports optional `agentId` to create a new thread under a specific agent. Telegram behavior: