diff --git a/apps/desktop/src/main/electron-env.d.ts b/apps/desktop/src/main/electron-env.d.ts index f87bda6e..65e50105 100644 --- a/apps/desktop/src/main/electron-env.d.ts +++ b/apps/desktop/src/main/electron-env.d.ts @@ -100,10 +100,9 @@ interface ProfileData { interface LocalChatEvent { agentId: string - conversationId?: string - sessionId?: string + conversationId: string streamId?: string - type?: 'error' + type?: 'error' content?: string event?: { type: 'message_start' | 'message_update' | 'message_end' | 'tool_execution_start' | 'tool_execution_update' | 'tool_execution_end' | 'compaction_start' | 'compaction_end' @@ -119,10 +118,9 @@ interface LocalChatEvent { interface LocalChatApproval { approvalId: string agentId: string - conversationId?: string - sessionId?: string + conversationId: string command: string - cwd?: string + cwd?: string riskLevel: 'safe' | 'needs-review' | 'dangerous' riskReasons: string[] expiresAtMs: number @@ -180,18 +178,14 @@ interface ElectronAPI { hub: { init: () => Promise getStatus: () => Promise - getAgentInfo: () => Promise + getAgentInfo: () => Promise info: () => Promise reconnect: (url: string) => Promise - listAgents: () => Promise listConversations: () => Promise - createAgent: (id?: string) => Promise createConversation: (id?: string) => Promise - getAgent: (id: string) => Promise getConversation: (id: string) => Promise - closeAgent: (id: string) => Promise closeConversation: (id: string) => Promise - sendMessage: (agentId: string, content: string, conversationId?: string) => Promise + sendMessage: (agentId: string, content: string, conversationId: string) => Promise registerToken: (token: string, agentId: string, conversationId: string, expiresAt: number) => Promise onDeviceConfirmRequest: (callback: (deviceId: string, agentId: string, conversationId: string, meta?: DeviceMeta) => void) => void offDeviceConfirmRequest: () => void @@ -257,13 +251,13 @@ interface ElectronAPI { last: () => Promise setEnabled: (enabled: boolean) => Promise<{ ok: boolean; enabled?: boolean; error?: string }> wake: (reason?: string) => Promise<{ ok: boolean; result?: unknown; error?: string }> - } + } localChat: { - subscribe: (agentId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }> - unsubscribe: (agentId: string) => Promise<{ ok: boolean }> - getHistory: (agentId: string, options?: { offset?: number; limit?: number; conversationId?: string }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number; contextWindowTokens?: number }> - send: (agentId: string, content: string, conversationId?: string) => Promise<{ ok?: boolean; error?: string }> - abort: (agentId: string, conversationId?: string) => Promise<{ ok?: boolean; error?: string }> + subscribe: (conversationId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }> + unsubscribe: (conversationId: string) => Promise<{ ok: boolean }> + getHistory: (conversationId: string, options?: { offset?: number; limit?: number }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number; contextWindowTokens?: number }> + send: (conversationId: string, content: string) => Promise<{ ok?: boolean; error?: string }> + abort: (conversationId: string) => Promise<{ ok?: boolean; error?: string }> resolveExecApproval: (approvalId: string, decision: string) => Promise<{ ok: boolean }> onEvent: (callback: (event: LocalChatEvent) => void) => void offEvent: () => void diff --git a/apps/desktop/src/main/ipc/agent.ts b/apps/desktop/src/main/ipc/agent.ts index dc2189f2..428198af 100644 --- a/apps/desktop/src/main/ipc/agent.ts +++ b/apps/desktop/src/main/ipc/agent.ts @@ -47,10 +47,10 @@ function getDefaultAgent() { const hub = getCurrentHub() if (!hub) return null - const agentIds = hub.listAgents() - if (agentIds.length === 0) return null + const conversationIds = hub.listConversations() + if (conversationIds.length === 0) return null - return hub.getAgent(agentIds[0]) ?? null + return hub.getConversation(conversationIds[0]) ?? null } /** diff --git a/apps/desktop/src/main/ipc/hub.ts b/apps/desktop/src/main/ipc/hub.ts index 8f54b569..c5b37822 100644 --- a/apps/desktop/src/main/ipc/hub.ts +++ b/apps/desktop/src/main/ipc/hub.ts @@ -10,7 +10,7 @@ import type { ConnectionState } from '@multica/sdk' // Singleton Hub instance let hub: Hub | null = null -let defaultAgentId: string | null = null +let defaultConversationId: string | null = null let mainWindowRef: BrowserWindow | null = null // Track which agents have active IPC subscriptions (for local direct chat) @@ -35,7 +35,7 @@ function safeLog(...args: unknown[]): void { /** * Initialize Hub on app startup. - * Creates Hub and a default Agent automatically. + * Creates Hub and a default conversation automatically. */ export async function initializeHub(): Promise { if (hub) { @@ -47,16 +47,16 @@ export async function initializeHub(): Promise { hub = new Hub(gatewayUrl) - // Create default agent if none exists - const agents = hub.listAgents() - if (agents.length === 0) { - safeLog('[Desktop] Creating default agent...') - const agent = hub.createAgent() - defaultAgentId = agent.sessionId - safeLog(`[Desktop] Default agent created: ${defaultAgentId}`) + // Create default conversation if none exists + const conversations = hub.listConversations() + if (conversations.length === 0) { + safeLog('[Desktop] Creating default conversation...') + const conversation = hub.createConversation() + defaultConversationId = conversation.sessionId + safeLog(`[Desktop] Default conversation created: ${defaultConversationId}`) } else { - defaultAgentId = agents[0] - safeLog(`[Desktop] Using existing agent: ${defaultAgentId}`) + defaultConversationId = conversations[0] + safeLog(`[Desktop] Using existing conversation: ${defaultConversationId}`) } } @@ -75,8 +75,8 @@ function getHub(): Hub { * Get the default agent. */ export function getDefaultAgent(): AsyncAgent | null { - if (!hub || !defaultAgentId) return null - return hub.getAgent(defaultAgentId) ?? null + if (!hub || !defaultConversationId) return null + return hub.getConversation(defaultConversationId) ?? null } /** @@ -107,14 +107,10 @@ 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, } }) @@ -128,7 +124,7 @@ export function registerHubIpcHandlers(): void { hubId: h.hubId, url: h.url, connectionState: h.connectionState, - agentCount: h.listAgents().length, + agentCount: h.listConversations().length, } }) @@ -142,12 +138,12 @@ export function registerHubIpcHandlers(): void { return { hubId: h.hubId, status: h.connectionState === 'connected' ? 'ready' : h.connectionState, - agentCount: h.listAgents().length, + agentCount: h.listConversations().length, gatewayConnected: h.connectionState === 'connected', gatewayUrl: h.url, defaultAgent: agent ? { - agentId: defaultAgentId ?? agent.sessionId, + agentId: defaultConversationId ?? agent.sessionId, status: agent.closed ? 'closed' : 'idle', } : null, @@ -163,7 +159,7 @@ export function registerHubIpcHandlers(): void { return null } return { - agentId: defaultAgentId ?? agent.sessionId, + agentId: defaultConversationId ?? agent.sessionId, status: agent.closed ? 'closed' : 'idle', } }) @@ -178,22 +174,7 @@ export function registerHubIpcHandlers(): void { }) /** - * List all agents. - */ - ipcMain.handle('hub:listAgents', async (): Promise => { - const h = getHub() - const agentIds = h.listAgents() - return agentIds.map((id) => { - const agent = h.getAgent(id) - return { - id, - closed: agent?.closed ?? true, - } - }) - }) - - /** - * List all conversations (alias of listAgents for clearer semantics). + * List all conversations. */ ipcMain.handle('hub:listConversations', async (): Promise => { const h = getHub() @@ -208,19 +189,7 @@ export function registerHubIpcHandlers(): void { }) /** - * Create a new agent. - */ - ipcMain.handle('hub:createAgent', async (_event, id?: string) => { - const h = getHub() - const agent = h.createAgent(id) - return { - id: agent.sessionId, - closed: agent.closed, - } - }) - - /** - * Create a new conversation (alias of createAgent). + * Create a new conversation. */ ipcMain.handle('hub:createConversation', async (_event, id?: string) => { const h = getHub() @@ -232,22 +201,7 @@ export function registerHubIpcHandlers(): void { }) /** - * Get a specific agent. - */ - ipcMain.handle('hub:getAgent', async (_event, id: string) => { - const h = getHub() - const agent = h.getAgent(id) - if (!agent) { - return { error: `Agent not found: ${id}` } - } - return { - id: agent.sessionId, - closed: agent.closed, - } - }) - - /** - * Get a specific conversation (alias of getAgent). + * Get a specific conversation. */ ipcMain.handle('hub:getConversation', async (_event, id: string) => { const h = getHub() @@ -262,16 +216,7 @@ export function registerHubIpcHandlers(): void { }) /** - * Close/delete an agent. - */ - ipcMain.handle('hub:closeAgent', async (_event, id: string) => { - const h = getHub() - const result = h.closeAgent(id) - return { ok: result } - }) - - /** - * Close/delete a conversation (alias of closeAgent). + * Close/delete a conversation. */ ipcMain.handle('hub:closeConversation', async (_event, id: string) => { const h = getHub() @@ -283,10 +228,10 @@ export function registerHubIpcHandlers(): void { * Send a message to an agent (for remote clients via Gateway). * Note: For local direct chat, use 'localChat:send' instead. */ - ipcMain.handle('hub:sendMessage', async (_event, agentId: string, content: string, conversationId?: string) => { + ipcMain.handle('hub:sendMessage', async (_event, agentId: string, content: string, conversationId: string) => { const h = getHub() - const resolvedConversationId = conversationId ?? agentId - const agent = h.getAgent(resolvedConversationId) + const resolvedConversationId = conversationId + const agent = h.getConversation(resolvedConversationId) if (!agent) { return { error: `Conversation not found: ${resolvedConversationId}` } } @@ -302,15 +247,14 @@ export function registerHubIpcHandlers(): void { * Subscribe to local agent events (for direct IPC chat without Gateway). * Uses agent.subscribe() which supports multiple subscribers. */ - ipcMain.handle('localChat:subscribe', async (_event, agentId: string) => { + ipcMain.handle('localChat:subscribe', async (_event, conversationId: string) => { const h = getHub() - const conversationId = agentId const conversation = h.getConversation(conversationId) if (!conversation) { - return { error: `Agent not found: ${conversationId}` } + return { error: `Conversation not found: ${conversationId}` } } if (conversation.closed) { - return { error: `Agent is closed: ${conversationId}` } + return { error: `Conversation is closed: ${conversationId}` } } const logicalAgentId = h.getConversationAgentId(conversationId) ?? conversationId @@ -336,7 +280,6 @@ export function registerHubIpcHandlers(): void { mainWindowRef.webContents.send('localChat:event', { agentId: logicalAgentId, conversationId, - streamId: null, event, }) return @@ -392,14 +335,14 @@ export function registerHubIpcHandlers(): void { /** * Unsubscribe from local agent events. */ - ipcMain.handle('localChat:unsubscribe', async (_event, agentId: string) => { - const unsubscribe = ipcAgentSubscriptions.get(agentId) + ipcMain.handle('localChat:unsubscribe', async (_event, conversationId: string) => { + const unsubscribe = ipcAgentSubscriptions.get(conversationId) if (unsubscribe) { unsubscribe() } - ipcAgentSubscriptions.delete(agentId) - getHub().removeLocalApprovalHandler(agentId) - safeLog(`[IPC] Local chat unsubscribed from agent: ${agentId}`) + ipcAgentSubscriptions.delete(conversationId) + getHub().removeLocalApprovalHandler(conversationId) + safeLog(`[IPC] Local chat unsubscribed from conversation: ${conversationId}`) return { ok: true } }) @@ -413,12 +356,11 @@ export function registerHubIpcHandlers(): void { */ ipcMain.handle('localChat:getHistory', async ( _event, - agentId: string, - options?: { offset?: number; limit?: number; conversationId?: string }, + conversationId: string, + options?: { offset?: number; limit?: number }, ) => { const h = getHub() - const conversationId = options?.conversationId ?? agentId - const agent = h.getAgent(conversationId) + const agent = h.getConversation(conversationId) if (!agent) { return { messages: [], total: 0, offset: 0, limit: 0, contextWindowTokens: undefined } } @@ -442,10 +384,10 @@ export function registerHubIpcHandlers(): void { * Send a message via local direct IPC (no Gateway). * Events will be pushed to renderer via 'localChat:event' channel. */ - ipcMain.handle('localChat:send', async (_event, agentId: string, content: string, conversationId?: string) => { + ipcMain.handle('localChat:send', async (_event, conversationId: string, content: string) => { const h = getHub() - const resolvedConversationId = conversationId ?? agentId - const agent = h.getAgent(resolvedConversationId) + const resolvedConversationId = conversationId + const agent = h.getConversation(resolvedConversationId) if (!agent) { return { error: `Conversation not found: ${resolvedConversationId}` } } @@ -455,14 +397,14 @@ export function registerHubIpcHandlers(): void { // Must be subscribed first to receive events if (!ipcAgentSubscriptions.has(resolvedConversationId)) { - return { error: 'Not subscribed to agent events. Call subscribe first.' } + return { error: 'Not subscribed to conversation events. Call subscribe first.' } } h.channelManager.clearLastRoute() const source = { type: 'local' as const } // Broadcast as local source (for consistency, though UI already knows) h.broadcastInbound({ - agentId: h.getConversationAgentId(resolvedConversationId) ?? agentId, + agentId: h.getConversationAgentId(resolvedConversationId) ?? resolvedConversationId, conversationId: resolvedConversationId, content, source, @@ -476,10 +418,10 @@ export function registerHubIpcHandlers(): void { /** * Abort the current agent run for local chat. */ - ipcMain.handle('localChat:abort', async (_event, agentId: string, conversationId?: string) => { + ipcMain.handle('localChat:abort', async (_event, conversationId: string) => { const h = getHub() - const resolvedConversationId = conversationId ?? agentId - const agent = h.getAgent(resolvedConversationId) + const resolvedConversationId = conversationId + const agent = h.getConversation(resolvedConversationId) if (!agent) { return { error: `Conversation not found: ${resolvedConversationId}` } } diff --git a/apps/desktop/src/main/ipc/profile.ts b/apps/desktop/src/main/ipc/profile.ts index 36ac1512..4b149aaf 100644 --- a/apps/desktop/src/main/ipc/profile.ts +++ b/apps/desktop/src/main/ipc/profile.ts @@ -13,10 +13,10 @@ function getDefaultAgent() { const hub = getCurrentHub() if (!hub) return null - const agentIds = hub.listAgents() - if (agentIds.length === 0) return null + const conversationIds = hub.listConversations() + if (conversationIds.length === 0) return null - return hub.getAgent(agentIds[0]) ?? null + return hub.getConversation(conversationIds[0]) ?? null } /** diff --git a/apps/desktop/src/main/ipc/provider.ts b/apps/desktop/src/main/ipc/provider.ts index db27c50e..20bfe6b1 100644 --- a/apps/desktop/src/main/ipc/provider.ts +++ b/apps/desktop/src/main/ipc/provider.ts @@ -53,10 +53,10 @@ function getDefaultAgent() { const hub = getCurrentHub() if (!hub) return null - const agentIds = hub.listAgents() - if (agentIds.length === 0) return null + const conversationIds = hub.listConversations() + if (conversationIds.length === 0) return null - return hub.getAgent(agentIds[0]) ?? null + return hub.getConversation(conversationIds[0]) ?? null } /** diff --git a/apps/desktop/src/main/ipc/skills.ts b/apps/desktop/src/main/ipc/skills.ts index 368c1d09..b96079f2 100644 --- a/apps/desktop/src/main/ipc/skills.ts +++ b/apps/desktop/src/main/ipc/skills.ts @@ -27,10 +27,10 @@ function getDefaultAgent() { const hub = getCurrentHub() if (!hub) return null - const agentIds = hub.listAgents() - if (agentIds.length === 0) return null + const conversationIds = hub.listConversations() + if (conversationIds.length === 0) return null - return hub.getAgent(agentIds[0]) ?? null + return hub.getConversation(conversationIds[0]) ?? null } /** diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 82375542..d1fa36cf 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -67,21 +67,20 @@ export interface CurrentProviderInfo { // Local chat event types (for direct IPC communication without Gateway) export interface LocalChatEvent { agentId: string - conversationId?: string - sessionId?: string + conversationId: string streamId?: string type?: 'error' - content?: string + content?: string event?: { type: 'message_start' | 'message_update' | 'message_end' | 'tool_execution_start' | 'tool_execution_update' | 'tool_execution_end' | 'compaction_start' | 'compaction_end' id?: string message?: { role: string - content?: Array<{ type: string; text?: string }> - } - [key: string]: unknown - } -} + content?: Array<{ type: string; text?: string }> + } + [key: string]: unknown + } +} // Inbound message event (from any source: local, gateway, channel) export type MessageSource = @@ -101,10 +100,9 @@ export interface InboundMessageEvent { export interface LocalChatApproval { approvalId: string agentId: string - conversationId?: string - sessionId?: string + conversationId: string command: string - cwd?: string + cwd?: string riskLevel: 'safe' | 'needs-review' | 'dangerous' riskReasons: string[] expiresAtMs: number @@ -159,18 +157,14 @@ const electronAPI = { hub: { init: () => ipcRenderer.invoke('hub:init'), getStatus: (): Promise => ipcRenderer.invoke('hub:getStatus'), - getAgentInfo: (): Promise => ipcRenderer.invoke('hub:getAgentInfo'), - info: () => ipcRenderer.invoke('hub:info'), + getAgentInfo: (): Promise => ipcRenderer.invoke('hub:getAgentInfo'), + info: () => ipcRenderer.invoke('hub:info'), reconnect: (url: string) => ipcRenderer.invoke('hub:reconnect', url), - listAgents: () => ipcRenderer.invoke('hub:listAgents'), listConversations: () => ipcRenderer.invoke('hub:listConversations'), - createAgent: (id?: string) => ipcRenderer.invoke('hub:createAgent', id), createConversation: (id?: string) => ipcRenderer.invoke('hub:createConversation', id), - getAgent: (id: string) => ipcRenderer.invoke('hub:getAgent', id), getConversation: (id: string) => ipcRenderer.invoke('hub:getConversation', id), - closeAgent: (id: string) => ipcRenderer.invoke('hub:closeAgent', id), closeConversation: (id: string) => ipcRenderer.invoke('hub:closeConversation', id), - sendMessage: (agentId: string, content: string, conversationId?: string) => + sendMessage: (agentId: string, content: string, conversationId: string) => ipcRenderer.invoke('hub:sendMessage', agentId, content, conversationId), registerToken: (token: string, agentId: string, conversationId: string, expiresAt: number) => ipcRenderer.invoke('hub:registerToken', token, agentId, conversationId, expiresAt), @@ -335,21 +329,21 @@ const electronAPI = { }, }, - // Local chat (direct IPC, no Gateway required) - localChat: { - /** Subscribe to agent events for local direct chat */ - subscribe: (agentId: string) => ipcRenderer.invoke('localChat:subscribe', agentId), - /** Unsubscribe from agent events */ - unsubscribe: (agentId: string) => ipcRenderer.invoke('localChat:unsubscribe', agentId), - /** Get message history for local chat with pagination (returns raw AgentMessageItem[]) */ - getHistory: (agentId: string, options?: { offset?: number; limit?: number; conversationId?: string }) => - ipcRenderer.invoke('localChat:getHistory', agentId, options), - /** Send message to agent via direct IPC (no Gateway) */ - send: (agentId: string, content: string, conversationId?: string) => - ipcRenderer.invoke('localChat:send', agentId, content, conversationId), + // Local chat (direct IPC, no Gateway required) + localChat: { + /** Subscribe to conversation events for local direct chat */ + subscribe: (conversationId: string) => ipcRenderer.invoke('localChat:subscribe', conversationId), + /** Unsubscribe from conversation events */ + unsubscribe: (conversationId: string) => ipcRenderer.invoke('localChat:unsubscribe', conversationId), + /** Get message history for local chat with pagination (returns raw AgentMessageItem[]) */ + getHistory: (conversationId: string, options?: { offset?: number; limit?: number }) => + ipcRenderer.invoke('localChat:getHistory', conversationId, options), + /** Send message to conversation via direct IPC (no Gateway) */ + send: (conversationId: string, content: string) => + ipcRenderer.invoke('localChat:send', conversationId, content), /** Abort the current agent run */ - abort: (agentId: string, conversationId?: string) => - ipcRenderer.invoke('localChat:abort', agentId, conversationId), + abort: (conversationId: string) => + ipcRenderer.invoke('localChat:abort', conversationId), /** Resolve an exec approval request */ resolveExecApproval: (approvalId: string, decision: string) => ipcRenderer.invoke('localChat:resolveExecApproval', approvalId, decision), diff --git a/apps/desktop/src/renderer/src/components/qr-code.tsx b/apps/desktop/src/renderer/src/components/qr-code.tsx index 2d9824e0..baedb220 100644 --- a/apps/desktop/src/renderer/src/components/qr-code.tsx +++ b/apps/desktop/src/renderer/src/components/qr-code.tsx @@ -20,7 +20,7 @@ export interface ConnectionQRCodeProps { gateway: string hubId: string agentId: string - conversationId?: string + conversationId: string expirySeconds?: number size?: number } @@ -131,8 +131,7 @@ export function ConnectionQRCode({ expirySeconds = 30, size = 200, }: ConnectionQRCodeProps) { - const resolvedConversationId = conversationId ?? agentId - const { token, expiresAt, refresh } = useQRToken(agentId, resolvedConversationId, expirySeconds) + const { token, expiresAt, refresh } = useQRToken(agentId, conversationId, expirySeconds) const remaining = useCountdown(expiresAt, refresh) // Derive QR data and URL from current token (computed during render) @@ -142,11 +141,11 @@ export function ConnectionQRCode({ gateway, hubId, agentId, - conversationId: resolvedConversationId, + conversationId, token, expires: expiresAt, }), - [gateway, hubId, agentId, resolvedConversationId, token, expiresAt] + [gateway, hubId, agentId, conversationId, token, expiresAt] ) const connectionUrl = useMemo(() => { @@ -154,12 +153,12 @@ export function ConnectionQRCode({ gateway, hub: hubId, agent: agentId, - conversation: resolvedConversationId, + conversation: conversationId, token, exp: expiresAt.toString(), }) return `multica://connect?${params.toString()}` - }, [gateway, hubId, agentId, resolvedConversationId, token, expiresAt]) + }, [gateway, hubId, agentId, conversationId, token, expiresAt]) return (
diff --git a/apps/desktop/src/renderer/src/components/telegram-qr.tsx b/apps/desktop/src/renderer/src/components/telegram-qr.tsx index 3f9c6f4b..c079602c 100644 --- a/apps/desktop/src/renderer/src/components/telegram-qr.tsx +++ b/apps/desktop/src/renderer/src/components/telegram-qr.tsx @@ -8,7 +8,7 @@ export interface TelegramConnectQRProps { gateway: string hubId: string agentId: string - conversationId?: string + conversationId: string expirySeconds?: number size?: number } @@ -28,8 +28,7 @@ export function TelegramConnectQR({ expirySeconds = 30, size = 200, }: TelegramConnectQRProps) { - const resolvedConversationId = conversationId ?? agentId - const { token, expiresAt, refresh } = useQRToken(agentId, resolvedConversationId, expirySeconds) + const { token, expiresAt, refresh } = useQRToken(agentId, conversationId, expirySeconds) const remaining = useCountdown(expiresAt, refresh) const [deepLink, setDeepLink] = useState(null) @@ -51,7 +50,7 @@ export function TelegramConnectQR({ gateway, hubId, agentId, - conversationId: resolvedConversationId, + conversationId, token, expires: expiresAt, }), @@ -82,7 +81,7 @@ export function TelegramConnectQR({ fetchCode() return () => { cancelled = true } - }, [token, expiresAt, gateway, hubId, agentId, resolvedConversationId]) + }, [token, expiresAt, gateway, hubId, agentId, conversationId]) if (loading) { return ( diff --git a/apps/desktop/src/renderer/src/hooks/use-local-chat.ts b/apps/desktop/src/renderer/src/hooks/use-local-chat.ts index 024f689c..15762177 100644 --- a/apps/desktop/src/renderer/src/hooks/use-local-chat.ts +++ b/apps/desktop/src/renderer/src/hooks/use-local-chat.ts @@ -47,8 +47,8 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { window.electronAPI.hub.init() .then((result) => { - const r = result as { defaultAgentId?: string; defaultConversationId?: string } - const defaultConversationId = r.defaultConversationId ?? r.defaultAgentId + const r = result as { defaultConversationId?: string } + const defaultConversationId = r.defaultConversationId console.log('[LocalChat] hub.init → defaultConversationId:', defaultConversationId) if (defaultConversationId) { setAgentId(defaultConversationId) @@ -68,7 +68,6 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { // Subscribe to events + fetch history once conversation is available useEffect(() => { if (!activeConversationId) return - const resolvedAgentId = agentId ?? activeConversationId setQueuedMessages([]) offsetRef.current = null setIsLoading(false) @@ -125,33 +124,32 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { chatRef.current.addUserMessage( event.content, event.agentId, - event.source as MessageSource, eventConversationId, + event.source as MessageSource, ) setIsLoading(true) } }) // Fetch history with pagination - window.electronAPI.localChat.getHistory(resolvedAgentId, { + window.electronAPI.localChat.getHistory(activeConversationId, { limit: DEFAULT_MESSAGES_LIMIT, - conversationId: activeConversationId, }) .then((result) => { console.log('[LocalChat] getHistory result:', result.messages?.length, 'messages, total:', result.total) if (result.messages?.length) { - chatRef.current.setHistory(result.messages as AgentMessageItem[], resolvedAgentId, { + chatRef.current.setHistory(result.messages as AgentMessageItem[], activeConversationId, activeConversationId, { total: result.total, offset: result.offset, contextWindowTokens: result.contextWindowTokens, - }, activeConversationId) + }) offsetRef.current = result.offset } else { - chatRef.current.setHistory([], resolvedAgentId, { + chatRef.current.setHistory([], activeConversationId, activeConversationId, { total: 0, offset: 0, contextWindowTokens: result.contextWindowTokens, - }, activeConversationId) + }) } }) .catch(() => {}) @@ -163,7 +161,7 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { window.electronAPI.hub.offInboundMessage() window.electronAPI.localChat.unsubscribe(activeConversationId).catch(() => {}) } - }, [agentId, activeConversationId]) + }, [activeConversationId]) useEffect(() => { isLoadingRef.current = isLoading @@ -172,11 +170,10 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { const dispatchMessageNow = useCallback((text: string) => { const trimmed = text.trim() if (!trimmed || !activeConversationId) return - const resolvedAgentId = agentId ?? activeConversationId - chatRef.current.addUserMessage(trimmed, resolvedAgentId, { type: 'local' }, activeConversationId) + chatRef.current.addUserMessage(trimmed, activeConversationId, activeConversationId, { type: 'local' }) chatRef.current.setError(null) setIsLoading(true) - window.electronAPI.localChat.send(resolvedAgentId, trimmed, activeConversationId) + window.electronAPI.localChat.send(activeConversationId, trimmed) .then((result) => { const response = result as { ok?: boolean; error?: string } | undefined if (response?.error) { @@ -186,7 +183,7 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { .catch(() => { setIsLoading(false) }) - }, [agentId, activeConversationId]) + }, [activeConversationId]) const sendMessage = useCallback((text: string) => { const trimmed = text.trim() @@ -225,10 +222,9 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { const abortGeneration = useCallback(() => { if (!activeConversationId) return - const resolvedAgentId = agentId ?? activeConversationId - window.electronAPI.localChat.abort(resolvedAgentId, activeConversationId).catch(() => {}) + window.electronAPI.localChat.abort(activeConversationId).catch(() => {}) setIsLoading(false) - }, [agentId, activeConversationId]) + }, [activeConversationId]) const loadMore = useCallback(async () => { const currentOffset = offsetRef.current @@ -237,20 +233,18 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { isLoadingMoreRef.current = true setIsLoadingMore(true) try { - const resolvedAgentId = agentId ?? activeConversationId const newOffset = Math.max(0, currentOffset - DEFAULT_MESSAGES_LIMIT) const limit = currentOffset - newOffset - const result = await window.electronAPI.localChat.getHistory(resolvedAgentId, { + const result = await window.electronAPI.localChat.getHistory(activeConversationId, { offset: newOffset, limit, - conversationId: activeConversationId, }) if (result.messages?.length) { - chatRef.current.prependHistory(result.messages as AgentMessageItem[], resolvedAgentId, { + chatRef.current.prependHistory(result.messages as AgentMessageItem[], activeConversationId, activeConversationId, { total: result.total, offset: result.offset, contextWindowTokens: result.contextWindowTokens, - }, activeConversationId) + }) offsetRef.current = result.offset } } catch { @@ -259,7 +253,7 @@ export function useLocalChat(options: UseLocalChatOptions = {}) { isLoadingMoreRef.current = false setIsLoadingMore(false) } - }, [agentId, activeConversationId]) + }, [activeConversationId]) const resolveApproval = useCallback( (approvalId: string, decision: ApprovalDecision) => { diff --git a/apps/desktop/src/renderer/src/pages/admin.tsx b/apps/desktop/src/renderer/src/pages/admin.tsx index 9e14fd79..a8d74510 100644 --- a/apps/desktop/src/renderer/src/pages/admin.tsx +++ b/apps/desktop/src/renderer/src/pages/admin.tsx @@ -118,6 +118,7 @@ export default function HomePage() { gateway={hubInfo?.url ?? 'http://localhost:3000'} hubId={hubInfo?.hubId ?? 'unknown'} agentId={primaryAgent?.id} + conversationId={primaryAgent?.id} expirySeconds={30} size={180} /> diff --git a/apps/desktop/src/renderer/src/pages/clients.tsx b/apps/desktop/src/renderer/src/pages/clients.tsx index 309968ed..8aa6dbed 100644 --- a/apps/desktop/src/renderer/src/pages/clients.tsx +++ b/apps/desktop/src/renderer/src/pages/clients.tsx @@ -34,6 +34,7 @@ function ChannelsContent() { gateway={hubInfo?.url ?? 'http://localhost:3000'} hubId={hubInfo?.hubId ?? 'unknown'} agentId={primaryAgent?.id ?? 'unknown'} + conversationId={primaryAgent?.id ?? 'unknown'} expirySeconds={30} size={200} /> diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx index b60a9132..a66508c1 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx @@ -124,6 +124,7 @@ export default function ConnectStep({ onNext, onBack }: ConnectStepProps) { gateway={hubInfo?.url ?? 'http://localhost:3000'} hubId={hubInfo?.hubId ?? 'unknown'} agentId={primaryAgent?.id ?? 'unknown'} + conversationId={primaryAgent?.id ?? 'unknown'} expirySeconds={30} size={180} /> diff --git a/apps/gateway/public/index.html b/apps/gateway/public/index.html index bef37336..49333633 100644 --- a/apps/gateway/public/index.html +++ b/apps/gateway/public/index.html @@ -726,7 +726,7 @@ from: deviceId, to: targetDeviceId, action: 'message', - payload: { agentId: targetAgentId, conversationId: targetAgentId, sessionId: targetAgentId, content: text }, + payload: { agentId: targetAgentId, conversationId: targetAgentId, content: text }, }); appendMsg('self', text); diff --git a/apps/gateway/telegram/telegram.controller.ts b/apps/gateway/telegram/telegram.controller.ts index 94651307..c65ffabb 100644 --- a/apps/gateway/telegram/telegram.controller.ts +++ b/apps/gateway/telegram/telegram.controller.ts @@ -33,8 +33,7 @@ export class TelegramController { gateway: string; hubId: string; agentId: string; - conversationId?: string; - sessionId?: string; + conversationId: string; token: string; expires: number; }, @@ -53,7 +52,7 @@ export class TelegramController { gateway: body.gateway, hubId: body.hubId, agentId: body.agentId, - ...((body.sessionId ?? body.conversationId) ? { conversationId: body.sessionId ?? body.conversationId } : {}), + conversationId: body.conversationId, token: body.token, expires: body.expires, }; diff --git a/apps/gateway/telegram/telegram.service.ts b/apps/gateway/telegram/telegram.service.ts index 7cf05c04..ceed8ebc 100644 --- a/apps/gateway/telegram/telegram.service.ts +++ b/apps/gateway/telegram/telegram.service.ts @@ -92,14 +92,6 @@ interface GenerateChannelWelcomeResult { text: string; } -interface ListAgentsResult { - agents: Array<{ id: string; closed: boolean }>; -} - -interface CreateAgentResult { - id: string; -} - interface ListConversationsResult { conversations: Array<{ id: string; closed: boolean }>; } @@ -611,7 +603,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { `Welcome back!\n\n` + `${statusEmoji} Status: ${statusText}\n` + `Agent: ${user.agentId}\n` + - `Session: ${user.conversationId ?? user.agentId}\n\n` + + `Conversation: ${user.conversationId ?? user.agentId}\n\n` + (online ? `Your agent is ready. Just send a message to start chatting.` : `Your Hub is offline. Make sure the Multica Desktop app is running.`); @@ -646,7 +638,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { `${statusEmoji} ${statusLabel}\n\n` + `Hub: ${user.hubId}\n` + `Agent: ${user.agentId}\n` + - `Session: ${user.conversationId ?? user.agentId}\n\n` + + `Conversation: ${user.conversationId ?? user.agentId}\n\n` + (online ? `Your Hub is online and ready to receive messages.` : `Your Hub is offline. Make sure the Multica Desktop app is running.`); @@ -668,9 +660,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { `Commands\n` + ` /start \u2014 Connect your account or see welcome\n` + ` /status \u2014 Check connection status\n` + - ` /new \u2014 Start a new isolated session\n` + - ` /session [id] \u2014 Show or switch current session\n` + - ` /sessions \u2014 List available sessions\n` + + ` /new \u2014 Start a new isolated conversation\n` + + ` /session [id] \u2014 Show or switch current conversation\n` + + ` /sessions \u2014 List available conversations\n` + ` /help \u2014 Show this message\n\n` + `How to connect\n` + ` 1. Open Multica Desktop app\n` + @@ -696,9 +688,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { await this.bot.api.setMyCommands([ { command: "start", description: "Connect or show welcome" }, { command: "status", description: "Check connection status" }, - { command: "new", description: "Create a new session" }, - { command: "session", description: "Show/switch current session" }, - { command: "sessions", description: "List sessions" }, + { command: "new", description: "Create a new conversation" }, + { command: "session", description: "Show/switch current conversation" }, + { command: "sessions", description: "List conversations" }, { command: "help", description: "Show help and instructions" }, ]); @@ -765,59 +757,28 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { await ctx.reply(welcome.text, { parse_mode: "HTML", reply_markup: welcome.keyboard }); } - private isMethodNotFoundError(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error); - return message.includes("METHOD_NOT_FOUND") || message.includes("Unknown RPC method"); - } - private async createConversationViaRpc(deviceId: string, hubId: string, agentId?: string): Promise<{ id: string }> { - try { - const created = await this.sendRpc<{ agentId?: string }, CreateConversationResult>( - deviceId, - hubId, - "createConversation", - agentId ? { agentId } : {}, - VERIFY_TIMEOUT_MS, - "Create session request timed out", - ); - return { id: created.id }; - } catch (error) { - if (!this.isMethodNotFoundError(error)) throw error; - const created = await this.sendRpc, CreateAgentResult>( - deviceId, - hubId, - "createAgent", - {}, - VERIFY_TIMEOUT_MS, - "Create session request timed out", - ); - return { id: created.id }; - } + const created = await this.sendRpc<{ agentId?: string }, CreateConversationResult>( + deviceId, + hubId, + "createConversation", + agentId ? { agentId } : {}, + VERIFY_TIMEOUT_MS, + "Create conversation request timed out", + ); + return { id: created.id }; } private async listConversationsViaRpc(deviceId: string, hubId: string): Promise> { - try { - const result = await this.sendRpc, ListConversationsResult>( - deviceId, - hubId, - "listConversations", - {}, - VERIFY_TIMEOUT_MS, - "List sessions request timed out", - ); - return result.conversations; - } catch (error) { - if (!this.isMethodNotFoundError(error)) throw error; - const result = await this.sendRpc, ListAgentsResult>( - deviceId, - hubId, - "listAgents", - {}, - VERIFY_TIMEOUT_MS, - "List sessions request timed out", - ); - return result.agents; - } + const result = await this.sendRpc, ListConversationsResult>( + deviceId, + hubId, + "listConversations", + {}, + VERIFY_TIMEOUT_MS, + "List conversations request timed out", + ); + return result.conversations; } private async handleNewConversationCommand(ctx: Context): Promise { @@ -856,14 +817,14 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { await this.bindConversationToContext(user, ctx, created.id); await ctx.reply( - `\u2705 New session created\n\n` + - `Session: ${created.id}\n\n` + - `All next messages in this Telegram thread will use this session.`, + `\u2705 New conversation created\n\n` + + `Conversation: ${created.id}\n\n` + + `All next messages in this Telegram thread will use this conversation.`, { parse_mode: "HTML" }, ); } catch (error) { const message = error instanceof Error ? error.message : String(error); - await ctx.reply(`Failed to create session: ${message}`); + await ctx.reply(`Failed to create conversation: ${message}`); } } @@ -886,28 +847,28 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { try { const conversations = await this.listConversationsViaRpc(user.deviceId, user.hubId); - const sessions = conversations.filter((item) => !item.closed).map((item) => item.id); - if (sessions.length === 0) { - await ctx.reply("No sessions found."); + const conversationIds = conversations.filter((item) => !item.closed).map((item) => item.id); + if (conversationIds.length === 0) { + await ctx.reply("No conversations found."); return; } const current = await this.resolveConversationForContext(user, ctx); - const lines = sessions.slice(0, 20).map((id) => { + const lines = conversationIds.slice(0, 20).map((id) => { const marker = id === current ? "\u2022 current" : ""; return `${id}${marker ? ` ${marker}` : ""}`; }); - const extra = sessions.length > 20 ? `\n...and ${sessions.length - 20} more` : ""; + const extra = conversationIds.length > 20 ? `\n...and ${conversationIds.length - 20} more` : ""; await ctx.reply( - `Available sessions\n\n` + + `Available conversations\n\n` + `${lines.join("\n")}${extra}\n\n` + `Use /session <id> to switch.`, { parse_mode: "HTML" }, ); } catch (error) { const message = error instanceof Error ? error.message : String(error); - await ctx.reply(`Failed to load sessions: ${message}`); + await ctx.reply(`Failed to load conversations: ${message}`); } } @@ -923,7 +884,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { const current = await this.resolveConversationForContext(user, ctx); if (!target) { await ctx.reply( - `Current session\n\n` + + `Current conversation\n\n` + `${current}\n\n` + `Use /session <id> to switch.`, { parse_mode: "HTML" }, @@ -945,7 +906,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { const exists = conversations.some((item) => item.id === target && !item.closed); if (!exists) { await ctx.reply( - `Session not found: ${target}\n\nUse /sessions to list available sessions.`, + `Conversation not found: ${target}\n\nUse /sessions to list available conversations.`, { parse_mode: "HTML" }, ); return; @@ -964,13 +925,13 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { await this.bindConversationToContext(user, ctx, target); await ctx.reply( - `\u2705 Session switched\n\n` + - `Current session: ${target}`, + `\u2705 Conversation switched\n\n` + + `Current conversation: ${target}`, { parse_mode: "HTML" }, ); } catch (error) { const message = error instanceof Error ? error.message : String(error); - await ctx.reply(`Failed to switch session: ${message}`); + await ctx.reply(`Failed to switch conversation: ${message}`); } } @@ -1372,19 +1333,14 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { ? `Telegram @${msg.from.username}` : `Telegram ${msg?.from?.first_name ?? telegramUserId}`, }); - const sessionId = - connectionInfo.conversationId - ?? result.sessionId - ?? result.conversationId - ?? result.mainConversationId - ?? result.agentId; + const conversationId = connectionInfo.conversationId ?? result.conversationId; // 5. Save to DB await this.userStore.upsert({ telegramUserId, hubId: connectionInfo.hubId, agentId: connectionInfo.agentId, - conversationId: sessionId, + conversationId, deviceId, telegramUsername: msg?.from?.username, telegramFirstName: msg?.from?.first_name, @@ -1395,7 +1351,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { await this.userStore.setThreadConversation( telegramUserId, threadRoute.chatId, - sessionId, + conversationId, threadRoute.threadId, ); } @@ -1408,7 +1364,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { `\u2705 Connected successfully!\n\n` + `Hub: ${result.hubId}\n` + `Agent: ${result.agentId}\n` + - `Session: ${sessionId}\n\n` + + `Conversation: ${conversationId}\n\n` + `You can now send messages to interact with your agent.`, { parse_mode: "HTML", reply_markup: successKeyboard }, ); @@ -1626,7 +1582,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { from: user.deviceId, to: user.hubId, action: "message", - payload: { agentId: user.agentId, conversationId, sessionId: conversationId, content: text }, + payload: { agentId: user.agentId, conversationId, content: text }, }; const sent = this.eventsGateway.routeFromVirtualDevice(message); @@ -1677,7 +1633,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { const streamPayload = msg.payload as StreamPayload; const event = streamPayload?.event; if (!event || !("type" in event)) return; - const conversationId = streamPayload?.sessionId ?? streamPayload?.conversationId; + const conversationId = streamPayload?.conversationId; const contextKey = this.makeConversationContextKey(deviceId, conversationId); // Start typing when LLM begins generating @@ -1745,10 +1701,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { caption?: string; filename?: string; conversationId?: string; - sessionId?: string; }; if (payload?.data) { - const conversationId = payload.sessionId ?? payload.conversationId; + const conversationId = payload.conversationId; const contextKey = this.makeConversationContextKey(deviceId, conversationId); void this.sendFileToTelegram( deviceId, @@ -1769,10 +1724,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { content?: string; agentId?: string; conversationId?: string; - sessionId?: string; }; if (payload?.content) { - const conversationId = payload.sessionId ?? payload.conversationId; + const conversationId = payload.conversationId; const contextKey = this.makeConversationContextKey(deviceId, conversationId); void this.sendToTelegram(deviceId, payload.content, conversationId).then(() => { void this.clearMessageContext(contextKey); @@ -1787,9 +1741,8 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { message?: string; code?: string; conversationId?: string; - sessionId?: string; }; - const conversationId = payload.sessionId ?? payload.conversationId; + const conversationId = payload.conversationId; const contextKey = this.makeConversationContextKey(deviceId, conversationId); this.stopTyping(contextKey); void this.clearMessageContext(contextKey); diff --git a/apps/server/app.controller.ts b/apps/server/app.controller.ts index c1fcd287..cc66e0b3 100644 --- a/apps/server/app.controller.ts +++ b/apps/server/app.controller.ts @@ -20,7 +20,7 @@ export class AppController { hubId: this.hub.hubId, url: this.hub.url, connectionState: this.hub.connectionState, - agentCount: this.hub.listAgents().length, + agentCount: this.hub.listConversations().length, }; } @@ -33,26 +33,6 @@ export class AppController { }; } - @Get("agents") - listAgents() { - return this.hub.listAgents().map((id) => { - const agent = this.hub.getAgent(id); - return { id, closed: agent?.closed ?? true }; - }); - } - - @Post("agents") - createAgent(@Body() body?: { id?: string }) { - const agent = this.hub.createAgent(body?.id); - return { id: agent.sessionId }; - } - - @Delete("agents/:id") - deleteAgent(@Param("id") id: string) { - const ok = this.hub.closeAgent(id); - return { ok }; - } - @Get("conversations") listConversations() { return this.hub.listConversations().map((id) => { diff --git a/apps/server/public/index.html b/apps/server/public/index.html index bcabc2fe..fa974c6e 100644 --- a/apps/server/public/index.html +++ b/apps/server/public/index.html @@ -232,7 +232,7 @@ color: var(--green); } - /* ── Agents section ── */ + /* ── Conversations section ── */ .section-header { display: flex; align-items: center; @@ -347,7 +347,7 @@
-
Agents
+
Conversations
@@ -359,17 +359,17 @@ - +
- Agents -
- Agent ID + Conversation ID Actions
@@ -419,9 +419,9 @@ } async function refresh() { - const [hubRes, agentsRes] = await Promise.all([ + const [hubRes, conversationsRes] = await Promise.all([ fetch(`${API}/hub`).then(r => r.json()), - fetch(`${API}/agents`).then(r => r.json()), + fetch(`${API}/conversations`).then(r => r.json()), ]); // Stats @@ -446,19 +446,19 @@ input.value = hubRes.url || ''; } - // Agent list + // Conversation list const list = document.getElementById('agent-list'); - if (agentsRes.length === 0) { - list.innerHTML = '
No agents yet. Create one to get started.
'; + if (conversationsRes.length === 0) { + list.innerHTML = '
No conversations yet. Create one to get started.
'; } else { - list.innerHTML = agentsRes.map(a => + list.innerHTML = conversationsRes.map(a => `
` + `
` + `${a.id}` + `` + `
` + `
` + - `` + + `` + `
` + `
` ).join(''); @@ -482,13 +482,13 @@ refresh(); } - async function createAgent() { - await fetch(`${API}/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); + async function createConversation() { + await fetch(`${API}/conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); refresh(); } - async function deleteAgent(id) { - await fetch(`${API}/agents/${id}`, { method: 'DELETE' }); + async function deleteConversation(id) { + await fetch(`${API}/conversations/${id}`, { method: 'DELETE' }); refresh(); } diff --git a/docs/development.md b/docs/development.md index fefc24a2..965a7d33 100644 --- a/docs/development.md +++ b/docs/development.md @@ -56,17 +56,13 @@ pnpm dev:local:archive - `agentId`: logical owner identity (capabilities/profile scope). - `conversationId`: isolated runtime thread under an agent. -- `sessionId`: runtime/storage id for a conversation (currently same as `conversationId`). +- `sessionId`: internal runner/storage identifier for a conversation. External protocols use `conversationId`. -Compatibility behavior: +Protocol rules: -- 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. +- Hub RPC is conversation-first: `createConversation/listConversations/deleteConversation`. +- All message, stream, and verify payloads use `conversationId` (no `sessionId` alias fields). +- New integrations should always pass `conversationId` explicitly. Telegram behavior: