From 7daab61297127f9f55dd6500cd468e6719eca3f8 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 25 Aug 2026 17:13:36 +0800 Subject: [PATCH 1/5] feat: add cross-session messaging Generated-by: Codex --- ...runtime-host-session-execution-ipc-main.ts | 37 ++- apps/desktop/src/preload/bridge-contract.d.ts | 8 + apps/desktop/src/preload/preload.ts | 31 ++ apps/desktop/src/renderer/app-shell.tsx | 263 ++++++++++++++++- .../src/renderer/desktop-slash-command.ts | 12 +- .../src/renderer/locales/shell-copy.ts | 96 ++++++ .../src/renderer/session-mailbox-picker.tsx | 115 ++++++++ .../src/renderer/styles/chat-message.css | 61 ++++ apps/desktop/src/renderer/styles/composer.css | 31 ++ packages/cli/src/pi-tui-runner.ts | 117 +++++++- .../cli/src/runtime-host-session-driver.ts | 16 + packages/cli/src/session-driver.ts | 9 +- packages/cli/src/tui-primary-guidance.ts | 2 + packages/core/package.json | 1 + .../src/__tests__/session-mailbox.test.ts | 96 ++++++ packages/core/src/session-mailbox.ts | 204 +++++++++++++ packages/core/src/session.ts | 2 + packages/core/src/slash-command-catalog.ts | 3 +- .../src/__tests__/protocol.test.ts | 46 +++ .../session-mailbox-coordinator.test.ts | 211 +++++++++++++ packages/runtime-host/src/protocol/index.ts | 5 +- .../runtime-host/src/protocol/operations.ts | 5 + .../src/protocol/session-mailbox.ts | 190 ++++++++++++ .../src/server/execution-composition.ts | 32 +- .../src/server/operation-dispatcher.ts | 6 + .../src/server/session-mailbox-coordinator.ts | 279 ++++++++++++++++++ packages/runtime/package.json | 1 + .../__tests__/session-mailbox-tools.test.ts | 86 ++++++ packages/runtime/src/session-mailbox-tools.ts | 136 +++++++++ packages/ui/src/__tests__/materialize.test.ts | 54 ++++ packages/ui/src/chat-turn.tsx | 70 ++++- packages/ui/src/composer.tsx | 42 +++ packages/ui/src/conversation-copy.ts | 8 + packages/ui/src/materialize.ts | 78 +++-- 34 files changed, 2311 insertions(+), 42 deletions(-) create mode 100644 apps/desktop/src/renderer/session-mailbox-picker.tsx create mode 100644 packages/core/src/__tests__/session-mailbox.test.ts create mode 100644 packages/core/src/session-mailbox.ts create mode 100644 packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts create mode 100644 packages/runtime-host/src/protocol/session-mailbox.ts create mode 100644 packages/runtime-host/src/server/session-mailbox-coordinator.ts create mode 100644 packages/runtime/src/__tests__/session-mailbox-tools.test.ts create mode 100644 packages/runtime/src/session-mailbox-tools.ts diff --git a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts index bd961f7fdd..041ac4010c 100644 --- a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts @@ -21,6 +21,7 @@ import { randomUUID } from "node:crypto"; import type { IpcMainInvokeEvent } from "electron"; import { MAX_ATTACHMENT_COUNT } from '@maka/core/attachments'; import { RuntimeHostOperationError } from '@maka/runtime-host/client'; +import { HOST_OPERATION_SPECS } from '@maka/runtime-host/protocol'; import { SKILL_INVOCATION_TOKEN_SOURCE } from '@maka/core/skill-invocation-token'; import { type SessionChangedEvent, @@ -86,7 +87,9 @@ type RuntimeHostSessionExecutionClient = Pick< | "submitMessage" | "updateSessionMetadata" | "updateSessionConfiguration" ->; +> & { + request?: DesktopRuntimeHostClient['request']; +}; export interface RuntimeHostSessionExecutionIpcDeps { client: RuntimeHostSessionExecutionClient; @@ -208,6 +211,38 @@ export function registerRuntimeHostSessionExecutionIpc( ], ); + handleReconnectableRead( + ipcMain, + 'sessions:mailboxTargets', + async (_event, sourceSessionId: unknown) => { + if (!deps.client.request) { + throw new Error('Session messaging is unavailable on this Runtime Host client'); + } + return deps.client.request('session.mailbox.targets', { + sourceSessionId: requiredId(sourceSessionId, 'Source Session'), + }); + }, + ); + + ipcMain.handle( + 'sessions:mailboxSend', + async (_event, sourceSessionId: unknown, targetSessionId: unknown, text: unknown) => { + if (!deps.client.request) { + throw new Error('Session messaging is unavailable on this Runtime Host client'); + } + return deps.client.request( + 'session.mailbox.send', + HOST_OPERATION_SPECS['session.mailbox.send'].decodeInput({ + sourceSessionId: requiredId(sourceSessionId, 'Source Session'), + targetSessionId: requiredId(targetSessionId, 'Target Session'), + messageId: newId(), + kind: 'request', + text, + }), + ); + }, + ); + ipcMain.handle( "sessions:send", async (event, sessionId: string, input: unknown) => { diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index d478543721..ecbec71a9e 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -717,6 +717,14 @@ export interface MakaBridge { completeHostIds: string[]; }>; create(input?: CreateSessionRequestInput): Promise; + listMailboxTargets( + sourceSessionId: string, + ): Promise; + sendMailboxMessage( + sourceSessionId: string, + targetSessionId: string, + text: string, + ): Promise; send( sessionId: string, command: diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 24f419d4f0..204764f8bf 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -1524,6 +1524,37 @@ const makaBridge = { const session = await ipcRenderer.invoke('sessions:create', scope, input) as SessionSummary; return projectSessionSummary(scope, session); }, + async listMailboxTargets(sourceSessionId: string) { + const source = await runtimeHostSessionRef(sourceSessionId); + const result = await ipcRenderer.invoke( + 'sessions:mailboxTargets', + source.scope, + source.sessionId, + ) as OperationOutput<'session.mailbox.targets'>; + return result.targets.map((target) => ({ + ...target, + sessionId: desktopSessionKey({ + hostId: source.scope.hostId, + sessionId: target.sessionId, + }), + })); + }, + async sendMailboxMessage(sourceSessionId: string, targetSessionId: string, text: string) { + const [source, target] = await Promise.all([ + runtimeHostSessionRef(sourceSessionId), + runtimeHostSessionRef(targetSessionId), + ]); + if (source.scope.hostId !== target.scope.hostId) { + throw new Error('Session messages require both tasks to use the same Runtime Host'); + } + return ipcRenderer.invoke( + 'sessions:mailboxSend', + source.scope, + source.sessionId, + target.sessionId, + text, + ); + }, async send( sessionId: string, command: diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index c599125ae6..7a2fdf9d30 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -35,6 +35,8 @@ import type { QuoteRef, } from '@maka/core/events'; import type { SessionSummary } from '@maka/core/session'; +import { sessionMailboxSentReceiptId } from '@maka/core/session-mailbox'; +import type { SessionMailboxTarget } from '@maka/runtime-host/protocol'; import type { OrchestrationMode } from '@maka/core/orchestration'; import type { ChatDefaultPermissionMode } from '@maka/core/settings'; import type { SlashCommandIdForSurface } from '@maka/core/slash-command-catalog'; @@ -71,7 +73,7 @@ import { reconcileInteractions, } from '@maka/ui'; import type { ConnectionEvent } from '@maka/core/connections'; -import { GitBranch, MessageCircleQuestion, Minimize2, Network } from '@maka/ui/icons'; +import { GitBranch, MessageCircleQuestion, Minimize2, Network, Pencil } from '@maka/ui/icons'; import { Button } from '@astryxdesign/core/Button'; import { useKeyboardHelp } from './keyboard-help'; import { useCommandPalette } from './command-palette'; @@ -98,6 +100,7 @@ import { UNRESOLVED_NEW_TASK_DRAFT_KEY } from './new-task-reload-intent'; import { useNewTaskChoice } from './use-new-task-choice'; import { NEW_TASK_PENDING_KEY } from './pending-items'; import { parseDesktopSlashCommand } from './desktop-slash-command'; +import { SessionMailboxPicker } from './session-mailbox-picker'; import { hasActiveTurnAtSubmit, mergeWorkspaceReferences, @@ -685,6 +688,34 @@ function AppShellContent({ const persistedComposerDefaults = loadComposerDefaults(); const [helpOpen, closeHelp, openHelp] = useKeyboardHelp(); const [paletteOpen, openPalette, closePalette] = useCommandPalette(); + const [mailboxFlow, setMailboxFlow] = useState<{ + sourceSessionId: string; + targets: readonly SessionMailboxTarget[]; + } | null>(null); + const [pendingMailboxTarget, setPendingMailboxTarget] = useState<{ + sourceSessionId: string; + target: SessionMailboxTarget; + } | null>(null); + const [mailboxDeliveryFeedback, setMailboxDeliveryFeedback] = useState<{ + sourceSessionId: string; + target: SessionMailboxTarget; + status: 'sending' | 'failed'; + } | null>(null); + useEffect(() => { + if ( + pendingMailboxTarget + && pendingMailboxTarget.sourceSessionId !== activeId + ) { + setPendingMailboxTarget(null); + setMailboxDeliveryFeedback(null); + } + if ( + mailboxDeliveryFeedback + && mailboxDeliveryFeedback.sourceSessionId !== activeId + ) { + setMailboxDeliveryFeedback(null); + } + }, [activeId, mailboxDeliveryFeedback, pendingMailboxTarget]); const [viewMode, setViewMode] = useState(() => readSessionListViewMode()); const composerRef = useRef(null); const retractedWorkspaceReferencesRef = useRef>({}); @@ -1379,6 +1410,36 @@ function AppShellContent({ sessionListCollapsed, setSessionListCollapsed, } = useShellLayout(); + const openMailboxTargetPicker = useCallback(async () => { + const sourceSessionId = activeIdRef.current; + if (!sourceSessionId) return false; + if (revisionDraftRef.current) { + const actionCopy = getDesktopConversationCopy(uiLocale).actions; + toastApi.info(actionCopy.revisionUnavailableTitle, actionCopy.revisionCommandUnsupported); + return false; + } + if (pendingAttachments.length > 0 || pendingQuotes.length > 0) { + toastApi.info( + shellCopy.sideChatContextPendingTitle, + shellCopy.sideChatContextPendingDescription, + ); + return false; + } + try { + const targets = await window.maka.sessions.listMailboxTargets(sourceSessionId); + setPendingMailboxTarget(null); + setMailboxDeliveryFeedback(null); + setMailboxFlow({ sourceSessionId, targets }); + return true; + } catch (error) { + showSessionError( + sourceSessionId, + shellCopy.mailboxFailedTitle, + localizedShellErrorMessage(error, shellCopy.mailboxFailedFallback, uiLocale), + ); + return false; + } + }, [pendingAttachments.length, pendingQuotes.length, shellCopy, toastApi, uiLocale]); const desktopSlashCommands = useMemo( () => { const streaming = turnActive || activeStreamingLive; @@ -1396,6 +1457,17 @@ function AppShellContent({ keywords: ['compact', 'context', '压缩', '上下文'], Icon: Minimize2, }, + rename: { + ...shellCopy.slashCommands.rename, + keywords: ['rename', 'name', 'title', '重命名', '名称'], + Icon: Pencil, + }, + send: { + ...shellCopy.slashCommands.send, + keywords: ['send', 'session', 'message', '发送', '任务', '会话'], + Icon: MessageCircleQuestion, + onChoose: () => { void openMailboxTargetPicker(); }, + }, side: { ...shellCopy.slashCommands.side, keywords: ['side', 'btw', '侧聊', '追问'], @@ -1414,7 +1486,7 @@ function AppShellContent({ }; return availableCommands.map(({ id }) => ({ id, ...presentation[id] })); }, - [activeId, activeStreamingLive, shellCopy.slashCommands, turnActive], + [activeId, activeStreamingLive, openMailboxTargetPicker, shellCopy.slashCommands, turnActive], ); const refreshProjectSkillsRef = useRef<() => Promise>(async () => {}); const { @@ -1632,7 +1704,7 @@ function AppShellContent({ newSessionPermissionMode: newTaskPermissionMode, }); - const hasModalOpen = helpOpen || paletteOpen || searchModalOpen; + const hasModalOpen = helpOpen || paletteOpen || searchModalOpen || mailboxFlow !== null; const shellObscured = hasModalOpen || settingsOpen; const contextCompactionPresentation = useMemo( () => @@ -1835,6 +1907,74 @@ function AppShellContent({ text: string, metadata?: ComposerSendMetadata, ): Promise { + const mailboxTarget = pendingMailboxTarget; + if (mailboxTarget) { + if (activeIdRef.current !== mailboxTarget.sourceSessionId) { + setPendingMailboxTarget(null); + setMailboxDeliveryFeedback(null); + return false; + } + if ( + pendingAttachments.length > 0 + || pendingQuotes.length > 0 + || (metadata?.workspaceFileReferences?.length ?? 0) > 0 + ) { + toastApi.info( + shellCopy.sideChatContextPendingTitle, + shellCopy.sideChatContextPendingDescription, + ); + return false; + } + setMailboxDeliveryFeedback({ ...mailboxTarget, status: 'sending' }); + try { + const result = await window.maka.sessions.sendMailboxMessage( + mailboxTarget.sourceSessionId, + mailboxTarget.target.sessionId, + text, + ); + setPendingMailboxTarget(null); + setMailboxDeliveryFeedback(null); + const receiptId = sessionMailboxSentReceiptId(result.messageId); + if (activeIdRef.current === mailboxTarget.sourceSessionId) { + setMessages((current) => { + if (current.some((message) => message.id === receiptId)) return current; + let anchorTurnId: string | undefined; + for (let index = current.length - 1; index >= 0; index -= 1) { + anchorTurnId = current[index]?.turnId; + if (anchorTurnId) break; + } + return [ + ...current, + { + type: 'system_note', + id: receiptId, + ...(anchorTurnId ? { turnId: anchorTurnId } : {}), + ts: Date.now(), + kind: 'session_mailbox_sent', + data: { + messageId: result.messageId, + targetSessionId: mailboxTarget.target.sessionId, + targetSessionName: mailboxTarget.target.name, + kind: 'request', + text, + disposition: result.disposition, + }, + }, + ]; + }); + } + void refreshMessages(mailboxTarget.sourceSessionId); + return true; + } catch (error) { + setMailboxDeliveryFeedback({ ...mailboxTarget, status: 'failed' }); + showSessionError( + mailboxTarget.sourceSessionId, + shellCopy.mailboxFailedTitle, + localizedShellErrorMessage(error, shellCopy.mailboxFailedFallback, uiLocale), + ); + return false; + } + } setNewTaskSendPending(true); try { return await sendWithAttachments(text, metadata); @@ -1971,6 +2111,65 @@ function AppShellContent({ return false; } } + if (slashCommand?.kind === 'rename') { + const sessionId = activeIdRef.current; + if (!sessionId) return false; + try { + await window.maka.sessions.rename(sessionId, slashCommand.name, { + revisionFamily: true, + }); + await refreshSessions(); + toastApi.success( + shellCopy.renameSuccessTitle, + shellCopy.renameSuccessDescription(slashCommand.name), + ); + return true; + } catch (error) { + showSessionError( + sessionId, + getShellCopy(uiLocale).sessionRowActions.renameFailedTitle, + localizedShellErrorMessage(error, shellCopy.tryAgainLater, uiLocale), + ); + return false; + } + } + if (slashCommand?.kind === 'rename_invalid') { + toastApi.info(shellCopy.renameUsageTitle, shellCopy.renameUsageDescription); + return false; + } + if (slashCommand?.kind === 'send') { + const sourceSessionId = activeIdRef.current; + if (!sourceSessionId) return false; + if ( + pendingAttachments.length > 0 || + pendingQuotes.length > 0 || + (metadata?.workspaceFileReferences?.length ?? 0) > 0 + ) { + toastApi.info( + shellCopy.sideChatContextPendingTitle, + shellCopy.sideChatContextPendingDescription, + ); + return false; + } + try { + const targets = await window.maka.sessions.listMailboxTargets(sourceSessionId); + setPendingMailboxTarget(null); + setMailboxDeliveryFeedback(null); + setMailboxFlow({ sourceSessionId, targets }); + return true; + } catch (error) { + showSessionError( + sourceSessionId, + shellCopy.mailboxFailedTitle, + localizedShellErrorMessage(error, shellCopy.mailboxFailedFallback, uiLocale), + ); + return false; + } + } + if (slashCommand?.kind === 'send_invalid') { + toastApi.info(shellCopy.mailboxUsageTitle, shellCopy.mailboxUsageDescription); + return false; + } if (slashCommand?.kind === 'side') { if (!activeIdRef.current) { toastApi.info( @@ -2634,6 +2833,45 @@ function AppShellContent({ ? navSelection.module : 'im_hub'; + const mailboxSendTargetNotice = (() => { + const feedback = mailboxDeliveryFeedback; + if (feedback && feedback.sourceSessionId === activeId) { + const name = feedback.target.name; + const dismiss = () => { + setMailboxDeliveryFeedback(null); + if (feedback.status === 'failed') setPendingMailboxTarget(null); + }; + switch (feedback.status) { + case 'sending': + return { + title: shellCopy.mailboxSendingTitle(name), + detail: shellCopy.mailboxSendingDescription, + cancelLabel: shellCopy.mailboxComposerCancel, + status: feedback.status, + onCancel: dismiss, + } as const; + case 'failed': + return { + title: shellCopy.mailboxFailedReceiptTitle(name), + detail: shellCopy.mailboxFailedReceiptDescription, + cancelLabel: shellCopy.mailboxComposerCancel, + status: feedback.status, + onCancel: dismiss, + } as const; + } + } + if (!pendingMailboxTarget || pendingMailboxTarget.sourceSessionId !== activeId) { + return undefined; + } + return { + title: shellCopy.mailboxComposerTitle(pendingMailboxTarget.target.name), + detail: shellCopy.mailboxComposerDescription, + cancelLabel: shellCopy.mailboxComposerCancel, + status: 'ready', + onCancel: () => setPendingMailboxTarget(null), + } as const; + })(); + return (
+ {mailboxFlow ? ( + { + if (!open) setMailboxFlow(null); + }} + onSelect={(target) => { + setMailboxDeliveryFeedback(null); + setPendingMailboxTarget({ + sourceSessionId: mailboxFlow.sourceSessionId, + target, + }); + setMailboxFlow(null); + window.requestAnimationFrame(() => composerRef.current?.focus()); + }} + /> + ) : null} + ; + mailboxComposerTitle(targetName: string): string; + mailboxComposerDescription: string; + mailboxComposerCancel: string; + mailboxSendingTitle(targetName: string): string; + mailboxSendingDescription: string; + mailboxDeliveredReceiptTitle(targetName: string): string; + mailboxDeliveredReceiptDescription: string; + mailboxQueuedReceiptTitle(targetName: string): string; + mailboxQueuedReceiptDescription: string; + mailboxFailedReceiptTitle(targetName: string): string; + mailboxFailedReceiptDescription: string; + mailboxReceiptDismiss: string; + renameUsageTitle: string; + renameUsageDescription: string; + renameSuccessTitle: string; + renameSuccessDescription(name: string): string; resumeStartedTitle: string; resumeStartedDescription: string; resumeFailedTitle: string; @@ -1192,6 +1220,8 @@ const SHELL_COPY_BY_LOCALE = { slashCommands: { compact: { name: '压缩上下文', description: '压缩旧历史并保留当前任务' }, graph: { name: '使用 Graph', description: '查看、切换或单次运行 Graph' }, + rename: { name: '重命名当前任务', description: '/rename <新名称>' }, + send: { name: '发送到其他任务', description: '先选择接收任务,再输入消息' }, side: { name: '打开侧聊', description: '在右侧开始一个具体话题' }, swarm: { name: '使用 Swarm', description: '查看、切换或单次运行 Swarm' }, }, @@ -1200,6 +1230,38 @@ const SHELL_COPY_BY_LOCALE = { sideChatContextPendingTitle: '先处理待发送的上下文', sideChatContextPendingDescription: '当前 Composer 还有附件、引用或文件 mention。请先发送或移除它们,再使用 /side。', + mailboxFailedTitle: '发送失败', + mailboxFailedFallback: '无法向目标任务发送消息。', + mailboxUsageTitle: '命令格式不完整', + mailboxUsageDescription: '请输入 /send;选择接收任务后再填写消息。', + mailboxPickerTitle: '发送到哪个任务?', + mailboxPickerPlaceholder: '搜索任务名称…', + mailboxPickerSearchLabel: '搜索可接收消息的任务', + mailboxPickerResultsLabel: '可达任务', + mailboxPickerEmpty: '当前项目中没有其他可接收消息的任务。', + mailboxPickerNoMatch: '没有匹配的任务。', + mailboxPickerHint: '使用自定义任务名称;未重命名时显示系统生成的名称', + mailboxPickerStatus: { + idle: '空闲 · 立即送达', + running: '运行中 · 排入下一轮', + waiting_for_user: '等待输入 · 排入下一轮', + }, + mailboxComposerTitle: (targetName: string) => `发送给“${targetName}”`, + mailboxComposerDescription: '在原输入框填写消息,按 Enter 发送;按 Esc 取消。', + mailboxComposerCancel: '取消', + mailboxSendingTitle: (targetName: string) => `正在发送给“${targetName}”…`, + mailboxSendingDescription: '正在等待 Runtime Host 确认。', + mailboxDeliveredReceiptTitle: (targetName: string) => `已送达“${targetName}”`, + mailboxDeliveredReceiptDescription: '目标任务空闲,消息已立即进入处理。', + mailboxQueuedReceiptTitle: (targetName: string) => `已排队给“${targetName}”`, + mailboxQueuedReceiptDescription: '目标任务正在运行,消息将在下一轮处理。', + mailboxFailedReceiptTitle: (targetName: string) => `发送给“${targetName}”失败`, + mailboxFailedReceiptDescription: '消息仍保留在输入框中;按 Enter 重试,或按 Esc 取消。', + mailboxReceiptDismiss: '知道了', + renameUsageTitle: '命令格式不完整', + renameUsageDescription: '请输入 /rename <新名称>。也可以点击窗口顶部的任务名称重命名。', + renameSuccessTitle: '任务已重命名', + renameSuccessDescription: (name: string) => `新名称:${name}`, resumeStartedTitle: '已开始安全恢复', resumeStartedDescription: '正在从最后一个完整执行边界继续', resumeFailedTitle: '恢复失败', @@ -1757,6 +1819,8 @@ const SHELL_COPY_BY_LOCALE = { slashCommands: { compact: { name: 'Compact context', description: 'Compact older history while preserving the current task' }, graph: { name: 'Use Graph', description: 'Inspect, switch, or run Graph once' }, + rename: { name: 'Rename current task', description: '/rename ' }, + send: { name: 'Send to another task', description: 'Choose the receiving task, then write the message' }, side: { name: 'Open side chat', description: 'Start a specific topic in the side panel' }, swarm: { name: 'Use Swarm', description: 'Inspect, switch, or run Swarm once' }, }, @@ -1766,6 +1830,38 @@ const SHELL_COPY_BY_LOCALE = { sideChatContextPendingTitle: 'Resolve pending context first', sideChatContextPendingDescription: 'The Composer still has attachments, quotes, or file mentions. Send or remove them before using /side.', + mailboxFailedTitle: 'Could not send message', + mailboxFailedFallback: 'The message could not be sent to the target task.', + mailboxUsageTitle: 'Incomplete command', + mailboxUsageDescription: 'Enter /send; choose the receiving task, then write the message.', + mailboxPickerTitle: 'Send to which task?', + mailboxPickerPlaceholder: 'Search task names…', + mailboxPickerSearchLabel: 'Search tasks that can receive this message', + mailboxPickerResultsLabel: 'Reachable tasks', + mailboxPickerEmpty: 'There are no other reachable tasks in this project.', + mailboxPickerNoMatch: 'No matching task.', + mailboxPickerHint: 'Shows custom task names, or the system-generated name when unchanged', + mailboxPickerStatus: { + idle: 'Idle · deliver now', + running: 'Running · queue next', + waiting_for_user: 'Waiting · queue next', + }, + mailboxComposerTitle: (targetName: string) => `Send to “${targetName}”`, + mailboxComposerDescription: 'Write in the composer and press Enter to send; press Esc to cancel.', + mailboxComposerCancel: 'Cancel', + mailboxSendingTitle: (targetName: string) => `Sending to “${targetName}”…`, + mailboxSendingDescription: 'Waiting for confirmation from the Runtime Host.', + mailboxDeliveredReceiptTitle: (targetName: string) => `Delivered to “${targetName}”`, + mailboxDeliveredReceiptDescription: 'The target was idle, so the message started immediately.', + mailboxQueuedReceiptTitle: (targetName: string) => `Queued for “${targetName}”`, + mailboxQueuedReceiptDescription: 'The target is running and will process it on the next turn.', + mailboxFailedReceiptTitle: (targetName: string) => `Could not send to “${targetName}”`, + mailboxFailedReceiptDescription: 'The message remains in the composer; press Enter to retry or Esc to cancel.', + mailboxReceiptDismiss: 'Dismiss', + renameUsageTitle: 'Incomplete command', + renameUsageDescription: 'Enter /rename . You can also click the task name in the title bar.', + renameSuccessTitle: 'Task renamed', + renameSuccessDescription: (name: string) => `New name: ${name}`, resumeStartedTitle: 'Safe recovery started', resumeStartedDescription: 'Continuing from the last complete execution boundary', resumeFailedTitle: 'Recovery failed', diff --git a/apps/desktop/src/renderer/session-mailbox-picker.tsx b/apps/desktop/src/renderer/session-mailbox-picker.tsx new file mode 100644 index 0000000000..bb0608cf12 --- /dev/null +++ b/apps/desktop/src/renderer/session-mailbox-picker.tsx @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useMemo, useRef } from 'react'; +import type { SessionMailboxTarget } from '@maka/runtime-host/protocol'; +import { + AstryxLocaleProvider, + CommandPalette, + CommandPaletteFooter, + CommandPaletteInput, + type SearchableItem, + type SearchSource, + useUiLocale, +} from '@maka/ui'; +import { getShellCopy } from './locales/shell-copy'; + +type TargetItem = SearchableItem<{ target: SessionMailboxTarget }>; + +export function SessionMailboxPicker(props: { + targets: readonly SessionMailboxTarget[]; + onOpenChange(open: boolean): void; + onSelect(target: SessionMailboxTarget): void; +}) { + const copy = getShellCopy(useUiLocale()).app; + const pendingTargetRef = useRef(); + const items = useMemo( + () => + props.targets.map((target) => ({ + id: target.sessionId, + label: target.name, + auxiliaryData: { target }, + })), + [props.targets], + ); + const itemById = useMemo(() => new Map(items.map((item) => [item.id, item])), [items]); + const searchSource = useMemo>( + () => ({ + bootstrap: () => items, + search: (query) => { + const normalized = query.trim().toLowerCase(); + if (!normalized) return items; + return items.filter((item) => item.label.toLowerCase().includes(normalized)); + }, + }), + [items], + ); + + const close = (open: boolean) => { + props.onOpenChange(open); + if (open) return; + const target = pendingTargetRef.current; + pendingTargetRef.current = undefined; + if (target) window.requestAnimationFrame(() => props.onSelect(target)); + }; + + return ( + + + )} + emptyBootstrapText={copy.mailboxPickerEmpty} + emptySearchText={copy.mailboxPickerNoMatch} + onValueChange={(itemId) => { + const target = itemById.get(itemId)?.auxiliaryData?.target; + if (!target || pendingTargetRef.current) return; + pendingTargetRef.current = target; + close(false); + }} + renderItem={(item) => { + const target = item.auxiliaryData?.target; + if (!target) return item.label; + return ( + <> + {target.name} + + {copy.mailboxPickerStatus[target.status]} + + + ); + }} + footer={( + {copy.mailboxPickerHint} + )} + /> + + ); +} diff --git a/apps/desktop/src/renderer/styles/chat-message.css b/apps/desktop/src/renderer/styles/chat-message.css index 3df4aa0064..56ffe75543 100644 --- a/apps/desktop/src/renderer/styles/chat-message.css +++ b/apps/desktop/src/renderer/styles/chat-message.css @@ -101,6 +101,67 @@ white-space: pre-wrap; } +/* Cross-task messages are transcript objects, not decorated prompt text. The + source/destination is persistent card chrome while the body stays the only + copyable message content. */ +.maka-session-mailbox-bubble { + min-width: min(360px, 72vw); + max-width: min(620px, 78vw); + overflow: hidden; + padding: 0; + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + background: color-mix(in srgb, var(--chat-user-bg) 74%, var(--background)); + box-shadow: var(--shadow-low); +} + +.maka-session-mailbox-header { + display: flex; + min-width: 0; + align-items: center; + gap: var(--space-1-5); + padding: var(--space-2) var(--space-3); + border-bottom: 1px solid color-mix(in srgb, var(--border) 62%, transparent); + color: var(--muted-foreground); + background: color-mix(in srgb, var(--accent) 9%, transparent); + white-space: nowrap; +} + +.maka-session-mailbox-header[data-direction='outgoing'] { + background: color-mix(in srgb, var(--success) 8%, transparent); +} + +.maka-session-mailbox-eyebrow { + flex: 0 0 auto; + font: var(--maka-text-meta); +} + +.maka-session-mailbox-session-name { + min-width: 0; + overflow: hidden; + color: var(--foreground); + font: var(--maka-text-supporting); + font-weight: var(--font-weight-semibold); + text-overflow: ellipsis; +} + +.maka-session-mailbox-disposition { + flex: 0 0 auto; + margin-left: auto; + color: var(--muted-foreground); + font: var(--maka-text-meta); +} + +.maka-session-mailbox-body { + padding: var(--space-2-5) var(--space-3) var(--space-2); + color: var(--foreground); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.maka-session-mailbox-bubble .maka-message-meta { + padding: 0 var(--space-3) var(--space-2); +} + .maka-chat-message-bubble-assistant { width: 100%; max-width: none; diff --git a/apps/desktop/src/renderer/styles/composer.css b/apps/desktop/src/renderer/styles/composer.css index 43c4f76250..496dc746ff 100644 --- a/apps/desktop/src/renderer/styles/composer.css +++ b/apps/desktop/src/renderer/styles/composer.css @@ -487,6 +487,37 @@ opacity: var(--opacity-disabled); } +/* Cross-Session routing is a durable delivery state, not a transient editor + hint. Give it card geometry so "sending / delivered / queued / failed" stays + legible after the input itself has cleared. */ +.maka-composer-mailbox-notice { + min-height: var(--h-control-lg); + height: auto; + padding: var(--space-2) var(--space-2-5); + border-radius: var(--radius-control); + background: var(--info-wash); + border-color: var(--info-wash-border); + color: var(--foreground); +} + +.maka-composer-mailbox-notice[data-delivery-status='delivered'] { + background: var(--success-wash); + border-color: var(--success-wash-border); +} + +.maka-composer-mailbox-notice[data-delivery-status='delivered'] > svg { + color: var(--success-text); +} + +.maka-composer-mailbox-notice[data-delivery-status='failed'] { + background: var(--destructive-wash); + border-color: var(--destructive-wash-border); +} + +.maka-composer-mailbox-notice[data-delivery-status='failed'] > svg { + color: var(--destructive-text); +} + /* The project picker, last in the composer footer's send-context group. It is rendered only while no session owns the composer — the project is a session-creation parameter. See packages/ui/src/workspace-picker.tsx. */ diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 61d335b547..9cfa04f0bd 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -73,7 +73,11 @@ import type { } from './pi-tui-contracts.js'; import { AUTO_RECAP_DISPLAY_LIMIT_BYTES, shouldAutoRecap } from './session-recap.js'; import type { InvocableSkillEntry } from '@maka/runtime/skill-invocation'; -import type { AgentGraphClientSnapshot, AgentGraphEpochSummary } from '@maka/runtime-host/protocol'; +import type { + AgentGraphClientSnapshot, + AgentGraphEpochSummary, + SessionMailboxTarget, +} from '@maka/runtime-host/protocol'; import type { AgentGraphEpochDirectory } from '@maka/runtime-host/client'; import { MakaSkillHighlightEditor } from './skill-highlight-editor.js'; import { parseGraphCommand, type ParsedGraphCommand } from '@maka/core/graph-command'; @@ -365,6 +369,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { // True while the /session picker is open mid-turn: Escape must close the // overlay, not arm the double-Escape interrupt for the running Turn (#3380). let sessionPickerOverlayOpen = false; + let pendingMailboxTarget: SessionMailboxTarget | undefined; let lastTurnEscapeAt = 0; let lastIdleEscapeAt = 0; let lastIdleCtrlCAt = 0; @@ -1098,6 +1103,32 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { let wizardAttempt = 0; editor.onSubmit = (prompt) => { + const mailboxTarget = pendingMailboxTarget; + if (mailboxTarget && prompt.trim()) { + pendingMailboxTarget = undefined; + editor.addToHistory(prompt); + state.entries.push({ + kind: 'notice', + level: 'info', + text: `Sending message to ${mailboxTarget.name}…`, + }); + requestRender(); + void input.driver + .sendMailboxMessage?.(mailboxTarget.sessionId, prompt.trim()) + .then((result) => { + state.entries.push({ + kind: 'notice', + level: 'info', + text: + result.disposition === 'queued' + ? `Message queued for ${mailboxTarget.name}.` + : `Message delivered to ${mailboxTarget.name}.`, + }); + requestRender(); + }) + .catch(reportError); + return; + } if (turnRunning) { // A quit/exit form typed while a turn is running must close the TUI, not // steer it into the model as prompt text (review finding on turnRunning @@ -3239,6 +3270,75 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { void goToSession(sessionId); }, }, + send: { + description: primaryGuidance.commands.send, + // This targets another Session and never mutates or steers the active + // Turn in the current Session, so it remains safe while that Turn runs. + midTurn: 'local', + run: (parts: string[]) => { + if (parts.length !== 1) { + state.entries.push({ + kind: 'notice', + level: 'error', + text: 'Usage: /send', + }); + requestRender(); + return; + } + if (!input.driver.listMailboxTargets || !input.driver.sendMailboxMessage) { + state.entries.push({ + kind: 'notice', + level: 'error', + text: 'Session messaging is unavailable on this runtime.', + }); + requestRender(); + return; + } + void input.driver + .listMailboxTargets() + .then((targets) => { + if (targets.length === 0) { + state.entries.push({ + kind: 'notice', + level: 'info', + text: 'No other reachable Sessions in this project.', + }); + requestRender(); + return; + } + showSelectPicker( + 'Send to Session', + 'Choose Session', + targets.map((target) => ({ + value: target.sessionId, + label: target.name, + description: + target.status === 'idle' + ? 'idle · deliver now' + : `${target.status.replaceAll('_', ' ')} · queue for next turn`, + })), + (item) => { + const target = targets.find((candidate) => candidate.sessionId === item.value); + if (!target) return; + pendingMailboxTarget = target; + editor.setText(''); + state.entries.push({ + kind: 'notice', + level: 'info', + text: `Sending the next message to ${target.name}. Type it and press Enter.`, + }); + requestRender(); + }, + { + minPrimaryColumnWidth: 24, + maxPrimaryColumnWidth: 56, + hint: '↑↓ move · Enter choose · Esc cancel', + }, + ); + }) + .catch(reportError); + }, + }, graph: { description: primaryGuidance.commands.graph, // parseGraphCommand answers status and refuses changes ahead of generic @@ -3336,6 +3436,21 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { return { consume: true }; } if (tui.hasOverlay()) return undefined; + if ( + pendingMailboxTarget + && matchesKey(data, Key.escape) + && editor.getText().length === 0 + ) { + const targetName = pendingMailboxTarget.name; + pendingMailboxTarget = undefined; + state.entries.push({ + kind: 'notice', + level: 'info', + text: `Cancelled message to ${targetName}.`, + }); + requestRender(); + return { consume: true }; + } const pendingSandboxBoundary = activeSandboxBoundaryRequest(state); if (pendingSandboxBoundary && !matchesKey(data, Key.ctrl('c'))) { if ( diff --git a/packages/cli/src/runtime-host-session-driver.ts b/packages/cli/src/runtime-host-session-driver.ts index 6b7cf0f2a8..0f35bec142 100644 --- a/packages/cli/src/runtime-host-session-driver.ts +++ b/packages/cli/src/runtime-host-session-driver.ts @@ -256,6 +256,22 @@ class RuntimeHostMakaSessionDriverImpl implements RuntimeHostMakaSessionDriver { .map(({ session }) => session); } + async listMailboxTargets() { + const sourceSessionId = this.#requireSession('list Session message targets'); + return (await this.#request('session.mailbox.targets', { sourceSessionId })).targets; + } + + async sendMailboxMessage(targetSessionId: string, text: string) { + const sourceSessionId = this.#requireSession('send a Session message'); + return this.#request('session.mailbox.send', { + sourceSessionId, + targetSessionId, + messageId: this.#newId(), + kind: 'request', + text, + }); + } + getSessionResumeAvailability(session: SessionSummary): Promise { return inspectRuntimeHostSessionResumeAvailability(session, this.#executionLocation); } diff --git a/packages/cli/src/session-driver.ts b/packages/cli/src/session-driver.ts index 42e179efab..9d98aab97b 100644 --- a/packages/cli/src/session-driver.ts +++ b/packages/cli/src/session-driver.ts @@ -28,7 +28,12 @@ import type { CreateSessionInput, TurnOrchestration } from '@maka/core/runtime-i import type { UserQuestionResponse } from '@maka/core/user-question'; import type { ContextDiagnostics } from '@maka/runtime/context-diagnostics'; import type { SkillInvocationResult } from '@maka/core/skill-invocation'; -import type { GoalControlAction, GoalProjection } from '@maka/runtime-host/protocol'; +import type { + GoalControlAction, + GoalProjection, + SessionMailboxSendResult, + SessionMailboxTarget, +} from '@maka/runtime-host/protocol'; export interface MakaSessionMoveResult { previousCwd: string; @@ -90,6 +95,8 @@ export class SkillInvocationBlockedError extends Error { export interface MakaSessionDriver { listSessions(): Promise; + listMailboxTargets?(): Promise; + sendMailboxMessage?(targetSessionId: string, text: string): Promise; getSessionResumeAvailability?(session: SessionSummary): Promise; preparePrompt( prompt: string, diff --git a/packages/cli/src/tui-primary-guidance.ts b/packages/cli/src/tui-primary-guidance.ts index 28c1621a99..a0edc72cc4 100644 --- a/packages/cli/src/tui-primary-guidance.ts +++ b/packages/cli/src/tui-primary-guidance.ts @@ -64,6 +64,7 @@ const TUI_PRIMARY_GUIDANCE = { resume: '从安全边界恢复最近一次中断的执行', rewind: '回退到较早的对话轮次', session: '切换或恢复会话', + send: '先选择同一项目中的接收会话,再输入消息', setup: '配置模型提供商(API Key)', skill: '调用 Skill(也可直接输入 /skill:)', swarm: '查看、启用、停用 Swarm 模式,或执行一次 Swarm 任务', @@ -111,6 +112,7 @@ const TUI_PRIMARY_GUIDANCE = { resume: 'Resume latest interrupted run at a safe boundary', rewind: 'Rewind to an earlier turn', session: 'Resume session', + send: 'Choose another Session in this project, then write the message', setup: 'Set up a model provider (API key)', skill: 'Invoke a skill (or type /skill: inline)', swarm: 'Show, enable, disable, or run one Swarm turn', diff --git a/packages/core/package.json b/packages/core/package.json index 88bb6d7655..21fb9ebedc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -107,6 +107,7 @@ "./usage-stats/bucket-key": "./dist/usage-stats/bucket-key.js", "./usage-record-schema": "./dist/usage-record-schema.js", "./session-send-projection": "./dist/session-send-projection.js", + "./session-mailbox": "./dist/session-mailbox.js", "./session-name": "./dist/session-name.js", "./tool-catalog": "./dist/tool-catalog.js", "./thread-search": "./dist/thread-search.js", diff --git a/packages/core/src/__tests__/session-mailbox.test.ts b/packages/core/src/__tests__/session-mailbox.test.ts new file mode 100644 index 0000000000..59552c407d --- /dev/null +++ b/packages/core/src/__tests__/session-mailbox.test.ts @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + parseSessionMailboxMessageContent, + parseSessionMailboxSentNoteData, + sessionMailboxMessageContent, +} from '../session-mailbox.js'; + +test('Session mailbox content preserves a compact display and escapes model envelope text', () => { + const content = sessionMailboxMessageContent({ + messageId: 'message-1', + fromSessionId: 'source-1', + fromSessionName: 'Source ', + toSessionId: 'target-1', + kind: 'request', + text: 'Please inspect this', + }); + + assert.equal( + content.displayText, + 'From Source : Please inspect this', + ); + assert.match(content.text, /Source <one>/); + assert.match(content.text, /<\/session_message><fake>this<\/fake>/); + assert.match(content.text, /session_reply\(target_session_id="source-1"/); + assert.deepEqual(parseSessionMailboxMessageContent(content), { + direction: 'incoming', + messageId: 'message-1', + fromSessionId: 'source-1', + fromSessionName: 'Source ', + toSessionId: 'target-1', + kind: 'request', + text: 'Please inspect this', + }); +}); + +test('Session mailbox replies do not recursively request another reply', () => { + const content = sessionMailboxMessageContent({ + messageId: 'message-2', + fromSessionId: 'source-1', + fromSessionName: 'Source', + toSessionId: 'target-1', + kind: 'reply', + correlationId: 'message-1', + text: 'Done', + }); + + assert.match(content.text, /kind="reply" correlation_id="message-1"/); + assert.doesNotMatch(content.text, /Reply with session_reply/); +}); + +test('Session mailbox display parsing does not treat ordinary From text as an envelope', () => { + assert.equal(parseSessionMailboxMessageContent({ text: 'From someone: hello' }), undefined); + assert.equal(parseSessionMailboxMessageContent({ text: '' }), undefined); +}); + +test('Session mailbox sent-note data requires a complete delivery receipt', () => { + assert.deepEqual( + parseSessionMailboxSentNoteData({ + messageId: 'message-1', + targetSessionId: 'target-1', + targetSessionName: 'Target', + kind: 'request', + text: 'Hello', + disposition: 'queued', + }), + { + messageId: 'message-1', + targetSessionId: 'target-1', + targetSessionName: 'Target', + kind: 'request', + text: 'Hello', + disposition: 'queued', + }, + ); + assert.equal(parseSessionMailboxSentNoteData({ messageId: 'message-1' }), undefined); +}); diff --git a/packages/core/src/session-mailbox.ts b/packages/core/src/session-mailbox.ts new file mode 100644 index 0000000000..9041b0d3f6 --- /dev/null +++ b/packages/core/src/session-mailbox.ts @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { MessageContent } from './events.js'; + +export const SESSION_MAILBOX_TEXT_MAX_BYTES = 16_000; +export const SESSION_MAILBOX_KINDS = ['request', 'reply', 'notification'] as const; +export type SessionMailboxKind = (typeof SESSION_MAILBOX_KINDS)[number]; + +export interface SessionMailboxEnvelope { + readonly messageId: string; + readonly fromSessionId: string; + readonly fromSessionName: string; + readonly toSessionId: string; + readonly kind: SessionMailboxKind; + readonly text: string; + readonly correlationId?: string; +} + +export interface SessionMailboxIncomingDisplay { + readonly direction: 'incoming'; + readonly messageId: string; + readonly fromSessionId: string; + readonly fromSessionName: string; + readonly toSessionId: string; + readonly kind: SessionMailboxKind; + readonly text: string; + readonly correlationId?: string; +} + +export interface SessionMailboxSentNoteData { + readonly messageId: string; + readonly targetSessionId: string; + readonly targetSessionName: string; + readonly kind: SessionMailboxKind; + readonly text: string; + readonly disposition: 'turn_started' | 'queued'; +} + +export function sessionMailboxSentReceiptId(messageId: string): string { + return `session-mailbox-sent:${messageId}`; +} + +/** + * Session messages are ordinary canonical user messages at the execution + * boundary. The model-facing text carries an explicit Host-authored + * envelope while the human-facing transcript stays compact. + */ +export function sessionMailboxMessageContent(envelope: SessionMailboxEnvelope): MessageContent { + const attributes = [ + `message_id="${escapeAttribute(envelope.messageId)}"`, + `from_session_id="${escapeAttribute(envelope.fromSessionId)}"`, + `from_session_name="${escapeAttribute(envelope.fromSessionName)}"`, + `to_session_id="${escapeAttribute(envelope.toSessionId)}"`, + `kind="${envelope.kind}"`, + ...(envelope.correlationId + ? [`correlation_id="${escapeAttribute(envelope.correlationId)}"`] + : []), + ].join(' '); + const replyInstruction = + envelope.kind === 'request' + ? `\nReply with session_reply(target_session_id=\"${envelope.fromSessionId}\", in_reply_to=\"${envelope.messageId}\", text=...).` + : ''; + return { + text: [ + ``, + `From session \"${escapeText(envelope.fromSessionName)}\":`, + escapeText(envelope.text), + replyInstruction, + '', + ].join('\n'), + displayText: `From ${envelope.fromSessionName}: ${envelope.text}`, + inlineReferences: [], + }; +} + +/** + * Recovers the display fields from the exact Host-authored envelope. This is + * deliberately stricter than looking for a human-readable `From:` prefix, so + * ordinary user messages can never accidentally turn into mailbox cards. + */ +export function parseSessionMailboxMessageContent( + content: Pick, +): SessionMailboxIncomingDisplay | undefined { + const opening = content.text.match(/^\n]+)>\n/); + if (!opening || !content.text.endsWith('\n')) return undefined; + const attributes = parseAttributes(opening[1] ?? ''); + const messageId = attributes.message_id; + const fromSessionId = attributes.from_session_id; + const toSessionId = attributes.to_session_id; + const kind = attributes.kind; + if ( + !messageId || + !fromSessionId || + !toSessionId || + !SESSION_MAILBOX_KINDS.includes(kind as SessionMailboxKind) + ) { + return undefined; + } + + const inner = content.text.slice(opening[0].length, -'\n'.length); + const firstLineEnd = inner.indexOf('\n'); + if (firstLineEnd < 0) return undefined; + const sourceLine = inner.slice(0, firstLineEnd); + const legacySource = sourceLine.match(/^From session "([\s\S]*)":$/); + const fromSessionName = attributes.from_session_name ?? legacySource?.[1]; + if (fromSessionName === undefined) return undefined; + + let body = inner.slice(firstLineEnd + 1); + if (kind === 'request') { + const replyInstruction = + `\n\nReply with session_reply(target_session_id="${fromSessionId}", ` + + `in_reply_to="${messageId}", text=...).`; + if (!body.endsWith(replyInstruction)) return undefined; + body = body.slice(0, -replyInstruction.length); + } + + return { + direction: 'incoming', + messageId: unescapeText(messageId), + fromSessionId: unescapeText(fromSessionId), + fromSessionName: unescapeText(fromSessionName), + toSessionId: unescapeText(toSessionId), + kind: kind as SessionMailboxKind, + text: unescapeText(body), + ...(attributes.correlation_id + ? { correlationId: unescapeText(attributes.correlation_id) } + : {}), + }; +} + +export function parseSessionMailboxSentNoteData( + value: unknown, +): SessionMailboxSentNoteData | undefined { + if (!isRecord(value)) return undefined; + if ( + typeof value.messageId !== 'string' || + typeof value.targetSessionId !== 'string' || + typeof value.targetSessionName !== 'string' || + typeof value.text !== 'string' || + !SESSION_MAILBOX_KINDS.includes(value.kind as SessionMailboxKind) || + (value.disposition !== 'turn_started' && value.disposition !== 'queued') + ) { + return undefined; + } + return { + messageId: value.messageId, + targetSessionId: value.targetSessionId, + targetSessionName: value.targetSessionName, + kind: value.kind as SessionMailboxKind, + text: value.text, + disposition: value.disposition, + }; +} + +function escapeAttribute(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); +} + +function escapeText(value: string): string { + return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); +} + +function parseAttributes(value: string): Record { + const attributes: Record = {}; + for (const match of value.matchAll(/(?:^| )([a-z_]+)="([^"]*)"/g)) { + const key = match[1]; + const attributeValue = match[2]; + if (key !== undefined && attributeValue !== undefined) attributes[key] = attributeValue; + } + return attributes; +} + +function unescapeText(value: string): string { + return value + .replaceAll('"', '"') + .replaceAll('>', '>') + .replaceAll('<', '<') + .replaceAll('&', '&'); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index b1dd1e694c..baf325804b 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -923,6 +923,7 @@ export interface SystemNoteMessage { | 'context_compacted' | 'context_compaction_failed_open' | 'step_limit' + | 'session_mailbox_sent' | 'error' | 'abort'; /** Shape depends on `kind`. */ @@ -1026,6 +1027,7 @@ const SYSTEM_NOTE_KINDS = new Set([ 'context_compacted', 'context_compaction_failed_open', 'step_limit', + 'session_mailbox_sent', 'error', 'abort', ]); diff --git a/packages/core/src/slash-command-catalog.ts b/packages/core/src/slash-command-catalog.ts index 211eea51bd..f033b7e0df 100644 --- a/packages/core/src/slash-command-catalog.ts +++ b/packages/core/src/slash-command-catalog.ts @@ -39,10 +39,11 @@ export const SLASH_COMMAND_CATALOG = [ { id: 'new', session: 'none', surfaces: ['tui'] }, { id: 'permissions', session: 'required', surfaces: ['tui'] }, { id: 'recap', session: 'required', surfaces: ['tui'] }, - { id: 'rename', session: 'required', surfaces: ['tui'] }, + { id: 'rename', session: 'required', surfaces: ['desktop', 'tui'] }, { id: 'resume', session: 'required', surfaces: ['tui'] }, { id: 'rewind', session: 'required', surfaces: ['tui'] }, { id: 'session', session: 'none', surfaces: ['tui'] }, + { id: 'send', session: 'required', surfaces: ['desktop', 'tui'] }, { id: 'setup', session: 'none', surfaces: ['tui'] }, { id: 'side', session: 'required', surfaces: ['desktop'] }, { id: 'skill', session: 'required', surfaces: ['tui'] }, diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts index c4f2fad83b..2e942fe4a7 100644 --- a/packages/runtime-host/src/__tests__/protocol.test.ts +++ b/packages/runtime-host/src/__tests__/protocol.test.ts @@ -189,6 +189,12 @@ describe('Runtime Host bootstrap protocol', () => { assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 47); }); + test('publishes a new compatibility epoch for Session mailbox operations', () => { + // Epoch 48 peers reject the two new closed operation keys, so they must + // fail the handshake before either side attempts Session messaging. + assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 48); + }); + test('adds credential rotation without changing existing credential inputs', () => { const issueInput = { principalKind: 'remote_owner', @@ -1104,6 +1110,46 @@ describe('Runtime Host bootstrap protocol', () => { ); }); + test('keeps Session mailbox discovery and delivery closed and bounded', () => { + const targets = { + requestId: 'mailbox-targets-1', + operation: 'session.mailbox.targets' as const, + input: { sourceSessionId: 'session-1' }, + }; + const send = { + requestId: 'mailbox-send-1', + operation: 'session.mailbox.send' as const, + input: { + sourceSessionId: 'session-1', + targetSessionId: 'session-2', + messageId: 'message-1', + kind: 'request' as const, + text: 'Please inspect this', + }, + }; + assert.deepEqual(decodeClientFrame(targets), targets); + assert.deepEqual(decodeClientFrame(send), send); + assert.throws( + () => decodeClientFrame({ ...send, input: { ...send.input, placement: 'current_turn' } }), + isInvalidFrame, + ); + assert.throws( + () => + decodeHostFrame({ + requestId: send.requestId, + operation: send.operation, + ok: true, + result: { + messageId: 'message-1', + targetSessionId: 'session-2', + disposition: 'queued', + turnId: 'turn-invalid', + }, + }), + isInvalidFrame, + ); + }); + test('decodes old-Epoch ambiguity only for operations that declare outcome_unknown', () => { const response = { requestId: 'submit-old-epoch', diff --git a/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts new file mode 100644 index 0000000000..965ef67b0e --- /dev/null +++ b/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { SessionSummary, StoredMessage } from '@maka/core/session'; +import type { HostMessageCoordinator } from '../server/message-coordinator.js'; +import type { ConnectionContext } from '../server/operation-dispatcher.js'; +import { HostSessionMailboxCoordinator } from '../server/session-mailbox-coordinator.js'; + +test('Session mailbox exposes only ordinary Sessions in the source workspace', async () => { + const coordinator = mailbox([ + session('source', '/one', { projectId: 'project-1' }), + session('target', '/one', { projectId: 'project-1', runningTurnIds: ['turn-live'] }), + session('other-project', '/one', { projectId: 'project-2' }), + session('side', '/one', { projectId: 'project-1', labels: ['mode:side_conversation'] }), + session('child', '/one', { + projectId: 'project-1', + subagentParent: { + kind: 'subagent', + parentSessionId: 'source', + spawnedBy: { parentRunId: 'run-1', parentTurnId: 'turn-1', toolCallId: 'call-1' }, + lifecycle: 'foreground', + }, + }), + ]); + + const outcome = await coordinator.handlers['session.mailbox.targets']( + { sourceSessionId: 'source' }, + connection(), + ); + + assert.deepEqual(outcome, { + ok: true, + result: { + targets: [{ sessionId: 'target', name: 'target', status: 'running' }], + }, + }); +}); + +test('Session mailbox submits a next-turn message with Host provenance', async () => { + let submitted: unknown; + const stored: StoredMessage[] = [{ + type: 'system_note', + id: 'start', + turnId: 'source-turn', + ts: 1, + kind: 'session_start', + }]; + const coordinator = mailbox( + [session('source', '/one'), session('target', '/one')], + async (input) => { + submitted = input; + return { ok: true, result: { disposition: 'turn_started', turnId: 'turn-2' } }; + }, + stored, + ); + + const outcome = await coordinator.handlers['session.mailbox.send']( + { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'message-1', + kind: 'request', + text: 'Please inspect this', + }, + connection(), + ); + + assert.deepEqual(outcome, { + ok: true, + result: { + messageId: 'message-1', + targetSessionId: 'target', + disposition: 'turn_started', + turnId: 'turn-2', + }, + }); + assert.deepEqual(submitted, { + originHostEpoch: 'epoch-1', + sessionId: 'target', + messageId: 'message-1', + content: { + text: [ + '', + 'From session "source":', + 'Please inspect this', + '', + 'Reply with session_reply(target_session_id="source", in_reply_to="message-1", text=...).', + '', + ].join('\n'), + displayText: 'From source: Please inspect this', + inlineReferences: [], + }, + placement: 'next_turn', + }); + assert.deepEqual(stored.at(-1), { + type: 'system_note', + id: 'session-mailbox-sent:message-1', + turnId: 'source-turn', + ts: 123, + kind: 'session_mailbox_sent', + data: { + messageId: 'message-1', + targetSessionId: 'target', + targetSessionName: 'target', + kind: 'request', + text: 'Please inspect this', + disposition: 'turn_started', + }, + }); +}); + +test('Session mailbox rejects self-send and cross-workspace targets', async () => { + const coordinator = mailbox([session('source', '/one'), session('target', '/two')]); + const self = await coordinator.handlers['session.mailbox.send']( + { + sourceSessionId: 'source', + targetSessionId: 'source', + messageId: 'message-1', + kind: 'notification', + text: 'No', + }, + connection(), + ); + const crossWorkspace = await coordinator.handlers['session.mailbox.send']( + { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'message-2', + kind: 'notification', + text: 'No', + }, + connection(), + ); + + assert.equal(self.ok, false); + if (!self.ok) assert.equal(self.error.code, 'invalid_request'); + assert.equal(crossWorkspace.ok, false); + if (!crossWorkspace.ok) assert.equal(crossWorkspace.error.code, 'not_found'); +}); + +function mailbox( + sessions: SessionSummary[], + submit: (input: unknown) => Promise = async () => ({ + ok: true, + result: { disposition: 'followup', queueRevision: 1 }, + }), + stored: StoredMessage[] = [], +): HostSessionMailboxCoordinator { + const messages = { + handlers: { 'turn.message.submit': submit }, + } as unknown as Pick; + return new HostSessionMailboxCoordinator({ + hostEpoch: 'epoch-1', + messages, + listSessions: async () => sessions, + sessionStore: { + readMessagesSnapshot: async () => stored, + appendMessage: async (_sessionId, message) => { + stored.push(message); + }, + }, + createId: () => 'message-generated', + now: () => 123, + }); +} + +function session(id: string, cwd: string, overrides: Partial = {}): SessionSummary { + return { + id, + cwd, + name: id, + isFlagged: false, + isArchived: false, + labels: [], + hasUnread: false, + status: 'active', + backend: 'ai-sdk', + llmConnectionSlug: 'connection', + connectionLocked: true, + model: 'model', + permissionMode: 'ask', + ...overrides, + }; +} + +function connection(): ConnectionContext { + return { + hostEpoch: 'epoch-1', + connectionId: 'connection-1', + principal: 'owner', + acquireResidency: () => ({ release: () => undefined }), + }; +} diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index 19d5dd1f5b..32b3bad38a 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -74,6 +74,7 @@ export * from './project-catalog.js'; export * from './project-catalog-change.js'; export * from './execution-inspect.js'; export * from './external-session.js'; +export * from './session-mailbox.js'; export * from './message.js'; export * from './operations.js'; export * from './runtime-resource.js'; @@ -91,7 +92,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const; export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const; // Increment when the same protocol version no longer guarantees safe Client-Host // interoperability. Mismatches are rejected before domain commands are admitted. -export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 48 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 49 as const; +// 49: Session mailbox target discovery and durable cross-Session delivery. +// Epoch-48 peers do not know these closed operation keys. // 48: Session branch creation accepts an explicit Side Conversation intent. // Older peers reject the strict input shape or cannot apply its snapshot semantics. // 47: Project registration can carry an explicit location preference. Epoch-46 diff --git a/packages/runtime-host/src/protocol/operations.ts b/packages/runtime-host/src/protocol/operations.ts index fc83a8eba4..de2afb4252 100644 --- a/packages/runtime-host/src/protocol/operations.ts +++ b/packages/runtime-host/src/protocol/operations.ts @@ -51,6 +51,7 @@ import { RUNTIME_RESOURCE_OPERATION_SPECS } from './runtime-resource.js'; import { SCHEDULED_TASK_OPERATION_SPECS } from './scheduled-task.js'; import { SESSION_CATALOG_OPERATION_SPECS } from './session-catalog.js'; import { SESSION_CONTINUITY_OPERATION_SPECS } from './session-continuity.js'; +import { SESSION_MAILBOX_OPERATION_SPECS } from './session-mailbox.js'; import { SESSION_TRANSCRIPT_OPERATION_SPECS } from './session-transcript.js'; import { SESSION_TURNS_OPERATION_SPECS } from './session-turns.js'; import { SESSION_REVISION_OPERATION_SPECS } from './session-revision.js'; @@ -164,6 +165,7 @@ export * from './runtime-policy.js'; export * from './runtime-resource.js'; export * from './scheduled-task.js'; export * from './session-catalog.js'; +export * from './session-mailbox.js'; export * from './session-revision.js'; export * from './session-retirement.js'; export * from './session-transcript.js'; @@ -199,6 +201,7 @@ export const HOST_OPERATION_SPECS = composeOperationSpecMaps( SESSION_TRANSCRIPT_OPERATION_SPECS, SESSION_TURNS_OPERATION_SPECS, SESSION_CATALOG_OPERATION_SPECS, + SESSION_MAILBOX_OPERATION_SPECS, SESSION_EFFECT_OPERATION_SPECS, SESSION_REVISION_OPERATION_SPECS, SESSION_RETIREMENT_OPERATION_SPECS, @@ -295,6 +298,8 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([ 'session.create', 'session.execution_boundary.query', 'session.lifecycle.set', + 'session.mailbox.send', + 'session.mailbox.targets', 'session.metadata.update', 'session.read_marker.set', 'session.recap.generate', diff --git a/packages/runtime-host/src/protocol/session-mailbox.ts b/packages/runtime-host/src/protocol/session-mailbox.ts new file mode 100644 index 0000000000..a1559ccf33 --- /dev/null +++ b/packages/runtime-host/src/protocol/session-mailbox.ts @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + SESSION_MAILBOX_KINDS, + SESSION_MAILBOX_TEXT_MAX_BYTES, + type SessionMailboxKind, +} from '@maka/core/session-mailbox'; +import { + assertAllowedKeys, + requireEncodedByteLimit, + requireEntityId, + requireExactRecord, + requireRecord, + requireUtf8String, +} from './codec.js'; +import { invalidProtocolFrame } from './errors.js'; +import { defineOperation } from './operation-spec.js'; + +export const SESSION_MAILBOX_TARGET_MAX_ITEMS = 64; +export const SESSION_MAILBOX_RESULT_MAX_BYTES = 64 * 1024; + +export interface SessionMailboxTarget { + readonly sessionId: string; + readonly name: string; + readonly status: 'idle' | 'running' | 'waiting_for_user'; +} + +export interface SessionMailboxTargetsInput { + readonly sourceSessionId: string; +} + +export interface SessionMailboxTargetsResult { + readonly targets: readonly SessionMailboxTarget[]; +} + +export interface SessionMailboxSendInput { + readonly sourceSessionId: string; + readonly targetSessionId: string; + readonly messageId: string; + readonly kind: SessionMailboxKind; + readonly text: string; + readonly correlationId?: string; +} + +export interface SessionMailboxSendResult { + readonly messageId: string; + readonly targetSessionId: string; + readonly disposition: 'turn_started' | 'queued'; + readonly turnId?: string; +} + +const ERRORS = [ + 'host_not_ready', + 'host_draining', + 'operation_unavailable', + 'not_found', + 'session_archived', + 'session_busy', + 'operation_conflict', + 'invalid_request', + 'outcome_unknown', + 'internal_failure', +] as const; + +export const SESSION_MAILBOX_OPERATION_SPECS = { + 'session.mailbox.targets': defineOperation({ + mode: 'query', + availability: 'ready', + errors: ERRORS, + decodeInput: decodeTargetsInput, + decodeOutput: decodeTargetsResult, + }), + 'session.mailbox.send': defineOperation({ + mode: 'command', + availability: 'ready', + errors: ERRORS, + decodeInput: decodeSendInput, + decodeOutput: decodeSendResult, + }), +} as const; + +function decodeTargetsInput(value: unknown): SessionMailboxTargetsInput { + const record = requireExactRecord(value, 'session.mailbox.targets input', ['sourceSessionId']); + return { + sourceSessionId: requireEntityId(record.sourceSessionId, 'sourceSessionId'), + }; +} + +function decodeTargetsResult(value: unknown): SessionMailboxTargetsResult { + const record = requireExactRecord(value, 'session.mailbox.targets result', ['targets']); + if (!Array.isArray(record.targets) || record.targets.length > SESSION_MAILBOX_TARGET_MAX_ITEMS) { + throw invalidProtocolFrame('Invalid Session mailbox targets'); + } + const targets = record.targets.map((value) => { + const target = requireExactRecord(value, 'Session mailbox target', [ + 'sessionId', + 'name', + 'status', + ]); + if ( + target.status !== 'idle' && + target.status !== 'running' && + target.status !== 'waiting_for_user' + ) { + throw invalidProtocolFrame('Invalid Session mailbox target status'); + } + return { + sessionId: requireEntityId(target.sessionId, 'target sessionId'), + name: requireUtf8String(target.name, 'target name', 320), + status: target.status as SessionMailboxTarget['status'], + }; + }); + const result = { targets }; + requireEncodedByteLimit( + result, + 'session.mailbox.targets result', + SESSION_MAILBOX_RESULT_MAX_BYTES, + ); + return result; +} + +function decodeSendInput(value: unknown): SessionMailboxSendInput { + const record = requireRecord(value, 'session.mailbox.send input'); + assertAllowedKeys(record, 'session.mailbox.send input', [ + 'sourceSessionId', + 'targetSessionId', + 'messageId', + 'kind', + 'text', + 'correlationId', + ]); + if (!SESSION_MAILBOX_KINDS.includes(record.kind as SessionMailboxKind)) { + throw invalidProtocolFrame('Invalid Session mailbox message kind'); + } + return { + sourceSessionId: requireEntityId(record.sourceSessionId, 'sourceSessionId'), + targetSessionId: requireEntityId(record.targetSessionId, 'targetSessionId'), + messageId: requireEntityId(record.messageId, 'messageId'), + kind: record.kind as SessionMailboxKind, + text: requireUtf8String(record.text, 'mailbox text', SESSION_MAILBOX_TEXT_MAX_BYTES), + ...(record.correlationId === undefined + ? {} + : { + correlationId: requireEntityId(record.correlationId, 'correlationId'), + }), + }; +} + +function decodeSendResult(value: unknown): SessionMailboxSendResult { + const record = requireRecord(value, 'session.mailbox.send result'); + assertAllowedKeys(record, 'session.mailbox.send result', [ + 'messageId', + 'targetSessionId', + 'disposition', + 'turnId', + ]); + if (record.disposition !== 'turn_started' && record.disposition !== 'queued') { + throw invalidProtocolFrame('Invalid Session mailbox disposition'); + } + if ( + (record.disposition === 'turn_started' && record.turnId === undefined) || + (record.disposition === 'queued' && record.turnId !== undefined) + ) { + throw invalidProtocolFrame('Invalid Session mailbox Turn identity'); + } + const result = { + messageId: requireEntityId(record.messageId, 'messageId'), + targetSessionId: requireEntityId(record.targetSessionId, 'targetSessionId'), + disposition: record.disposition, + ...(record.turnId === undefined ? {} : { turnId: requireEntityId(record.turnId, 'turnId') }), + }; + return result as SessionMailboxSendResult; +} diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index 639a8beb7e..2497c48dbd 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -38,6 +38,7 @@ import { import { buildToolsForAgentDefinition } from '@maka/runtime/agent-catalog'; import { buildHostCapabilitiesFromBinding } from '@maka/runtime/tool-catalog-derive'; import { buildHistoryTools } from '@maka/runtime/history-tools'; +import { buildSessionMailboxTools } from '@maka/runtime/session-mailbox-tools'; import { createLocalContinuationSafetyInspector } from '@maka/runtime/continuation-safety'; import { createConfiguredSubagentCatalog } from '@maka/runtime/configured-subagent-catalog'; import { @@ -128,6 +129,7 @@ import { } from './host-composition.js'; import { HostInteractionCoordinator } from './interaction-coordinator.js'; import { HostInteractiveTurnCoordinator } from './interactive-turn-coordinator.js'; +import { HostSessionMailboxCoordinator } from './session-mailbox-coordinator.js'; import { ensureBootstrapRuntimePolicy } from './bootstrap-runtime-policy.js'; import { hostedExecutionRunProfile } from './hosted-execution-tool-profile.js'; import { HostMemoryCoordinator } from './memory-coordinator.js'; @@ -279,6 +281,7 @@ export async function createExecutionRuntimeHostComposition( let runtimeResources: HostRuntimeResourceCoordinator | undefined; let continuity: SessionContinuityCoordinator | undefined; let manager: SessionManager | undefined; + let sessionMailbox: HostSessionMailboxCoordinator | undefined; let graphCoordinator: AgentGraphCoordinator | undefined; let graphSupervisorWake: AgentGraphSupervisorWakeCoordinator | undefined; const graphWakeActivities = new SessionActivityRegistry(); @@ -378,7 +381,11 @@ export async function createExecutionRuntimeHostComposition( createHostWebFetchToolFromService(webFetchService), ...runtimePolicy.modelTools, ]; - const hostTools = [...childHostTools, ...historyTools]; + const sessionMailboxTools = buildSessionMailboxTools({ + list: (sourceSessionId) => requireSessionMailbox(sessionMailbox).listTargets(sourceSessionId), + send: (input) => requireSessionMailbox(sessionMailbox).sendFromSession(input), + }); + const hostTools = [...childHostTools, ...historyTools, ...sessionMailboxTools]; const childAgentTools = createHostChildAgentToolComposition({ taskLedger, builtinTools, @@ -444,7 +451,9 @@ export async function createExecutionRuntimeHostComposition( const projects = new HostProjectCatalogCoordinator( openedProjectCatalog, { publish: () => hostChanges.publishProjectCatalog() }, - { publish: (sessionId: string) => hostChanges.publishSessionCatalog(sessionId) }, + { + publish: (sessionId: string) => hostChanges.publishSessionCatalog(sessionId), + }, projectMembership, context.requestDrain, new HostProjectDirectoryAuthority(options.projectDirectoryRoots), @@ -490,6 +499,12 @@ export async function createExecutionRuntimeHostComposition( onProjectionChanged: (sessionId) => requireContinuity(continuity).enqueueCanonicalRefresh(sessionId), }); + sessionMailbox = new HostSessionMailboxCoordinator({ + hostEpoch: context.hostEpoch, + messages, + listSessions: () => requireSessionManager(manager).listSessions(), + sessionStore: stores.sessionStore, + }); const rootAdmissionOwner = new RootAdmissionOwner(stores.agentRunStore); const canonicalProjectionReader = new CanonicalSessionProjectionReader({ stores, @@ -1465,6 +1480,7 @@ export async function createExecutionRuntimeHostComposition( handlers: [ executionInspect.handlers, messages.handlers, + requireSessionMailbox(sessionMailbox).handlers, interactions.handlers, sessionEffectCoordinator.handlers, continuityCoordinator.handlers, @@ -1663,7 +1679,10 @@ function adaptManagedWorkspaceFilesystemWorker( async execute(input) { // Read-only operations never participate in CAS; the adapter says so // explicitly (#3484) instead of relying on an absent optional field. - const result = await worker.execute({ ...input, expectedIdentity: 'unchecked' }); + const result = await worker.execute({ + ...input, + expectedIdentity: 'unchecked', + }); switch (result.kind) { case 'read': case 'read_image': @@ -1769,6 +1788,13 @@ function requireSessionManager(manager: SessionManager | undefined): SessionMana return manager; } +function requireSessionMailbox( + coordinator: HostSessionMailboxCoordinator | undefined, +): HostSessionMailboxCoordinator { + if (!coordinator) throw new Error('Runtime Host Session mailbox coordinator is not composed'); + return coordinator; +} + function requireGraphCoordinator( coordinator: AgentGraphCoordinator | undefined, ): AgentGraphCoordinator { diff --git a/packages/runtime-host/src/server/operation-dispatcher.ts b/packages/runtime-host/src/server/operation-dispatcher.ts index 70a705555a..008cb7df91 100644 --- a/packages/runtime-host/src/server/operation-dispatcher.ts +++ b/packages/runtime-host/src/server/operation-dispatcher.ts @@ -115,12 +115,14 @@ export type SessionRetirementOperationKey = Extract< 'session.lifecycle.set' | 'session.remove' >; export type SessionEffectOperationKey = Extract; +export type SessionMailboxOperationKey = Extract; export type SessionCatalogOperationKey = Exclude< Extract, | SessionContinuityOperationKey | SessionRevisionOperationKey | SessionRetirementOperationKey | SessionEffectOperationKey + | SessionMailboxOperationKey >; export type TaskLedgerOperationKey = Extract; export type ArtifactOperationKey = Extract; @@ -179,6 +181,10 @@ export type SessionRetirementOperationHandlerMap = Pick< SessionRetirementOperationKey >; export type SessionEffectOperationHandlerMap = Pick; +export type SessionMailboxOperationHandlerMap = Pick< + OperationHandlerMap, + SessionMailboxOperationKey +>; export type TaskLedgerOperationHandlerMap = Pick; export type ArtifactOperationHandlerMap = Pick; export type SkillCatalogOperationHandlerMap = Pick; diff --git a/packages/runtime-host/src/server/session-mailbox-coordinator.ts b/packages/runtime-host/src/server/session-mailbox-coordinator.ts new file mode 100644 index 0000000000..76e34350e7 --- /dev/null +++ b/packages/runtime-host/src/server/session-mailbox-coordinator.ts @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { randomUUID } from 'node:crypto'; +import { isSideConversationSession } from '@maka/core/side-conversation'; +import { isLinkedSubagentSession, type SessionSummary, type StoredMessage } from '@maka/core/session'; +import { + sessionMailboxMessageContent, + sessionMailboxSentReceiptId, + type SessionMailboxKind, +} from '@maka/core/session-mailbox'; +import type { SessionStore } from '@maka/storage/session-store'; +import type { + SessionMailboxSendInput, + SessionMailboxSendResult, + SessionMailboxTarget, +} from '../protocol/index.js'; +import type { HostMessageCoordinator } from './message-coordinator.js'; +import type { + ConnectionContext, + SessionMailboxOperationHandlerMap, +} from './operation-dispatcher.js'; + +type MailboxErrorCode = + | 'host_not_ready' + | 'host_draining' + | 'operation_unavailable' + | 'not_found' + | 'session_archived' + | 'session_busy' + | 'operation_conflict' + | 'invalid_request' + | 'outcome_unknown' + | 'internal_failure'; + +type MailboxOutcome = + | { readonly ok: true; readonly result: T } + | { + readonly ok: false; + readonly error: { + readonly code: MailboxErrorCode; + readonly message: string; + }; + }; + +export interface HostSessionMailboxCoordinatorOptions { + readonly hostEpoch: string; + readonly messages: Pick; + readonly listSessions: () => Promise; + readonly sessionStore: Pick; + readonly createId?: () => string; + readonly now?: () => number; +} + +/** Host-owned routing between ordinary root Sessions in one workspace. */ +export class HostSessionMailboxCoordinator { + readonly handlers: SessionMailboxOperationHandlerMap = { + 'session.mailbox.targets': (input) => this.#targets(input.sourceSessionId), + 'session.mailbox.send': (input, context) => this.#send(input, context.connectionId), + }; + + readonly #hostEpoch: string; + readonly #messages: Pick; + readonly #listSessions: () => Promise; + readonly #sessionStore: Pick; + readonly #createId: () => string; + readonly #now: () => number; + + constructor(options: HostSessionMailboxCoordinatorOptions) { + this.#hostEpoch = options.hostEpoch; + this.#messages = options.messages; + this.#listSessions = options.listSessions; + this.#sessionStore = options.sessionStore; + this.#createId = options.createId ?? randomUUID; + this.#now = options.now ?? Date.now; + } + + async listTargets(sourceSessionId: string): Promise { + const outcome = await this.#targets(sourceSessionId); + if (!outcome.ok) throw new Error(outcome.error.message); + return outcome.result.targets; + } + + async sendFromSession(input: { + readonly sourceSessionId: string; + readonly targetSessionId: string; + readonly kind: SessionMailboxKind; + readonly text: string; + readonly correlationId?: string; + }): Promise { + const outcome = await this.#send( + { ...input, messageId: this.#createId() }, + `session-mailbox:${input.sourceSessionId}`, + ); + if (!outcome.ok) throw new Error(outcome.error.message); + return outcome.result; + } + + async #targets( + sourceSessionId: string, + ): Promise> { + const sessions = await this.#listSessions(); + const source = sessions.find((session) => session.id === sourceSessionId); + if (!source || !isMailboxRoot(source)) { + return failure('not_found', 'Source Session is not available for Session messaging'); + } + if (source.isArchived) return failure('session_archived', 'Source Session is archived'); + const targets = sessions + .filter( + (candidate) => + candidate.id !== source.id && + !candidate.isArchived && + isMailboxRoot(candidate) && + sharesWorkspace(source, candidate), + ) + .map(toMailboxTarget) + .slice(0, 64); + return success({ targets }); + } + + async #send( + input: SessionMailboxSendInput, + initiatingConnectionId: string, + ): Promise> { + if (input.sourceSessionId === input.targetSessionId) { + return failure('invalid_request', 'A Session cannot send a message to itself'); + } + const sessions = await this.#listSessions(); + const source = sessions.find((session) => session.id === input.sourceSessionId); + const target = sessions.find((session) => session.id === input.targetSessionId); + if (!source || !isMailboxRoot(source)) { + return failure('not_found', 'Source Session is not available for Session messaging'); + } + if (source.isArchived) return failure('session_archived', 'Source Session is archived'); + if ( + !target || + target.isArchived || + !isMailboxRoot(target) || + !sharesWorkspace(source, target) + ) { + return failure('not_found', 'Target Session is not reachable from this Session'); + } + const submitted = await this.#messages.handlers['turn.message.submit']( + { + originHostEpoch: this.#hostEpoch, + sessionId: target.id, + messageId: input.messageId, + content: sessionMailboxMessageContent({ + messageId: input.messageId, + fromSessionId: source.id, + fromSessionName: source.name, + toSessionId: target.id, + kind: input.kind, + text: input.text, + ...(input.correlationId ? { correlationId: input.correlationId } : {}), + }), + placement: 'next_turn', + }, + connectionContext(this.#hostEpoch, initiatingConnectionId), + ); + if (!submitted.ok) { + return failure( + submitted.error.code === 'unauthorized' ? 'operation_unavailable' : submitted.error.code, + submitted.error.message, + ); + } + const disposition = submitted.result.disposition === 'turn_started' ? 'turn_started' : 'queued'; + await this.#recordSentMessage(source.id, { + messageId: input.messageId, + targetSessionId: target.id, + targetSessionName: target.name, + kind: input.kind, + text: input.text, + disposition, + }); + return success({ + messageId: input.messageId, + targetSessionId: target.id, + disposition, + ...(submitted.result.disposition === 'turn_started' + ? { turnId: submitted.result.turnId } + : {}), + }); + } + + async #recordSentMessage( + sourceSessionId: string, + data: { + readonly messageId: string; + readonly targetSessionId: string; + readonly targetSessionName: string; + readonly kind: SessionMailboxKind; + readonly text: string; + readonly disposition: 'turn_started' | 'queued'; + }, + ): Promise { + const receiptId = sessionMailboxSentReceiptId(data.messageId); + try { + const messages = await this.#sessionStore.readMessagesSnapshot(sourceSessionId); + if (messages.some((message) => message.id === receiptId)) return; + const anchorTurnId = latestTurnId(messages); + await this.#sessionStore.appendMessage(sourceSessionId, { + type: 'system_note', + id: receiptId, + ...(anchorTurnId ? { turnId: anchorTurnId } : {}), + ts: this.#now(), + kind: 'session_mailbox_sent', + data, + }); + } catch { + // Delivery already succeeded and cannot be rolled back. The Client also + // projects an optimistic receipt, so a storage-only receipt failure must + // never invite a retry that sends the same message twice. + } + } +} + +function latestTurnId(messages: readonly StoredMessage[]): string | undefined { + for (let index = messages.length - 1; index >= 0; index -= 1) { + const turnId = messages[index]?.turnId; + if (turnId) return turnId; + } + return undefined; +} + +function isMailboxRoot(session: SessionSummary): boolean { + return !isLinkedSubagentSession(session) && !isSideConversationSession(session.labels); +} + +function sharesWorkspace(source: SessionSummary, target: SessionSummary): boolean { + if (source.projectId && target.projectId) return source.projectId === target.projectId; + return source.cwd !== undefined && source.cwd === target.cwd; +} + +function toMailboxTarget(session: SessionSummary): SessionMailboxTarget { + return { + sessionId: session.id, + name: session.name, + status: + session.status === 'waiting_for_user' + ? 'waiting_for_user' + : (session.runningTurnIds?.length ?? 0) > 0 + ? 'running' + : 'idle', + }; +} + +function connectionContext(hostEpoch: string, connectionId: string): ConnectionContext { + return { + hostEpoch, + connectionId, + principal: 'runtime_host', + acquireResidency: () => ({ release: () => undefined }), + }; +} + +function success(result: T): MailboxOutcome { + return { ok: true, result }; +} + +function failure(code: MailboxErrorCode, message: string): MailboxOutcome { + return { ok: false, error: { code, message } }; +} diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a12c9120b9..e234c1129b 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -20,6 +20,7 @@ "./test-connection": "./dist/test-connection.js", "./model-fetcher": "./dist/model-fetcher.js", "./session-manager": "./dist/session-manager.js", + "./session-mailbox-tools": "./dist/session-mailbox-tools.js", "./test-only/fake-backend": "./dist/test-only/fake-backend.js", "./test-only/observation-text-reader": "./dist/__tests__/observation-text-reader.js", "./filesystem-worker": "./dist/filesystem-worker/index.js", diff --git a/packages/runtime/src/__tests__/session-mailbox-tools.test.ts b/packages/runtime/src/__tests__/session-mailbox-tools.test.ts new file mode 100644 index 0000000000..1c2519d930 --- /dev/null +++ b/packages/runtime/src/__tests__/session-mailbox-tools.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + SESSION_LIST_TOOL_NAME, + SESSION_REPLY_TOOL_NAME, + SESSION_SEND_TOOL_NAME, + buildSessionMailboxTools, +} from '../session-mailbox-tools.js'; +import type { MakaTool, MakaToolContext } from '../tool-runtime.js'; + +test('Session mailbox tools bind the current Session as the source', async () => { + const sends: unknown[] = []; + const tools = buildSessionMailboxTools({ + list: async () => [{ sessionId: 'target-1', name: 'Target', status: 'idle' }], + send: async (input) => { + sends.push(input); + return { + messageId: 'message-1', + targetSessionId: input.targetSessionId, + disposition: 'queued', + }; + }, + }); + + assert.match(String(await tool(tools, SESSION_LIST_TOOL_NAME).impl({}, context())), /target-1/); + await tool(tools, SESSION_SEND_TOOL_NAME).impl( + { target_session_id: 'target-1', text: 'Please check', kind: 'request' }, + context(), + ); + await tool(tools, SESSION_REPLY_TOOL_NAME).impl( + { target_session_id: 'target-1', in_reply_to: 'request-1', text: 'Done' }, + context(), + ); + + assert.deepEqual(sends, [ + { + sourceSessionId: 'source-1', + targetSessionId: 'target-1', + kind: 'request', + text: 'Please check', + }, + { + sourceSessionId: 'source-1', + targetSessionId: 'target-1', + kind: 'reply', + text: 'Done', + correlationId: 'request-1', + }, + ]); +}); + +function tool(tools: MakaTool[], name: string): MakaTool { + const found = tools.find((candidate) => candidate.name === name); + assert.ok(found); + return found; +} + +function context(): MakaToolContext { + return { + sessionId: 'source-1', + turnId: 'turn-1', + cwd: '/workspace', + toolCallId: 'call-1', + abortSignal: new AbortController().signal, + emitOutput: () => undefined, + }; +} diff --git a/packages/runtime/src/session-mailbox-tools.ts b/packages/runtime/src/session-mailbox-tools.ts new file mode 100644 index 0000000000..082c7abf49 --- /dev/null +++ b/packages/runtime/src/session-mailbox-tools.ts @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { z } from 'zod'; +import { SESSION_MAILBOX_TEXT_MAX_BYTES } from '@maka/core/session-mailbox'; +import type { MakaTool } from './tool-runtime.js'; + +export const SESSION_LIST_TOOL_NAME = 'session_list'; +export const SESSION_SEND_TOOL_NAME = 'session_send'; +export const SESSION_REPLY_TOOL_NAME = 'session_reply'; + +export interface SessionMailboxToolTarget { + readonly sessionId: string; + readonly name: string; + readonly status: 'idle' | 'running' | 'waiting_for_user'; +} + +export interface SessionMailboxToolSendResult { + readonly messageId: string; + readonly targetSessionId: string; + readonly disposition: 'turn_started' | 'queued'; + readonly turnId?: string; +} + +export interface SessionMailboxToolAuthority { + list(sourceSessionId: string): Promise; + send(input: { + sourceSessionId: string; + targetSessionId: string; + kind: 'request' | 'reply' | 'notification'; + text: string; + correlationId?: string; + }): Promise; +} + +const targetSchema = z + .string() + .min(1) + .max(128) + .regex(/^[A-Za-z0-9_-]+$/) + .describe(`Target Session id returned by ${SESSION_LIST_TOOL_NAME}.`); +const textSchema = z + .string() + .trim() + .min(1) + .refine((value) => new TextEncoder().encode(value).byteLength <= SESSION_MAILBOX_TEXT_MAX_BYTES, { + message: `Session message exceeds ${SESSION_MAILBOX_TEXT_MAX_BYTES} UTF-8 bytes`, + }); + +export function buildSessionMailboxTools(authority: SessionMailboxToolAuthority): MakaTool[] { + return [ + { + name: SESSION_LIST_TOOL_NAME, + displayName: 'Session List', + description: 'List other user Sessions in the same project that this Session may contact.', + parameters: z.object({}), + impl: async (_input, ctx) => { + const targets = await authority.list(ctx.sessionId); + return targets.length === 0 + ? 'No reachable Sessions.' + : targets + .map((target) => `- ${target.sessionId} | ${target.name} | ${target.status}`) + .join('\n'); + }, + }, + { + name: SESSION_SEND_TOOL_NAME, + displayName: 'Session Send', + description: + 'Send a durable message to another Session in the same project. Requests ask the target ' + + `to process the message and reply with ${SESSION_REPLY_TOOL_NAME}; notifications do not require a reply.`, + parameters: z.object({ + target_session_id: targetSchema, + text: textSchema, + kind: z.enum(['request', 'notification']).default('request'), + }), + impl: async (input, ctx) => + formatDelivery( + await authority.send({ + sourceSessionId: ctx.sessionId, + targetSessionId: input.target_session_id, + kind: input.kind, + text: input.text, + }), + ), + }, + { + name: SESSION_REPLY_TOOL_NAME, + displayName: 'Session Reply', + description: 'Reply to a Session request using its source Session and message identities.', + parameters: z.object({ + target_session_id: targetSchema, + in_reply_to: z + .string() + .min(1) + .max(128) + .regex(/^[A-Za-z0-9_-]+$/), + text: textSchema, + }), + impl: async (input, ctx) => + formatDelivery( + await authority.send({ + sourceSessionId: ctx.sessionId, + targetSessionId: input.target_session_id, + kind: 'reply', + text: input.text, + correlationId: input.in_reply_to, + }), + ), + }, + ]; +} + +function formatDelivery(result: SessionMailboxToolSendResult): string { + return [ + `Session message ${result.messageId} ${result.disposition === 'queued' ? 'queued' : 'delivered'}.`, + `target=${result.targetSessionId}`, + ...(result.turnId ? [`turn=${result.turnId}`] : []), + ].join('\n'); +} diff --git a/packages/ui/src/__tests__/materialize.test.ts b/packages/ui/src/__tests__/materialize.test.ts index 197db51e71..a4ce93fba2 100644 --- a/packages/ui/src/__tests__/materialize.test.ts +++ b/packages/ui/src/__tests__/materialize.test.ts @@ -20,6 +20,7 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; import type { StoredMessage } from '@maka/core/session'; +import { sessionMailboxMessageContent } from '@maka/core/session-mailbox'; import { materializeChat, materializeTools, @@ -254,6 +255,59 @@ describe("materializeChat message metadata", () => { goalId: "goal-1", }); }); + + test('projects incoming and outgoing mailbox messages as structured cards with body-only text', () => { + const incoming = sessionMailboxMessageContent({ + messageId: 'mail-in', + fromSessionId: 'source', + fromSessionName: '来源任务: A', + toSessionId: 'target', + kind: 'request', + text: '正文里也可以有 From:,但不会被误解析。', + }); + const messages: StoredMessage[] = [ + { + type: 'user', + id: 'mail-in', + turnId: 'target-turn', + ts: 1, + ...incoming, + }, + { + type: 'system_note', + id: 'mail-out', + turnId: 'target-turn', + ts: 2, + kind: 'session_mailbox_sent', + data: { + messageId: 'sent-1', + targetSessionId: 'other', + targetSessionName: '目标任务', + kind: 'request', + text: '发出的正文', + disposition: 'queued', + }, + }, + ]; + + const chat = materializeChat(messages, 'zh'); + assert.equal(chat[0]?.text, '正文里也可以有 From:,但不会被误解析。'); + assert.deepEqual(chat[0]?.sessionMailbox, { + direction: 'incoming', + sessionId: 'source', + sessionName: '来源任务: A', + kind: 'request', + }); + assert.equal(chat[1]?.text, '发出的正文'); + assert.deepEqual(chat[1]?.sessionMailbox, { + direction: 'outgoing', + sessionId: 'other', + sessionName: '目标任务', + kind: 'request', + disposition: 'queued', + }); + assert.deepEqual(materializeTurns(messages, 'zh')[0]?.notes[0], chat[1]); + }); }); // ── #1307: the timeline model stays flat (fold is a render concern) ────────── diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index e0e6f27383..04a38fca2b 100644 --- a/packages/ui/src/chat-turn.tsx +++ b/packages/ui/src/chat-turn.tsx @@ -19,7 +19,7 @@ import { memo, useEffect, useMemo, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react'; import { useMountedRef } from './use-mounted-ref.js'; -import { ICON_SIZE, AlertOctagon, Ban, Check, Copy, GitBranch, Info, Pencil, RefreshCcw, Timer } from './icons.js'; +import { ICON_SIZE, AlertOctagon, ArrowDown, ArrowUp, Ban, Check, Copy, GitBranch, Info, Pencil, RefreshCcw, Timer } from './icons.js'; import { type ClipboardCopyPhase, useClipboardCopyFeedback } from './clipboard-feedback.js'; import { Markdown } from './markdown.js'; import { @@ -59,6 +59,7 @@ import { } from '@maka/core/events'; import { finalAssistantReplyText, + type ChatItem, type TurnTimelineItem, type TurnViewModel, } from './materialize.js'; @@ -172,6 +173,7 @@ const UserMessageBody = memo(function UserMessageBody(props: { attachments?: readonly AttachmentRef[]; quotes?: readonly QuoteRef[]; inlineReferences?: readonly InlineReference[]; + sessionMailbox?: ChatItem['sessionMailbox']; onReadAttachmentBytes?: ReadAttachmentBytes; /** When set on a user message, show an edit affordance that starts a revision draft. */ onEditUserMessage?: () => void; @@ -185,6 +187,13 @@ const UserMessageBody = memo(function UserMessageBody(props: { const editActionLabel = props.editDisabled ? (props.editDisabledReason ?? copyText.editMessageDisabledRunning) : copyText.editMessage; + const renderedText = props.inlineReferences ? ( + + ) : ( + + {props.text} + + ); const userMetadata = ( ) : null} - {props.inlineReferences ? ( - - ) : ( - - {props.text} - - )} + {props.sessionMailbox ? ( +
+ {props.sessionMailbox.direction === 'incoming' + ?
+ ) : null} + {props.sessionMailbox ? ( +
{renderedText}
+ ) : renderedText}
); @@ -534,12 +561,14 @@ export const TurnView = memo(function TurnView(props: { {turn.user && ( )} - {turn.notes.map((note) => ( + {turn.notes.filter((note) => !note.sessionMailbox).map((note) => ( @@ -753,6 +784,21 @@ export const TurnView = memo(function TurnView(props: { ); })} + {turn.notes.filter((note) => note.sessionMailbox?.direction === 'outgoing').map((note) => ( + + + + ))} ); }); diff --git a/packages/ui/src/composer.tsx b/packages/ui/src/composer.tsx index 49a09e979c..ce0cee2101 100644 --- a/packages/ui/src/composer.tsx +++ b/packages/ui/src/composer.tsx @@ -136,6 +136,8 @@ export interface ComposerSlashCommandOption { description?: string; keywords?: readonly string[]; Icon?: LucideIcon; + /** Run immediately when the command is chosen from the `/` menu. */ + onChoose?(): void; } type ComposerSlashSuggestion = @@ -360,6 +362,14 @@ export const Composer = forwardRef< cancelLabel: string; onCancel(): void; }; + /** Optional routing banner for a message addressed to another Session. */ + sendTargetNotice?: { + title: string; + detail?: string; + cancelLabel: string; + status?: 'ready' | 'sending' | 'delivered' | 'queued' | 'failed'; + onCancel(): void; + }; /** * Where a NEW chat starts. Rendered at the end of the footer's send-context * group and only while no session owns the composer: the project is fixed @@ -1000,6 +1010,10 @@ export const Composer = forwardRef< onSelect: (item): string | ChatComposerToken => { const suggestion = item.auxiliaryData as ComposerSlashSuggestion; if (suggestion.kind === 'command') { + if (suggestion.command.onChoose) { + suggestion.command.onChoose(); + return ''; + } return `/${suggestion.command.id} `; } return inlineReferenceToken({ @@ -1226,6 +1240,12 @@ export const Composer = forwardRef< if (event.key === 'Escape' && dragActive) { setDragActive(false); } + if (event.key === 'Escape' && props.sendTargetNotice) { + event.preventDefault(); + if (props.sendTargetNotice.status === 'sending') return; + props.sendTargetNotice.onCancel(); + return; + } // Esc during streaming interrupts the model. We don't preventDefault // unconditionally so Esc still works to close modals when the composer // happens to be focused outside a streaming turn. @@ -1539,6 +1559,28 @@ export const Composer = forwardRef< />
)} + {!props.hidden && props.sendTargetNotice && ( +
+
+ )} {!props.hidden && queueCount > 0 ? ( string; + mailboxOutgoingAriaLabel: (sessionName: string) => string; userAriaLabel: string; systemAriaLabel: string; assistantAriaLabel: string; @@ -497,6 +503,7 @@ const CONVERSATION_COPY = { }, messages: { you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', workingPhrases: ['正在琢磨…', '正在推敲…', '正在盘算…', '正在钻研…', '正在忙活…', '正在梳理…', '正在打磨…', '正在鼓捣…', '正在酝酿…', '正在攻坚…', '正在权衡…', '正在拾掇…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, 'zh')}后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryReason: { network: '网络中断', provider_capacity: '模型服务暂时满载', provider_unavailable: '模型服务暂时不可用', rate_limit: '触发模型速率限制', timeout: '请求超时', unknown: '模型请求失败' }, safeResumePending: '正在验证…', safeResume: '安全恢复', thinking: '深度思考', truncated: '已截断', copied: '已复制', copying: '复制中', copyFailed: '复制失败', copy: '复制', editMessage: '编辑并重发', editMessageDisabledRunning: '当前回答仍在进行中,结束后再编辑', editMessageDisabledAttachments: '包含附件的历史消息暂不支持编辑并重发', editMessageDisabledQuotes: '包含引用的历史消息暂不支持编辑并重发', editMessageDisabledTransformedText: '通过显式技能发送的历史消息暂不支持编辑并重发', + mailboxIncoming: '来自任务', mailboxOutgoing: '发送到任务', mailboxDelivered: '已送达 · 已开始处理', mailboxQueued: '已排队 · 将在下一轮处理', mailboxIncomingAriaLabel: (sessionName) => `来自任务“${sessionName}”的消息`, mailboxOutgoingAriaLabel: (sessionName) => `发送到任务“${sessionName}”的消息`, userAriaLabel: '你发送的消息', systemAriaLabel: '系统消息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: (context) => `回答操作${context ? `:${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action}回答${context ? `:${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action}消息${context ? `:${context}` : ''}`, sourceAriaLabel: '本轮回答的来源', derivativesAriaLabel: '本轮回答的衍生', scheduledTaskTriggered: '定时任务触发', scheduledTaskTitle: (id) => `由定时任务触发 · ${id}`, legacyAutomationTriggered: '旧版自动化(仅历史)', legacyAutomationTitle: (id) => `由旧版自动化触发 · ${id} · 仅保留历史,不会再次执行`, goalContinued: 'Goal 自动继续', goalTitle: (id) => `由 Goal 继续执行 · ${id}`, agentGraphTriggered: 'Agent Graph 自动继续', agentGraphTitle: (graphId) => `由 Agent Graph 调度器触发 · ${graphId}`, thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中断)', abortedByStop: '(已中断 · 由停止按钮触发)', systemNotes: { @@ -645,6 +652,7 @@ const CONVERSATION_COPY = { }, messages: { you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, 'en')} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryReason: { network: 'Network interrupted', provider_capacity: 'The model service is temporarily at capacity', provider_unavailable: 'Model service temporarily unavailable', rate_limit: 'Model rate limit reached', timeout: 'Request timed out', unknown: 'Model request failed' }, safeResumePending: 'Checking…', safeResume: 'Safe recovery', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages sent with an explicit skill', + mailboxIncoming: 'From task', mailboxOutgoing: 'Sent to task', mailboxDelivered: 'Delivered · processing started', mailboxQueued: 'Queued · will process next turn', mailboxIncomingAriaLabel: (sessionName) => `Message from task “${sessionName}”`, mailboxOutgoingAriaLabel: (sessionName) => `Message sent to task “${sessionName}”`, userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: (context) => `Response actions${context ? `: ${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action} response${context ? `: ${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action} message${context ? `: ${context}` : ''}`, sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this one', scheduledTaskTriggered: 'Triggered by scheduled task', scheduledTaskTitle: (id) => `Triggered by scheduled task · ${id}`, legacyAutomationTriggered: 'Legacy Automation (history only)', legacyAutomationTitle: (id) => `Triggered by legacy Automation · ${id} · Historical only; it will not run again`, goalContinued: 'Continued by Goal', goalTitle: (id) => `Continued by Goal · ${id}`, agentGraphTriggered: 'Continued by Agent Graph', agentGraphTitle: (graphId) => `Triggered by the Agent Graph scheduler · ${graphId}`, thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: '(Interrupted)', abortedByStop: '(Interrupted · Stop button)', systemNotes: { diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts index 2f26a26174..d87a97c66b 100644 --- a/packages/ui/src/materialize.ts +++ b/packages/ui/src/materialize.ts @@ -39,6 +39,11 @@ import type { import type { ToolActivityStatus } from '@maka/core/tool-result-status'; import type { ShellRunToolResult } from '@maka/core/shell-run-result'; import type { StoredMessage, TurnRecord, TurnStatus, UserMessage } from '@maka/core/session'; +import { + parseSessionMailboxMessageContent, + parseSessionMailboxSentNoteData, + type SessionMailboxKind, +} from '@maka/core/session-mailbox'; import type { UiLocale } from '@maka/core/ui-locale'; import type { LiveSteeringProjection, @@ -62,6 +67,21 @@ export interface ChatItem { inlineReferences?: InlineReference[]; /** Present when the Host authored this message instead of the user. */ hostOrigin?: NonNullable; + /** Structured cross-task message chrome; `text` remains body-only. */ + sessionMailbox?: + | { + direction: 'incoming'; + sessionId: string; + sessionName: string; + kind: SessionMailboxKind; + } + | { + direction: 'outgoing'; + sessionId: string; + sessionName: string; + kind: SessionMailboxKind; + disposition: 'turn_started' | 'queued'; + }; } /** @@ -157,22 +177,7 @@ export function materializeChat( const items: ChatItem[] = []; for (const message of messages) { if (message.type === "user") { - items.push({ - id: message.id, - role: "user", - text: message.displayText ?? message.text, - ts: message.ts, - ...(message.attachments && message.attachments.length > 0 - ? { attachments: message.attachments } - : {}), - ...(message.quotes && message.quotes.length > 0 - ? { quotes: message.quotes } - : {}), - ...(message.inlineReferences !== undefined - ? { inlineReferences: message.inlineReferences } - : {}), - ...(message.origin ? { hostOrigin: message.origin } : {}), - }); + items.push(chatItemFromUserMessage(message)); } if (message.type === "assistant") items.push({ @@ -181,7 +186,10 @@ export function materializeChat( text: message.text, ts: message.ts, }); - if ( + if (message.type === 'system_note' && message.kind === 'session_mailbox_sent') { + const item = chatItemFromMailboxSentNote(message); + if (item) items.push(item); + } else if ( message.type === "system_note" && VISIBLE_SYSTEM_NOTES.has(message.kind) ) { @@ -755,6 +763,9 @@ export function materializeTurns( if (message.ts !== undefined && message.ts >= turn.startedAt) { turn.durationMs = message.ts - turn.startedAt; } + } else if (message.type === 'system_note' && message.kind === 'session_mailbox_sent') { + const note = chatItemFromMailboxSentNote(message); + if (note) turn.notes.push(note); } else if ( message.type === "system_note" && VISIBLE_SYSTEM_NOTES.has(message.kind) @@ -1073,10 +1084,11 @@ function chatItemFromContent( content: MessageContent, hostOrigin?: NonNullable, ): ChatItem { + const mailbox = parseSessionMailboxMessageContent(content); return { id, role: "user", - text: content.displayText ?? content.text, + text: mailbox?.text ?? content.displayText ?? content.text, ts, ...(content.attachments && content.attachments.length > 0 ? { attachments: content.attachments } @@ -1088,6 +1100,36 @@ function chatItemFromContent( ? { inlineReferences: content.inlineReferences } : {}), ...(hostOrigin ? { hostOrigin } : {}), + ...(mailbox + ? { + sessionMailbox: { + direction: 'incoming' as const, + sessionId: mailbox.fromSessionId, + sessionName: mailbox.fromSessionName, + kind: mailbox.kind, + }, + } + : {}), + }; +} + +function chatItemFromMailboxSentNote( + message: Extract, +): ChatItem | undefined { + const receipt = parseSessionMailboxSentNoteData(message.data); + if (!receipt) return undefined; + return { + id: message.id, + role: 'system', + text: receipt.text, + ts: message.ts, + sessionMailbox: { + direction: 'outgoing', + sessionId: receipt.targetSessionId, + sessionName: receipt.targetSessionName, + kind: receipt.kind, + disposition: receipt.disposition, + }, }; } From 834505ea8859093aa8c9c3c5e801bc10036a1093 Mon Sep 17 00:00:00 2001 From: 982945902 <982945902@qq.com> Date: Tue, 25 Aug 2026 20:11:48 +0800 Subject: [PATCH 2/5] fix: harden session mailbox delivery and flows Generated-by: Codex --- apps/desktop/e2e/session-mailbox.spec.ts | 114 +++++++ .../__tests__/workhub-session-port.test.ts | 35 ++ apps/desktop/src/renderer/app-shell.tsx | 237 ++----------- .../src/renderer/session-mailbox-picker.tsx | 2 +- .../src/renderer/styles/chat-message.css | 14 +- apps/desktop/src/renderer/styles/composer.css | 11 +- .../src/renderer/use-app-shell-mailbox.tsx | 258 +++++++++++++++ .../cli/src/__tests__/pi-tui-runner.test.ts | 74 ++++- packages/cli/src/pi-tui-pickers.ts | 102 ++++++ packages/cli/src/pi-tui-runner.ts | 49 ++- packages/core/package.json | 1 + .../core/src/__tests__/runtime-event.test.ts | 8 + .../src/__tests__/session-mailbox.test.ts | 55 +++ packages/core/src/agent-run.ts | 3 + packages/core/src/session-mailbox.ts | 82 ++++- packages/core/src/session.ts | 2 + packages/core/src/turn-origin.ts | 37 +++ .../execution-model-composition.test.ts | 3 + .../src/__tests__/message-coordinator.test.ts | 42 +++ .../session-mailbox-coordinator.test.ts | 172 +++++++++- .../src/server/execution-composition.ts | 1 + .../src/server/hosted-execution-recovery.ts | 2 + .../src/server/message-coordinator.ts | 88 ++++- .../src/server/root-admission-owner.ts | 1 + .../src/server/root-turn-coordinator.ts | 4 + .../src/server/session-mailbox-coordinator.ts | 313 ++++++++++++++---- packages/storage/src/agent-run-store.ts | 25 +- packages/ui/src/__tests__/materialize.test.ts | 74 ++++- packages/ui/src/chat-turn.tsx | 8 +- packages/ui/src/composer.tsx | 2 +- packages/ui/src/conversation-copy.ts | 5 +- packages/ui/src/materialize.ts | 79 ++++- 32 files changed, 1535 insertions(+), 368 deletions(-) create mode 100644 apps/desktop/e2e/session-mailbox.spec.ts create mode 100644 apps/desktop/src/renderer/use-app-shell-mailbox.tsx diff --git a/apps/desktop/e2e/session-mailbox.spec.ts b/apps/desktop/e2e/session-mailbox.spec.ts new file mode 100644 index 0000000000..828d8d6858 --- /dev/null +++ b/apps/desktop/e2e/session-mailbox.spec.ts @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { Page } from '@playwright/test'; +import { expect, test, COMPOSER_INPUT } from './fixtures'; + +async function establishSourceAndTarget( + page: Page, + targetName: string, +): Promise { + const composer = page.locator(COMPOSER_INPUT); + await composer.fill('建立发送方任务'); + await composer.press('Enter'); + await expect(page.getByText('Fake backend received: 建立发送方任务')).toBeVisible(); + return page.evaluate(async (name) => { + const source = (await window.maka.sessions.list())[0]; + if (!source) throw new Error('source Session was not created'); + const target = await window.maka.sessions.create({ + name, + cwd: source.cwd, + projectId: source.projectId, + }); + return target.id; + }, targetName); +} + +async function selectMailboxTarget( + page: Page, + query: string, + targetName: string, +): Promise { + const composer = page.locator(COMPOSER_INPUT); + await composer.fill('/send'); + await composer.press('Enter'); + const search = page.getByPlaceholder('搜索任务名称…'); + await expect(search).toBeVisible(); + await search.fill(query); + const target = page.getByRole('option', { name: new RegExp(targetName) }); + await expect(target).toBeVisible(); + await target.click(); +} + +test('selects a searchable /send target and settles delivery into the transcript', async ({ + window: page, +}, testInfo) => { + const composer = page.locator(COMPOSER_INPUT); + await establishSourceAndTarget(page, '支付回调恢复检查'); + await selectMailboxTarget(page, '恢复检查', '支付回调恢复检查'); + + await expect(page.getByText('发送给“支付回调恢复检查”')).toBeVisible(); + await composer.fill('请检查回执恢复链路'); + await composer.press('Enter'); + + const card = page.locator('.maka-session-mailbox-bubble', { + hasText: '请检查回执恢复链路', + }); + await expect(card).toContainText('支付回调恢复检查'); + await expect(card).toContainText(/已送达|已排队/); + await expect(page.getByText('发送给“支付回调恢复检查”')).toHaveCount(0); + const dismissToast = page.getByRole('button', { name: '关闭通知' }); + if (await dismissToast.isVisible()) await dismissToast.click(); + await page.screenshot({ path: testInfo.outputPath('session-mailbox-card.png') }); +}); + +test('cancels a selected /send target without consuming the next message', async ({ + window: page, +}) => { + const composer = page.locator(COMPOSER_INPUT); + await establishSourceAndTarget(page, '可以取消的任务'); + await selectMailboxTarget(page, '可以取消', '可以取消的任务'); + + const notice = page.locator('[data-send-target-notice="true"]'); + await expect(notice).toContainText('发送给“可以取消的任务”'); + await notice.getByRole('button', { name: '取消' }).click(); + await expect(notice).toHaveCount(0); + + await composer.fill('取消后仍是普通消息'); + await composer.press('Enter'); + await expect(page.getByText('Fake backend received: 取消后仍是普通消息')).toBeVisible(); + await expect(page.locator('.maka-session-mailbox-bubble')).toHaveCount(0); +}); + +test('keeps failed /send delivery actionable after the target becomes unavailable', async ({ + window: page, +}) => { + const composer = page.locator(COMPOSER_INPUT); + const targetId = await establishSourceAndTarget(page, '即将归档的任务'); + await selectMailboxTarget(page, '即将归档', '即将归档的任务'); + await page.evaluate((sessionId) => window.maka.sessions.archive(sessionId), targetId); + + await composer.fill('这条发送应当失败'); + await composer.press('Enter'); + const notice = page.locator('[data-send-target-notice="true"]'); + await expect(notice).toHaveAttribute('data-delivery-status', 'failed'); + await expect(notice).toContainText('发送给“即将归档的任务”失败'); + await expect(composer).toHaveText('这条发送应当失败'); + await expect(page.locator('.maka-session-mailbox-bubble')).toHaveCount(0); +}); diff --git a/apps/desktop/src/main/__tests__/workhub-session-port.test.ts b/apps/desktop/src/main/__tests__/workhub-session-port.test.ts index 719d9c81ca..7ecb60a4a5 100644 --- a/apps/desktop/src/main/__tests__/workhub-session-port.test.ts +++ b/apps/desktop/src/main/__tests__/workhub-session-port.test.ts @@ -390,6 +390,41 @@ test('desktop adapter projects Session catalog facts without owning copies', asy ]); }); +test('desktop adapter invalidates and re-reads a renamed Session even when activity time is unchanged', async () => { + let sessionName = 'Original name'; + let onChanged: (() => void) | undefined; + const adapter = createDesktopWorkHubSessionPort({ + transcripts: unusedTranscripts, + sessions: { + list: async () => [desktopSession('ordinary', { + name: sessionName, + lastMessageAt: 30, + })], + listTurns: async () => [], + create: async () => { throw new Error('not used'); }, + send: async () => { throw new Error('not used'); }, + stop: async () => {}, + subscribeChanges: (handler) => { + onChanged = handler; + return () => {}; + }, + }, + projectName: () => 'Maka', + newTurnId: () => 'unused', + }); + + assert.equal((await adapter.list())[0]?.sessionName, 'Original name'); + let invalidations = 0; + adapter.subscribe(() => { + invalidations += 1; + }); + sessionName = 'Renamed without a new message'; + onChanged?.(); + + assert.equal(invalidations, 1); + assert.equal((await adapter.list())[0]?.sessionName, 'Renamed without a new message'); +}); + test('desktop adapter preserves per-Host catalog coverage for ownership reconciliation', async () => { const localSessionId = desktopSessionKey({ hostId: 'local-host', sessionId: 'local' }); const adapter = createDesktopWorkHubSessionPort({ diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index 7a2fdf9d30..008b95fb29 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -35,8 +35,6 @@ import type { QuoteRef, } from '@maka/core/events'; import type { SessionSummary } from '@maka/core/session'; -import { sessionMailboxSentReceiptId } from '@maka/core/session-mailbox'; -import type { SessionMailboxTarget } from '@maka/runtime-host/protocol'; import type { OrchestrationMode } from '@maka/core/orchestration'; import type { ChatDefaultPermissionMode } from '@maka/core/settings'; import type { SlashCommandIdForSurface } from '@maka/core/slash-command-catalog'; @@ -100,7 +98,7 @@ import { UNRESOLVED_NEW_TASK_DRAFT_KEY } from './new-task-reload-intent'; import { useNewTaskChoice } from './use-new-task-choice'; import { NEW_TASK_PENDING_KEY } from './pending-items'; import { parseDesktopSlashCommand } from './desktop-slash-command'; -import { SessionMailboxPicker } from './session-mailbox-picker'; +import { useAppShellMailbox } from './use-app-shell-mailbox'; import { hasActiveTurnAtSubmit, mergeWorkspaceReferences, @@ -688,34 +686,6 @@ function AppShellContent({ const persistedComposerDefaults = loadComposerDefaults(); const [helpOpen, closeHelp, openHelp] = useKeyboardHelp(); const [paletteOpen, openPalette, closePalette] = useCommandPalette(); - const [mailboxFlow, setMailboxFlow] = useState<{ - sourceSessionId: string; - targets: readonly SessionMailboxTarget[]; - } | null>(null); - const [pendingMailboxTarget, setPendingMailboxTarget] = useState<{ - sourceSessionId: string; - target: SessionMailboxTarget; - } | null>(null); - const [mailboxDeliveryFeedback, setMailboxDeliveryFeedback] = useState<{ - sourceSessionId: string; - target: SessionMailboxTarget; - status: 'sending' | 'failed'; - } | null>(null); - useEffect(() => { - if ( - pendingMailboxTarget - && pendingMailboxTarget.sourceSessionId !== activeId - ) { - setPendingMailboxTarget(null); - setMailboxDeliveryFeedback(null); - } - if ( - mailboxDeliveryFeedback - && mailboxDeliveryFeedback.sourceSessionId !== activeId - ) { - setMailboxDeliveryFeedback(null); - } - }, [activeId, mailboxDeliveryFeedback, pendingMailboxTarget]); const [viewMode, setViewMode] = useState(() => readSessionListViewMode()); const composerRef = useRef(null); const retractedWorkspaceReferencesRef = useRef>({}); @@ -729,6 +699,16 @@ function AppShellContent({ revisionDraftRef.current = draft; setRevisionDraft(draft); }, []); + const mailbox = useAppShellMailbox({ + activeSessionId: activeId, + uiLocale, + revisionActive: revisionDraft !== null, + hasPendingComposerContext: pendingAttachments.length > 0 || pendingQuotes.length > 0, + composerRef, + setMessages, + toastInfo: toastApi.info, + showSessionError, + }); useEffect(() => { const draft = revisionDraftRef.current; if (!draft) return; @@ -1410,36 +1390,7 @@ function AppShellContent({ sessionListCollapsed, setSessionListCollapsed, } = useShellLayout(); - const openMailboxTargetPicker = useCallback(async () => { - const sourceSessionId = activeIdRef.current; - if (!sourceSessionId) return false; - if (revisionDraftRef.current) { - const actionCopy = getDesktopConversationCopy(uiLocale).actions; - toastApi.info(actionCopy.revisionUnavailableTitle, actionCopy.revisionCommandUnsupported); - return false; - } - if (pendingAttachments.length > 0 || pendingQuotes.length > 0) { - toastApi.info( - shellCopy.sideChatContextPendingTitle, - shellCopy.sideChatContextPendingDescription, - ); - return false; - } - try { - const targets = await window.maka.sessions.listMailboxTargets(sourceSessionId); - setPendingMailboxTarget(null); - setMailboxDeliveryFeedback(null); - setMailboxFlow({ sourceSessionId, targets }); - return true; - } catch (error) { - showSessionError( - sourceSessionId, - shellCopy.mailboxFailedTitle, - localizedShellErrorMessage(error, shellCopy.mailboxFailedFallback, uiLocale), - ); - return false; - } - }, [pendingAttachments.length, pendingQuotes.length, shellCopy, toastApi, uiLocale]); + const openMailboxTargetPicker = mailbox.openTargetPicker; const desktopSlashCommands = useMemo( () => { const streaming = turnActive || activeStreamingLive; @@ -1704,7 +1655,7 @@ function AppShellContent({ newSessionPermissionMode: newTaskPermissionMode, }); - const hasModalOpen = helpOpen || paletteOpen || searchModalOpen || mailboxFlow !== null; + const hasModalOpen = helpOpen || paletteOpen || searchModalOpen || mailbox.modalOpen; const shellObscured = hasModalOpen || settingsOpen; const contextCompactionPresentation = useMemo( () => @@ -1907,74 +1858,11 @@ function AppShellContent({ text: string, metadata?: ComposerSendMetadata, ): Promise { - const mailboxTarget = pendingMailboxTarget; - if (mailboxTarget) { - if (activeIdRef.current !== mailboxTarget.sourceSessionId) { - setPendingMailboxTarget(null); - setMailboxDeliveryFeedback(null); - return false; - } - if ( - pendingAttachments.length > 0 - || pendingQuotes.length > 0 - || (metadata?.workspaceFileReferences?.length ?? 0) > 0 - ) { - toastApi.info( - shellCopy.sideChatContextPendingTitle, - shellCopy.sideChatContextPendingDescription, - ); - return false; - } - setMailboxDeliveryFeedback({ ...mailboxTarget, status: 'sending' }); - try { - const result = await window.maka.sessions.sendMailboxMessage( - mailboxTarget.sourceSessionId, - mailboxTarget.target.sessionId, - text, - ); - setPendingMailboxTarget(null); - setMailboxDeliveryFeedback(null); - const receiptId = sessionMailboxSentReceiptId(result.messageId); - if (activeIdRef.current === mailboxTarget.sourceSessionId) { - setMessages((current) => { - if (current.some((message) => message.id === receiptId)) return current; - let anchorTurnId: string | undefined; - for (let index = current.length - 1; index >= 0; index -= 1) { - anchorTurnId = current[index]?.turnId; - if (anchorTurnId) break; - } - return [ - ...current, - { - type: 'system_note', - id: receiptId, - ...(anchorTurnId ? { turnId: anchorTurnId } : {}), - ts: Date.now(), - kind: 'session_mailbox_sent', - data: { - messageId: result.messageId, - targetSessionId: mailboxTarget.target.sessionId, - targetSessionName: mailboxTarget.target.name, - kind: 'request', - text, - disposition: result.disposition, - }, - }, - ]; - }); - } - void refreshMessages(mailboxTarget.sourceSessionId); - return true; - } catch (error) { - setMailboxDeliveryFeedback({ ...mailboxTarget, status: 'failed' }); - showSessionError( - mailboxTarget.sourceSessionId, - shellCopy.mailboxFailedTitle, - localizedShellErrorMessage(error, shellCopy.mailboxFailedFallback, uiLocale), - ); - return false; - } - } + const mailboxResult = await mailbox.sendPending( + text, + (metadata?.workspaceFileReferences?.length ?? 0) > 0, + ); + if (mailboxResult !== undefined) return mailboxResult; setNewTaskSendPending(true); try { return await sendWithAttachments(text, metadata); @@ -2138,33 +2026,9 @@ function AppShellContent({ return false; } if (slashCommand?.kind === 'send') { - const sourceSessionId = activeIdRef.current; - if (!sourceSessionId) return false; - if ( - pendingAttachments.length > 0 || - pendingQuotes.length > 0 || - (metadata?.workspaceFileReferences?.length ?? 0) > 0 - ) { - toastApi.info( - shellCopy.sideChatContextPendingTitle, - shellCopy.sideChatContextPendingDescription, - ); - return false; - } - try { - const targets = await window.maka.sessions.listMailboxTargets(sourceSessionId); - setPendingMailboxTarget(null); - setMailboxDeliveryFeedback(null); - setMailboxFlow({ sourceSessionId, targets }); - return true; - } catch (error) { - showSessionError( - sourceSessionId, - shellCopy.mailboxFailedTitle, - localizedShellErrorMessage(error, shellCopy.mailboxFailedFallback, uiLocale), - ); - return false; - } + return mailbox.openTargetPicker( + (metadata?.workspaceFileReferences?.length ?? 0) > 0, + ); } if (slashCommand?.kind === 'send_invalid') { toastApi.info(shellCopy.mailboxUsageTitle, shellCopy.mailboxUsageDescription); @@ -2833,45 +2697,6 @@ function AppShellContent({ ? navSelection.module : 'im_hub'; - const mailboxSendTargetNotice = (() => { - const feedback = mailboxDeliveryFeedback; - if (feedback && feedback.sourceSessionId === activeId) { - const name = feedback.target.name; - const dismiss = () => { - setMailboxDeliveryFeedback(null); - if (feedback.status === 'failed') setPendingMailboxTarget(null); - }; - switch (feedback.status) { - case 'sending': - return { - title: shellCopy.mailboxSendingTitle(name), - detail: shellCopy.mailboxSendingDescription, - cancelLabel: shellCopy.mailboxComposerCancel, - status: feedback.status, - onCancel: dismiss, - } as const; - case 'failed': - return { - title: shellCopy.mailboxFailedReceiptTitle(name), - detail: shellCopy.mailboxFailedReceiptDescription, - cancelLabel: shellCopy.mailboxComposerCancel, - status: feedback.status, - onCancel: dismiss, - } as const; - } - } - if (!pendingMailboxTarget || pendingMailboxTarget.sourceSessionId !== activeId) { - return undefined; - } - return { - title: shellCopy.mailboxComposerTitle(pendingMailboxTarget.target.name), - detail: shellCopy.mailboxComposerDescription, - cancelLabel: shellCopy.mailboxComposerCancel, - status: 'ready', - onCancel: () => setPendingMailboxTarget(null), - } as const; - })(); - return (
- {mailboxFlow ? ( - { - if (!open) setMailboxFlow(null); - }} - onSelect={(target) => { - setMailboxDeliveryFeedback(null); - setPendingMailboxTarget({ - sourceSessionId: mailboxFlow.sourceSessionId, - target, - }); - setMailboxFlow(null); - window.requestAnimationFrame(() => composerRef.current?.focus()); - }} - /> - ) : null} + {mailbox.picker} (); + const pendingTargetRef = useRef(undefined); const items = useMemo( () => props.targets.map((target) => ({ diff --git a/apps/desktop/src/renderer/styles/chat-message.css b/apps/desktop/src/renderer/styles/chat-message.css index 56ffe75543..70db39168c 100644 --- a/apps/desktop/src/renderer/styles/chat-message.css +++ b/apps/desktop/src/renderer/styles/chat-message.css @@ -109,9 +109,10 @@ max-width: min(620px, 78vw); overflow: hidden; padding: 0; - border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); - background: color-mix(in srgb, var(--chat-user-bg) 74%, var(--background)); - box-shadow: var(--shadow-low); + border: var(--border-width-hairline) solid var(--border); + border-radius: var(--radius-surface); + background: var(--surface-raised); + box-shadow: none; } .maka-session-mailbox-header { @@ -120,14 +121,15 @@ align-items: center; gap: var(--space-1-5); padding: var(--space-2) var(--space-3); - border-bottom: 1px solid color-mix(in srgb, var(--border) 62%, transparent); + border-bottom: var(--border-width-hairline) solid var(--info-wash-border); color: var(--muted-foreground); - background: color-mix(in srgb, var(--accent) 9%, transparent); + background: var(--info-wash); white-space: nowrap; } .maka-session-mailbox-header[data-direction='outgoing'] { - background: color-mix(in srgb, var(--success) 8%, transparent); + border-bottom-color: var(--success-wash-border); + background: var(--success-wash); } .maka-session-mailbox-eyebrow { diff --git a/apps/desktop/src/renderer/styles/composer.css b/apps/desktop/src/renderer/styles/composer.css index 496dc746ff..a3abd0f040 100644 --- a/apps/desktop/src/renderer/styles/composer.css +++ b/apps/desktop/src/renderer/styles/composer.css @@ -494,21 +494,12 @@ min-height: var(--h-control-lg); height: auto; padding: var(--space-2) var(--space-2-5); - border-radius: var(--radius-control); + border-radius: var(--radius-surface); background: var(--info-wash); border-color: var(--info-wash-border); color: var(--foreground); } -.maka-composer-mailbox-notice[data-delivery-status='delivered'] { - background: var(--success-wash); - border-color: var(--success-wash-border); -} - -.maka-composer-mailbox-notice[data-delivery-status='delivered'] > svg { - color: var(--success-text); -} - .maka-composer-mailbox-notice[data-delivery-status='failed'] { background: var(--destructive-wash); border-color: var(--destructive-wash-border); diff --git a/apps/desktop/src/renderer/use-app-shell-mailbox.tsx b/apps/desktop/src/renderer/use-app-shell-mailbox.tsx new file mode 100644 index 0000000000..5275acb482 --- /dev/null +++ b/apps/desktop/src/renderer/use-app-shell-mailbox.tsx @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type Dispatch, + type RefObject, + type SetStateAction, +} from 'react'; +import type { StoredMessage } from '@maka/core/session'; +import { sessionMailboxSentReceiptId } from '@maka/core/session-mailbox'; +import type { UiLocale } from '@maka/core/ui-locale'; +import type { SessionMailboxTarget } from '@maka/runtime-host/protocol'; +import type { ComposerHandle } from '@maka/ui'; +import { SessionMailboxPicker } from './session-mailbox-picker'; +import { getShellCopy, localizedShellErrorMessage } from './locales/shell-copy'; +import { getDesktopConversationCopy } from './locales/conversation-copy'; + +interface AppShellMailboxInput { + readonly activeSessionId?: string; + readonly uiLocale: UiLocale; + readonly revisionActive: boolean; + readonly hasPendingComposerContext: boolean; + readonly composerRef: RefObject; + readonly setMessages: Dispatch>; + readonly toastInfo: (title: string, description?: string) => void; + readonly showSessionError: (sessionId: string, title: string, description?: string) => void; +} + +interface PendingMailboxTarget { + readonly sourceSessionId: string; + readonly target: SessionMailboxTarget; +} + +export function useAppShellMailbox(input: AppShellMailboxInput) { + const { + activeSessionId, + uiLocale, + revisionActive, + hasPendingComposerContext, + composerRef, + setMessages, + toastInfo, + showSessionError, + } = input; + const copy = useMemo(() => getShellCopy(uiLocale).app, [uiLocale]); + const conversationActions = useMemo( + () => getDesktopConversationCopy(uiLocale).actions, + [uiLocale], + ); + const activeSessionIdRef = useRef(activeSessionId); + activeSessionIdRef.current = activeSessionId; + const [pickerFlow, setPickerFlow] = useState<{ + sourceSessionId: string; + targets: readonly SessionMailboxTarget[]; + } | null>(null); + const [pendingTarget, setPendingTarget] = useState(null); + const [deliveryFeedback, setDeliveryFeedback] = useState< + (PendingMailboxTarget & { status: 'sending' | 'failed' }) | null + >(null); + + useEffect(() => { + if (pendingTarget && pendingTarget.sourceSessionId !== activeSessionId) { + setPendingTarget(null); + setDeliveryFeedback(null); + } + if (deliveryFeedback && deliveryFeedback.sourceSessionId !== activeSessionId) { + setDeliveryFeedback(null); + } + if (pickerFlow && pickerFlow.sourceSessionId !== activeSessionId) { + setPickerFlow(null); + } + }, [activeSessionId, deliveryFeedback, pendingTarget, pickerFlow]); + + const openTargetPicker = useCallback(async (hasWorkspaceReferences = false) => { + const sourceSessionId = activeSessionIdRef.current; + if (!sourceSessionId) return false; + if (revisionActive) { + toastInfo( + conversationActions.revisionUnavailableTitle, + conversationActions.revisionCommandUnsupported, + ); + return false; + } + if (hasPendingComposerContext || hasWorkspaceReferences) { + toastInfo(copy.sideChatContextPendingTitle, copy.sideChatContextPendingDescription); + return false; + } + try { + const targets = await window.maka.sessions.listMailboxTargets(sourceSessionId); + setPendingTarget(null); + setDeliveryFeedback(null); + setPickerFlow({ sourceSessionId, targets }); + return true; + } catch (error) { + showSessionError( + sourceSessionId, + copy.mailboxFailedTitle, + localizedShellErrorMessage(error, copy.mailboxFailedFallback, uiLocale), + ); + return false; + } + }, [ + conversationActions, + copy, + hasPendingComposerContext, + revisionActive, + showSessionError, + toastInfo, + uiLocale, + ]); + + const sendPending = useCallback(async ( + text: string, + hasWorkspaceReferences = false, + ): Promise => { + if (!pendingTarget) return undefined; + if (activeSessionIdRef.current !== pendingTarget.sourceSessionId) { + setPendingTarget(null); + setDeliveryFeedback(null); + return false; + } + if (hasPendingComposerContext || hasWorkspaceReferences) { + toastInfo(copy.sideChatContextPendingTitle, copy.sideChatContextPendingDescription); + return false; + } + + setDeliveryFeedback({ ...pendingTarget, status: 'sending' }); + try { + const result = await window.maka.sessions.sendMailboxMessage( + pendingTarget.sourceSessionId, + pendingTarget.target.sessionId, + text, + ); + setPendingTarget(null); + setDeliveryFeedback(null); + const receiptId = sessionMailboxSentReceiptId(result.messageId); + if (activeSessionIdRef.current === pendingTarget.sourceSessionId) { + setMessages((current) => { + if (current.some((message) => message.id === receiptId)) return current; + let anchorTurnId: string | undefined; + for (let index = current.length - 1; index >= 0; index -= 1) { + anchorTurnId = current[index]?.turnId; + if (anchorTurnId) break; + } + return [...current, { + type: 'system_note', + id: receiptId, + ...(anchorTurnId ? { turnId: anchorTurnId } : {}), + ts: Date.now(), + kind: 'session_mailbox_sent', + data: { + messageId: result.messageId, + targetSessionId: pendingTarget.target.sessionId, + targetSessionName: pendingTarget.target.name, + kind: 'request', + text, + disposition: result.disposition, + ...(result.turnId ? { turnId: result.turnId } : {}), + }, + }]; + }); + } + return true; + } catch (error) { + setDeliveryFeedback({ ...pendingTarget, status: 'failed' }); + showSessionError( + pendingTarget.sourceSessionId, + copy.mailboxFailedTitle, + localizedShellErrorMessage(error, copy.mailboxFailedFallback, uiLocale), + ); + return false; + } + }, [ + copy, + hasPendingComposerContext, + pendingTarget, + setMessages, + showSessionError, + toastInfo, + uiLocale, + ]); + + const sendTargetNotice = useMemo(() => { + if (deliveryFeedback && deliveryFeedback.sourceSessionId === activeSessionId) { + const dismiss = () => { + setDeliveryFeedback(null); + if (deliveryFeedback.status === 'failed') setPendingTarget(null); + }; + return deliveryFeedback.status === 'sending' + ? { + title: copy.mailboxSendingTitle(deliveryFeedback.target.name), + detail: copy.mailboxSendingDescription, + cancelLabel: copy.mailboxComposerCancel, + status: 'sending' as const, + onCancel: dismiss, + } + : { + title: copy.mailboxFailedReceiptTitle(deliveryFeedback.target.name), + detail: copy.mailboxFailedReceiptDescription, + cancelLabel: copy.mailboxComposerCancel, + status: 'failed' as const, + onCancel: dismiss, + }; + } + if (!pendingTarget || pendingTarget.sourceSessionId !== activeSessionId) return undefined; + return { + title: copy.mailboxComposerTitle(pendingTarget.target.name), + detail: copy.mailboxComposerDescription, + cancelLabel: copy.mailboxComposerCancel, + status: 'ready' as const, + onCancel: () => setPendingTarget(null), + }; + }, [activeSessionId, copy, deliveryFeedback, pendingTarget]); + + const picker = pickerFlow ? ( + { + if (!open) setPickerFlow(null); + }} + onSelect={(target) => { + setDeliveryFeedback(null); + setPendingTarget({ sourceSessionId: pickerFlow.sourceSessionId, target }); + setPickerFlow(null); + window.requestAnimationFrame(() => composerRef.current?.focus()); + }} + /> + ) : null; + + return { + openTargetPicker, + sendPending, + sendTargetNotice, + picker, + modalOpen: pickerFlow !== null, + } as const; +} diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 84d559537f..b50e6c40e8 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -39,7 +39,11 @@ import { type SessionSummary, type StoredMessage } from '@maka/core/session'; import { type ThinkingLevel } from '@maka/core/model-thinking'; import { type UserQuestionResponse } from '@maka/core/user-question'; import type { SkillInvocationResult } from '@maka/core/skill-invocation'; -import type { AgentGraphClientSnapshot } from '@maka/runtime-host/protocol'; +import type { + AgentGraphClientSnapshot, + SessionMailboxSendResult, + SessionMailboxTarget, +} from '@maka/runtime-host/protocol'; import { SessionActivityRegistry } from '@maka/runtime/goal-turn-lifecycle'; import { type ContextDiagnostics } from '@maka/runtime/context-diagnostics'; import type { GoalProjection } from '@maka/runtime-host/protocol'; @@ -3034,6 +3038,57 @@ describe('Maka Pi TUI runner', () => { ]); }); + test('/send opens a searchable Session picker and sends the next composed message', async () => { + const terminal = new FakeTerminal(); + const driver = new SlashCommandDriver(); + driver.mailboxTargets.push( + { sessionId: 'session-alpha', name: 'Alpha task', status: 'idle' }, + { sessionId: 'session-needle', name: 'Needle review task', status: 'running' }, + ); + const run = runMakaPiTui({ + title: 'Maka', + driver, + cwd: '/repo', + model: 'gpt-5.5', + connectionSlug: 'openai', + permissionMode: 'ask', + terminal, + }); + + await waitForTuiPaint(terminal); + terminal.input('/send'); + terminal.input('\r'); + await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('Send to Session')); + await waitFor(() => { + const out = plainTerminalOutput(terminal.screenOutput()); + return out.includes('Alpha task') && out.includes('Needle review task'); + }); + + terminal.input('needle'); + await waitFor(() => { + const out = plainTerminalOutput(terminal.screenOutput()); + return out.includes('Needle review task') && !out.includes('Alpha task'); + }); + terminal.input('\r'); + terminal.input('Please inspect the recovery path'); + terminal.input('\r'); + await waitFor(() => driver.mailboxSends.length === 1); + assert.deepEqual(driver.mailboxSends, [ + { + targetSessionId: 'session-needle', + text: 'Please inspect the recovery path', + }, + ]); + + exitMaka(terminal); + await Promise.race([ + run, + delay(CLOSE_BUDGET_MS).then(() => { + throw new Error('TUI did not close during test cleanup'); + }), + ]); + }); + test('switches models in a fresh conversation without a cache warning', async () => { const terminal = new FakeTerminal(); const driver = new SlashCommandDriver(); @@ -7324,6 +7379,8 @@ class SlashCommandDriver implements MakaSessionDriver { readonly sessionSwitchOptions: Array = []; readonly renames: string[] = []; readonly moves: string[] = []; + readonly mailboxTargets: SessionMailboxTarget[] = []; + readonly mailboxSends: Array<{ targetSessionId: string; text: string }> = []; startNewSessionCalls = 0; resumeCalls = 0; contextDiagnosticsRequests = 0; @@ -7480,6 +7537,21 @@ class SlashCommandDriver implements MakaSessionDriver { oldCwdDirty: true, }; } + async listMailboxTargets(): Promise { + return this.mailboxTargets; + } + async sendMailboxMessage( + targetSessionId: string, + text: string, + ): Promise { + this.mailboxSends.push({ targetSessionId, text }); + return { + messageId: `mailbox-${this.mailboxSends.length}`, + targetSessionId, + disposition: 'turn_started', + turnId: `mailbox-turn-${this.mailboxSends.length}`, + }; + } async setPermissionMode(mode: PermissionMode): Promise { this.permissionModes.push(mode); this.activeBoundaryDisplayMode = mode; diff --git a/packages/cli/src/pi-tui-pickers.ts b/packages/cli/src/pi-tui-pickers.ts index 18dae33449..de20b8b17a 100644 --- a/packages/cli/src/pi-tui-pickers.ts +++ b/packages/cli/src/pi-tui-pickers.ts @@ -750,6 +750,108 @@ export class ModelSearchOverlay implements Component { } } +export interface SearchableSelectOverlayInput { + readonly title: string; + readonly rightLabel: string; + readonly items: readonly SelectItem[]; + readonly hint?: string; + readonly searchLabel?: string; + readonly emptyLabel?: string; + readonly onSelect: (item: SelectItem) => void; + readonly onCancel: () => void; +} + +/** A compact searchable single-select used by command pickers such as `/send`. */ +export class SearchableSelectOverlay implements Component { + private readonly searchEditor: Editor; + private filtered: readonly SelectItem[]; + private list: SelectList; + + constructor( + tui: TUI, + private readonly input: SearchableSelectOverlayInput, + ) { + this.filtered = input.items; + this.list = this.buildList(); + this.searchEditor = new Editor(tui, editorTheme(), { paddingX: 0 }); + this.searchEditor.onChange = (text) => this.applyQuery(text); + } + + private buildList(): SelectList { + const list = new SelectList([...this.filtered], 10, selectListTheme(), { + minPrimaryColumnWidth: 24, + maxPrimaryColumnWidth: 56, + }); + list.onSelect = (selected) => this.input.onSelect(selected); + list.onCancel = () => this.input.onCancel(); + return list; + } + + private applyQuery(text: string): void { + const query = text.trim().toLocaleLowerCase(); + this.filtered = query + ? this.input.items.filter((item) => + [item.label, item.description, item.value] + .filter((value): value is string => typeof value === 'string') + .some((value) => value.toLocaleLowerCase().includes(query)), + ) + : this.input.items; + this.list = this.buildList(); + } + + invalidate(): void { + this.searchEditor.invalidate(); + this.list.invalidate(); + } + + handleInput(data: string): void { + if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) { + this.input.onCancel(); + return; + } + if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) { + this.list.handleInput(data); + return; + } + if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) { + if (!isKeyRepeat(data) && this.filtered.length > 0) this.list.handleInput(data); + return; + } + this.searchEditor.handleInput(data); + } + + render(width: number): string[] { + const safeWidth = Math.max(1, width); + const searchLabel = this.input.searchLabel ?? '搜索'; + this.searchEditor.focused = true; + return [ + padLine( + `${this.input.title} ${ansi.accent(this.input.rightLabel)} ${ansi.dim(String(this.filtered.length))}`, + safeWidth, + ), + padLine(this.input.hint ?? '输入搜索 · ↑↓ 选择 · Enter 确认 · Esc 取消', safeWidth), + padLine('', safeWidth), + ...this.renderFieldRow(this.searchEditor, searchLabel, safeWidth), + padLine('', safeWidth), + ...(this.filtered.length === 0 + ? [padLine(ansi.dim(this.input.emptyLabel ?? '没有匹配的会话'), safeWidth)] + : this.list.render(safeWidth).map((line) => formatPickerItemLine(line, safeWidth))), + padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth), + ]; + } + + private renderFieldRow(editor: Editor, label: string, width: number): string[] { + const prefix = `${label} `; + const prefixWidth = visibleWidth(prefix); + const editorLines = editor.render(Math.max(1, width - prefixWidth)).slice(1, -1); + return editorLines.length === 0 + ? [padLine(prefix, width)] + : editorLines.map((line, index) => + padLine(`${index === 0 ? prefix : ' '.repeat(prefixWidth)}${line}`, width), + ); + } +} + /** * #1611: `current` marks an option that is genuinely in force, so choosing it * is a no-op. A read-only session is neither of these options, and marking diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 9cfa04f0bd..67d1288bf5 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -134,6 +134,7 @@ import { ModelSearchOverlay, OnboardingWizard, PickerOverlay, + SearchableSelectOverlay, UserQuestionOverlay, modelPickerItems, permissionModePickerItems, @@ -3306,35 +3307,31 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { requestRender(); return; } - showSelectPicker( - 'Send to Session', - 'Choose Session', - targets.map((target) => ({ - value: target.sessionId, - label: target.name, - description: - target.status === 'idle' - ? 'idle · deliver now' - : `${target.status.replaceAll('_', ' ')} · queue for next turn`, - })), - (item) => { + const items = targets.map((target) => ({ + value: target.sessionId, + label: target.name, + description: + target.status === 'idle' + ? 'idle · deliver now' + : `${target.status.replaceAll('_', ' ')} · queue for next turn`, + })); + let overlay: OverlayHandle | undefined; + const picker = new SearchableSelectOverlay(tui, { + title: 'Send to Session', + rightLabel: 'Choose Session', + items, + hint: 'type to search · ↑↓ move · Enter choose · Esc cancel', + onSelect: (item) => { + overlay?.hide(); const target = targets.find((candidate) => candidate.sessionId === item.value); if (!target) return; pendingMailboxTarget = target; editor.setText(''); - state.entries.push({ - kind: 'notice', - level: 'info', - text: `Sending the next message to ${target.name}. Type it and press Enter.`, - }); requestRender(); }, - { - minPrimaryColumnWidth: 24, - maxPrimaryColumnWidth: 56, - hint: '↑↓ move · Enter choose · Esc cancel', - }, - ); + onCancel: () => overlay?.hide(), + }); + overlay = showBottomPicker(picker); }) .catch(reportError); }, @@ -3436,11 +3433,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { return { consume: true }; } if (tui.hasOverlay()) return undefined; - if ( - pendingMailboxTarget - && matchesKey(data, Key.escape) - && editor.getText().length === 0 - ) { + if (pendingMailboxTarget && matchesKey(data, Key.escape) && editor.getText().length === 0) { const targetName = pendingMailboxTarget.name; pendingMailboxTarget = undefined; state.entries.push({ diff --git a/packages/core/package.json b/packages/core/package.json index 21fb9ebedc..1855822ced 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -108,6 +108,7 @@ "./usage-record-schema": "./dist/usage-record-schema.js", "./session-send-projection": "./dist/session-send-projection.js", "./session-mailbox": "./dist/session-mailbox.js", + "./turn-origin": "./dist/turn-origin.js", "./session-name": "./dist/session-name.js", "./tool-catalog": "./dist/tool-catalog.js", "./thread-search": "./dist/thread-search.js", diff --git a/packages/core/src/__tests__/runtime-event.test.ts b/packages/core/src/__tests__/runtime-event.test.ts index 120d2eac3a..85166a52d7 100644 --- a/packages/core/src/__tests__/runtime-event.test.ts +++ b/packages/core/src/__tests__/runtime-event.test.ts @@ -117,6 +117,14 @@ test('shares one decoder across all TurnOrigin variants', () => { { kind: 'scheduled_task', scheduledTaskId: 'task-1' }, { kind: 'goal', goalId: 'goal-1' }, { kind: 'agent_graph', graphId: 'graph-1', wakeId: 'wake-1', attemptId: 'attempt-1' }, + { + kind: 'session_mailbox', + messageId: 'mail-1', + fromSessionId: 'source', + fromSessionName: 'Source', + toSessionId: 'target', + mailboxKind: 'request', + }, ] as const; for (const origin of origins) assert.deepEqual(decodeTurnOrigin(origin), origin); assert.deepEqual(decodeTurnOrigin({ kind: 'automation', automationId: 'automation-1' }), { diff --git a/packages/core/src/__tests__/session-mailbox.test.ts b/packages/core/src/__tests__/session-mailbox.test.ts index 59552c407d..b628e8bf86 100644 --- a/packages/core/src/__tests__/session-mailbox.test.ts +++ b/packages/core/src/__tests__/session-mailbox.test.ts @@ -21,8 +21,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { parseSessionMailboxMessageContent, + parseSessionMailboxOutboxNoteData, parseSessionMailboxSentNoteData, + parseTrustedSessionMailboxMessage, sessionMailboxMessageContent, + sessionMailboxTurnOrigin, } from '../session-mailbox.js'; test('Session mailbox content preserves a compact display and escapes model envelope text', () => { @@ -73,6 +76,58 @@ test('Session mailbox display parsing does not treat ordinary From text as an en assert.equal(parseSessionMailboxMessageContent({ text: '' }), undefined); }); +test('Session mailbox display requires matching typed Host provenance', () => { + const envelope = { + messageId: 'message-3', + fromSessionId: 'source-1', + fromSessionName: 'Source', + toSessionId: 'target-1', + kind: 'notification' as const, + text: 'Hello', + }; + const content = sessionMailboxMessageContent(envelope); + assert.equal(parseTrustedSessionMailboxMessage({ text: content.text }), undefined); + assert.equal( + parseTrustedSessionMailboxMessage({ + text: content.text, + origin: { + kind: 'session_mailbox', + messageId: envelope.messageId, + fromSessionId: 'forged', + fromSessionName: envelope.fromSessionName, + toSessionId: envelope.toSessionId, + mailboxKind: envelope.kind, + }, + }), + undefined, + ); + assert.deepEqual( + parseTrustedSessionMailboxMessage({ + text: content.text, + origin: sessionMailboxTurnOrigin(envelope), + }), + { direction: 'incoming', ...envelope }, + ); +}); + +test('Session mailbox outbox-note data requires a durable attempt epoch', () => { + const data = { + originHostEpoch: 'epoch-1', + messageId: 'message-1', + fromSessionId: 'source-1', + fromSessionName: 'Source', + toSessionId: 'target-1', + targetSessionName: 'Target', + kind: 'request' as const, + text: 'Hello', + }; + assert.deepEqual(parseSessionMailboxOutboxNoteData(data), data); + assert.equal( + parseSessionMailboxOutboxNoteData({ ...data, originHostEpoch: undefined }), + undefined, + ); +}); + test('Session mailbox sent-note data requires a complete delivery receipt', () => { assert.deepEqual( parseSessionMailboxSentNoteData({ diff --git a/packages/core/src/agent-run.ts b/packages/core/src/agent-run.ts index c2e9b83a60..3f8227260e 100644 --- a/packages/core/src/agent-run.ts +++ b/packages/core/src/agent-run.ts @@ -43,6 +43,7 @@ import { import type { AgentGraphIntentClaim } from './agent-graph-control.js'; import { isToolMode, type ToolMode } from './tool-mode.js'; import { decodeRunCompositionSnapshot, type RunCompositionSnapshot } from './run-composition.js'; +import type { TurnOrigin } from './turn-origin.js'; export const AGENT_RUN_STATUSES = [ 'created', @@ -79,6 +80,8 @@ export type RootExecutionDescriptor = kind: 'external_message'; inputDigest?: `sha256:${string}`; maxSteps?: number; + /** Host-authored source for non-user messages admitted through the message authority. */ + origin?: TurnOrigin; } | { kind: 'regenerate'; sourceTurnId: string } | { kind: 'context_compact' } diff --git a/packages/core/src/session-mailbox.ts b/packages/core/src/session-mailbox.ts index 9041b0d3f6..48f06e221f 100644 --- a/packages/core/src/session-mailbox.ts +++ b/packages/core/src/session-mailbox.ts @@ -18,6 +18,7 @@ */ import type { MessageContent } from './events.js'; +import type { TurnOrigin } from './turn-origin.js'; export const SESSION_MAILBOX_TEXT_MAX_BYTES = 16_000; export const SESSION_MAILBOX_KINDS = ['request', 'reply', 'notification'] as const; @@ -51,12 +52,35 @@ export interface SessionMailboxSentNoteData { readonly kind: SessionMailboxKind; readonly text: string; readonly disposition: 'turn_started' | 'queued'; + readonly turnId?: string; +} + +/** Durable sender-side intent written before target admission begins. */ +export interface SessionMailboxOutboxNoteData extends SessionMailboxEnvelope { + readonly originHostEpoch: string; + readonly targetSessionName: string; } export function sessionMailboxSentReceiptId(messageId: string): string { return `session-mailbox-sent:${messageId}`; } +export function sessionMailboxOutboxAttemptId(messageId: string, originHostEpoch: string): string { + return `session-mailbox-outbox:${messageId}:${originHostEpoch}`; +} + +export function sessionMailboxTurnOrigin(envelope: SessionMailboxEnvelope): TurnOrigin { + return { + kind: 'session_mailbox', + messageId: envelope.messageId, + fromSessionId: envelope.fromSessionId, + fromSessionName: envelope.fromSessionName, + toSessionId: envelope.toSessionId, + mailboxKind: envelope.kind, + ...(envelope.correlationId ? { correlationId: envelope.correlationId } : {}), + }; +} + /** * Session messages are ordinary canonical user messages at the execution * boundary. The model-facing text carries an explicit Host-authored @@ -129,6 +153,9 @@ export function parseSessionMailboxMessageContent( `in_reply_to="${messageId}", text=...).`; if (!body.endsWith(replyInstruction)) return undefined; body = body.slice(0, -replyInstruction.length); + } else { + if (!body.endsWith('\n')) return undefined; + body = body.slice(0, -1); } return { @@ -145,6 +172,57 @@ export function parseSessionMailboxMessageContent( }; } +/** + * Projects a mailbox envelope only when the durable UserMessage carries the + * matching Runtime Host-authored origin. Text syntax alone is never provenance. + */ +export function parseTrustedSessionMailboxMessage(input: { + readonly text: string; + readonly origin?: TurnOrigin; +}): SessionMailboxIncomingDisplay | undefined { + if (input.origin?.kind !== 'session_mailbox') return undefined; + const parsed = parseSessionMailboxMessageContent(input); + if (!parsed) return undefined; + return parsed.messageId === input.origin.messageId && + parsed.fromSessionId === input.origin.fromSessionId && + parsed.fromSessionName === input.origin.fromSessionName && + parsed.toSessionId === input.origin.toSessionId && + parsed.kind === input.origin.mailboxKind && + parsed.correlationId === input.origin.correlationId + ? parsed + : undefined; +} + +export function parseSessionMailboxOutboxNoteData( + value: unknown, +): SessionMailboxOutboxNoteData | undefined { + if (!isRecord(value)) return undefined; + if ( + typeof value.originHostEpoch !== 'string' || + typeof value.messageId !== 'string' || + typeof value.fromSessionId !== 'string' || + typeof value.fromSessionName !== 'string' || + typeof value.toSessionId !== 'string' || + typeof value.targetSessionName !== 'string' || + typeof value.text !== 'string' || + !SESSION_MAILBOX_KINDS.includes(value.kind as SessionMailboxKind) || + (value.correlationId !== undefined && typeof value.correlationId !== 'string') + ) { + return undefined; + } + return { + originHostEpoch: value.originHostEpoch, + messageId: value.messageId, + fromSessionId: value.fromSessionId, + fromSessionName: value.fromSessionName, + toSessionId: value.toSessionId, + targetSessionName: value.targetSessionName, + kind: value.kind as SessionMailboxKind, + text: value.text, + ...(value.correlationId !== undefined ? { correlationId: value.correlationId } : {}), + }; +} + export function parseSessionMailboxSentNoteData( value: unknown, ): SessionMailboxSentNoteData | undefined { @@ -155,7 +233,8 @@ export function parseSessionMailboxSentNoteData( typeof value.targetSessionName !== 'string' || typeof value.text !== 'string' || !SESSION_MAILBOX_KINDS.includes(value.kind as SessionMailboxKind) || - (value.disposition !== 'turn_started' && value.disposition !== 'queued') + (value.disposition !== 'turn_started' && value.disposition !== 'queued') || + (value.turnId !== undefined && typeof value.turnId !== 'string') ) { return undefined; } @@ -166,6 +245,7 @@ export function parseSessionMailboxSentNoteData( kind: value.kind as SessionMailboxKind, text: value.text, disposition: value.disposition, + ...(value.turnId !== undefined ? { turnId: value.turnId } : {}), }; } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index baf325804b..1424f2dc78 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -923,6 +923,7 @@ export interface SystemNoteMessage { | 'context_compacted' | 'context_compaction_failed_open' | 'step_limit' + | 'session_mailbox_outbox' | 'session_mailbox_sent' | 'error' | 'abort'; @@ -1027,6 +1028,7 @@ const SYSTEM_NOTE_KINDS = new Set([ 'context_compacted', 'context_compaction_failed_open', 'step_limit', + 'session_mailbox_outbox', 'session_mailbox_sent', 'error', 'abort', diff --git a/packages/core/src/turn-origin.ts b/packages/core/src/turn-origin.ts index 854a51631d..37f3c75004 100644 --- a/packages/core/src/turn-origin.ts +++ b/packages/core/src/turn-origin.ts @@ -31,12 +31,23 @@ export type TurnOrigin = wakeId: string; /** Durable identity of one delivery attempt for the wake. */ attemptId: string; + } + | { + /** Runtime Host-authored provenance for one cross-Session mailbox message. */ + kind: 'session_mailbox'; + messageId: string; + fromSessionId: string; + fromSessionName: string; + toSessionId: string; + mailboxKind: 'request' | 'reply' | 'notification'; + correlationId?: string; }; type ScheduledTaskOrigin = Extract; type LegacyAutomationOrigin = Extract; type GoalOrigin = Extract; type AgentGraphOrigin = Extract; +type SessionMailboxOrigin = Extract; const SCHEDULED_TASK_ORIGIN_SHAPE = defineObjectShape()( ['kind', 'scheduledTaskId'], @@ -51,6 +62,10 @@ const AGENT_GRAPH_ORIGIN_SHAPE = defineObjectShape()( ['kind', 'graphId', 'wakeId', 'attemptId'], [], ); +const SESSION_MAILBOX_ORIGIN_SHAPE = defineObjectShape()( + ['kind', 'messageId', 'fromSessionId', 'fromSessionName', 'toSessionId', 'mailboxKind'], + ['correlationId'], +); /** Decode a persisted or runtime turn origin, normalizing released Automation rows. */ export function decodeTurnOrigin(value: unknown): TurnOrigin | undefined { @@ -90,5 +105,27 @@ export function decodeTurnOrigin(value: unknown): TurnOrigin | undefined { attemptId: value.attemptId, }; } + if ( + hasExactShape(value, SESSION_MAILBOX_ORIGIN_SHAPE) && + value.kind === 'session_mailbox' && + typeof value.messageId === 'string' && + typeof value.fromSessionId === 'string' && + typeof value.fromSessionName === 'string' && + typeof value.toSessionId === 'string' && + (value.mailboxKind === 'request' || + value.mailboxKind === 'reply' || + value.mailboxKind === 'notification') && + (value.correlationId === undefined || typeof value.correlationId === 'string') + ) { + return { + kind: 'session_mailbox', + messageId: value.messageId, + fromSessionId: value.fromSessionId, + fromSessionName: value.fromSessionName, + toSessionId: value.toSessionId, + mailboxKind: value.mailboxKind, + ...(value.correlationId !== undefined ? { correlationId: value.correlationId } : {}), + }; + } return undefined; } diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts index 1f3b73161e..2b931db90c 100644 --- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts @@ -1221,6 +1221,9 @@ test('production Host executes a canonical ai-sdk Session against a real provide 'memory_extract', 'memory_remember', 'request_sandbox_boundary', + 'session_list', + 'session_reply', + 'session_send', 'task_create', 'task_get', 'task_list', diff --git a/packages/runtime-host/src/__tests__/message-coordinator.test.ts b/packages/runtime-host/src/__tests__/message-coordinator.test.ts index 934227bd5a..a7b1425d87 100644 --- a/packages/runtime-host/src/__tests__/message-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/message-coordinator.test.ts @@ -73,6 +73,48 @@ test('idle submit starts exactly one root Turn and retry identity is connection- assert.equal(fixture.liveResidencies(), 0); }); +test('trusted submit receipts bind mailbox provenance across reconciliation', async () => { + const fixture = createFixture(); + fixture.coordinator.reserveRootTurn(ROOT); + const owner = fixture.coordinator.bindRun(ROOT); + const input = { + originHostEpoch: 'epoch-1', + sessionId: ROOT.sessionId, + messageId: 'mailbox-message', + content: { text: 'trusted mailbox payload' }, + placement: 'next_turn', + } as const; + const origin = { + kind: 'session_mailbox', + messageId: 'mailbox-message', + fromSessionId: 'source', + fromSessionName: 'Source', + toSessionId: ROOT.sessionId, + mailboxKind: 'request', + } as const; + + const submitted = await fixture.coordinator.submitTrusted(input, operationContext(), origin); + assert.equal(submitted.ok, true); + assert.deepEqual(await fixture.coordinator.reconcileTrustedSubmit(input, origin), submitted); + const conflict = await fixture.coordinator.reconcileTrustedSubmit(input, { + ...origin, + fromSessionId: 'forged', + }); + assert.equal(conflict?.ok, false); + const publicRetry = await fixture.coordinator.handlers['turn.message.submit']( + input, + operationContext(), + ); + assert.equal(publicRetry.ok, false); + + owner.release(); + const batch = fixture.coordinator.beginTerminalTransition(ROOT); + assert.deepEqual(batch.sources[0]?.origin, origin); + const nextRoot = { sessionId: ROOT.sessionId, turnId: 'mailbox-turn', runId: 'mailbox-run' }; + fixture.coordinator.commitNextRoot(batch, nextRoot); + fixture.coordinator.abandonRootReservation(nextRoot); +}); + test('submit re-runs admission when the queue revision moves during preflight', async () => { let preflightCalls = 0; const fixture = createFixture(undefined, async () => { diff --git a/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts index 965ef67b0e..131688a841 100644 --- a/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts @@ -24,7 +24,7 @@ import type { HostMessageCoordinator } from '../server/message-coordinator.js'; import type { ConnectionContext } from '../server/operation-dispatcher.js'; import { HostSessionMailboxCoordinator } from '../server/session-mailbox-coordinator.js'; -test('Session mailbox exposes only ordinary Sessions in the source workspace', async () => { +test('Session mailbox exposes only ordinary Sessions in the source project', async () => { const coordinator = mailbox([ session('source', '/one', { projectId: 'project-1' }), session('target', '/one', { projectId: 'project-1', runningTurnIds: ['turn-live'] }), @@ -56,17 +56,21 @@ test('Session mailbox exposes only ordinary Sessions in the source workspace', a test('Session mailbox submits a next-turn message with Host provenance', async () => { let submitted: unknown; - const stored: StoredMessage[] = [{ - type: 'system_note', - id: 'start', - turnId: 'source-turn', - ts: 1, - kind: 'session_start', - }]; + let submittedOrigin: unknown; + const stored: StoredMessage[] = [ + { + type: 'system_note', + id: 'start', + turnId: 'source-turn', + ts: 1, + kind: 'session_start', + }, + ]; const coordinator = mailbox( [session('source', '/one'), session('target', '/one')], - async (input) => { + async (input, origin) => { submitted = input; + submittedOrigin = origin; return { ok: true, result: { disposition: 'turn_started', turnId: 'turn-2' } }; }, stored, @@ -110,6 +114,18 @@ test('Session mailbox submits a next-turn message with Host provenance', async ( }, placement: 'next_turn', }); + assert.deepEqual(submittedOrigin, { + kind: 'session_mailbox', + messageId: 'message-1', + fromSessionId: 'source', + fromSessionName: 'source', + toSessionId: 'target', + mailboxKind: 'request', + }); + assert.equal(stored[1]?.type, 'system_note'); + if (stored[1]?.type === 'system_note') { + assert.equal(stored[1].kind, 'session_mailbox_outbox'); + } assert.deepEqual(stored.at(-1), { type: 'system_note', id: 'session-mailbox-sent:message-1', @@ -123,11 +139,12 @@ test('Session mailbox submits a next-turn message with Host provenance', async ( kind: 'request', text: 'Please inspect this', disposition: 'turn_started', + turnId: 'turn-2', }, }); }); -test('Session mailbox rejects self-send and cross-workspace targets', async () => { +test('Session mailbox rejects self-send and cross-project targets', async () => { const coordinator = mailbox([session('source', '/one'), session('target', '/two')]); const self = await coordinator.handlers['session.mailbox.send']( { @@ -139,7 +156,7 @@ test('Session mailbox rejects self-send and cross-workspace targets', async () = }, connection(), ); - const crossWorkspace = await coordinator.handlers['session.mailbox.send']( + const crossProject = await coordinator.handlers['session.mailbox.send']( { sourceSessionId: 'source', targetSessionId: 'target', @@ -152,21 +169,144 @@ test('Session mailbox rejects self-send and cross-workspace targets', async () = assert.equal(self.ok, false); if (!self.ok) assert.equal(self.error.code, 'invalid_request'); - assert.equal(crossWorkspace.ok, false); - if (!crossWorkspace.ok) assert.equal(crossWorkspace.error.code, 'not_found'); + assert.equal(crossProject.ok, false); + if (!crossProject.ok) assert.equal(crossProject.error.code, 'not_found'); +}); + +test('Session mailbox never admits a target message before the sender outbox is durable', async () => { + let submitCount = 0; + const coordinator = new HostSessionMailboxCoordinator({ + hostEpoch: 'epoch-1', + messages: { + submitTrusted: async () => { + submitCount += 1; + return { ok: true, result: { disposition: 'followup', queueRevision: 1 } }; + }, + reconcileTrustedSubmit: async () => undefined, + } as unknown as Pick, + listSessions: async () => [session('source', '/one'), session('target', '/one')], + sessionStore: { + readMessagesSnapshot: async () => [], + appendMessage: async () => { + throw new Error('disk unavailable'); + }, + }, + }); + + await assert.rejects( + coordinator.handlers['session.mailbox.send']( + { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'message-1', + kind: 'request', + text: 'Do not lose me', + }, + connection(), + ), + /disk unavailable/, + ); + assert.equal(submitCount, 0); +}); + +test('Session mailbox recovery settles a durable outbox from target proof without redelivery', async () => { + const sessions = [session('source', '/one'), session('target', '/one')]; + const stored = new Map([ + ['source', []], + ['target', []], + ]); + let receiptAppendFails = true; + let initialSubmitCount = 0; + const store = { + readMessagesSnapshot: async (sessionId: string) => stored.get(sessionId) ?? [], + appendMessage: async (sessionId: string, message: StoredMessage) => { + if ( + receiptAppendFails && + message.type === 'system_note' && + message.kind === 'session_mailbox_sent' + ) { + throw new Error('receipt write failed'); + } + stored.get(sessionId)?.push(message); + }, + }; + const initial = new HostSessionMailboxCoordinator({ + hostEpoch: 'epoch-1', + messages: { + submitTrusted: async () => { + initialSubmitCount += 1; + return { ok: true, result: { disposition: 'turn_started', turnId: 'target-turn' } }; + }, + reconcileTrustedSubmit: async () => undefined, + } as unknown as Pick, + listSessions: async () => sessions, + sessionStore: store, + now: () => 10, + }); + + const sent = await initial.handlers['session.mailbox.send']( + { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'message-1', + kind: 'request', + text: 'Recover the receipt', + }, + connection(), + ); + assert.equal(sent.ok, true); + assert.equal(initialSubmitCount, 1); + assert.deepEqual( + stored + .get('source') + ?.map((message) => (message.type === 'system_note' ? message.kind : message.type)), + ['session_mailbox_outbox'], + ); + + receiptAppendFails = false; + let recoverySubmitCount = 0; + let reconcileCount = 0; + const recovered = new HostSessionMailboxCoordinator({ + hostEpoch: 'epoch-2', + messages: { + submitTrusted: async () => { + recoverySubmitCount += 1; + return { ok: true, result: { disposition: 'turn_started', turnId: 'duplicate' } }; + }, + reconcileTrustedSubmit: async () => { + reconcileCount += 1; + return { ok: true, result: { disposition: 'turn_started', turnId: 'target-turn' } }; + }, + } as unknown as Pick, + listSessions: async () => sessions, + sessionStore: store, + now: () => 20, + }); + await recovered.recover(); + + assert.equal(reconcileCount, 1); + assert.equal(recoverySubmitCount, 0); + assert.deepEqual( + stored + .get('source') + ?.map((message) => (message.type === 'system_note' ? message.kind : message.type)), + ['session_mailbox_outbox', 'session_mailbox_sent'], + ); }); function mailbox( sessions: SessionSummary[], - submit: (input: unknown) => Promise = async () => ({ + submit: (input: unknown, origin?: unknown) => Promise = async () => ({ ok: true, result: { disposition: 'followup', queueRevision: 1 }, }), stored: StoredMessage[] = [], ): HostSessionMailboxCoordinator { const messages = { - handlers: { 'turn.message.submit': submit }, - } as unknown as Pick; + submitTrusted: async (input: unknown, _context: unknown, origin: unknown) => + submit(input, origin), + reconcileTrustedSubmit: async () => undefined, + } as unknown as Pick; return new HostSessionMailboxCoordinator({ hostEpoch: 'epoch-1', messages, diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index 2497c48dbd..69f74fc20a 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -1501,6 +1501,7 @@ export async function createExecutionRuntimeHostComposition( ), ); await coordinator.recover(); + await requireSessionMailbox(sessionMailbox).recover(); rootRecoveryCompleted = true; }, }, diff --git a/packages/runtime-host/src/server/hosted-execution-recovery.ts b/packages/runtime-host/src/server/hosted-execution-recovery.ts index 5187583b16..58e272d09c 100644 --- a/packages/runtime-host/src/server/hosted-execution-recovery.ts +++ b/packages/runtime-host/src/server/hosted-execution-recovery.ts @@ -215,6 +215,8 @@ export function requireHostedExecutionMessageContent(admission: RootTurnAdmissio export function hostedExecutionMessageOrigin(execution: RootExecutionDescriptor) { switch (execution.kind) { + case 'external_message': + return execution.origin; case 'scheduled_task': return { kind: 'scheduled_task' as const, diff --git a/packages/runtime-host/src/server/message-coordinator.ts b/packages/runtime-host/src/server/message-coordinator.ts index 8d8ac47e9b..5fa3ebbb6b 100644 --- a/packages/runtime-host/src/server/message-coordinator.ts +++ b/packages/runtime-host/src/server/message-coordinator.ts @@ -27,6 +27,7 @@ import { type MessageContent, } from '@maka/core/events'; import type { RuntimeEvent } from '@maka/core/runtime-event'; +import { decodeTurnOrigin, type TurnOrigin } from '@maka/core/turn-origin'; import { RuntimeMessageAuthorityInvariantError, type RuntimeMessageAuthority, @@ -186,6 +187,7 @@ export type CandidateSnapshotPreflight = ( interface LiveEntry { readonly entryId: string; readonly messageId: string; + readonly origin?: TurnOrigin; content: MessageContent; modelContent: MessageContent; readonly initiatingConnectionId: string; @@ -209,6 +211,7 @@ interface InterruptReceipt { interface PendingSubmit { readonly payload: CanonicalSubmitPayload; + readonly origin?: TurnOrigin; readonly result: Promise>; } @@ -338,6 +341,47 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { this.#preflightSessionSnapshot = options.preflightSessionSnapshot; } + /** Internal Host-only admission path carrying provenance unavailable on the wire. */ + submitTrusted( + input: TurnMessageSubmitInput, + context: ConnectionContext, + origin: TurnOrigin, + ): Promise> { + return this.submit(input, context, origin); + } + + /** + * Rebuild a previously returned submit outcome from durable Host facts. + * `undefined` means no admission was ever committed and a retry is safe. + */ + async reconcileTrustedSubmit( + input: TurnMessageSubmitInput, + origin: TurnOrigin, + ): Promise | undefined> { + const payload = canonicalSubmitPayload(input, origin); + const receipt = await this.#readSubmitReceipt( + input.sessionId, + input.messageId, + input.originHostEpoch, + ); + if (receipt) { + return samePayload(receipt.payload, payload) + ? success(receipt.result) + : failure('operation_conflict', 'Message identity has a different payload'); + } + const durable = await this.#durableProof.readRootTurnSourceMessageReceipt( + input.sessionId, + input.messageId, + ); + if (!durable) return undefined; + if (!sameSourcePayload(durable, payload)) { + return failure('operation_conflict', 'Durable message identity has different provenance'); + } + return durable.sourceMessage.disposition === 'turn_started' + ? success({ disposition: 'turn_started', turnId: durable.admission.turnId }) + : success({ disposition: 'followup', queueRevision: 0 }); + } + projection(sessionId: string): SessionMessageQueueProjection { const state = this.#sessions.get(sessionId); if (!state) { @@ -527,13 +571,14 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { private submit( input: TurnMessageSubmitInput, context: ConnectionContext, + origin?: TurnOrigin, ): Promise> { - const payload = canonicalSubmitPayload(input); + const payload = canonicalSubmitPayload(input, origin); const isCurrentEpoch = input.originHostEpoch === this.#hostEpoch; if (isCurrentEpoch) { const pending = this.#pendingSubmits.get(operationKey(input.sessionId, input.messageId)); if (pending) { - return samePayload(pending.payload, payload) + return samePayload(pending.payload, payload) && isDeepStrictEqual(pending.origin, origin) ? pending.result : Promise.resolve( failure('operation_conflict', 'Message identity has a different payload'), @@ -543,10 +588,10 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { if (this.#failStopped) { return Promise.resolve(failure('host_draining', 'Runtime Host message authority has failed')); } - if (!isCurrentEpoch) return this.#submitAdmitted(input, payload, context.connectionId); + if (!isCurrentEpoch) return this.#submitAdmitted(input, payload, context.connectionId, origin); const key = operationKey(input.sessionId, input.messageId); - const result = this.#submitAdmitted(input, payload, context.connectionId); - this.#pendingSubmits.set(key, { payload, result }); + const result = this.#submitAdmitted(input, payload, context.connectionId, origin); + this.#pendingSubmits.set(key, { payload, ...(origin ? { origin } : {}), result }); void result.then( () => this.#deletePendingSubmit(key, result), () => this.#deletePendingSubmit(key, result), @@ -558,6 +603,7 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { input: TurnMessageSubmitInput, payload: CanonicalSubmitPayload, initiatingConnectionId: string, + origin?: TurnOrigin, ): Promise> { return this.#sessionAdmission.run(input.sessionId, async (admission) => { if (this.#failStopped) { @@ -616,6 +662,7 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { } const sourceMessage: RootTurnSourceMessage = { messageId: input.messageId, + ...(origin ? { origin } : {}), content: payload.content, submittedContentDigest: messageContentDigest(payload.content), placement: input.placement, @@ -712,6 +759,7 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { ), { messageId: input.messageId, + ...(origin ? { origin } : {}), content: prepared.content, submittedContentDigest: messageContentDigest(payload.content), placement: input.placement, @@ -738,6 +786,7 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { const entry: LiveEntry = { entryId, messageId: input.messageId, + ...(origin ? { origin } : {}), content: payload.content, modelContent: prepared.content, initiatingConnectionId, @@ -1381,13 +1430,27 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority { async #readSubmitReceipt( sessionId: string, messageId: string, + hostEpoch = this.#hostEpoch, ): Promise<{ payload: CanonicalSubmitPayload; result: TurnMessageSubmitResult } | undefined> { - const receipt = await this.#receipts.read(this.#hostEpoch, 'submit', sessionId, messageId); + const receipt = await this.#receipts.read(hostEpoch, 'submit', sessionId, messageId); if (!receipt) return undefined; try { + if ( + !receipt.payload || + typeof receipt.payload !== 'object' || + Array.isArray(receipt.payload) + ) { + throw new Error('payload must be an object'); + } + const { origin: rawOrigin, ...wirePayload } = receipt.payload as Record; + const origin = rawOrigin === undefined ? undefined : decodeTurnOrigin(rawOrigin); + if (rawOrigin !== undefined && origin === undefined) { + throw new Error('payload origin is malformed'); + } return { payload: canonicalSubmitPayload( - MESSAGE_OPERATION_SPECS['turn.message.submit'].decodeInput(receipt.payload), + MESSAGE_OPERATION_SPECS['turn.message.submit'].decodeInput(wirePayload), + origin, ), result: MESSAGE_OPERATION_SPECS['turn.message.submit'].decodeOutput(receipt.result), }; @@ -1825,13 +1888,15 @@ function sameSourcePayload( (durableDigest ? durableDigest === messageContentDigest(input.content) : messageContentsEqual(source.content, input.content)) && - source.placement === input.placement + source.placement === input.placement && + isDeepStrictEqual(source.origin, input.origin) ); } function sourceFromEntry(entry: LiveEntry): RootFollowupSource { return { messageId: entry.messageId, + ...(entry.origin ? { origin: entry.origin } : {}), content: normalizeMessageContent(entry.modelContent), submittedContentDigest: messageContentDigest(entry.content), placement: entry.placement, @@ -1940,15 +2005,20 @@ interface CanonicalSubmitPayload { readonly messageId: string; readonly content: MessageContent; readonly placement: MessagePlacement; + readonly origin?: TurnOrigin; } -function canonicalSubmitPayload(input: TurnMessageSubmitInput): CanonicalSubmitPayload { +function canonicalSubmitPayload( + input: TurnMessageSubmitInput, + origin?: TurnOrigin, +): CanonicalSubmitPayload { return { originHostEpoch: input.originHostEpoch, sessionId: input.sessionId, messageId: input.messageId, content: normalizeMessageContent(input.content), placement: input.placement, + ...(origin ? { origin } : {}), }; } diff --git a/packages/runtime-host/src/server/root-admission-owner.ts b/packages/runtime-host/src/server/root-admission-owner.ts index b199fb930b..4b68f35f93 100644 --- a/packages/runtime-host/src/server/root-admission-owner.ts +++ b/packages/runtime-host/src/server/root-admission-owner.ts @@ -132,6 +132,7 @@ function sameRootAdmission(left: RootTurnAdmission, right: RootTurnAdmission): b source.messageId === other.messageId && source.placement === other.placement && source.disposition === other.disposition && + isDeepStrictEqual(source.origin, other.origin) && source.submittedContentDigest === other.submittedContentDigest && messageContentsEqual(source.content, other.content) ); diff --git a/packages/runtime-host/src/server/root-turn-coordinator.ts b/packages/runtime-host/src/server/root-turn-coordinator.ts index c05a8a019d..b6f3942a2b 100644 --- a/packages/runtime-host/src/server/root-turn-coordinator.ts +++ b/packages/runtime-host/src/server/root-turn-coordinator.ts @@ -1045,6 +1045,7 @@ export class RootTurnCoordinator implements HostedExecutionAuthority { execution: { kind: 'external_message', inputDigest: messageContentDigest(content), + ...(input.sourceMessage.origin ? { origin: input.sourceMessage.origin } : {}), }, normalizedInput: canonicalContent.content, sourceMessages: [ @@ -2297,6 +2298,9 @@ export class RootTurnCoordinator implements HostedExecutionAuthority { execution: { kind: 'external_message', inputDigest: messageContentDigest(batch.submittedContent), + ...(batch.sources.length === 1 && batch.sources[0]?.origin + ? { origin: batch.sources[0].origin } + : {}), }, normalizedInput: batch.content, sourceMessages: batch.sources, diff --git a/packages/runtime-host/src/server/session-mailbox-coordinator.ts b/packages/runtime-host/src/server/session-mailbox-coordinator.ts index 76e34350e7..ca8e3d37e6 100644 --- a/packages/runtime-host/src/server/session-mailbox-coordinator.ts +++ b/packages/runtime-host/src/server/session-mailbox-coordinator.ts @@ -19,11 +19,21 @@ import { randomUUID } from 'node:crypto'; import { isSideConversationSession } from '@maka/core/side-conversation'; -import { isLinkedSubagentSession, type SessionSummary, type StoredMessage } from '@maka/core/session'; +import { + isLinkedSubagentSession, + type SessionSummary, + type StoredMessage, +} from '@maka/core/session'; import { sessionMailboxMessageContent, + sessionMailboxOutboxAttemptId, sessionMailboxSentReceiptId, + sessionMailboxTurnOrigin, + parseSessionMailboxOutboxNoteData, + parseSessionMailboxSentNoteData, type SessionMailboxKind, + type SessionMailboxOutboxNoteData, + type SessionMailboxSentNoteData, } from '@maka/core/session-mailbox'; import type { SessionStore } from '@maka/storage/session-store'; import type { @@ -31,6 +41,7 @@ import type { SessionMailboxSendResult, SessionMailboxTarget, } from '../protocol/index.js'; +import { SESSION_MAILBOX_TARGET_MAX_ITEMS } from '../protocol/index.js'; import type { HostMessageCoordinator } from './message-coordinator.js'; import type { ConnectionContext, @@ -61,14 +72,14 @@ type MailboxOutcome = export interface HostSessionMailboxCoordinatorOptions { readonly hostEpoch: string; - readonly messages: Pick; + readonly messages: Pick; readonly listSessions: () => Promise; readonly sessionStore: Pick; readonly createId?: () => string; readonly now?: () => number; } -/** Host-owned routing between ordinary root Sessions in one workspace. */ +/** Host-owned routing between ordinary root Sessions in one project. */ export class HostSessionMailboxCoordinator { readonly handlers: SessionMailboxOperationHandlerMap = { 'session.mailbox.targets': (input) => this.#targets(input.sourceSessionId), @@ -76,7 +87,7 @@ export class HostSessionMailboxCoordinator { }; readonly #hostEpoch: string; - readonly #messages: Pick; + readonly #messages: Pick; readonly #listSessions: () => Promise; readonly #sessionStore: Pick; readonly #createId: () => string; @@ -112,6 +123,42 @@ export class HostSessionMailboxCoordinator { return outcome.result; } + /** Repair durable sender outboxes after a Host restart. */ + async recover(): Promise { + const sessions = await this.#listSessions(); + const byId = new Map(sessions.map((session) => [session.id, session])); + for (const source of sessions) { + if (!isMailboxRoot(source)) continue; + let messages: readonly StoredMessage[]; + try { + messages = await this.#sessionStore.readMessagesSnapshot(source.id); + } catch { + continue; + } + const settled = new Set( + messages.flatMap((message) => + message.type === 'system_note' && message.kind === 'session_mailbox_sent' + ? [parseSessionMailboxSentNoteData(message.data)?.messageId].filter( + (id): id is string => id !== undefined, + ) + : [], + ), + ); + const pending = new Map(); + for (const message of messages) { + if (message.type !== 'system_note' || message.kind !== 'session_mailbox_outbox') continue; + const outbox = parseSessionMailboxOutboxNoteData(message.data); + if (outbox && !settled.has(outbox.messageId)) pending.set(outbox.messageId, outbox); + } + for (const outbox of pending.values()) { + if (!byId.has(outbox.toSessionId)) continue; + await this.#deliverOutbox(outbox, `session-mailbox-recovery:${source.id}`, true).catch( + () => undefined, + ); + } + } + } + async #targets( sourceSessionId: string, ): Promise> { @@ -127,10 +174,10 @@ export class HostSessionMailboxCoordinator { candidate.id !== source.id && !candidate.isArchived && isMailboxRoot(candidate) && - sharesWorkspace(source, candidate), + sharesProject(source, candidate), ) .map(toMailboxTarget) - .slice(0, 64); + .slice(0, SESSION_MAILBOX_TARGET_MAX_ITEMS); return success({ targets }); } @@ -148,50 +195,73 @@ export class HostSessionMailboxCoordinator { return failure('not_found', 'Source Session is not available for Session messaging'); } if (source.isArchived) return failure('session_archived', 'Source Session is archived'); - if ( - !target || - target.isArchived || - !isMailboxRoot(target) || - !sharesWorkspace(source, target) - ) { + if (!target || target.isArchived || !isMailboxRoot(target) || !sharesProject(source, target)) { return failure('not_found', 'Target Session is not reachable from this Session'); } - const submitted = await this.#messages.handlers['turn.message.submit']( - { - originHostEpoch: this.#hostEpoch, - sessionId: target.id, - messageId: input.messageId, - content: sessionMailboxMessageContent({ - messageId: input.messageId, - fromSessionId: source.id, - fromSessionName: source.name, - toSessionId: target.id, - kind: input.kind, - text: input.text, - ...(input.correlationId ? { correlationId: input.correlationId } : {}), - }), - placement: 'next_turn', - }, - connectionContext(this.#hostEpoch, initiatingConnectionId), - ); - if (!submitted.ok) { - return failure( - submitted.error.code === 'unauthorized' ? 'operation_unavailable' : submitted.error.code, - submitted.error.message, - ); - } - const disposition = submitted.result.disposition === 'turn_started' ? 'turn_started' : 'queued'; - await this.#recordSentMessage(source.id, { + const outbox: SessionMailboxOutboxNoteData = { + originHostEpoch: this.#hostEpoch, messageId: input.messageId, - targetSessionId: target.id, + fromSessionId: source.id, + fromSessionName: source.name, + toSessionId: target.id, targetSessionName: target.name, kind: input.kind, text: input.text, + ...(input.correlationId ? { correlationId: input.correlationId } : {}), + }; + const existing = await this.#readExistingResult(source.id, outbox); + if (existing) return existing; + await this.#persistOutboxAttempt(source.id, outbox); + return this.#deliverOutbox(outbox, initiatingConnectionId, false); + } + + async #deliverOutbox( + initialOutbox: SessionMailboxOutboxNoteData, + initiatingConnectionId: string, + reconcileFirst: boolean, + ): Promise> { + let outbox = initialOutbox; + let submitted = reconcileFirst + ? await this.#messages.reconcileTrustedSubmit( + submitInput(outbox), + sessionMailboxTurnOrigin(outbox), + ) + : undefined; + if (submitted === undefined) { + if (outbox.originHostEpoch !== this.#hostEpoch) { + outbox = { ...outbox, originHostEpoch: this.#hostEpoch }; + await this.#persistOutboxAttempt(outbox.fromSessionId, outbox); + } + submitted = await this.#messages.submitTrusted( + submitInput(outbox), + connectionContext(this.#hostEpoch, initiatingConnectionId), + sessionMailboxTurnOrigin(outbox), + ); + } + if (!submitted.ok) { + return failure(submitted.error.code, submitted.error.message); + } + const disposition = submitted.result.disposition === 'turn_started' ? 'turn_started' : 'queued'; + const receipt: SessionMailboxSentNoteData = { + messageId: outbox.messageId, + targetSessionId: outbox.toSessionId, + targetSessionName: outbox.targetSessionName, + kind: outbox.kind, + text: outbox.text, disposition, - }); + ...(submitted.result.disposition === 'turn_started' + ? { turnId: submitted.result.turnId } + : {}), + }; + try { + await this.#recordSentMessage(outbox.fromSessionId, receipt); + } catch { + // The pre-admission outbox remains durable. Startup recovery replays the + // Host receipt/proof and repairs this settlement without redelivery. + } return success({ - messageId: input.messageId, - targetSessionId: target.id, + messageId: outbox.messageId, + targetSessionId: outbox.toSessionId, disposition, ...(submitted.result.disposition === 'turn_started' ? { turnId: submitted.result.turnId } @@ -199,35 +269,84 @@ export class HostSessionMailboxCoordinator { }); } + async #readExistingResult( + sourceSessionId: string, + outbox: SessionMailboxOutboxNoteData, + ): Promise | undefined> { + const messages = await this.#sessionStore.readMessagesSnapshot(sourceSessionId); + const receiptMessage = messages.find( + (message) => message.id === sessionMailboxSentReceiptId(outbox.messageId), + ); + if (!receiptMessage) return undefined; + const receipt = + receiptMessage.type === 'system_note' + ? parseSessionMailboxSentNoteData(receiptMessage.data) + : undefined; + if (!receipt || !receiptMatchesOutbox(receipt, outbox)) { + return failure('operation_conflict', 'Mailbox message identity has different durable data'); + } + return success({ + messageId: receipt.messageId, + targetSessionId: receipt.targetSessionId, + disposition: receipt.disposition, + ...(receipt.turnId ? { turnId: receipt.turnId } : {}), + }); + } + + async #persistOutboxAttempt( + sourceSessionId: string, + data: SessionMailboxOutboxNoteData, + ): Promise { + const id = sessionMailboxOutboxAttemptId(data.messageId, data.originHostEpoch); + const messages = await this.#sessionStore.readMessagesSnapshot(sourceSessionId); + const existing = messages.find((message) => message.id === id); + if (existing) { + const persisted = + existing.type === 'system_note' + ? parseSessionMailboxOutboxNoteData(existing.data) + : undefined; + if (!persisted || !outboxDataEqual(persisted, data)) { + throw new Error('Mailbox outbox identity has different durable data'); + } + return; + } + const anchorTurnId = latestTurnId(messages); + await this.#sessionStore.appendMessage(sourceSessionId, { + type: 'system_note', + id, + ...(anchorTurnId ? { turnId: anchorTurnId } : {}), + ts: this.#now(), + kind: 'session_mailbox_outbox', + data, + }); + } + async #recordSentMessage( sourceSessionId: string, - data: { - readonly messageId: string; - readonly targetSessionId: string; - readonly targetSessionName: string; - readonly kind: SessionMailboxKind; - readonly text: string; - readonly disposition: 'turn_started' | 'queued'; - }, + data: SessionMailboxSentNoteData, ): Promise { const receiptId = sessionMailboxSentReceiptId(data.messageId); - try { - const messages = await this.#sessionStore.readMessagesSnapshot(sourceSessionId); - if (messages.some((message) => message.id === receiptId)) return; - const anchorTurnId = latestTurnId(messages); - await this.#sessionStore.appendMessage(sourceSessionId, { - type: 'system_note', - id: receiptId, - ...(anchorTurnId ? { turnId: anchorTurnId } : {}), - ts: this.#now(), - kind: 'session_mailbox_sent', - data, - }); - } catch { - // Delivery already succeeded and cannot be rolled back. The Client also - // projects an optimistic receipt, so a storage-only receipt failure must - // never invite a retry that sends the same message twice. + const messages = await this.#sessionStore.readMessagesSnapshot(sourceSessionId); + const existing = messages.find((message) => message.id === receiptId); + if (existing) { + const persisted = + existing.type === 'system_note' + ? parseSessionMailboxSentNoteData(existing.data) + : undefined; + if (!persisted || !sentDataEqual(persisted, data)) { + throw new Error('Mailbox receipt identity has different durable data'); + } + return; } + const anchorTurnId = latestTurnId(messages); + await this.#sessionStore.appendMessage(sourceSessionId, { + type: 'system_note', + id: receiptId, + ...(anchorTurnId ? { turnId: anchorTurnId } : {}), + ts: this.#now(), + kind: 'session_mailbox_sent', + data, + }); } } @@ -243,11 +362,71 @@ function isMailboxRoot(session: SessionSummary): boolean { return !isLinkedSubagentSession(session) && !isSideConversationSession(session.labels); } -function sharesWorkspace(source: SessionSummary, target: SessionSummary): boolean { +function sharesProject(source: SessionSummary, target: SessionSummary): boolean { if (source.projectId && target.projectId) return source.projectId === target.projectId; return source.cwd !== undefined && source.cwd === target.cwd; } +function submitInput(outbox: SessionMailboxOutboxNoteData) { + return { + originHostEpoch: outbox.originHostEpoch, + sessionId: outbox.toSessionId, + messageId: outbox.messageId, + content: sessionMailboxMessageContent(outbox), + placement: 'next_turn' as const, + }; +} + +function receiptMatchesOutbox( + receipt: SessionMailboxSentNoteData, + outbox: SessionMailboxOutboxNoteData, +): boolean { + return ( + receipt.messageId === outbox.messageId && + receipt.targetSessionId === outbox.toSessionId && + receipt.targetSessionName === outbox.targetSessionName && + receipt.kind === outbox.kind && + receipt.text === outbox.text + ); +} + +function sentDataEqual( + left: SessionMailboxSentNoteData, + right: SessionMailboxSentNoteData, +): boolean { + return ( + receiptMatchesOutbox(left, { + originHostEpoch: '', + messageId: right.messageId, + fromSessionId: '', + fromSessionName: '', + toSessionId: right.targetSessionId, + targetSessionName: right.targetSessionName, + kind: right.kind, + text: right.text, + }) && + left.disposition === right.disposition && + left.turnId === right.turnId + ); +} + +function outboxDataEqual( + left: SessionMailboxOutboxNoteData, + right: SessionMailboxOutboxNoteData, +): boolean { + return ( + left.originHostEpoch === right.originHostEpoch && + left.messageId === right.messageId && + left.fromSessionId === right.fromSessionId && + left.fromSessionName === right.fromSessionName && + left.toSessionId === right.toSessionId && + left.targetSessionName === right.targetSessionName && + left.kind === right.kind && + left.text === right.text && + left.correlationId === right.correlationId + ); +} + function toMailboxTarget(session: SessionSummary): SessionMailboxTarget { return { sessionId: session.id, diff --git a/packages/storage/src/agent-run-store.ts b/packages/storage/src/agent-run-store.ts index 069635e85d..70d2889d0d 100644 --- a/packages/storage/src/agent-run-store.ts +++ b/packages/storage/src/agent-run-store.ts @@ -77,6 +77,8 @@ import { isTurnOrchestrationSource, type TurnOrchestration, } from '@maka/core/orchestration'; +import { decodeTurnOrigin } from '@maka/core/turn-origin'; +import type { TurnOrigin } from '@maka/core/turn-origin'; import { scanToolLedger, validateGenericToolLedgerAppend, @@ -94,6 +96,8 @@ const ROOT_TURN_ADMISSION_MAX_AGGREGATED_ATTACHMENTS = export interface RootTurnSourceMessage { messageId: string; content: MessageContent; + /** Trusted Host-authored provenance; never accepted from message protocol input. */ + origin?: TurnOrigin; submittedContentDigest?: `sha256:${string}`; placement: 'current_turn' | 'next_turn'; disposition: 'steering' | 'followup' | 'turn_started'; @@ -1652,12 +1656,21 @@ function normalizeRootTurnSourceMessages(value: unknown): readonly RootTurnSourc 'content', 'placement', 'disposition', + ...(Object.hasOwn(item, 'origin') ? ['origin'] : []), ...(Object.hasOwn(item, 'submittedContentDigest') ? ['submittedContentDigest'] : []), ]) ) { throw new Error(`Invalid root turn source message at index ${index}`); } - const { messageId, content, submittedContentDigest, placement, disposition } = item; + const { + messageId, + content, + origin: rawOrigin, + submittedContentDigest, + placement, + disposition, + } = item; + const origin = rawOrigin === undefined ? undefined : decodeTurnOrigin(rawOrigin); if ( typeof messageId !== 'string' || !isSafeId(messageId) || @@ -1667,6 +1680,7 @@ function normalizeRootTurnSourceMessages(value: unknown): readonly RootTurnSourc disposition !== 'turn_started') || (disposition === 'steering' && placement !== 'current_turn') || (disposition === 'followup' && placement !== 'next_turn') || + (rawOrigin !== undefined && origin === undefined) || (submittedContentDigest !== undefined && !isSha256Digest(submittedContentDigest)) ) { throw new Error(`Invalid root turn source message at index ${index}`); @@ -1682,6 +1696,7 @@ function normalizeRootTurnSourceMessages(value: unknown): readonly RootTurnSourc `root turn source message content at index ${index}`, MAX_ATTACHMENT_COUNT, ), + ...(origin !== undefined ? { origin: Object.freeze(origin) } : {}), ...(submittedContentDigest !== undefined ? { submittedContentDigest } : {}), placement, disposition, @@ -1709,6 +1724,7 @@ function rootTurnAdmissionPayloadsEqual( source.messageId === other.messageId && source.placement === other.placement && source.disposition === other.disposition && + isDeepStrictEqual(source.origin, other.origin) && source.submittedContentDigest === other.submittedContentDigest && messageContentsEqual(source.content, other.content) ); @@ -1873,7 +1889,7 @@ function normalizeRootExecutionDescriptor(value: unknown): RootExecutionDescript throw new Error('Invalid root execution descriptor'); } if (value.kind === 'external_message') { - const allowedKeys = ['kind', 'inputDigest', 'maxSteps']; + const allowedKeys = ['kind', 'inputDigest', 'maxSteps', 'origin']; if (!Object.keys(value).every((key) => allowedKeys.includes(key))) { throw new Error('Invalid root execution descriptor'); } @@ -1888,10 +1904,15 @@ function normalizeRootExecutionDescriptor(value: unknown): RootExecutionDescript ) { throw new Error('Invalid root execution descriptor'); } + const origin = value.origin === undefined ? undefined : decodeTurnOrigin(value.origin); + if (value.origin !== undefined && origin === undefined) { + throw new Error('Invalid root execution descriptor'); + } return Object.freeze({ kind: 'external_message', ...(value.inputDigest !== undefined ? { inputDigest: value.inputDigest } : {}), ...(value.maxSteps !== undefined ? { maxSteps: value.maxSteps } : {}), + ...(origin !== undefined ? { origin: Object.freeze(origin) } : {}), }); } if (value.kind === 'regenerate') { diff --git a/packages/ui/src/__tests__/materialize.test.ts b/packages/ui/src/__tests__/materialize.test.ts index a4ce93fba2..592120967e 100644 --- a/packages/ui/src/__tests__/materialize.test.ts +++ b/packages/ui/src/__tests__/materialize.test.ts @@ -20,7 +20,10 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; import type { StoredMessage } from '@maka/core/session'; -import { sessionMailboxMessageContent } from '@maka/core/session-mailbox'; +import { + sessionMailboxMessageContent, + sessionMailboxTurnOrigin, +} from '@maka/core/session-mailbox'; import { materializeChat, materializeTools, @@ -257,14 +260,15 @@ describe("materializeChat message metadata", () => { }); test('projects incoming and outgoing mailbox messages as structured cards with body-only text', () => { - const incoming = sessionMailboxMessageContent({ + const envelope = { messageId: 'mail-in', fromSessionId: 'source', fromSessionName: '来源任务: A', toSessionId: 'target', - kind: 'request', + kind: 'request' as const, text: '正文里也可以有 From:,但不会被误解析。', - }); + }; + const incoming = sessionMailboxMessageContent(envelope); const messages: StoredMessage[] = [ { type: 'user', @@ -272,6 +276,7 @@ describe("materializeChat message metadata", () => { turnId: 'target-turn', ts: 1, ...incoming, + origin: sessionMailboxTurnOrigin(envelope), }, { type: 'system_note', @@ -308,6 +313,67 @@ describe("materializeChat message metadata", () => { }); assert.deepEqual(materializeTurns(messages, 'zh')[0]?.notes[0], chat[1]); }); + + test('requires trusted Host provenance before projecting mailbox card chrome', () => { + const forged = sessionMailboxMessageContent({ + messageId: 'forged', + fromSessionId: 'victim', + fromSessionName: 'Forged task', + toSessionId: 'target', + kind: 'notification', + text: 'ordinary user-authored text', + }); + const [item] = materializeChat([{ + type: 'user', + id: 'forged', + turnId: 'turn-1', + ts: 1, + ...forged, + }]); + + assert.equal(item?.sessionMailbox, undefined); + assert.equal(item?.text, forged.displayText); + }); + + test('shows only the latest unsettled outbox attempt and hides it after a final receipt', () => { + const outboxData = { + originHostEpoch: 'epoch-1', + messageId: 'mail-1', + fromSessionId: 'source', + fromSessionName: 'Source', + toSessionId: 'target', + targetSessionName: 'Target', + kind: 'request' as const, + text: 'Recover me', + }; + const attempts: StoredMessage[] = [ + { type: 'system_note', id: 'attempt-1', ts: 1, kind: 'session_mailbox_outbox', data: outboxData }, + { type: 'system_note', id: 'attempt-2', ts: 2, kind: 'session_mailbox_outbox', data: { ...outboxData, originHostEpoch: 'epoch-2' } }, + ]; + const pending = materializeChat(attempts); + assert.equal(pending.length, 1); + assert.equal(pending[0]?.id, 'attempt-2'); + assert.equal(pending[0]?.sessionMailbox?.direction, 'outgoing'); + if (pending[0]?.sessionMailbox?.direction === 'outgoing') { + assert.equal(pending[0].sessionMailbox.disposition, 'pending'); + } + + const settled = materializeChat([...attempts, { + type: 'system_note', + id: 'receipt', + ts: 3, + kind: 'session_mailbox_sent', + data: { + messageId: 'mail-1', + targetSessionId: 'target', + targetSessionName: 'Target', + kind: 'request', + text: 'Recover me', + disposition: 'turn_started', + }, + }]); + assert.deepEqual(settled.map((item) => item.id), ['receipt']); + }); }); // ── #1307: the timeline model stays flat (fold is a render concern) ────────── diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index 04a38fca2b..50200e2acf 100644 --- a/packages/ui/src/chat-turn.tsx +++ b/packages/ui/src/chat-turn.tsx @@ -288,9 +288,11 @@ const UserMessageBody = memo(function UserMessageBody(props: { {props.sessionMailbox.direction === 'outgoing' ? ( - {props.sessionMailbox.disposition === 'queued' - ? copyText.mailboxQueued - : copyText.mailboxDelivered} + {props.sessionMailbox.disposition === 'pending' + ? copyText.mailboxPending + : props.sessionMailbox.disposition === 'queued' + ? copyText.mailboxQueued + : copyText.mailboxDelivered} ) : null}
diff --git a/packages/ui/src/composer.tsx b/packages/ui/src/composer.tsx index ce0cee2101..1e2bcf85eb 100644 --- a/packages/ui/src/composer.tsx +++ b/packages/ui/src/composer.tsx @@ -367,7 +367,7 @@ export const Composer = forwardRef< title: string; detail?: string; cancelLabel: string; - status?: 'ready' | 'sending' | 'delivered' | 'queued' | 'failed'; + status?: 'ready' | 'sending' | 'failed'; onCancel(): void; }; /** diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index fb2e52b62c..2479070b16 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -283,6 +283,7 @@ export interface ConversationCopy { mailboxOutgoing: string; mailboxDelivered: string; mailboxQueued: string; + mailboxPending: string; mailboxIncomingAriaLabel: (sessionName: string) => string; mailboxOutgoingAriaLabel: (sessionName: string) => string; userAriaLabel: string; @@ -503,7 +504,7 @@ const CONVERSATION_COPY = { }, messages: { you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', workingPhrases: ['正在琢磨…', '正在推敲…', '正在盘算…', '正在钻研…', '正在忙活…', '正在梳理…', '正在打磨…', '正在鼓捣…', '正在酝酿…', '正在攻坚…', '正在权衡…', '正在拾掇…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, 'zh')}后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryReason: { network: '网络中断', provider_capacity: '模型服务暂时满载', provider_unavailable: '模型服务暂时不可用', rate_limit: '触发模型速率限制', timeout: '请求超时', unknown: '模型请求失败' }, safeResumePending: '正在验证…', safeResume: '安全恢复', thinking: '深度思考', truncated: '已截断', copied: '已复制', copying: '复制中', copyFailed: '复制失败', copy: '复制', editMessage: '编辑并重发', editMessageDisabledRunning: '当前回答仍在进行中,结束后再编辑', editMessageDisabledAttachments: '包含附件的历史消息暂不支持编辑并重发', editMessageDisabledQuotes: '包含引用的历史消息暂不支持编辑并重发', editMessageDisabledTransformedText: '通过显式技能发送的历史消息暂不支持编辑并重发', - mailboxIncoming: '来自任务', mailboxOutgoing: '发送到任务', mailboxDelivered: '已送达 · 已开始处理', mailboxQueued: '已排队 · 将在下一轮处理', mailboxIncomingAriaLabel: (sessionName) => `来自任务“${sessionName}”的消息`, mailboxOutgoingAriaLabel: (sessionName) => `发送到任务“${sessionName}”的消息`, + mailboxIncoming: '来自任务', mailboxOutgoing: '发送到任务', mailboxDelivered: '已送达 · 已开始处理', mailboxQueued: '已排队 · 将在下一轮处理', mailboxPending: '送达待确认 · 正在恢复', mailboxIncomingAriaLabel: (sessionName) => `来自任务“${sessionName}”的消息`, mailboxOutgoingAriaLabel: (sessionName) => `发送到任务“${sessionName}”的消息`, userAriaLabel: '你发送的消息', systemAriaLabel: '系统消息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: (context) => `回答操作${context ? `:${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action}回答${context ? `:${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action}消息${context ? `:${context}` : ''}`, sourceAriaLabel: '本轮回答的来源', derivativesAriaLabel: '本轮回答的衍生', scheduledTaskTriggered: '定时任务触发', scheduledTaskTitle: (id) => `由定时任务触发 · ${id}`, legacyAutomationTriggered: '旧版自动化(仅历史)', legacyAutomationTitle: (id) => `由旧版自动化触发 · ${id} · 仅保留历史,不会再次执行`, goalContinued: 'Goal 自动继续', goalTitle: (id) => `由 Goal 继续执行 · ${id}`, agentGraphTriggered: 'Agent Graph 自动继续', agentGraphTitle: (graphId) => `由 Agent Graph 调度器触发 · ${graphId}`, thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中断)', abortedByStop: '(已中断 · 由停止按钮触发)', systemNotes: { @@ -652,7 +653,7 @@ const CONVERSATION_COPY = { }, messages: { you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, 'en')} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryReason: { network: 'Network interrupted', provider_capacity: 'The model service is temporarily at capacity', provider_unavailable: 'Model service temporarily unavailable', rate_limit: 'Model rate limit reached', timeout: 'Request timed out', unknown: 'Model request failed' }, safeResumePending: 'Checking…', safeResume: 'Safe recovery', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages sent with an explicit skill', - mailboxIncoming: 'From task', mailboxOutgoing: 'Sent to task', mailboxDelivered: 'Delivered · processing started', mailboxQueued: 'Queued · will process next turn', mailboxIncomingAriaLabel: (sessionName) => `Message from task “${sessionName}”`, mailboxOutgoingAriaLabel: (sessionName) => `Message sent to task “${sessionName}”`, + mailboxIncoming: 'From task', mailboxOutgoing: 'Sent to task', mailboxDelivered: 'Delivered · processing started', mailboxQueued: 'Queued · will process next turn', mailboxPending: 'Delivery pending · recovering', mailboxIncomingAriaLabel: (sessionName) => `Message from task “${sessionName}”`, mailboxOutgoingAriaLabel: (sessionName) => `Message sent to task “${sessionName}”`, userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: (context) => `Response actions${context ? `: ${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action} response${context ? `: ${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action} message${context ? `: ${context}` : ''}`, sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this one', scheduledTaskTriggered: 'Triggered by scheduled task', scheduledTaskTitle: (id) => `Triggered by scheduled task · ${id}`, legacyAutomationTriggered: 'Legacy Automation (history only)', legacyAutomationTitle: (id) => `Triggered by legacy Automation · ${id} · Historical only; it will not run again`, goalContinued: 'Continued by Goal', goalTitle: (id) => `Continued by Goal · ${id}`, agentGraphTriggered: 'Continued by Agent Graph', agentGraphTitle: (graphId) => `Triggered by the Agent Graph scheduler · ${graphId}`, thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: '(Interrupted)', abortedByStop: '(Interrupted · Stop button)', systemNotes: { diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts index d87a97c66b..75d5587025 100644 --- a/packages/ui/src/materialize.ts +++ b/packages/ui/src/materialize.ts @@ -40,8 +40,9 @@ import type { ToolActivityStatus } from '@maka/core/tool-result-status'; import type { ShellRunToolResult } from '@maka/core/shell-run-result'; import type { StoredMessage, TurnRecord, TurnStatus, UserMessage } from '@maka/core/session'; import { - parseSessionMailboxMessageContent, + parseSessionMailboxOutboxNoteData, parseSessionMailboxSentNoteData, + parseTrustedSessionMailboxMessage, type SessionMailboxKind, } from '@maka/core/session-mailbox'; import type { UiLocale } from '@maka/core/ui-locale'; @@ -80,7 +81,7 @@ export interface ChatItem { sessionId: string; sessionName: string; kind: SessionMailboxKind; - disposition: 'turn_started' | 'queued'; + disposition: 'pending' | 'turn_started' | 'queued'; }; } @@ -175,6 +176,7 @@ export function materializeChat( locale: UiLocale = "en", ): ChatItem[] { const items: ChatItem[] = []; + const visibleOutboxIds = visibleSessionMailboxOutboxNoteIds(messages); for (const message of messages) { if (message.type === "user") { items.push(chatItemFromUserMessage(message)); @@ -189,6 +191,13 @@ export function materializeChat( if (message.type === 'system_note' && message.kind === 'session_mailbox_sent') { const item = chatItemFromMailboxSentNote(message); if (item) items.push(item); + } else if ( + message.type === 'system_note' && + message.kind === 'session_mailbox_outbox' && + visibleOutboxIds.has(message.id) + ) { + const item = chatItemFromMailboxOutboxNote(message); + if (item) items.push(item); } else if ( message.type === "system_note" && VISIBLE_SYSTEM_NOTES.has(message.kind) @@ -665,6 +674,7 @@ export function materializeTurns( locale: UiLocale = "en", ): TurnViewModel[] { const turnRecords = deriveTurnRecords(messages); + const visibleOutboxIds = visibleSessionMailboxOutboxNoteIds(messages); const turnRecordById = new Map( turnRecords.map((turn) => [turn.turnId, turn]), ); @@ -766,6 +776,13 @@ export function materializeTurns( } else if (message.type === 'system_note' && message.kind === 'session_mailbox_sent') { const note = chatItemFromMailboxSentNote(message); if (note) turn.notes.push(note); + } else if ( + message.type === 'system_note' && + message.kind === 'session_mailbox_outbox' && + visibleOutboxIds.has(message.id) + ) { + const note = chatItemFromMailboxOutboxNote(message); + if (note) turn.notes.push(note); } else if ( message.type === "system_note" && VISIBLE_SYSTEM_NOTES.has(message.kind) @@ -1084,7 +1101,10 @@ function chatItemFromContent( content: MessageContent, hostOrigin?: NonNullable, ): ChatItem { - const mailbox = parseSessionMailboxMessageContent(content); + const mailbox = parseTrustedSessionMailboxMessage({ + text: content.text, + origin: hostOrigin, + }); return { id, role: "user", @@ -1113,6 +1133,59 @@ function chatItemFromContent( }; } +function chatItemFromMailboxOutboxNote( + message: Extract, +): ChatItem | undefined { + const outbox = parseSessionMailboxOutboxNoteData(message.data); + if (!outbox) return undefined; + return { + id: message.id, + role: 'system', + text: outbox.text, + ts: message.ts, + sessionMailbox: { + direction: 'outgoing', + sessionId: outbox.toSessionId, + sessionName: outbox.targetSessionName, + kind: outbox.kind, + disposition: 'pending', + }, + }; +} + +/** Show only the latest unsettled delivery attempt for each mailbox message. */ +function visibleSessionMailboxOutboxNoteIds( + messages: readonly StoredMessage[], +): ReadonlySet { + const terminalMessageIds = new Set(); + const seenOutboxMessageIds = new Set(); + const visibleNoteIds = new Set(); + // Receipts follow their outboxes durably. Walking backward therefore finds + // a terminal receipt before any of its attempts and the newest unsettled + // attempt before older retries, without adding two full transcript passes. + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + if (message?.type !== 'system_note') continue; + if (message.kind === 'session_mailbox_sent') { + const receipt = parseSessionMailboxSentNoteData(message.data); + if (receipt) terminalMessageIds.add(receipt.messageId); + continue; + } + if (message.kind !== 'session_mailbox_outbox') continue; + const outbox = parseSessionMailboxOutboxNoteData(message.data); + if ( + !outbox || + terminalMessageIds.has(outbox.messageId) || + seenOutboxMessageIds.has(outbox.messageId) + ) { + continue; + } + seenOutboxMessageIds.add(outbox.messageId); + visibleNoteIds.add(message.id); + } + return visibleNoteIds; +} + function chatItemFromMailboxSentNote( message: Extract, ): ChatItem | undefined { From 64c28421238de4dad71b624261394b97dad1b755 Mon Sep 17 00:00:00 2001 From: 982945902 <982945902@qq.com> Date: Tue, 25 Aug 2026 20:16:08 +0800 Subject: [PATCH 3/5] docs: add session mailbox interaction evidence Generated-by: Codex --- apps/desktop/e2e/session-mailbox.spec.ts | 1 + docs/images/pr/session-mailbox-after-send.png | Bin 0 -> 145696 bytes docs/images/pr/session-mailbox-before-send.png | Bin 0 -> 140018 bytes 3 files changed, 1 insertion(+) create mode 100644 docs/images/pr/session-mailbox-after-send.png create mode 100644 docs/images/pr/session-mailbox-before-send.png diff --git a/apps/desktop/e2e/session-mailbox.spec.ts b/apps/desktop/e2e/session-mailbox.spec.ts index 828d8d6858..187293e6ea 100644 --- a/apps/desktop/e2e/session-mailbox.spec.ts +++ b/apps/desktop/e2e/session-mailbox.spec.ts @@ -64,6 +64,7 @@ test('selects a searchable /send target and settles delivery into the transcript await selectMailboxTarget(page, '恢复检查', '支付回调恢复检查'); await expect(page.getByText('发送给“支付回调恢复检查”')).toBeVisible(); + await page.screenshot({ path: testInfo.outputPath('session-mailbox-before-send.png') }); await composer.fill('请检查回执恢复链路'); await composer.press('Enter'); diff --git a/docs/images/pr/session-mailbox-after-send.png b/docs/images/pr/session-mailbox-after-send.png new file mode 100644 index 0000000000000000000000000000000000000000..5d33b78e33545fbc55d0790b53cec0989ec0164e GIT binary patch literal 145696 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 literal 0 HcmV?d00001 diff --git a/docs/images/pr/session-mailbox-before-send.png b/docs/images/pr/session-mailbox-before-send.png new file mode 100644 index 0000000000000000000000000000000000000000..c4f6378da7a1bce5079dab633967bbef27b4dfba GIT binary patch literal 140018 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 a36cbvV4tm+1>Xq*krb5^Df;lq_x}L`Zjt@~ literal 0 HcmV?d00001 From 051f6a75546e8a7cd749abad7162d80cc6f629b5 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 26 Aug 2026 10:13:28 +0800 Subject: [PATCH 4/5] fix: retain queued mailbox provenance --- .../src/__tests__/message-coordinator.test.ts | 52 ++++++++++ .../__tests__/root-turn-coordinator.test.ts | 95 +++++++++++++++++++ .../src/server/message-coordinator.ts | 11 ++- 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/packages/runtime-host/src/__tests__/message-coordinator.test.ts b/packages/runtime-host/src/__tests__/message-coordinator.test.ts index a7b1425d87..d3d1db9331 100644 --- a/packages/runtime-host/src/__tests__/message-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/message-coordinator.test.ts @@ -115,6 +115,58 @@ test('trusted submit receipts bind mailbox provenance across reconciliation', as fixture.coordinator.abandonRootReservation(nextRoot); }); +test('trusted follow-ups cross root handoffs one at a time to retain provenance', async () => { + const fixture = createFixture(); + fixture.coordinator.reserveRootTurn(ROOT); + const owner = fixture.coordinator.bindRun(ROOT); + const trusted = (messageId: string) => { + const origin = { + kind: 'session_mailbox' as const, + messageId, + fromSessionId: 'source', + fromSessionName: 'Source', + toSessionId: ROOT.sessionId, + mailboxKind: 'request' as const, + }; + return fixture.coordinator.submitTrusted( + { + originHostEpoch: 'epoch-1', + sessionId: ROOT.sessionId, + messageId, + content: { text: `trusted mailbox payload ${messageId}` }, + placement: 'next_turn', + }, + operationContext(), + origin, + ); + }; + + assert.equal((await trusted('mailbox-message-1')).ok, true); + assert.equal((await trusted('mailbox-message-2')).ok, true); + owner.release(); + + const firstBatch = fixture.coordinator.beginTerminalTransition(ROOT); + assert.deepEqual( + firstBatch.sources.map((source) => source.messageId), + ['mailbox-message-1'], + ); + assert.equal(firstBatch.sources[0]?.origin?.kind, 'session_mailbox'); + const secondRoot = { sessionId: ROOT.sessionId, turnId: 'turn-2', runId: 'run-2' }; + fixture.coordinator.commitNextRoot(firstBatch, secondRoot); + + const secondOwner = fixture.coordinator.bindRun(secondRoot); + secondOwner.release(); + const secondBatch = fixture.coordinator.beginTerminalTransition(secondRoot); + assert.deepEqual( + secondBatch.sources.map((source) => source.messageId), + ['mailbox-message-2'], + ); + assert.equal(secondBatch.sources[0]?.origin?.kind, 'session_mailbox'); + const thirdRoot = { sessionId: ROOT.sessionId, turnId: 'turn-3', runId: 'run-3' }; + fixture.coordinator.commitNextRoot(secondBatch, thirdRoot); + fixture.coordinator.abandonRootReservation(thirdRoot); +}); + test('submit re-runs admission when the queue revision moves during preflight', async () => { let preflightCalls = 0; const fixture = createFixture(undefined, async () => { diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts index aad3fdb039..34ceb2a472 100644 --- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts @@ -55,6 +55,11 @@ import type { BackendSendInput, } from '@maka/core/backend-types'; import type { SessionEvent } from '@maka/core/events'; +import { + parseTrustedSessionMailboxMessage, + sessionMailboxMessageContent, + sessionMailboxTurnOrigin, +} from '@maka/core/session-mailbox'; import type { MakaTool } from '@maka/runtime/tool-runtime'; import { clientCapabilityConnectionIdentity } from './fixtures/client-capability.js'; import { @@ -3364,6 +3369,96 @@ test('mixed-Client queued follow-ups preserve each submitting connection through } }); +test('queued mailbox messages persist as separately trusted UserMessages', { + timeout: 20_000, +}, async () => { + let backend: LinkedChildAuthorityBackend | undefined; + const fixture = await createFailureFixture({ + registerBackend: (backends) => { + backends.register('ai-sdk', (context) => { + backend = new LinkedChildAuthorityBackend(context.sessionId); + return backend; + }); + }, + }); + + try { + const firstTurnId = 'turn-before-mailbox-followups'; + const started = await fixture.interactiveTurns.handlers['turn.start']( + { + sessionId: fixture.sessionId, + turnId: firstTurnId, + content: { text: HOLD_EXTERNAL_PROMPT }, + }, + operationContext(fixture.hostEpoch, fixture.acquireResidency), + ); + assertStartedTurn(started); + + const envelopes = ['first queued message', 'second queued message'].map((text, index) => ({ + messageId: `mailbox-followup-${index + 1}`, + fromSessionId: 'source-session', + fromSessionName: 'Source session', + toSessionId: fixture.sessionId, + kind: 'request' as const, + text, + })); + for (const envelope of envelopes) { + const submitted = await fixture.messages.submitTrusted( + { + originHostEpoch: fixture.hostEpoch, + sessionId: fixture.sessionId, + messageId: envelope.messageId, + content: sessionMailboxMessageContent(envelope), + placement: 'next_turn', + }, + operationContext(fixture.hostEpoch, fixture.acquireResidency), + sessionMailboxTurnOrigin(envelope), + ); + assert.equal(submitted.ok && submitted.result.disposition, 'followup'); + } + + backend?.release(); + await waitUntil( + () => fixture.coordinator.readRootState(fixture.sessionId).kind === 'idle', + 5_000, + ); + + const admissions = await fixture.stores.agentRunStore.listRootTurnAdmissionsForRecovery( + fixture.sessionId, + ); + assert.deepEqual( + admissions.map((admission) => admission.sourceMessages.map((source) => source.messageId)), + [[], ['mailbox-followup-1'], ['mailbox-followup-2']], + ); + assert.deepEqual( + admissions + .slice(1) + .map((admission) => + admission.execution.kind === 'external_message' ? admission.execution.origin : undefined, + ), + envelopes.map(sessionMailboxTurnOrigin), + ); + + const messages = await fixture.stores.sessionStore.readMessages(fixture.sessionId); + const recoveredMailboxMessages = messages + .filter((message) => message.type === 'user' && message.origin?.kind === 'session_mailbox') + .map((message) => { + assert.equal(message.type, 'user'); + if (message.type !== 'user') return undefined; + return parseTrustedSessionMailboxMessage(message); + }); + assert.deepEqual( + recoveredMailboxMessages.map((message) => message?.text), + envelopes.map((envelope) => envelope.text), + ); + } finally { + backend?.release(); + await fixture.coordinator.close(); + await fixture.messages.close(); + await fixture.dispose(); + } +}); + test('queued follow-up degrades lost Session tools and rebinds ephemeral tools to its Client', { timeout: 20_000, }, async () => { diff --git a/packages/runtime-host/src/server/message-coordinator.ts b/packages/runtime-host/src/server/message-coordinator.ts index 5fa3ebbb6b..1c9e4831a7 100644 --- a/packages/runtime-host/src/server/message-coordinator.ts +++ b/packages/runtime-host/src/server/message-coordinator.ts @@ -2048,10 +2048,15 @@ function canonicalFollowupBatch(entries: readonly LiveEntry[]): { } function sameInitiatingClientPrefix(entries: readonly LiveEntry[]): LiveEntry[] { - const initiatingConnectionId = entries[0]?.initiatingConnectionId; - if (!initiatingConnectionId) return []; + const first = entries[0]; + if (!first) return []; + // Host-authored messages carry provenance for exactly one payload. Keep + // them as individual roots so the persisted UserMessage retains that + // provenance and can be reconstructed as trusted UI after a restart. + if (first.origin) return [first]; const boundary = entries.findIndex( - (entry) => entry.initiatingConnectionId !== initiatingConnectionId, + (entry) => + entry.initiatingConnectionId !== first.initiatingConnectionId || entry.origin !== undefined, ); return entries.slice(0, boundary === -1 ? entries.length : boundary); } From bfa83491754f1f011541ce84f23967ee28f20e41 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 26 Aug 2026 17:18:13 +0800 Subject: [PATCH 5/5] fix: settle rejected session mailbox deliveries --- .../src/__tests__/session-mailbox.test.ts | 38 +++++ packages/core/src/session-mailbox.ts | 33 ++++ packages/core/src/session.ts | 2 + .../session-mailbox-coordinator.test.ts | 137 ++++++++++++++++ .../src/server/session-mailbox-coordinator.ts | 152 +++++++++++++++--- packages/ui/src/__tests__/materialize.test.ts | 13 ++ packages/ui/src/materialize.ts | 6 + 7 files changed, 360 insertions(+), 21 deletions(-) diff --git a/packages/core/src/__tests__/session-mailbox.test.ts b/packages/core/src/__tests__/session-mailbox.test.ts index b628e8bf86..0a00424a4a 100644 --- a/packages/core/src/__tests__/session-mailbox.test.ts +++ b/packages/core/src/__tests__/session-mailbox.test.ts @@ -19,7 +19,9 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import { decodeCanonicalMessage } from '../session.js'; import { + parseSessionMailboxFailedNoteData, parseSessionMailboxMessageContent, parseSessionMailboxOutboxNoteData, parseSessionMailboxSentNoteData, @@ -136,6 +138,7 @@ test('Session mailbox sent-note data requires a complete delivery receipt', () = targetSessionName: 'Target', kind: 'request', text: 'Hello', + correlationId: 'request-1', disposition: 'queued', }), { @@ -144,8 +147,43 @@ test('Session mailbox sent-note data requires a complete delivery receipt', () = targetSessionName: 'Target', kind: 'request', text: 'Hello', + correlationId: 'request-1', disposition: 'queued', }, ); assert.equal(parseSessionMailboxSentNoteData({ messageId: 'message-1' }), undefined); }); + +test('Session mailbox failed-note data retains the terminal delivery identity', () => { + const data = { + originHostEpoch: 'epoch-1', + messageId: 'reply-1', + fromSessionId: 'source-1', + fromSessionName: 'Source', + toSessionId: 'target-1', + targetSessionName: 'Target', + kind: 'reply' as const, + text: 'Done', + correlationId: 'request-1', + errorCode: 'session_busy', + errorMessage: 'Target is busy', + }; + assert.deepEqual(parseSessionMailboxFailedNoteData(data), data); + assert.deepEqual( + decodeCanonicalMessage({ + type: 'system_note', + id: 'session-mailbox-failed:reply-1', + ts: 1, + kind: 'session_mailbox_failed', + data, + }), + { + type: 'system_note', + id: 'session-mailbox-failed:reply-1', + ts: 1, + kind: 'session_mailbox_failed', + data, + }, + ); + assert.equal(parseSessionMailboxFailedNoteData({ ...data, errorCode: undefined }), undefined); +}); diff --git a/packages/core/src/session-mailbox.ts b/packages/core/src/session-mailbox.ts index 48f06e221f..47970c143b 100644 --- a/packages/core/src/session-mailbox.ts +++ b/packages/core/src/session-mailbox.ts @@ -51,6 +51,7 @@ export interface SessionMailboxSentNoteData { readonly targetSessionName: string; readonly kind: SessionMailboxKind; readonly text: string; + readonly correlationId?: string; readonly disposition: 'turn_started' | 'queued'; readonly turnId?: string; } @@ -61,10 +62,20 @@ export interface SessionMailboxOutboxNoteData extends SessionMailboxEnvelope { readonly targetSessionName: string; } +/** Durable terminal rejection for one sender-side delivery identity. */ +export interface SessionMailboxFailedNoteData extends SessionMailboxOutboxNoteData { + readonly errorCode: string; + readonly errorMessage: string; +} + export function sessionMailboxSentReceiptId(messageId: string): string { return `session-mailbox-sent:${messageId}`; } +export function sessionMailboxFailedReceiptId(messageId: string): string { + return `session-mailbox-failed:${messageId}`; +} + export function sessionMailboxOutboxAttemptId(messageId: string, originHostEpoch: string): string { return `session-mailbox-outbox:${messageId}:${originHostEpoch}`; } @@ -233,6 +244,7 @@ export function parseSessionMailboxSentNoteData( typeof value.targetSessionName !== 'string' || typeof value.text !== 'string' || !SESSION_MAILBOX_KINDS.includes(value.kind as SessionMailboxKind) || + (value.correlationId !== undefined && typeof value.correlationId !== 'string') || (value.disposition !== 'turn_started' && value.disposition !== 'queued') || (value.turnId !== undefined && typeof value.turnId !== 'string') ) { @@ -244,11 +256,32 @@ export function parseSessionMailboxSentNoteData( targetSessionName: value.targetSessionName, kind: value.kind as SessionMailboxKind, text: value.text, + ...(value.correlationId !== undefined ? { correlationId: value.correlationId } : {}), disposition: value.disposition, ...(value.turnId !== undefined ? { turnId: value.turnId } : {}), }; } +export function parseSessionMailboxFailedNoteData( + value: unknown, +): SessionMailboxFailedNoteData | undefined { + const outbox = parseSessionMailboxOutboxNoteData(value); + if ( + !outbox || + !isRecord(value) || + typeof value.errorCode !== 'string' || + value.errorCode.length === 0 || + typeof value.errorMessage !== 'string' + ) { + return undefined; + } + return { + ...outbox, + errorCode: value.errorCode, + errorMessage: value.errorMessage, + }; +} + function escapeAttribute(value: string): string { return value .replaceAll('&', '&') diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index da8bb9e350..3f2022b7e5 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -948,6 +948,7 @@ export interface SystemNoteMessage { | 'context_compaction_failed_open' | 'step_limit' | 'session_mailbox_outbox' + | 'session_mailbox_failed' | 'session_mailbox_sent' | 'error' | 'abort'; @@ -1053,6 +1054,7 @@ const SYSTEM_NOTE_KINDS = new Set([ 'context_compaction_failed_open', 'step_limit', 'session_mailbox_outbox', + 'session_mailbox_failed', 'session_mailbox_sent', 'error', 'abort', diff --git a/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts index 131688a841..ad7c87f496 100644 --- a/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-mailbox-coordinator.test.ts @@ -294,6 +294,143 @@ test('Session mailbox recovery settles a durable outbox from target proof withou ); }); +test('Session mailbox terminal rejection settles the outbox without delayed recovery delivery', async () => { + const stored: StoredMessage[] = []; + let submitCount = 0; + const coordinator = mailbox( + [session('source', '/one'), session('target', '/one')], + async () => { + submitCount += 1; + return { + ok: false, + error: { code: 'session_busy', message: 'Target Session is busy' }, + }; + }, + stored, + ); + const input = { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'message-rejected', + kind: 'request' as const, + text: 'Try once', + }; + + const rejected = await coordinator.handlers['session.mailbox.send'](input, connection()); + assert.equal(rejected.ok, false); + if (!rejected.ok) assert.equal(rejected.error.code, 'session_busy'); + assert.equal(submitCount, 1); + assert.deepEqual( + stored.map((message) => (message.type === 'system_note' ? message.kind : message.type)), + ['session_mailbox_outbox', 'session_mailbox_failed'], + ); + + const exactRetry = await coordinator.handlers['session.mailbox.send'](input, connection()); + assert.deepEqual(exactRetry, rejected); + await coordinator.recover(); + assert.equal(submitCount, 1); +}); + +test('Session mailbox keeps outcome-unknown delivery pending for proof-first recovery', async () => { + const stored: StoredMessage[] = []; + const messagesBySession = new Map([ + ['source', stored], + ['target', []], + ]); + let submitCount = 0; + let reconcileCount = 0; + const coordinator = new HostSessionMailboxCoordinator({ + hostEpoch: 'epoch-1', + messages: { + submitTrusted: async () => { + submitCount += 1; + return { + ok: false, + error: { code: 'outcome_unknown', message: 'Delivery outcome is unknown' }, + }; + }, + reconcileTrustedSubmit: async () => { + reconcileCount += 1; + return { + ok: false, + error: { code: 'outcome_unknown', message: 'Delivery outcome is still unknown' }, + }; + }, + } as unknown as Pick, + listSessions: async () => [session('source', '/one'), session('target', '/one')], + sessionStore: { + readMessagesSnapshot: async (sessionId) => messagesBySession.get(sessionId) ?? [], + appendMessage: async (sessionId, message) => { + messagesBySession.get(sessionId)?.push(message); + }, + }, + }); + + const result = await coordinator.handlers['session.mailbox.send']( + { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'message-unknown', + kind: 'request', + text: 'Confirm before retrying', + }, + connection(), + ); + assert.equal(result.ok, false); + if (!result.ok) assert.equal(result.error.code, 'outcome_unknown'); + assert.deepEqual( + stored.map((message) => (message.type === 'system_note' ? message.kind : message.type)), + ['session_mailbox_outbox'], + ); + + await coordinator.recover(); + assert.equal(submitCount, 1); + assert.equal(reconcileCount, 1); + assert.equal(stored.length, 1); +}); + +test('Session mailbox success receipt fences reply correlation drift', async () => { + const stored: StoredMessage[] = []; + let submitCount = 0; + const coordinator = mailbox( + [session('source', '/one'), session('target', '/one')], + async () => { + submitCount += 1; + return { ok: true, result: { disposition: 'followup', queueRevision: 1 } }; + }, + stored, + ); + const input = { + sourceSessionId: 'source', + targetSessionId: 'target', + messageId: 'reply-1', + kind: 'reply' as const, + text: 'Done', + correlationId: 'request-1', + }; + + const sent = await coordinator.handlers['session.mailbox.send'](input, connection()); + assert.equal(sent.ok, true); + assert.equal(submitCount, 1); + const receipt = stored.find( + (message) => message.type === 'system_note' && message.kind === 'session_mailbox_sent', + ); + assert.equal(receipt?.type, 'system_note'); + if (receipt?.type === 'system_note') { + assert.equal((receipt.data as { correlationId?: string }).correlationId, 'request-1'); + } + + const exactRetry = await coordinator.handlers['session.mailbox.send'](input, connection()); + assert.deepEqual(exactRetry, sent); + const drifted = await coordinator.handlers['session.mailbox.send']( + { ...input, correlationId: 'request-2' }, + connection(), + ); + assert.equal(drifted.ok, false); + if (!drifted.ok) assert.equal(drifted.error.code, 'operation_conflict'); + assert.equal(submitCount, 1); +}); + function mailbox( sessions: SessionSummary[], submit: (input: unknown, origin?: unknown) => Promise = async () => ({ diff --git a/packages/runtime-host/src/server/session-mailbox-coordinator.ts b/packages/runtime-host/src/server/session-mailbox-coordinator.ts index ca8e3d37e6..dacc3c2cee 100644 --- a/packages/runtime-host/src/server/session-mailbox-coordinator.ts +++ b/packages/runtime-host/src/server/session-mailbox-coordinator.ts @@ -25,13 +25,16 @@ import { type StoredMessage, } from '@maka/core/session'; import { + parseSessionMailboxFailedNoteData, sessionMailboxMessageContent, + sessionMailboxFailedReceiptId, sessionMailboxOutboxAttemptId, sessionMailboxSentReceiptId, sessionMailboxTurnOrigin, parseSessionMailboxOutboxNoteData, parseSessionMailboxSentNoteData, type SessionMailboxKind, + type SessionMailboxFailedNoteData, type SessionMailboxOutboxNoteData, type SessionMailboxSentNoteData, } from '@maka/core/session-mailbox'; @@ -60,6 +63,19 @@ type MailboxErrorCode = | 'outcome_unknown' | 'internal_failure'; +const MAILBOX_ERROR_CODES = new Set([ + 'host_not_ready', + 'host_draining', + 'operation_unavailable', + 'not_found', + 'session_archived', + 'session_busy', + 'operation_conflict', + 'invalid_request', + 'outcome_unknown', + 'internal_failure', +]); + type MailboxOutcome = | { readonly ok: true; readonly result: T } | { @@ -135,15 +151,17 @@ export class HostSessionMailboxCoordinator { } catch { continue; } - const settled = new Set( - messages.flatMap((message) => - message.type === 'system_note' && message.kind === 'session_mailbox_sent' - ? [parseSessionMailboxSentNoteData(message.data)?.messageId].filter( - (id): id is string => id !== undefined, - ) - : [], - ), - ); + const settled = new Set(); + for (const message of messages) { + if (message.type !== 'system_note') continue; + if (message.kind === 'session_mailbox_sent') { + const receipt = parseSessionMailboxSentNoteData(message.data); + if (receipt) settled.add(receipt.messageId); + } else if (message.kind === 'session_mailbox_failed') { + const rejection = parseSessionMailboxFailedNoteData(message.data); + if (rejection) settled.add(rejection.messageId); + } + } const pending = new Map(); for (const message of messages) { if (message.type !== 'system_note' || message.kind !== 'session_mailbox_outbox') continue; @@ -239,6 +257,13 @@ export class HostSessionMailboxCoordinator { ); } if (!submitted.ok) { + if (submitted.error.code !== 'outcome_unknown') { + await this.#recordFailedMessage(outbox.fromSessionId, { + ...outbox, + errorCode: submitted.error.code, + errorMessage: submitted.error.message, + }); + } return failure(submitted.error.code, submitted.error.message); } const disposition = submitted.result.disposition === 'turn_started' ? 'turn_started' : 'queued'; @@ -248,6 +273,7 @@ export class HostSessionMailboxCoordinator { targetSessionName: outbox.targetSessionName, kind: outbox.kind, text: outbox.text, + ...(outbox.correlationId ? { correlationId: outbox.correlationId } : {}), disposition, ...(submitted.result.disposition === 'turn_started' ? { turnId: submitted.result.turnId } @@ -277,20 +303,43 @@ export class HostSessionMailboxCoordinator { const receiptMessage = messages.find( (message) => message.id === sessionMailboxSentReceiptId(outbox.messageId), ); - if (!receiptMessage) return undefined; - const receipt = - receiptMessage.type === 'system_note' - ? parseSessionMailboxSentNoteData(receiptMessage.data) + const failedMessage = messages.find( + (message) => message.id === sessionMailboxFailedReceiptId(outbox.messageId), + ); + if (receiptMessage && failedMessage) { + return failure( + 'operation_conflict', + 'Mailbox message identity has multiple terminal results', + ); + } + if (receiptMessage) { + const receipt = + receiptMessage.type === 'system_note' + ? parseSessionMailboxSentNoteData(receiptMessage.data) + : undefined; + if (!receipt || !receiptMatchesOutbox(receipt, outbox)) { + return failure('operation_conflict', 'Mailbox message identity has different durable data'); + } + return success({ + messageId: receipt.messageId, + targetSessionId: receipt.targetSessionId, + disposition: receipt.disposition, + ...(receipt.turnId ? { turnId: receipt.turnId } : {}), + }); + } + if (!failedMessage) return undefined; + const rejection = + failedMessage.type === 'system_note' + ? parseSessionMailboxFailedNoteData(failedMessage.data) : undefined; - if (!receipt || !receiptMatchesOutbox(receipt, outbox)) { + if ( + !rejection || + !failureMatchesOutbox(rejection, outbox) || + !isMailboxErrorCode(rejection.errorCode) + ) { return failure('operation_conflict', 'Mailbox message identity has different durable data'); } - return success({ - messageId: receipt.messageId, - targetSessionId: receipt.targetSessionId, - disposition: receipt.disposition, - ...(receipt.turnId ? { turnId: receipt.turnId } : {}), - }); + return failure(rejection.errorCode, rejection.errorMessage); } async #persistOutboxAttempt( @@ -348,6 +397,34 @@ export class HostSessionMailboxCoordinator { data, }); } + + async #recordFailedMessage( + sourceSessionId: string, + data: SessionMailboxFailedNoteData, + ): Promise { + const receiptId = sessionMailboxFailedReceiptId(data.messageId); + const messages = await this.#sessionStore.readMessagesSnapshot(sourceSessionId); + const existing = messages.find((message) => message.id === receiptId); + if (existing) { + const persisted = + existing.type === 'system_note' + ? parseSessionMailboxFailedNoteData(existing.data) + : undefined; + if (!persisted || !failedDataEqual(persisted, data)) { + throw new Error('Mailbox failure identity has different durable data'); + } + return; + } + const anchorTurnId = latestTurnId(messages); + await this.#sessionStore.appendMessage(sourceSessionId, { + type: 'system_note', + id: receiptId, + ...(anchorTurnId ? { turnId: anchorTurnId } : {}), + ts: this.#now(), + kind: 'session_mailbox_failed', + data, + }); + } } function latestTurnId(messages: readonly StoredMessage[]): string | undefined { @@ -386,7 +463,8 @@ function receiptMatchesOutbox( receipt.targetSessionId === outbox.toSessionId && receipt.targetSessionName === outbox.targetSessionName && receipt.kind === outbox.kind && - receipt.text === outbox.text + receipt.text === outbox.text && + receipt.correlationId === outbox.correlationId ); } @@ -404,12 +482,44 @@ function sentDataEqual( targetSessionName: right.targetSessionName, kind: right.kind, text: right.text, + ...(right.correlationId ? { correlationId: right.correlationId } : {}), }) && left.disposition === right.disposition && left.turnId === right.turnId ); } +function failureMatchesOutbox( + failureData: SessionMailboxFailedNoteData, + outbox: SessionMailboxOutboxNoteData, +): boolean { + return ( + failureData.messageId === outbox.messageId && + failureData.fromSessionId === outbox.fromSessionId && + failureData.fromSessionName === outbox.fromSessionName && + failureData.toSessionId === outbox.toSessionId && + failureData.targetSessionName === outbox.targetSessionName && + failureData.kind === outbox.kind && + failureData.text === outbox.text && + failureData.correlationId === outbox.correlationId + ); +} + +function failedDataEqual( + left: SessionMailboxFailedNoteData, + right: SessionMailboxFailedNoteData, +): boolean { + return ( + outboxDataEqual(left, right) && + left.errorCode === right.errorCode && + left.errorMessage === right.errorMessage + ); +} + +function isMailboxErrorCode(value: string): value is MailboxErrorCode { + return MAILBOX_ERROR_CODES.has(value as MailboxErrorCode); +} + function outboxDataEqual( left: SessionMailboxOutboxNoteData, right: SessionMailboxOutboxNoteData, diff --git a/packages/ui/src/__tests__/materialize.test.ts b/packages/ui/src/__tests__/materialize.test.ts index 592120967e..79f92a5131 100644 --- a/packages/ui/src/__tests__/materialize.test.ts +++ b/packages/ui/src/__tests__/materialize.test.ts @@ -373,6 +373,19 @@ describe("materializeChat message metadata", () => { }, }]); assert.deepEqual(settled.map((item) => item.id), ['receipt']); + + const rejected = materializeChat([...attempts, { + type: 'system_note', + id: 'rejection', + ts: 3, + kind: 'session_mailbox_failed', + data: { + ...outboxData, + errorCode: 'session_busy', + errorMessage: 'Target Session is busy', + }, + }]); + assert.deepEqual(rejected, []); }); }); diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts index 75d5587025..6cb4ea00a2 100644 --- a/packages/ui/src/materialize.ts +++ b/packages/ui/src/materialize.ts @@ -40,6 +40,7 @@ import type { ToolActivityStatus } from '@maka/core/tool-result-status'; import type { ShellRunToolResult } from '@maka/core/shell-run-result'; import type { StoredMessage, TurnRecord, TurnStatus, UserMessage } from '@maka/core/session'; import { + parseSessionMailboxFailedNoteData, parseSessionMailboxOutboxNoteData, parseSessionMailboxSentNoteData, parseTrustedSessionMailboxMessage, @@ -1171,6 +1172,11 @@ function visibleSessionMailboxOutboxNoteIds( if (receipt) terminalMessageIds.add(receipt.messageId); continue; } + if (message.kind === 'session_mailbox_failed') { + const rejection = parseSessionMailboxFailedNoteData(message.data); + if (rejection) terminalMessageIds.add(rejection.messageId); + continue; + } if (message.kind !== 'session_mailbox_outbox') continue; const outbox = parseSessionMailboxOutboxNoteData(message.data); if (