feat(platform): align conversation semantics across surfaces

This commit is contained in:
Jiayuan Zhang 2026-02-17 03:00:06 +08:00
parent b7b3d323b8
commit f0f6055031
4 changed files with 50 additions and 23 deletions

View file

@ -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,

View file

@ -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<Record<string, never>, 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,

View file

@ -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 };
}
}

View file

@ -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: