diff --git a/src/services/compact/compact.test.ts b/src/services/compact/compact.test.ts
index 8c9d1faeb..ae62be186 100644
--- a/src/services/compact/compact.test.ts
+++ b/src/services/compact/compact.test.ts
@@ -45,6 +45,9 @@ const _realDiskOutputModule = await import(
const _realMessagesModule = await import(
`../../utils/messages.js?real=${Date.now()}-${Math.random()}`
)
+const _realSystemFactoriesModule = await import(
+ `../../utils/messages/systemFactories.js?real=${Date.now()}-${Math.random()}`
+)
const _realSlowOperationsModule = await import(
`../../utils/slowOperations.js?real=${Date.now()}-${Math.random()}`
)
@@ -663,6 +666,9 @@ afterAll(async () => {
MAX_TASK_OUTPUT_BYTES_DISPLAY: _realDiskOutputModule.MAX_TASK_OUTPUT_BYTES_DISPLAY,
}))
mock.module('../../utils/messages.js', () => ({ ..._realMessagesModule }))
+ mock.module('../../utils/messages/systemFactories.js', () => ({
+ ..._realSystemFactoriesModule,
+ }))
mock.module('../../utils/slowOperations.js', () => ({
..._realSlowOperationsModule,
}))
diff --git a/src/utils/messages.ts b/src/utils/messages.ts
index f49f184c6..c476930df 100644
--- a/src/utils/messages.ts
+++ b/src/utils/messages.ts
@@ -4246,328 +4246,25 @@ function createToolUseMessage(
})
}
-export function createSystemMessage(
- content: string,
- level: SystemMessageLevel,
- toolUseID?: string,
- preventContinuation?: boolean,
-): SystemInformationalMessage {
- return {
- type: 'system',
- subtype: 'informational',
- content,
- isMeta: false,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- toolUseID,
- level,
- ...(preventContinuation && { preventContinuation }),
- }
-}
-
-export function createPermissionRetryMessage(
- commands: string[],
-): SystemPermissionRetryMessage {
- return {
- type: 'system',
- subtype: 'permission_retry',
- content: `Allowed ${commands.join(', ')}`,
- commands,
- level: 'info',
- isMeta: false,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- }
-}
-
-export function createBridgeStatusMessage(
- url: string,
- upgradeNudge?: string,
-): SystemBridgeStatusMessage {
- return {
- type: 'system',
- subtype: 'bridge_status',
- content: `/remote-control is active. Code in CLI or at ${url}`,
- url,
- upgradeNudge,
- isMeta: false,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- }
-}
-
-export function createScheduledTaskFireMessage(
- content: string,
-): SystemScheduledTaskFireMessage {
- return {
- type: 'system',
- subtype: 'scheduled_task_fire',
- content,
- isMeta: false,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- }
-}
-
-export function createStopHookSummaryMessage(
- hookCount: number,
- hookInfos: StopHookInfo[],
- hookErrors: string[],
- preventedContinuation: boolean,
- stopReason: string | undefined,
- hasOutput: boolean,
- level: SystemMessageLevel,
- toolUseID?: string,
- hookLabel?: string,
- totalDurationMs?: number,
-): SystemStopHookSummaryMessage {
- return {
- type: 'system',
- subtype: 'stop_hook_summary',
- hookCount,
- hookInfos,
- hookErrors,
- preventedContinuation,
- stopReason,
- hasOutput,
- level,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- toolUseID,
- hookLabel,
- totalDurationMs,
- }
-}
-
-export function createTurnDurationMessage(
- durationMs: number,
- budget?: { tokens: number; limit: number; nudges: number },
- messageCount?: number,
-): SystemTurnDurationMessage {
- return {
- type: 'system',
- subtype: 'turn_duration',
- durationMs,
- budgetTokens: budget?.tokens,
- budgetLimit: budget?.limit,
- budgetNudges: budget?.nudges,
- messageCount,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- isMeta: false,
- }
-}
-
-export function createAwaySummaryMessage(
- content: string,
-): SystemAwaySummaryMessage {
- return {
- type: 'system',
- subtype: 'away_summary',
- content,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- isMeta: false,
- }
-}
-
-export function createMemorySavedMessage(
- writtenPaths: string[],
-): SystemMemorySavedMessage {
- return {
- type: 'system',
- subtype: 'memory_saved',
- writtenPaths,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- isMeta: false,
- }
-}
-
-export function createAgentsKilledMessage(): SystemAgentsKilledMessage {
- return {
- type: 'system',
- subtype: 'agents_killed',
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- isMeta: false,
- }
-}
-
-export function createApiMetricsMessage(metrics: {
- ttftMs: number
- otps: number
- isP50?: boolean
- hookDurationMs?: number
- turnDurationMs?: number
- toolDurationMs?: number
- classifierDurationMs?: number
- toolCount?: number
- hookCount?: number
- classifierCount?: number
- configWriteCount?: number
-}): SystemApiMetricsMessage {
- return {
- type: 'system',
- subtype: 'api_metrics',
- ttftMs: metrics.ttftMs,
- otps: metrics.otps,
- isP50: metrics.isP50,
- hookDurationMs: metrics.hookDurationMs,
- turnDurationMs: metrics.turnDurationMs,
- toolDurationMs: metrics.toolDurationMs,
- classifierDurationMs: metrics.classifierDurationMs,
- toolCount: metrics.toolCount,
- hookCount: metrics.hookCount,
- classifierCount: metrics.classifierCount,
- configWriteCount: metrics.configWriteCount,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- isMeta: false,
- }
-}
-
-export function createCommandInputMessage(
- content: string,
-): SystemLocalCommandMessage {
- return {
- type: 'system',
- subtype: 'local_command',
- content,
- level: 'info',
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- isMeta: false,
- }
-}
-
-export function createCompactBoundaryMessage(
- trigger: 'manual' | 'auto',
- preTokens: number,
- lastPreCompactMessageUuid?: UUID,
- userContext?: string,
- messagesSummarized?: number,
-): SystemCompactBoundaryMessage {
- return {
- type: 'system',
- subtype: 'compact_boundary',
- content: `Conversation compacted`,
- isMeta: false,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- level: 'info',
- compactMetadata: {
- trigger,
- preTokens,
- userContext,
- messagesSummarized,
- },
- ...(lastPreCompactMessageUuid && {
- logicalParentUuid: lastPreCompactMessageUuid,
- }),
- }
-}
-
-export function createMicrocompactBoundaryMessage(
- trigger: 'auto',
- preTokens: number,
- tokensSaved: number,
- compactedToolIds: string[],
- clearedAttachmentUUIDs: string[],
-): SystemMicrocompactBoundaryMessage {
- logForDebugging(
- `[microcompact] saved ~${formatTokens(tokensSaved)} tokens (cleared ${compactedToolIds.length} tool results)`,
- )
- return {
- type: 'system',
- subtype: 'microcompact_boundary',
- content: 'Context microcompacted',
- isMeta: false,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- level: 'info',
- microcompactMetadata: {
- trigger,
- preTokens,
- tokensSaved,
- compactedToolIds,
- clearedAttachmentUUIDs,
- },
- }
-}
-
-export function createSystemAPIErrorMessage(
- error: APIError,
- retryInMs: number,
- retryAttempt: number,
- maxRetries: number,
-): SystemAPIErrorMessage {
- return {
- type: 'system',
- subtype: 'api_error',
- level: 'error',
- cause: error.cause instanceof Error ? error.cause : undefined,
- error,
- retryInMs,
- retryAttempt,
- maxRetries,
- timestamp: new Date().toISOString(),
- uuid: randomUUID(),
- }
-}
-
-/**
- * Checks if a message is a compact boundary marker
- */
-export function isCompactBoundaryMessage(
- message: Message | NormalizedMessage,
-): message is SystemCompactBoundaryMessage {
- return message?.type === 'system' && message.subtype === 'compact_boundary'
-}
-
-/**
- * Finds the index of the last compact boundary marker in the messages array
- * @returns The index of the last compact boundary, or -1 if none found
- */
-export function findLastCompactBoundaryIndex<
- T extends Message | NormalizedMessage,
->(messages: T[]): number {
- // Scan backwards to find the most recent compact boundary
- for (let i = messages.length - 1; i >= 0; i--) {
- const message = messages[i]
- if (message && isCompactBoundaryMessage(message)) {
- return i
- }
- }
- return -1 // No boundary found
-}
-
-/**
- * Returns messages from the last compact boundary onward (including the boundary).
- * If no boundary exists, returns all messages.
- *
- * Also filters snipped messages by default (when HISTORY_SNIP is enabled) —
- * the REPL keeps full history for UI scrollback, so model-facing paths need
- * both compact-slice AND snip-filter applied. Pass `{ includeSnipped: true }`
- * to opt out (e.g., REPL.tsx fullscreen compact handler which preserves
- * snipped messages in scrollback).
- *
- * Note: The boundary itself is a system message and will be filtered by normalizeMessagesForAPI.
- */
-export function getMessagesAfterCompactBoundary<
- T extends Message | NormalizedMessage,
->(messages: T[], options?: { includeSnipped?: boolean }): T[] {
- const boundaryIndex = findLastCompactBoundaryIndex(messages)
- const sliced = boundaryIndex === -1 ? messages : messages.slice(boundaryIndex)
- if (!options?.includeSnipped && feature('HISTORY_SNIP')) {
- /* eslint-disable @typescript-eslint/no-require-imports */
- const { projectSnippedView } =
- require('../services/compact/snipProjection.js') as typeof import('../services/compact/snipProjection.js')
- /* eslint-enable @typescript-eslint/no-require-imports */
- return projectSnippedView(sliced as Message[]) as T[]
- }
- return sliced
-}
+export {
+ createAgentsKilledMessage,
+ createApiMetricsMessage,
+ createAwaySummaryMessage,
+ createBridgeStatusMessage,
+ createCommandInputMessage,
+ createCompactBoundaryMessage,
+ createMemorySavedMessage,
+ createMicrocompactBoundaryMessage,
+ createPermissionRetryMessage,
+ createScheduledTaskFireMessage,
+ createStopHookSummaryMessage,
+ createSystemAPIErrorMessage,
+ createSystemMessage,
+ createTurnDurationMessage,
+ findLastCompactBoundaryIndex,
+ getMessagesAfterCompactBoundary,
+ isCompactBoundaryMessage,
+} from "./messages/systemFactories.js"
export function shouldShowUserMessage(
message: NormalizedMessage,
diff --git a/src/utils/messages/systemFactories.test.ts b/src/utils/messages/systemFactories.test.ts
new file mode 100644
index 000000000..4a0b49cd4
--- /dev/null
+++ b/src/utils/messages/systemFactories.test.ts
@@ -0,0 +1,233 @@
+import { feature } from 'bun:bundle'
+import { expect, test } from 'bun:test'
+import {
+ createAgentsKilledMessage,
+ createApiMetricsMessage,
+ createAwaySummaryMessage,
+ createBridgeStatusMessage,
+ createCommandInputMessage,
+ createCompactBoundaryMessage,
+ createMemorySavedMessage,
+ createMicrocompactBoundaryMessage,
+ createPermissionRetryMessage,
+ createScheduledTaskFireMessage,
+ createStopHookSummaryMessage,
+ createSystemAPIErrorMessage,
+ createSystemMessage,
+ createTurnDurationMessage,
+ findLastCompactBoundaryIndex,
+ getMessagesAfterCompactBoundary,
+ isCompactBoundaryMessage,
+} from './systemFactories.js'
+import type { Message } from '../../types/message.js'
+
+test('createSystemMessage builds an informational system message', () => {
+ const message = createSystemMessage('ready', 'info', 'toolu_1', true)
+
+ expect(message).toMatchObject({
+ type: 'system',
+ subtype: 'informational',
+ content: 'ready',
+ level: 'info',
+ toolUseID: 'toolu_1',
+ preventContinuation: true,
+ })
+})
+
+test('system factory helpers build their expected message shapes', () => {
+ expect(createPermissionRetryMessage(['Bash(ls)', 'Read(*)'])).toMatchObject({
+ type: 'system',
+ subtype: 'permission_retry',
+ content: 'Allowed Bash(ls), Read(*)',
+ commands: ['Bash(ls)', 'Read(*)'],
+ level: 'info',
+ isMeta: false,
+ })
+ expect(createBridgeStatusMessage('https://remote.test', 'upgrade')).toMatchObject({
+ type: 'system',
+ subtype: 'bridge_status',
+ content: '/remote-control is active. Code in CLI or at https://remote.test',
+ url: 'https://remote.test',
+ upgradeNudge: 'upgrade',
+ })
+ expect(createScheduledTaskFireMessage('run task')).toMatchObject({
+ type: 'system',
+ subtype: 'scheduled_task_fire',
+ content: 'run task',
+ isMeta: false,
+ })
+ expect(
+ createStopHookSummaryMessage(
+ 2,
+ [{ hookName: 'stop', command: 'echo ok', matcher: undefined }],
+ ['failed'],
+ true,
+ 'stop_sequence',
+ true,
+ 'warning',
+ 'toolu_stop',
+ 'Stop',
+ 123,
+ ),
+ ).toMatchObject({
+ type: 'system',
+ subtype: 'stop_hook_summary',
+ hookCount: 2,
+ hookErrors: ['failed'],
+ preventedContinuation: true,
+ stopReason: 'stop_sequence',
+ hasOutput: true,
+ level: 'warning',
+ toolUseID: 'toolu_stop',
+ hookLabel: 'Stop',
+ totalDurationMs: 123,
+ })
+ expect(
+ createTurnDurationMessage(
+ 250,
+ { tokens: 10, limit: 100, nudges: 1 },
+ 4,
+ ),
+ ).toMatchObject({
+ type: 'system',
+ subtype: 'turn_duration',
+ durationMs: 250,
+ budgetTokens: 10,
+ budgetLimit: 100,
+ budgetNudges: 1,
+ messageCount: 4,
+ isMeta: false,
+ })
+ expect(createAwaySummaryMessage('away')).toMatchObject({
+ type: 'system',
+ subtype: 'away_summary',
+ content: 'away',
+ isMeta: false,
+ })
+ expect(createMemorySavedMessage(['/tmp/memory.md'])).toMatchObject({
+ type: 'system',
+ subtype: 'memory_saved',
+ writtenPaths: ['/tmp/memory.md'],
+ isMeta: false,
+ })
+ expect(createAgentsKilledMessage()).toMatchObject({
+ type: 'system',
+ subtype: 'agents_killed',
+ isMeta: false,
+ })
+ expect(
+ createApiMetricsMessage({
+ ttftMs: 12,
+ otps: 34,
+ isP50: true,
+ hookDurationMs: 5,
+ turnDurationMs: 6,
+ toolDurationMs: 7,
+ classifierDurationMs: 8,
+ toolCount: 2,
+ hookCount: 3,
+ classifierCount: 1,
+ configWriteCount: 4,
+ }),
+ ).toMatchObject({
+ type: 'system',
+ subtype: 'api_metrics',
+ ttftMs: 12,
+ otps: 34,
+ isP50: true,
+ hookDurationMs: 5,
+ turnDurationMs: 6,
+ toolDurationMs: 7,
+ classifierDurationMs: 8,
+ toolCount: 2,
+ hookCount: 3,
+ classifierCount: 1,
+ configWriteCount: 4,
+ isMeta: false,
+ })
+ expect(createCommandInputMessage('test')).toMatchObject({
+ type: 'system',
+ subtype: 'local_command',
+ content: 'test',
+ level: 'info',
+ isMeta: false,
+ })
+})
+
+test('microcompact and API error factories preserve branch-specific fields', () => {
+ const microcompact = createMicrocompactBoundaryMessage(
+ 'auto',
+ 1000,
+ 250,
+ ['toolu_1'],
+ ['attachment_1'],
+ )
+ expect(microcompact).toMatchObject({
+ type: 'system',
+ subtype: 'microcompact_boundary',
+ content: 'Context microcompacted',
+ level: 'info',
+ isMeta: false,
+ microcompactMetadata: {
+ trigger: 'auto',
+ preTokens: 1000,
+ tokensSaved: 250,
+ compactedToolIds: ['toolu_1'],
+ clearedAttachmentUUIDs: ['attachment_1'],
+ },
+ })
+
+ const cause = new Error('network down')
+ const errorWithCause = { message: 'api failed', cause } as any
+ expect(createSystemAPIErrorMessage(errorWithCause, 100, 2, 5)).toMatchObject({
+ type: 'system',
+ subtype: 'api_error',
+ level: 'error',
+ error: errorWithCause,
+ cause,
+ retryInMs: 100,
+ retryAttempt: 2,
+ maxRetries: 5,
+ })
+
+ const errorWithoutErrorCause = { message: 'api failed', cause: 'string cause' } as any
+ expect(
+ createSystemAPIErrorMessage(errorWithoutErrorCause, 200, 1, 3).cause,
+ ).toBeUndefined()
+})
+
+test('compact boundary helpers find and slice from the latest boundary', () => {
+ const first = createSystemMessage('old', 'info')
+ const boundary = createCompactBoundaryMessage('manual', 100)
+ const last = createSystemMessage('new', 'info')
+
+ expect(isCompactBoundaryMessage(boundary)).toBe(true)
+ expect(findLastCompactBoundaryIndex([first, boundary, last])).toBe(1)
+ expect(getMessagesAfterCompactBoundary([first, boundary, last])).toEqual([
+ boundary,
+ last,
+ ])
+})
+
+test('compact boundary slicing also applies snip projection when enabled', () => {
+ const removed = createSystemMessage('removed', 'info')
+ const kept = createSystemMessage('kept', 'info')
+ const snipBoundary = {
+ ...createSystemMessage('snipped', 'info'),
+ subtype: 'snip_boundary',
+ snipMetadata: { removedUuids: [removed.uuid] },
+ } as Message
+
+ const messages = [removed, snipBoundary, kept]
+ const result = getMessagesAfterCompactBoundary(messages)
+
+ if (feature('HISTORY_SNIP')) {
+ expect(result).toEqual([snipBoundary, kept])
+ } else {
+ expect(result).toEqual(messages)
+ }
+
+ expect(getMessagesAfterCompactBoundary(messages, { includeSnipped: true })).toEqual(
+ messages,
+ )
+})
diff --git a/src/utils/messages/systemFactories.ts b/src/utils/messages/systemFactories.ts
new file mode 100644
index 000000000..b60d00087
--- /dev/null
+++ b/src/utils/messages/systemFactories.ts
@@ -0,0 +1,329 @@
+import { feature } from "bun:bundle"
+import type { APIError } from "@anthropic-ai/sdk"
+import { randomUUID, type UUID } from "crypto"
+import type { Message, NormalizedMessage, StopHookInfo, SystemAgentsKilledMessage, SystemAPIErrorMessage, SystemApiMetricsMessage, SystemAwaySummaryMessage, SystemBridgeStatusMessage, SystemCompactBoundaryMessage, SystemInformationalMessage, SystemLocalCommandMessage, SystemMemorySavedMessage, SystemMicrocompactBoundaryMessage, SystemPermissionRetryMessage, SystemScheduledTaskFireMessage, SystemStopHookSummaryMessage, SystemTurnDurationMessage, SystemMessageLevel } from "../../types/message.js"
+import { formatTokens } from "../format.js"
+import { logForDebugging } from "../debug.js"
+
+export function createSystemMessage(
+ content: string,
+ level: SystemMessageLevel,
+ toolUseID?: string,
+ preventContinuation?: boolean,
+): SystemInformationalMessage {
+ return {
+ type: 'system',
+ subtype: 'informational',
+ content,
+ isMeta: false,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ toolUseID,
+ level,
+ ...(preventContinuation && { preventContinuation }),
+ }
+}
+
+export function createPermissionRetryMessage(
+ commands: string[],
+): SystemPermissionRetryMessage {
+ return {
+ type: 'system',
+ subtype: 'permission_retry',
+ content: `Allowed ${commands.join(', ')}`,
+ commands,
+ level: 'info',
+ isMeta: false,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ }
+}
+
+export function createBridgeStatusMessage(
+ url: string,
+ upgradeNudge?: string,
+): SystemBridgeStatusMessage {
+ return {
+ type: 'system',
+ subtype: 'bridge_status',
+ content: `/remote-control is active. Code in CLI or at ${url}`,
+ url,
+ upgradeNudge,
+ isMeta: false,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ }
+}
+
+export function createScheduledTaskFireMessage(
+ content: string,
+): SystemScheduledTaskFireMessage {
+ return {
+ type: 'system',
+ subtype: 'scheduled_task_fire',
+ content,
+ isMeta: false,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ }
+}
+
+export function createStopHookSummaryMessage(
+ hookCount: number,
+ hookInfos: StopHookInfo[],
+ hookErrors: string[],
+ preventedContinuation: boolean,
+ stopReason: string | undefined,
+ hasOutput: boolean,
+ level: SystemMessageLevel,
+ toolUseID?: string,
+ hookLabel?: string,
+ totalDurationMs?: number,
+): SystemStopHookSummaryMessage {
+ return {
+ type: 'system',
+ subtype: 'stop_hook_summary',
+ hookCount,
+ hookInfos,
+ hookErrors,
+ preventedContinuation,
+ stopReason,
+ hasOutput,
+ level,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ toolUseID,
+ hookLabel,
+ totalDurationMs,
+ }
+}
+
+export function createTurnDurationMessage(
+ durationMs: number,
+ budget?: { tokens: number; limit: number; nudges: number },
+ messageCount?: number,
+): SystemTurnDurationMessage {
+ return {
+ type: 'system',
+ subtype: 'turn_duration',
+ durationMs,
+ budgetTokens: budget?.tokens,
+ budgetLimit: budget?.limit,
+ budgetNudges: budget?.nudges,
+ messageCount,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ isMeta: false,
+ }
+}
+
+export function createAwaySummaryMessage(
+ content: string,
+): SystemAwaySummaryMessage {
+ return {
+ type: 'system',
+ subtype: 'away_summary',
+ content,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ isMeta: false,
+ }
+}
+
+export function createMemorySavedMessage(
+ writtenPaths: string[],
+): SystemMemorySavedMessage {
+ return {
+ type: 'system',
+ subtype: 'memory_saved',
+ writtenPaths,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ isMeta: false,
+ }
+}
+
+export function createAgentsKilledMessage(): SystemAgentsKilledMessage {
+ return {
+ type: 'system',
+ subtype: 'agents_killed',
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ isMeta: false,
+ }
+}
+
+export function createApiMetricsMessage(metrics: {
+ ttftMs: number
+ otps: number
+ isP50?: boolean
+ hookDurationMs?: number
+ turnDurationMs?: number
+ toolDurationMs?: number
+ classifierDurationMs?: number
+ toolCount?: number
+ hookCount?: number
+ classifierCount?: number
+ configWriteCount?: number
+}): SystemApiMetricsMessage {
+ return {
+ type: 'system',
+ subtype: 'api_metrics',
+ ttftMs: metrics.ttftMs,
+ otps: metrics.otps,
+ isP50: metrics.isP50,
+ hookDurationMs: metrics.hookDurationMs,
+ turnDurationMs: metrics.turnDurationMs,
+ toolDurationMs: metrics.toolDurationMs,
+ classifierDurationMs: metrics.classifierDurationMs,
+ toolCount: metrics.toolCount,
+ hookCount: metrics.hookCount,
+ classifierCount: metrics.classifierCount,
+ configWriteCount: metrics.configWriteCount,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ isMeta: false,
+ }
+}
+
+export function createCommandInputMessage(
+ content: string,
+): SystemLocalCommandMessage {
+ return {
+ type: 'system',
+ subtype: 'local_command',
+ content,
+ level: 'info',
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ isMeta: false,
+ }
+}
+
+export function createCompactBoundaryMessage(
+ trigger: 'manual' | 'auto',
+ preTokens: number,
+ lastPreCompactMessageUuid?: UUID,
+ userContext?: string,
+ messagesSummarized?: number,
+): SystemCompactBoundaryMessage {
+ return {
+ type: 'system',
+ subtype: 'compact_boundary',
+ content: `Conversation compacted`,
+ isMeta: false,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ level: 'info',
+ compactMetadata: {
+ trigger,
+ preTokens,
+ userContext,
+ messagesSummarized,
+ },
+ ...(lastPreCompactMessageUuid && {
+ logicalParentUuid: lastPreCompactMessageUuid,
+ }),
+ }
+}
+
+export function createMicrocompactBoundaryMessage(
+ trigger: 'auto',
+ preTokens: number,
+ tokensSaved: number,
+ compactedToolIds: string[],
+ clearedAttachmentUUIDs: string[],
+): SystemMicrocompactBoundaryMessage {
+ logForDebugging(
+ `[microcompact] saved ~${formatTokens(tokensSaved)} tokens (cleared ${compactedToolIds.length} tool results)`,
+ )
+ return {
+ type: 'system',
+ subtype: 'microcompact_boundary',
+ content: 'Context microcompacted',
+ isMeta: false,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ level: 'info',
+ microcompactMetadata: {
+ trigger,
+ preTokens,
+ tokensSaved,
+ compactedToolIds,
+ clearedAttachmentUUIDs,
+ },
+ }
+}
+
+export function createSystemAPIErrorMessage(
+ error: APIError,
+ retryInMs: number,
+ retryAttempt: number,
+ maxRetries: number,
+): SystemAPIErrorMessage {
+ return {
+ type: 'system',
+ subtype: 'api_error',
+ level: 'error',
+ cause: error.cause instanceof Error ? error.cause : undefined,
+ error,
+ retryInMs,
+ retryAttempt,
+ maxRetries,
+ timestamp: new Date().toISOString(),
+ uuid: randomUUID(),
+ }
+}
+
+/**
+ * Checks if a message is a compact boundary marker
+ */
+export function isCompactBoundaryMessage(
+ message: Message | NormalizedMessage,
+): message is SystemCompactBoundaryMessage {
+ return message?.type === 'system' && message.subtype === 'compact_boundary'
+}
+
+/**
+ * Finds the index of the last compact boundary marker in the messages array
+ * @returns The index of the last compact boundary, or -1 if none found
+ */
+export function findLastCompactBoundaryIndex<
+ T extends Message | NormalizedMessage,
+>(messages: T[]): number {
+ // Scan backwards to find the most recent compact boundary
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const message = messages[i]
+ if (message && isCompactBoundaryMessage(message)) {
+ return i
+ }
+ }
+ return -1 // No boundary found
+}
+
+/**
+ * Returns messages from the last compact boundary onward (including the boundary).
+ * If no boundary exists, returns all messages.
+ *
+ * Also filters snipped messages by default (when HISTORY_SNIP is enabled) —
+ * the REPL keeps full history for UI scrollback, so model-facing paths need
+ * both compact-slice AND snip-filter applied. Pass `{ includeSnipped: true }`
+ * to opt out (e.g., REPL.tsx fullscreen compact handler which preserves
+ * snipped messages in scrollback).
+ *
+ * Note: The boundary itself is a system message and will be filtered by normalizeMessagesForAPI.
+ */
+export function getMessagesAfterCompactBoundary<
+ T extends Message | NormalizedMessage,
+>(messages: T[], options?: { includeSnipped?: boolean }): T[] {
+ const boundaryIndex = findLastCompactBoundaryIndex(messages)
+ const sliced = boundaryIndex === -1 ? messages : messages.slice(boundaryIndex)
+ if (!options?.includeSnipped && feature('HISTORY_SNIP')) {
+ /* eslint-disable @typescript-eslint/no-require-imports */
+ const { projectSnippedView } =
+ require('../../services/compact/snipProjection.js') as typeof import('../../services/compact/snipProjection.js')
+ /* eslint-enable @typescript-eslint/no-require-imports */
+ return projectSnippedView(sliced as Message[]) as T[]
+ }
+ return sliced
+}