Use exhaustive switch on message.role throughout coding-agent

- addMessageToChat: exhaustive switch for all AgentMessage roles
- renderSessionContext: delegates to addMessageToChat, special handling for assistant tool calls and tool results
- export-html formatMessage: exhaustive switch for all AgentMessage roles
- Removed isHookMessage, isBashExecutionMessage type guards in favor of role checks
- Fixed imports and removed unused getLatestCompactionEntry
This commit is contained in:
Mario Zechner 2025-12-28 14:12:08 +01:00
parent ecef601d19
commit b921298af7
11 changed files with 442 additions and 376 deletions

View file

@ -34,7 +34,7 @@ import type {
TurnEndEvent,
TurnStartEvent,
} from "./hooks/index.js";
import { type BashExecutionMessage, type HookMessage, isHookMessage } from "./messages.js";
import type { BashExecutionMessage, HookMessage } from "./messages.js";
import type { ModelRegistry } from "./model-registry.js";
import type { CompactionEntry, SessionManager } from "./session-manager.js";
import type { SettingsManager, SkillsSettings } from "./settings-manager.js";
@ -218,8 +218,8 @@ export class AgentSession {
// Handle session persistence
if (event.type === "message_end") {
// Check if this is a hook message (has _hookData marker)
if (isHookMessage(event.message)) {
// Check if this is a hook message
if (event.message.role === "hookMessage") {
// Persist as CustomMessageEntry
this.sessionManager.appendCustomMessageEntry(
event.message.customType,
@ -227,10 +227,15 @@ export class AgentSession {
event.message.display,
event.message.details,
);
} else {
// Regular message - persist as SessionMessageEntry
} else if (
event.message.role === "user" ||
event.message.role === "assistant" ||
event.message.role === "toolResult"
) {
// Regular LLM message - persist as SessionMessageEntry
this.sessionManager.appendMessage(event.message);
}
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
// Track assistant message for auto-compaction (checked on agent_end)
if (event.message.role === "assistant") {

View file

@ -8,8 +8,8 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { AssistantMessage, Model, Usage } from "@mariozechner/pi-ai";
import { complete } from "@mariozechner/pi-ai";
import { convertToLlm } from "./messages.js";
import { type CompactionEntry, createSummaryMessage, type SessionEntry } from "./session-manager.js";
import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "./messages.js";
import type { CompactionEntry, SessionEntry } from "./session-manager.js";
/**
* Extract AgentMessage from an entry if it produces one.
@ -20,14 +20,10 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | null {
return entry.message;
}
if (entry.type === "custom_message") {
return {
role: "user",
content: entry.content,
timestamp: new Date(entry.timestamp).getTime(),
};
return createHookMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
}
if (entry.type === "branch_summary") {
return createSummaryMessage(entry.summary, entry.timestamp);
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
}
return null;
}
@ -116,59 +112,65 @@ export function shouldCompact(contextTokens: number, contextWindow: number, sett
export function estimateTokens(message: AgentMessage): number {
let chars = 0;
// Handle bashExecution messages
if (message.role === "bashExecution") {
const bash = message as unknown as { command: string; output: string };
chars = bash.command.length + bash.output.length;
return Math.ceil(chars / 4);
}
// Handle user messages
if (message.role === "user") {
const content = (message as { content: string | Array<{ type: string; text?: string }> }).content;
if (typeof content === "string") {
chars = content.length;
} else if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text" && block.text) {
chars += block.text.length;
switch (message.role) {
case "user": {
const content = (message as { content: string | Array<{ type: string; text?: string }> }).content;
if (typeof content === "string") {
chars = content.length;
} else if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text" && block.text) {
chars += block.text.length;
}
}
}
return Math.ceil(chars / 4);
}
return Math.ceil(chars / 4);
}
// Handle assistant messages
if (message.role === "assistant") {
const assistant = message as AssistantMessage;
for (const block of assistant.content) {
if (block.type === "text") {
chars += block.text.length;
} else if (block.type === "thinking") {
chars += block.thinking.length;
} else if (block.type === "toolCall") {
chars += block.name.length + JSON.stringify(block.arguments).length;
case "assistant": {
const assistant = message as AssistantMessage;
for (const block of assistant.content) {
if (block.type === "text") {
chars += block.text.length;
} else if (block.type === "thinking") {
chars += block.thinking.length;
} else if (block.type === "toolCall") {
chars += block.name.length + JSON.stringify(block.arguments).length;
}
}
return Math.ceil(chars / 4);
}
return Math.ceil(chars / 4);
}
// Handle tool results
if (message.role === "toolResult") {
const toolResult = message as { content: Array<{ type: string; text?: string }> };
for (const block of toolResult.content) {
if (block.type === "text" && block.text) {
chars += block.text.length;
case "hookMessage":
case "toolResult": {
if (typeof message.content === "string") {
chars = message.content.length;
} else {
for (const block of message.content) {
if (block.type === "text" && block.text) {
chars += block.text.length;
}
if (block.type === "image") {
chars += 4800; // Estimate images as 4000 chars, or 1200 tokens
}
}
}
return Math.ceil(chars / 4);
}
case "bashExecution": {
chars = message.command.length + message.output.length;
return Math.ceil(chars / 4);
}
case "branchSummary":
case "compactionSummary": {
chars = message.summary.length;
return Math.ceil(chars / 4);
}
return Math.ceil(chars / 4);
}
return 0;
}
/**
* Find valid cut points: indices of user, assistant, or bashExecution messages.
* Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
* Never cut at tool results (they must follow their tool call).
* When we cut at an assistant message with tool calls, its tool results follow it
* and will be kept.
@ -178,16 +180,34 @@ function findValidCutPoints(entries: SessionEntry[], startIndex: number, endInde
const cutPoints: number[] = [];
for (let i = startIndex; i < endIndex; i++) {
const entry = entries[i];
switch (entry.type) {
case "message": {
const role = entry.message.role;
switch (role) {
case "bashExecution":
case "hookMessage":
case "branchSummary":
case "compactionSummary":
case "user":
case "assistant":
cutPoints.push(i);
break;
case "toolResult":
break;
}
break;
}
case "thinking_level_change":
case "model_change":
case "compaction":
case "branch_summary":
case "custom":
case "custom_message":
case "label":
}
// branch_summary and custom_message are user-role messages, valid cut points
if (entry.type === "branch_summary" || entry.type === "custom_message") {
cutPoints.push(i);
} else if (entry.type === "message") {
const role = entry.message.role;
// user, assistant, and bashExecution are valid cut points
// toolResult must stay with its preceding tool call
if (role === "user" || role === "assistant" || role === "bashExecution") {
cutPoints.push(i);
}
}
}
return cutPoints;

View file

@ -1,4 +1,4 @@
import type { AgentState } from "@mariozechner/pi-agent-core";
import type { AgentMessage, AgentState } from "@mariozechner/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
import { existsSync, readFileSync, writeFileSync } from "fs";
import hljs from "highlight.js";
@ -7,7 +7,6 @@ import { homedir } from "os";
import * as path from "path";
import { basename } from "path";
import { APP_NAME, getCustomThemesDir, getThemesDir, VERSION } from "../config.js";
import { type BashExecutionMessage, isBashExecutionMessage } from "./messages.js";
import type { SessionManager } from "./session-manager.js";
// ============================================================================
@ -821,110 +820,136 @@ function formatToolExecution(
return { html, bgColor };
}
function formatMessage(message: Message, toolResultsMap: Map<string, ToolResultMessage>, colors: ThemeColors): string {
function formatMessage(
message: AgentMessage,
toolResultsMap: Map<string, ToolResultMessage>,
colors: ThemeColors,
): string {
let html = "";
const timestamp = (message as { timestamp?: number }).timestamp;
const timestampHtml = timestamp ? `<div class="message-timestamp">${formatTimestamp(timestamp)}</div>` : "";
// Handle bash execution messages (user-executed via ! command)
if (isBashExecutionMessage(message)) {
const bashMsg = message as unknown as BashExecutionMessage;
const isError = bashMsg.cancelled || (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null);
switch (message.role) {
case "bashExecution": {
const isError = message.cancelled || (message.exitCode !== 0 && message.exitCode !== null);
html += `<div class="tool-execution user-bash${isError ? " user-bash-error" : ""}">`;
html += timestampHtml;
html += `<div class="tool-command">$ ${escapeHtml(bashMsg.command)}</div>`;
html += `<div class="tool-execution user-bash${isError ? " user-bash-error" : ""}">`;
html += timestampHtml;
html += `<div class="tool-command">$ ${escapeHtml(message.command)}</div>`;
if (bashMsg.output) {
const lines = bashMsg.output.split("\n");
html += formatExpandableOutput(lines, 10);
if (message.output) {
const lines = message.output.split("\n");
html += formatExpandableOutput(lines, 10);
}
if (message.cancelled) {
html += `<div class="bash-status warning">(cancelled)</div>`;
} else if (message.exitCode !== 0 && message.exitCode !== null) {
html += `<div class="bash-status error">(exit ${message.exitCode})</div>`;
}
if (message.truncated && message.fullOutputPath) {
html += `<div class="bash-truncation warning">Output truncated. Full output: ${escapeHtml(message.fullOutputPath)}</div>`;
}
html += `</div>`;
break;
}
case "user": {
const userMsg = message as UserMessage;
let textContent = "";
const images: ImageContent[] = [];
if (bashMsg.cancelled) {
html += `<div class="bash-status warning">(cancelled)</div>`;
} else if (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null) {
html += `<div class="bash-status error">(exit ${bashMsg.exitCode})</div>`;
}
if (bashMsg.truncated && bashMsg.fullOutputPath) {
html += `<div class="bash-truncation warning">Output truncated. Full output: ${escapeHtml(bashMsg.fullOutputPath)}</div>`;
}
html += `</div>`;
return html;
}
if (message.role === "user") {
const userMsg = message as UserMessage;
let textContent = "";
const images: ImageContent[] = [];
if (typeof userMsg.content === "string") {
textContent = userMsg.content;
} else {
for (const block of userMsg.content) {
if (block.type === "text") {
textContent += block.text;
} else if (block.type === "image") {
images.push(block as ImageContent);
if (typeof userMsg.content === "string") {
textContent = userMsg.content;
} else {
for (const block of userMsg.content) {
if (block.type === "text") {
textContent += block.text;
} else if (block.type === "image") {
images.push(block as ImageContent);
}
}
}
}
html += `<div class="user-message">${timestampHtml}`;
html += `<div class="user-message">${timestampHtml}`;
// Render images first
if (images.length > 0) {
html += `<div class="message-images">`;
for (const img of images) {
html += `<img src="data:${img.mimeType};base64,${img.data}" alt="User uploaded image" class="message-image" />`;
// Render images first
if (images.length > 0) {
html += `<div class="message-images">`;
for (const img of images) {
html += `<img src="data:${img.mimeType};base64,${img.data}" alt="User uploaded image" class="message-image" />`;
}
html += `</div>`;
}
// Render text as markdown (server-side)
if (textContent.trim()) {
html += `<div class="markdown-content">${renderMarkdown(textContent)}</div>`;
}
html += `</div>`;
break;
}
case "assistant": {
html += timestampHtml ? `<div class="assistant-message">${timestampHtml}` : "";
// Render text as markdown (server-side)
if (textContent.trim()) {
html += `<div class="markdown-content">${renderMarkdown(textContent)}</div>`;
}
html += `</div>`;
} else if (message.role === "assistant") {
const assistantMsg = message as AssistantMessage;
html += timestampHtml ? `<div class="assistant-message">${timestampHtml}` : "";
for (const content of assistantMsg.content) {
if (content.type === "text" && content.text.trim()) {
// Render markdown server-side
html += `<div class="assistant-text markdown-content">${renderMarkdown(content.text)}</div>`;
} else if (content.type === "thinking" && content.thinking.trim()) {
html += `<div class="thinking-text">${escapeHtml(content.thinking.trim()).replace(/\n/g, "<br>")}</div>`;
for (const content of message.content) {
if (content.type === "text" && content.text.trim()) {
// Render markdown server-side
html += `<div class="assistant-text markdown-content">${renderMarkdown(content.text)}</div>`;
} else if (content.type === "thinking" && content.thinking.trim()) {
html += `<div class="thinking-text">${escapeHtml(content.thinking.trim()).replace(/\n/g, "<br>")}</div>`;
}
}
}
for (const content of assistantMsg.content) {
if (content.type === "toolCall") {
const toolResult = toolResultsMap.get(content.id);
const { html: toolHtml, bgColor } = formatToolExecution(
content.name,
content.arguments as Record<string, unknown>,
toolResult,
colors,
);
html += `<div class="tool-execution" style="background-color: ${bgColor}">${toolHtml}</div>`;
for (const content of message.content) {
if (content.type === "toolCall") {
const toolResult = toolResultsMap.get(content.id);
const { html: toolHtml, bgColor } = formatToolExecution(
content.name,
content.arguments as Record<string, unknown>,
toolResult,
colors,
);
html += `<div class="tool-execution" style="background-color: ${bgColor}">${toolHtml}</div>`;
}
}
}
const hasToolCalls = assistantMsg.content.some((c) => c.type === "toolCall");
if (!hasToolCalls) {
if (assistantMsg.stopReason === "aborted") {
html += '<div class="error-text">Aborted</div>';
} else if (assistantMsg.stopReason === "error") {
html += `<div class="error-text">Error: ${escapeHtml(assistantMsg.errorMessage || "Unknown error")}</div>`;
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
if (!hasToolCalls) {
if (message.stopReason === "aborted") {
html += '<div class="error-text">Aborted</div>';
} else if (message.stopReason === "error") {
html += `<div class="error-text">Error: ${escapeHtml(message.errorMessage || "Unknown error")}</div>`;
}
}
}
if (timestampHtml) {
html += "</div>";
if (timestampHtml) {
html += "</div>";
}
break;
}
case "toolResult":
// Tool results are rendered inline with tool calls
break;
case "hookMessage":
// Hook messages with display:true shown as info boxes
if (message.display) {
const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
html += `<div class="hook-message">${timestampHtml}<div class="hook-type">[${escapeHtml(message.customType)}]</div><div class="markdown-content">${renderMarkdown(content)}</div></div>`;
}
break;
case "compactionSummary":
// Rendered separately via formatCompaction
break;
case "branchSummary":
// Rendered as compaction-like summary
html += `<div class="compaction-container expanded"><div class="compaction-content"><div class="compaction-summary"><div class="compaction-summary-header">Branch Summary</div><div class="compaction-summary-content">${escapeHtml(message.summary).replace(/\n/g, "<br>")}</div></div></div></div>`;
break;
default: {
// Exhaustive check
const _exhaustive: never = message;
}
}

View file

@ -8,6 +8,21 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
<summary>
`;
export const COMPACTION_SUMMARY_SUFFIX = `
</summary>`;
export const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
<summary>
`;
export const BRANCH_SUMMARY_SUFFIX = `</summary>`;
/**
* Message type for bash executions via the ! command.
*/
@ -35,28 +50,30 @@ export interface HookMessage<T = unknown> {
timestamp: number;
}
export interface BranchSummaryMessage {
role: "branchSummary";
summary: string;
fromId: string;
timestamp: number;
}
export interface CompactionSummaryMessage {
role: "compactionSummary";
summary: string;
tokensBefore: number;
timestamp: number;
}
// Extend CustomAgentMessages via declaration merging
declare module "@mariozechner/pi-agent-core" {
interface CustomAgentMessages {
bashExecution: BashExecutionMessage;
hookMessage: HookMessage;
branchSummary: BranchSummaryMessage;
compactionSummary: CompactionSummaryMessage;
}
}
/**
* Type guard for BashExecutionMessage.
*/
export function isBashExecutionMessage(msg: AgentMessage | Message): msg is BashExecutionMessage {
return msg.role === "bashExecution";
}
/**
* Type guard for HookMessage.
*/
export function isHookMessage(msg: AgentMessage | Message): msg is HookMessage {
return msg.role === "hookMessage";
}
/**
* Convert a BashExecutionMessage to user message text for LLM context.
*/
@ -78,6 +95,46 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
return text;
}
export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
return {
role: "branchSummary",
summary,
fromId,
timestamp: new Date(timestamp).getTime(),
};
}
export function createCompactionSummaryMessage(
summary: string,
tokensBefore: number,
timestamp: string,
): CompactionSummaryMessage {
return {
role: "compactionSummary",
summary: summary,
tokensBefore,
timestamp: new Date(timestamp).getTime(),
};
}
/** Convert CustomMessageEntry to AgentMessage format */
export function createHookMessage(
customType: string,
content: string | (TextContent | ImageContent)[],
display: boolean,
details: unknown | undefined,
timestamp: string,
): HookMessage {
return {
role: "hookMessage",
customType,
content,
display,
details,
timestamp: new Date(timestamp).getTime(),
};
}
/**
* Transform AgentMessages (including custom types) to LLM-compatible Messages.
*
@ -89,30 +146,44 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
export function convertToLlm(messages: AgentMessage[]): Message[] {
return messages
.map((m): Message | null => {
if (isBashExecutionMessage(m)) {
// Convert bash execution to user message
return {
role: "user",
content: [{ type: "text", text: bashExecutionToText(m) }],
timestamp: m.timestamp,
};
switch (m.role) {
case "bashExecution":
return {
role: "user",
content: [{ type: "text", text: bashExecutionToText(m) }],
timestamp: m.timestamp,
};
case "hookMessage": {
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
return {
role: "user",
content,
timestamp: m.timestamp,
};
}
case "branchSummary":
return {
role: "user",
content: [{ type: "text" as const, text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX }],
timestamp: m.timestamp,
};
case "compactionSummary":
return {
role: "user",
content: [
{ type: "text" as const, text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX },
],
timestamp: m.timestamp,
};
case "user":
case "assistant":
case "toolResult":
return m;
default:
// biome-ignore lint/correctness/noSwitchDeclarations: fine
const _exhaustiveCheck: never = m;
return null;
}
if (isHookMessage(m)) {
// Convert hook message to user message for LLM
// Normalize string content to array format
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
return {
role: "user",
content,
timestamp: m.timestamp,
};
}
// Pass through standard LLM roles
if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") {
return m as Message;
}
// Filter out unknown message types
return null;
})
.filter((m) => m !== null);
}

View file

@ -1,5 +1,5 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
import { randomUUID } from "crypto";
import {
appendFileSync,
@ -15,6 +15,13 @@ import {
} from "fs";
import { join, resolve } from "path";
import { getAgentDir as getDefaultAgentDir } from "../config.js";
import {
type BashExecutionMessage,
createBranchSummaryMessage,
createCompactionSummaryMessage,
createHookMessage,
type HookMessage,
} from "./messages.js";
export const CURRENT_SESSION_VERSION = 2;
@ -59,9 +66,12 @@ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
details?: T;
}
export interface BranchSummaryEntry extends SessionEntryBase {
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
type: "branch_summary";
fromId: string;
summary: string;
/** Hook-specific data (not sent to LLM) */
details?: T;
}
/**
@ -145,35 +155,6 @@ export interface SessionInfo {
allMessagesText: string;
}
export const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
<summary>
`;
export const SUMMARY_SUFFIX = `
</summary>`;
/** Exported for compaction.test.ts */
export function createSummaryMessage(summary: string, timestamp: string): AgentMessage {
return {
role: "user",
content: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,
timestamp: new Date(timestamp).getTime(),
};
}
/** Convert CustomMessageEntry to AgentMessage format */
function createCustomMessage(entry: CustomMessageEntry): AgentMessage {
return {
role: "hookMessage",
customType: entry.customType,
content: entry.content,
display: entry.display,
details: entry.details,
timestamp: new Date(entry.timestamp).getTime(),
} as AgentMessage;
}
/** Generate a unique short ID (8 hex chars, collision-checked) */
function generateId(byId: { has(id: string): boolean }): string {
for (let i = 0; i < 100; i++) {
@ -328,9 +309,21 @@ export function buildSessionContext(
// 3. Emit messages after compaction
const messages: AgentMessage[] = [];
const appendMessage = (entry: SessionEntry) => {
if (entry.type === "message") {
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(
createHookMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
);
} else if (entry.type === "branch_summary" && entry.summary) {
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
}
};
if (compaction) {
// Emit summary first
messages.push(createSummaryMessage(compaction.summary, compaction.timestamp));
messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp));
// Find compaction index in path
const compactionIdx = path.findIndex((e) => e.type === "compaction" && e.id === compaction.id);
@ -343,37 +336,19 @@ export function buildSessionContext(
foundFirstKept = true;
}
if (foundFirstKept) {
if (entry.type === "message") {
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(createCustomMessage(entry));
} else if (entry.type === "branch_summary") {
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
}
appendMessage(entry);
}
}
// Emit messages after compaction
for (let i = compactionIdx + 1; i < path.length; i++) {
const entry = path[i];
if (entry.type === "message") {
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(createCustomMessage(entry));
} else if (entry.type === "branch_summary") {
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
}
appendMessage(entry);
}
} else {
// No compaction - emit all messages, handle branch summaries and custom messages
for (const entry of path) {
if (entry.type === "message") {
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(createCustomMessage(entry));
} else if (entry.type === "branch_summary") {
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
}
appendMessage(entry);
}
}
@ -597,8 +572,13 @@ export class SessionManager {
this._persist(entry);
}
/** Append a message as child of current leaf, then advance leaf. Returns entry id. */
appendMessage(message: AgentMessage): string {
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
* Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
* Reason: we want these to be top-level entries in the session, not message session entries,
* so it is easier to find them.
* These need to be appended via appendCompaction() and appendBranchSummary() methods.
*/
appendMessage(message: Message | HookMessage | BashExecutionMessage): string {
const entry: SessionMessageEntry = {
type: "message",
id: generateId(this.byId),
@ -851,6 +831,7 @@ export class SessionManager {
id: generateId(this.byId),
parentId: branchFromId,
timestamp: new Date().toISOString(),
fromId: branchFromId,
summary,
};
this._appendEntry(entry);