diff --git a/backend/database/queries/message-queries.ts b/backend/database/queries/message-queries.ts index 31dc79f7..40070490 100644 --- a/backend/database/queries/message-queries.ts +++ b/backend/database/queries/message-queries.ts @@ -2,6 +2,7 @@ import { getDatabase } from '../index'; import type { DatabaseMessage } from '$shared/types/database/schema'; import type { UnifiedMessage } from '$shared/types/unified'; import { debug } from '$shared/utils/logger'; +import { trimSubAgentForWire } from '$shared/utils/subagent-wire-trim'; /** Parse message JSON from a DB row. */ function parseMessage(row: DatabaseMessage): UnifiedMessage { @@ -25,7 +26,10 @@ export const messageQueries = { } const path = this.getPathToRoot(session.head_message_id); - return path.map(parseMessage); + // Trim sub-agent tool noise from the display payload. The DB rows keep the + // full data; only what is sent to the frontend is slimmed. See + // trimSubAgentForWire for the rationale. + return path.map(parseMessage).map(trimSubAgentForWire); }, /** @@ -51,6 +55,44 @@ export const messageQueries = { return message; }, + /** + * Get the full (untrimmed) sub-agent messages nested under a message's + * tool_use blocks, matched by `parent.toolUseId`. + * + * The wire payload trims sub-agent noise (see trimSubAgentForWire), so the + * frontend's in-memory copy is slimmed. The Debug modal uses this to fetch + * the complete sub-agent data straight from the DB and rebuild the full view. + */ + getSubAgentMessages(messageId: string): UnifiedMessage[] { + const db = getDatabase(); + const row = db.prepare(` + SELECT * FROM messages WHERE id = ? + `).get(messageId) as DatabaseMessage | null; + if (!row) return []; + + const parent = parseMessage(row); + if (parent.type !== 'assistant') return []; + + const toolUseIds = new Set( + parent.content + .filter(block => block.type === 'tool_use') + .map(block => (block as { id: string }).id) + ); + if (toolUseIds.size === 0) return []; + + const rows = db.prepare(` + SELECT * FROM messages WHERE session_id = ? + ORDER BY created_at ASC + `).all(row.session_id) as DatabaseMessage[]; + + return rows + .map(parseMessage) + .filter(msg => { + const parentToolUseId = msg.parent?.toolUseId; + return parentToolUseId != null && toolUseIds.has(parentToolUseId); + }); + }, + create(messageData: { session_id: string; message: UnifiedMessage; diff --git a/backend/ws/chat/stream.ts b/backend/ws/chat/stream.ts index 41610527..c757363f 100644 --- a/backend/ws/chat/stream.ts +++ b/backend/ws/chat/stream.ts @@ -10,8 +10,9 @@ import { t } from 'elysia'; import { createRouter } from '$shared/utils/ws-server'; import { streamManager, type StreamEvent } from '../../chat/stream-manager'; -import type { EngineType } from '$shared/types/unified'; +import type { EngineType, UnifiedMessage } from '$shared/types/unified'; import { debug } from '$shared/utils/logger'; +import { trimSubAgentForWire } from '$shared/utils/subagent-wire-trim'; import { ws } from '$backend/utils/ws'; import { broadcastPresence } from '../projects/status'; import { sessionQueries, messageQueries } from '../../database/queries'; @@ -280,7 +281,7 @@ export const streamHandler = createRouter() case 'message': { ws.emit.chatSession(chatSessionId, 'chat:message', { processId: event.processId, - message: event.data.message, + message: trimSubAgentForWire(event.data.message), usage: event.data.usage, timestamp: event.data.timestamp, message_id: event.data.message_id, @@ -424,7 +425,7 @@ export const streamHandler = createRouter() case 'message': { ws.emit.chatSession(chatSessionId, 'chat:message', { processId: event.processId, - message: event.data.message, + message: trimSubAgentForWire(event.data.message), usage: event.data.usage, timestamp: event.data.timestamp, message_id: event.data.message_id, @@ -618,7 +619,14 @@ export const streamHandler = createRouter() streamId: streamState.streamId, status: streamState.status, processId: streamState.processId, - messages: streamState.messages, + // Trim sub-agent noise from the catch-up buffer too (see trimSubAgentForWire). + // Buffer entries wrap the UnifiedMessage under `.message`; leave the rest intact. + messages: streamState.messages.map(entry => { + const wrapped = entry as { message?: UnifiedMessage }; + return wrapped?.message + ? { ...wrapped, message: trimSubAgentForWire(wrapped.message) } + : entry; + }), currentPartialText: streamState.currentPartialText, currentReasoningText: streamState.currentReasoningText, error: streamState.error, diff --git a/backend/ws/messages/crud.ts b/backend/ws/messages/crud.ts index 7f571acb..f3424dbd 100644 --- a/backend/ws/messages/crud.ts +++ b/backend/ws/messages/crud.ts @@ -44,6 +44,19 @@ export const crudHandler = createRouter() return loadMessage(message); }) + // Get full (untrimmed) sub-agent messages nested under a message. + // Used by the Debug modal to show complete data despite the wire trimming + // sub-agent noise from the normal chat payload. + .http('messages:get-subagents', { + data: t.Object({ + messageId: t.String({ minLength: 1 }) + }), + response: t.Array(t.Any()) + }, async ({ data, conn }) => { + requireMessageAccess(conn, data.messageId); + return messageQueries.getSubAgentMessages(data.messageId); + }) + // Delete message .http('messages:delete', { data: t.Object({ diff --git a/frontend/components/chat/modal/DebugModal.svelte b/frontend/components/chat/modal/DebugModal.svelte index 3e114b8d..5f657c43 100644 --- a/frontend/components/chat/modal/DebugModal.svelte +++ b/frontend/components/chat/modal/DebugModal.svelte @@ -5,12 +5,21 @@ - Display raw message data - JSON formatting - Copy to clipboard + + The normal chat payload trims sub-agent (Task) tool noise for bandwidth + (see shared/utils/subagent-wire-trim.ts), so the in-memory message can carry + slimmed sub-agent activity. On open we fetch the full untrimmed sub-agent + messages straight from the DB and rebuild the complete subActivities so the + debug view always shows the real, complete data. --> @@ -42,7 +102,7 @@ Raw Message
-
{JSON.stringify(message, null, 2)}
+
{JSON.stringify(displayMessage, null, 2)}