mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
refactor(messages): extract system factories (6 of 8) (#1903)
* refactor(messages): extract system factories * test(messages): cover system factory extraction
This commit is contained in:
@@ -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,
|
||||
}))
|
||||
|
||||
+19
-322
@@ -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,
|
||||
|
||||
@@ -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('<command-name>test</command-name>')).toMatchObject({
|
||||
type: 'system',
|
||||
subtype: 'local_command',
|
||||
content: '<command-name>test</command-name>',
|
||||
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,
|
||||
)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user