Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,14 @@ exports[`IPC message snapshots ClientMessage types conversation_search serialize
}
`;

exports[`IPC message snapshots ClientMessage types message_content_request serializes to expected JSON 1`] = `
{
"messageId": "msg-001",
"sessionId": "sess-001",
"type": "message_content_request",
}
`;

exports[`IPC message snapshots ClientMessage types ipc_blob_probe serializes to expected JSON 1`] = `
{
"nonceSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Expand Down Expand Up @@ -1096,6 +1104,49 @@ exports[`IPC message snapshots ClientMessage types notification_intent_result se
}
`;

exports[`IPC message snapshots ClientMessage types recording_status serializes to expected JSON 1`] = `
{
"sessionId": "rec-001",
"status": "started",
"type": "recording_status",
}
`;

exports[`IPC message snapshots ClientMessage types heartbeat_config serializes to expected JSON 1`] = `
{
"action": "get",
"type": "heartbeat_config",
}
`;

exports[`IPC message snapshots ClientMessage types heartbeat_runs_list serializes to expected JSON 1`] = `
{
"type": "heartbeat_runs_list",
}
`;

exports[`IPC message snapshots ClientMessage types heartbeat_run_now serializes to expected JSON 1`] = `
{
"type": "heartbeat_run_now",
}
`;

exports[`IPC message snapshots ClientMessage types heartbeat_checklist_read serializes to expected JSON 1`] = `
{
"type": "heartbeat_checklist_read",
}
`;

exports[`IPC message snapshots ClientMessage types heartbeat_checklist_write serializes to expected JSON 1`] = `
{
"content":
"- [ ] Check email
- [ ] Review PRs"
,
"type": "heartbeat_checklist_write",
}
`;

exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
{
"success": true,
Expand Down Expand Up @@ -1308,6 +1359,24 @@ exports[`IPC message snapshots ServerMessage types conversation_search_response
}
`;

exports[`IPC message snapshots ServerMessage types message_content_response serializes to expected JSON 1`] = `
{
"messageId": "msg-001",
"sessionId": "sess-001",
"text": "Full message content here",
"toolCalls": [
{
"input": {
"command": "ls",
},
"name": "bash",
"result": "output",
},
],
"type": "message_content_response",
}
`;

exports[`IPC message snapshots ServerMessage types error serializes to expected JSON 1`] = `
{
"message": "Something went wrong",
Expand Down Expand Up @@ -1360,6 +1429,7 @@ exports[`IPC message snapshots ServerMessage types model_info serializes to expe

exports[`IPC message snapshots ServerMessage types history_response serializes to expected JSON 1`] = `
{
"hasMore": false,
"messages": [
{
"role": "user",
Expand Down Expand Up @@ -2934,3 +3004,68 @@ exports[`IPC message snapshots ServerMessage types approved_device_remove_respon
"type": "approved_device_remove_response",
}
`;

exports[`IPC message snapshots ServerMessage types recording_start serializes to expected JSON 1`] = `
{
"recordingId": "rec-001",
"type": "recording_start",
}
`;

exports[`IPC message snapshots ServerMessage types recording_stop serializes to expected JSON 1`] = `
{
"recordingId": "rec-001",
"type": "recording_stop",
}
`;

exports[`IPC message snapshots ServerMessage types heartbeat_config_response serializes to expected JSON 1`] = `
{
"activeHoursEnd": 17,
"activeHoursStart": 9,
"enabled": true,
"intervalMs": 3600000,
"nextRunAt": 1700003600000,
"success": true,
"type": "heartbeat_config_response",
}
`;

exports[`IPC message snapshots ServerMessage types heartbeat_runs_list_response serializes to expected JSON 1`] = `
{
"runs": [
{
"createdAt": 1700000000000,
"id": "hb-run-001",
"result": "All systems nominal",
"title": "Morning heartbeat",
},
],
"type": "heartbeat_runs_list_response",
}
`;

exports[`IPC message snapshots ServerMessage types heartbeat_run_now_response serializes to expected JSON 1`] = `
{
"success": true,
"type": "heartbeat_run_now_response",
}
`;

exports[`IPC message snapshots ServerMessage types heartbeat_checklist_response serializes to expected JSON 1`] = `
{
"content":
"- [ ] Check email
- [ ] Review PRs"
,
"isDefault": false,
"type": "heartbeat_checklist_response",
}
`;

exports[`IPC message snapshots ServerMessage types heartbeat_checklist_write_response serializes to expected JSON 1`] = `
{
"success": true,
"type": "heartbeat_checklist_write_response",
}
`;
12 changes: 12 additions & 0 deletions assistant/src/__tests__/ipc-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
limit: 20,
maxMessagesPerConversation: 3,
},
message_content_request: {
type: 'message_content_request',
sessionId: 'sess-001',
messageId: 'msg-001',
},
ipc_blob_probe: {
type: 'ipc_blob_probe',
probeId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
Expand Down Expand Up @@ -842,6 +847,13 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
},
],
},
message_content_response: {
type: 'message_content_response',
sessionId: 'sess-001',
messageId: 'msg-001',
text: 'Full message content here',
toolCalls: [{ name: 'bash', result: 'output', input: { command: 'ls' } }],
},
error: {
type: 'error',
message: 'Something went wrong',
Expand Down
65 changes: 63 additions & 2 deletions assistant/src/daemon/handlers/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ConversationSearchRequest,
DeleteQueuedMessage,
HistoryRequest,
MessageContentRequest,
RegenerateRequest,
SandboxSetRequest,
SecretResponse,
Expand Down Expand Up @@ -737,17 +738,37 @@ export function handleHistoryRequest(
})))
: m.surfaces;

// Apply text truncation when maxTextChars is set
let wasTruncated = false;
let text = m.text;
if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) {
text = text.slice(0, msg.maxTextChars) + ' \u2026 [truncated]';
wasTruncated = true;
}

