fix: harden resume after compaction failures (#195)
* fix: harden resume after compaction failures * test: cover resume compaction safeguards * fix: address resume safeguard review findings
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
||||
loadTranscriptFile,
|
||||
removeExtraFields,
|
||||
} from './sessionStorage.js'
|
||||
import { jsonStringify } from './slowOperations.js'
|
||||
import type { ContentReplacementRecord } from './toolResultStorage.js'
|
||||
|
||||
// Dead code elimination: ant-only tool names are conditionally required so
|
||||
@@ -71,6 +72,37 @@ const SEND_USER_FILE_TOOL_NAME: string | null = feature('KAIROS')
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
// Hard cap for reconstructed resume payloads before REPL boot. 8 MiB keeps
|
||||
// resume bounded well below the multi-GB failure mode we saw while leaving
|
||||
// enough room for normal compacted sessions plus resume hook context.
|
||||
const MAX_RESUME_MESSAGE_BYTES = 8 * 1024 * 1024
|
||||
|
||||
export class ResumeTranscriptTooLargeError extends Error {
|
||||
constructor(
|
||||
readonly bytes: number,
|
||||
readonly maxBytes: number,
|
||||
readonly messageCount: number,
|
||||
) {
|
||||
super(
|
||||
`Reconstructed transcript is too large to resume safely (${(
|
||||
bytes / (1024 * 1024)
|
||||
).toFixed(1)} MiB > ${(maxBytes / (1024 * 1024)).toFixed(1)} MiB, ${messageCount} messages).`,
|
||||
)
|
||||
this.name = 'ResumeTranscriptTooLargeError'
|
||||
}
|
||||
}
|
||||
|
||||
function assertResumeMessageSize(messages: Message[]): void {
|
||||
const bytes = Buffer.byteLength(jsonStringify(messages), 'utf8')
|
||||
if (bytes > MAX_RESUME_MESSAGE_BYTES) {
|
||||
throw new ResumeTranscriptTooLargeError(
|
||||
bytes,
|
||||
MAX_RESUME_MESSAGE_BYTES,
|
||||
messages.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms legacy attachment types to current types for backward compatibility
|
||||
*/
|
||||
@@ -561,11 +593,16 @@ export async function loadConversationForResume(
|
||||
const deserialized = deserializeMessagesWithInterruptDetection(messages!)
|
||||
messages = deserialized.messages
|
||||
|
||||
// Reject oversized resumes before running side-effectful resume hooks.
|
||||
assertResumeMessageSize(messages)
|
||||
|
||||
// Process session start hooks for resume
|
||||
const hookMessages = await processSessionStartHooks('resume', { sessionId })
|
||||
|
||||
// Append hook messages to the conversation
|
||||
// Append hook messages to the conversation and guard again in case hook
|
||||
// output itself pushes the session over the safe resume limit.
|
||||
messages.push(...hookMessages)
|
||||
assertResumeMessageSize(messages)
|
||||
|
||||
return {
|
||||
messages,
|
||||
|
||||
Reference in New Issue
Block a user