// Apply tool result truncation when maxToolResultChars is set
const truncatedToolCalls = msg.maxToolResultChars !== undefined && filteredToolCalls.length > 0
? filteredToolCalls.map((tc) => {
if (tc.result !== undefined && tc.result.length > msg.maxToolResultChars!) {
wasTruncated = true;
return { ...tc, result: tc.result.slice(0, msg.maxToolResultChars!) + ' \u2026 [truncated]' };
}
return tc;
})
: filteredToolCalls;

return {
...(m.id ? { id: m.id } : {}),
role: m.role,
text: m.text,
text,
timestamp: m.timestamp,
...(filteredToolCalls.length > 0 ? { toolCalls: filteredToolCalls, toolCallsBeforeText: m.toolCallsBeforeText } : {}),
...(truncatedToolCalls.length > 0 ? { toolCalls: truncatedToolCalls, toolCallsBeforeText: m.toolCallsBeforeText } : {}),
...(attachments ? { attachments } : {}),
...(m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}),
Comment thread
ashleeradka marked this conversation as resolved.
Comment thread
ashleeradka marked this conversation as resolved.
...(m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}),
...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}),
...(m.subagentNotification ? { subagentNotification: m.subagentNotification } : {}),
...(wasTruncated ? { wasTruncated: true } : {}),
};
});

Expand Down Expand Up @@ -888,6 +909,45 @@ export function handleConversationSearch(
});
}

export function handleMessageContentRequest(
msg: MessageContentRequest,
socket: net.Socket,
ctx: HandlerContext,
): void {
const dbMessage = conversationStore.getMessageById(msg.messageId, msg.sessionId);
if (!dbMessage) {
ctx.send(socket, { type: 'error', message: `Message ${msg.messageId} not found in session ${msg.sessionId}` });
return;
}

let text: string | undefined;
let toolCalls: Array<{ name: string; result?: string; input?: Record<string, unknown> }> | undefined;

try {
const content = JSON.parse(dbMessage.content);
const rendered = renderHistoryContent(content);
text = rendered.text || undefined;
if (rendered.toolCalls.length > 0) {
toolCalls = rendered.toolCalls.map((tc) => ({
Comment thread
ashleeradka marked this conversation as resolved.
name: tc.name,
input: tc.input,
...(tc.result !== undefined ? { result: tc.result } : {}),
}));
}
} catch {
// Raw text content (not JSON)
text = dbMessage.content || undefined;
}

ctx.send(socket, {
type: 'message_content_response',
sessionId: msg.sessionId,
messageId: msg.messageId,
...(text !== undefined ? { text } : {}),
...(toolCalls ? { toolCalls } : {}),
});
}

export const sessionHandlers = defineHandlers({
user_message: handleUserMessage,
confirmation_response: handleConfirmationResponse,
Expand All @@ -900,6 +960,7 @@ export const sessionHandlers = defineHandlers({
cancel: handleCancel,
delete_queued_message: handleDeleteQueuedMessage,
history_request: handleHistoryRequest,
message_content_request: handleMessageContentRequest,
undo: handleUndo,
regenerate: handleRegenerate,
usage_request: handleUsageRequest,
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/daemon/ipc-contract-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"integration_list",
"ipc_blob_probe",
"link_open_request",
"message_content_request",
"model_get",
"model_set",
"notification_intent_result",
Expand Down Expand Up @@ -250,6 +251,7 @@
"memory_recalled",
"memory_status",
"message_complete",
"message_content_response",
"message_dequeued",
"message_queued",
"message_queued_deleted",
Expand Down
26 changes: 24 additions & 2 deletions assistant/src/daemon/ipc-contract/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,24 @@ export interface HistoryRequest {
includeSurfaceData?: boolean;
/** Shorthand: 'light' = all include flags false (default), 'full' = all include flags true. */
mode?: 'light' | 'full';
/** Truncate message text fields beyond this character limit. When omitted, full text is returned. */
maxTextChars?: number;
/** Truncate tool result strings beyond this character limit. When omitted, full results are returned. */
maxToolResultChars?: number;
}

export interface MessageContentRequest {
type: 'message_content_request';
sessionId: string;
messageId: string;
}

export interface MessageContentResponse {
type: 'message_content_response';
sessionId: string;
messageId: string;
text?: string;
toolCalls?: Array<{ name: string; result?: string; input?: Record<string, unknown> }>;
}

export interface UndoRequest {
Expand Down Expand Up @@ -276,6 +294,8 @@ export interface HistoryResponse {
error?: string;
conversationId?: string;
};
/** True when text or tool result content was truncated due to maxTextChars/maxToolResultChars. */
wasTruncated?: boolean;
}>;
/** Whether older messages exist beyond the returned page. */
hasMore: boolean;
Expand Down Expand Up @@ -362,7 +382,8 @@ export type _SessionsClientMessages =
| SessionSwitchRequest
| SessionRenameRequest
| SessionsClearRequest
| ConversationSearchRequest;
| ConversationSearchRequest
| MessageContentRequest;

export type _SessionsServerMessages =
| AuthResult
Expand All @@ -381,4 +402,5 @@ export type _SessionsServerMessages =
| SessionTitleUpdated
| SessionListResponse
| SessionsClearResponse
| ConversationSearchResponse;
| ConversationSearchResponse
| MessageContentResponse;
15 changes: 15 additions & 0 deletions assistant/src/memory/conversation-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,21 @@ export function getMessages(conversationId: string): MessageRow[] {
.map(parseMessage);
}

/** Fetch a single message by ID, optionally scoped to a specific conversation. */
export function getMessageById(messageId: string, conversationId?: string): MessageRow | null {
const db = getDb();
const conditions = [eq(messages.id, messageId)];
if (conversationId) {
conditions.push(eq(messages.conversationId, conversationId));
}
const row = db
.select()
.from(messages)
.where(and(...conditions))
.get();
return row ? parseMessage(row) : null;
}

export interface PaginatedMessagesResult {
messages: MessageRow[];
/** Whether older messages exist beyond the returned page. */
Expand Down
Loading
Loading