@caupulican/pi-agent-core 0.81.3 → 0.81.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,6 +117,16 @@ export declare function findCutPoint(entries: SessionEntry[], startIndex: number
117
117
  * If previousSummary is provided, uses the update prompt to merge.
118
118
  */
119
119
  export declare function generateSummary(currentMessages: AgentMessage[], model: Model<any>, reserveTokens: number, apiKey: string | undefined, headers?: Record<string, string>, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, preDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>, factsBlock?: string, chunked?: boolean): Promise<string>;
120
+ /** Hard ceiling for the facts-scaled summary budget; also the worst case selection must assume. */
121
+ export declare const SUMMARY_BUDGET_MAX_TOKENS = 4000;
122
+ /**
123
+ * Whether a candidate summarizer can ingest a summarization input of the given size in ONE
124
+ * request (unchunked), using the same window arithmetic as {@link getSummarizerInputBound} with
125
+ * the worst-case (facts-scaled) summary budget. Hosts use this at SELECTION time: a model that
126
+ * fails this must not be handed the job — chunking cannot rescue recall-gated summarization, and
127
+ * local servers silently truncate over-window prompts instead of erroring.
128
+ */
129
+ export declare function summarizerCanIngest(model: Model<any>, estimatedInputTokens: number): boolean;
120
130
  export interface CompactionPreparation {
121
131
  /** UUID of first entry to keep */
122
132
  firstKeptEntryId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from \"@caupulican/pi-ai\";\nimport { completeSimple } from \"@caupulican/pi-ai\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { buildSessionContext, type CompactionEntry, type SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage, StreamFn, ThinkingLevel } from \"../types.ts\";\nimport { type CompactionFacts, extractCompactionFacts, renderFactsBlock } from \"./extraction.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tserializeConversation,\n} from \"./utils.ts\";\nimport { buildRetryPrompt, type VerificationReport, verifySummary } from \"./verification.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\n/** Details stored in CompactionEntry.details for file tracking */\nexport interface CompactionDetails {\n\treadFiles: string[];\n\tmodifiedFiles: string[];\n}\n\n/**\n * Extract file operations from messages and previous compaction entries.\n */\nfunction extractFileOperations(\n\tmessages: AgentMessage[],\n\tentries: SessionEntry[],\n\tprevCompactionIndex: number,\n): FileOperations {\n\tconst fileOps = createFileOps();\n\n\t// Collect from previous compaction's details (if pi-generated)\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\tif (!prevCompaction.fromHook && prevCompaction.details) {\n\t\t\t// fromHook field kept for session file compatibility\n\t\t\tconst details = prevCompaction.details as CompactionDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\tfor (const f of details.modifiedFiles) fileOps.edited.add(f);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Extract from tool calls in messages\n\tfor (const msg of messages) {\n\t\textractFileOpsFromMessage(msg, fileOps);\n\t}\n\n\treturn fileOps;\n}\n\n// ============================================================================\n// Message Extraction\n// ============================================================================\n\n/**\n * Extract AgentMessage from an entry if it produces one.\n * Returns undefined for entries that don't contribute to LLM context.\n */\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"message\") {\n\t\treturn entry.message;\n\t}\n\tif (entry.type === \"custom_message\") {\n\t\treturn createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);\n\t}\n\tif (entry.type === \"branch_summary\") {\n\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\t}\n\tif (entry.type === \"compaction\") {\n\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\t}\n\treturn undefined;\n}\n\nfunction getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\treturn getMessageFromEntry(entry);\n}\n\n/** Result from compact() - SessionManager adds uuid/parentUuid when saving */\nexport interface CompactionResult<T = unknown> {\n\tsummary: string;\n\tfirstKeptEntryId: string;\n\ttokensBefore: number;\n\t/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */\n\tdetails?: T;\n\tverification?: VerificationReport;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n\t/**\n\t * Compaction also triggers once context exceeds this fraction of the model's window — not only when\n\t * it's nearly full (`contextWindow - reserveTokens`). On large-window models, waiting until nearly\n\t * full means every turn pays a huge input cost; a fractional cap keeps per-turn input bounded\n\t * (cost guard). The effective trigger is the LOWER of the two, so small-window models keep the\n\t * reserve-based behavior while large windows compact earlier. `0`/`1`+ disables the fractional cap.\n\t */\n\ttriggerPercent?: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\ttriggerPercent: 0.7,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Minimum projected space saving for the EARLY (fractional) compaction trigger to fire. Anti-thrashing\n * (cost guard, #30): an early compaction whose summary would barely shrink the context (mostly recent,\n * protected content) just burns a summarization call for little gain — skip it and let the context grow\n * until either the saving is worthwhile or the hard (near-full) trigger forces it. Does NOT gate the\n * hard trigger, so overflow is always avoided.\n */\nexport const MIN_COMPACTION_SAVINGS = 0.12;\n\n/**\n * Check if compaction should trigger based on context usage.\n *\n * Two triggers:\n * - HARD: context exceeds `contextWindow - reserveTokens` (near-full) or an explicit `triggerTokens`\n * override — always compact (prevents overflow).\n * - EARLY (fractional, cost guard): context exceeds `contextWindow * triggerPercent` — compact only if\n * the summary would actually save enough (`MIN_COMPACTION_SAVINGS`), so we don't thrash for tiny gains.\n */\nexport function shouldCompact(\n\tcontextTokens: number,\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n\ttriggerTokens?: number,\n): boolean {\n\tif (!settings.enabled) return false;\n\n\t// Hard trigger: near-full, or a caller-supplied lower override. Always compacts (avoid overflow).\n\tconst reserveTrigger = contextWindow - settings.reserveTokens;\n\tconst hardTrigger = triggerTokens === undefined ? reserveTrigger : Math.min(reserveTrigger, triggerTokens);\n\tif (contextTokens > hardTrigger) return true;\n\n\t// Early fractional trigger: bounds per-turn input cost on large-window models, gated by anti-thrashing.\n\tconst pct = settings.triggerPercent ?? 0;\n\tif (pct > 0 && pct < 1) {\n\t\tconst fractionalTrigger = Math.floor(contextWindow * pct);\n\t\tif (contextTokens > fractionalTrigger) {\n\t\t\t// Projected saving ≈ the non-protected fraction (everything but the recent tail we keep).\n\t\t\tconst projectedSavings = contextTokens > 0 ? 1 - settings.keepRecentTokens / contextTokens : 0;\n\t\t\treturn projectedSavings >= MIN_COMPACTION_SAVINGS;\n\t\t}\n\t}\n\treturn false;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\":\n\t\tcase \"compactionSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tswitch (entry.type) {\n\t\t\tcase \"message\": {\n\t\t\t\tconst role = entry.message.role;\n\t\t\t\tswitch (role) {\n\t\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\tcase \"custom\":\n\t\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\tcase \"user\":\n\t\t\t\t\tcase \"assistant\":\n\t\t\t\t\t\tcutPoints.push(i);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"toolResult\":\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"thinking_level_change\":\n\t\t\tcase \"model_change\":\n\t\t\tcase \"compaction\":\n\t\t\tcase \"branch_summary\":\n\t\t\tcase \"custom\":\n\t\t\tcase \"custom_message\":\n\t\t\tcase \"label\":\n\t\t\tcase \"session_info\":\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// branch_summary and custom_message are user-role messages, valid cut points\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\t// branch_summary and custom_message are user-role messages, can start a turn\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\treturn i;\n\t\t}\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = cutPoints[0]; // Default: keep from first message (not header)\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at session header or compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `Checkpoint the conversation above. Format from your instructions, sections in this order:\n## Active Task\n### Mandatory Rules\n## Files\n## Done\n## Constraints & Preferences\n## Key Decisions\n## Blocked / Open\n## Critical Context\n\nPre-extracted facts (verified by tooling — include ALL of them, merged with what you read):\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens. Concrete beats complete.`;\n\nconst UPDATE_SUMMARIZATION_PROMPT = `Update the checkpoint in <previous-summary> with the NEW turns above. RULES:\n- PRESERVE every existing ### Mandatory Rules bullet VERBATIM; append new ones.\n- Continue the ## Done numbering; move finished work from Blocked/Open.\n- Update ## Active Task to the newest unfulfilled user input; apply the cancellation rule.\n- Keep ## Files current (add new, keep still-relevant, drop obsolete).\n- Preserve exact paths, commands, errors.\n\nSame section order. Pre-extracted facts:\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens.`;\n\nfunction createSummarizationOptions(\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n): SimpleStreamOptions {\n\tconst options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };\n\tif (model.reasoning && thinkingLevel && thinkingLevel !== \"off\") {\n\t\toptions.reasoning = thinkingLevel;\n\t}\n\treturn options;\n}\n\nasync function completeSummarization(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (!streamFn) {\n\t\treturn completeSimple(model, context, options);\n\t}\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\n/**\n * Generate a summary of the conversation using the LLM.\n * If previousSummary is provided, uses the update prompt to merge.\n */\nexport async function generateSummary(\n\tcurrentMessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n\tpreviousSummary?: string,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\tfactsBlock = \"files:\\nactions:\\nprohibitions:\",\n\tchunked = false,\n): Promise<string> {\n\tconst summaryBudget = getSummaryBudget(reserveTokens, model);\n\tconst maxTokens = summaryBudget;\n\n\tlet promptSuffix = fillPromptTemplate(\n\t\tpreviousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT,\n\t\tfactsBlock,\n\t\tsummaryBudget,\n\t);\n\tif (customInstructions) {\n\t\tpromptSuffix = `${promptSuffix}\\n\\nAdditional focus: ${customInstructions}`;\n\t}\n\n\tconst llmMessages = convertToLlm(currentMessages);\n\tlet conversationText = serializeConversation(llmMessages);\n\tif (preDigest) {\n\t\ttry {\n\t\t\tconversationText = await preDigest(conversationText, signal);\n\t\t} catch {\n\t\t\t// Keep the verbatim conversation when an optional pre-digest fails.\n\t\t}\n\t}\n\n\tconst inputBound = getSummarizerInputBound(model, maxTokens);\n\tconst initialPromptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tif (estimateStringTokens(initialPromptText) > inputBound) {\n\t\tif (!chunked) {\n\t\t\tthrow new Error(\"input-overflow: summarization request exceeds summarizer window\");\n\t\t}\n\t\tconversationText = await summarizeChunks(\n\t\t\tconversationText,\n\t\t\tmodel,\n\t\t\tmaxTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t\tinputBound,\n\t\t);\n\t}\n\n\tconst promptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{\n\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\n\treturn truncateSummaryToBudget(extractTextContent(response), summaryBudget);\n}\n\nfunction fillPromptTemplate(template: string, factsBlock: string, budget: number): string {\n\treturn template.replaceAll(\"{FACTS_BLOCK}\", factsBlock).replaceAll(\"{BUDGET}\", String(budget));\n}\n\nfunction getSummaryBudget(reserveTokens: number, model: Model<any>): number {\n\tconst modelMaxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;\n\treturn Math.max(1, Math.min(1_500, Math.floor(0.8 * reserveTokens), modelMaxTokens));\n}\n\nfunction getSummarizerInputBound(model: Model<any>, maxTokens: number): number {\n\tconst contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;\n\treturn contextWindow === Number.POSITIVE_INFINITY ? contextWindow : Math.max(1, contextWindow - maxTokens - 2_000);\n}\n\nfunction buildSummarizationPrompt(\n\tconversationText: string,\n\tpreviousSummary: string | undefined,\n\tpromptSuffix: string,\n): string {\n\tlet promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n`;\n\tif (previousSummary) {\n\t\tpromptText += `<previous-summary>\\n${previousSummary}\\n</previous-summary>\\n\\n`;\n\t}\n\treturn promptText + promptSuffix;\n}\n\nasync function summarizeChunks(\n\tconversationText: string,\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n\tstreamFn: StreamFn | undefined,\n\tinputBound: number,\n): Promise<string> {\n\tconst maxChunkTokens = Math.max(1, inputBound);\n\tconst maxChunkChars = Math.max(1, maxChunkTokens * 4);\n\tconst chunks = splitText(conversationText, maxChunkChars);\n\tconst retainedChunks = chunks.slice(-4);\n\tconst omittedChunks = chunks.length - retainedChunks.length;\n\tconst summaries: string[] = [];\n\n\tfor (let i = 0; i < retainedChunks.length; i++) {\n\t\tconst promptText = `<conversation-chunk index=\"${i + 1}\" total=\"${retainedChunks.length}\">\\n${retainedChunks[i]}\\n</conversation-chunk>\\n\\nSummarize this chunk for a later checkpoint merge. Preserve exact file paths, commands, errors, user prohibitions, and active work. Output concise notes only.`;\n\t\tconst response = await completeSummarization(\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\t\tstreamFn,\n\t\t);\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t\t}\n\t\tsummaries.push(extractTextContent(response));\n\t}\n\n\tconst omittedNote =\n\t\tomittedChunks > 0\n\t\t\t? `## Critical Context\\n${omittedChunks} older oversized chunks omitted; deterministic facts supplied separately.\\n\\n`\n\t\t\t: \"\";\n\treturn `${omittedNote}${summaries.join(\"\\n\\n\")}`;\n}\n\nfunction splitText(text: string, maxChars: number): string[] {\n\tconst chunks: string[] = [];\n\tfor (let start = 0; start < text.length; start += maxChars) {\n\t\tchunks.push(text.slice(start, start + maxChars));\n\t}\n\treturn chunks.length > 0 ? chunks : [\"\"];\n}\n\nfunction extractTextContent(message: AssistantMessage): string {\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n\nfunction truncateSummaryToBudget(summary: string, budget: number): string {\n\tconst maxTokens = Math.floor(budget * 1.3);\n\tif (estimateStringTokens(summary) <= maxTokens) {\n\t\treturn summary;\n\t}\n\n\tlet current = summary;\n\tfor (const heading of [\n\t\t\"Critical Context\",\n\t\t\"Blocked / Open\",\n\t\t\"Key Decisions\",\n\t\t\"Constraints & Preferences\",\n\t\t\"Done\",\n\t\t\"Files\",\n\t]) {\n\t\tconst next = removeSummarySection(current, heading);\n\t\tif (next === current) {\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent = next;\n\t\tif (estimateStringTokens(current) <= maxTokens) {\n\t\t\treturn current;\n\t\t}\n\t}\n\treturn current;\n}\n\nfunction removeSummarySection(summary: string, heading: string): string {\n\tconst lines = summary.split(/\\r?\\n/);\n\tconst kept: string[] = [];\n\tlet skipping = false;\n\tfor (const line of lines) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tskipping = match[1].trim().toLowerCase() === heading.toLowerCase();\n\t\t\tif (skipping) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!skipping) {\n\t\t\tkept.push(line);\n\t\t}\n\t}\n\treturn kept.join(\"\\n\").trim();\n}\n\nfunction estimateStringTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\n// ============================================================================\n// Compaction Preparation (for extensions)\n// ============================================================================\n\nexport interface CompactionPreparation {\n\t/** UUID of first entry to keep */\n\tfirstKeptEntryId: string;\n\t/** Messages that will be summarized and discarded */\n\tmessagesToSummarize: AgentMessage[];\n\t/** Messages that will be turned into turn prefix summary (if splitting) */\n\tturnPrefixMessages: AgentMessage[];\n\t/** Whether this is a split turn (cut point in middle of turn) */\n\tisSplitTurn: boolean;\n\ttokensBefore: number;\n\t/** Summary from previous compaction, for iterative update */\n\tpreviousSummary?: string;\n\t/** File operations extracted from messagesToSummarize */\n\tfileOps: FileOperations;\n\t/** Facts extracted from the compacted span for verification gating */\n\tfacts?: CompactionFacts;\n\t/** Compaction settions from settings.jsonl\t*/\n\tsettings: CompactionSettings;\n}\n\nexport function prepareCompaction(\n\tpathEntries: SessionEntry[],\n\tsettings: CompactionSettings,\n): CompactionPreparation | undefined {\n\tif (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = pathEntries.length - 1; i >= 0; i--) {\n\t\tif (pathEntries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet previousSummary: string | undefined;\n\tlet boundaryStart = 0;\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;\n\t\tpreviousSummary = prevCompaction.summary;\n\t\tconst firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);\n\t\tboundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;\n\t}\n\tconst boundaryEnd = pathEntries.length;\n\n\tconst tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;\n\n\tconst cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Get UUID of first kept entry\n\tconst firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];\n\tif (!firstKeptEntry?.id) {\n\t\treturn undefined; // Session needs migration\n\t}\n\tconst firstKeptEntryId = firstKeptEntry.id;\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\n\t// Messages to summarize (will be discarded after summary)\n\tconst messagesToSummarize: AgentMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\tif (msg) messagesToSummarize.push(msg);\n\t}\n\n\t// Messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AgentMessage[] = [];\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {\n\t\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\t\tif (msg) turnPrefixMessages.push(msg);\n\t\t}\n\t}\n\n\t// Extract file operations from messages and previous compaction\n\tconst fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);\n\n\t// Also extract file ops from turn prefix if splitting\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (const msg of turnPrefixMessages) {\n\t\t\textractFileOpsFromMessage(msg, fileOps);\n\t\t}\n\t}\n\n\tconst facts = extractCompactionFacts(pathEntries, boundaryStart, historyEnd);\n\n\treturn {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn: cutPoint.isSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tfacts,\n\t\tsettings,\n\t};\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.`;\n\n/**\n * Generate summaries for compaction using prepared data.\n * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.\n *\n * @param preparation - Pre-calculated preparation from prepareCompaction()\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tpreparation: CompactionPreparation,\n\tmodel: Model<any>,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tcustomInstructions?: string,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\texecutionOptions?: { chunked?: boolean; allowVerificationFailure?: boolean },\n): Promise<CompactionResult> {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t\tfacts: factsFromPreparation,\n\t} = preparation;\n\n\tconst facts = factsFromPreparation ?? {\n\t\tfiles: [],\n\t\tactions: [],\n\t\tprohibitions: [],\n\t\tcancelledText: \"\",\n\t\tactiveTaskSource: \"\",\n\t};\n\tconst factsBlock = renderFactsBlock(facts);\n\tlet verification: VerificationReport | undefined;\n\tlet summary = \"\";\n\n\tif (isSplitTurn && messagesToSummarize.length > 0) {\n\t\tlet historySummary = \"No prior history.\";\n\t\tlet historyInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\thistorySummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\thistoryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(historySummary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\thistoryInstructions = buildRetryPrompt(verification, historySummary);\n\t\t}\n\n\t\tconst turnPrefixSummary = await generateTurnPrefixSummary(\n\t\t\tturnPrefixMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t);\n\t\tsummary = `${historySummary}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixSummary}`;\n\t} else {\n\t\tlet customSummaryInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\tsummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\tcustomSummaryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(summary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\tcustomSummaryInstructions = buildRetryPrompt(verification, summary);\n\t\t}\n\t}\n\n\t// Compute file lists and append to summary\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t\tverification,\n\t};\n}\n\nfunction formatVerificationFailures(verification: VerificationReport): string {\n\treturn verification.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(\", \");\n}\n\nexport function createDeterministicCompaction(preparation: CompactionPreparation): CompactionResult {\n\tconst { firstKeptEntryId, tokensBefore, fileOps, facts } = preparation;\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tconst factsText = renderFactsBlock(\n\t\tfacts ?? {\n\t\t\tfiles: [],\n\t\t\tactions: [],\n\t\t\tprohibitions: [],\n\t\t\tcancelledText: \"\",\n\t\t\tactiveTaskSource: \"\",\n\t\t},\n\t);\n\tconst fileLines = facts?.files.length\n\t\t? facts.files.map((file) => `- ${file.path} — ${file.note || file.kind} (${file.kind})`)\n\t\t: [`- read: ${readFiles.length}`, `- modified: ${modifiedFiles.length}`];\n\tconst mandatoryRuleLines = facts?.prohibitions.length ? facts.prohibitions.map((rule) => `- ${rule}`) : [\"(none)\"];\n\tconst doneLines = facts?.actions.length\n\t\t? facts.actions.map((action, index) => `${index + 1}. ${action}`)\n\t\t: [\"1. CHECKPOINT deterministic fallback — repeated compaction retries exhausted\"];\n\tconst summary = [\n\t\t\"## Active Task\",\n\t\tfacts?.activeTaskSource ? `User: ${facts.activeTaskSource}` : \"Continue from the deterministic compact snapshot.\",\n\t\t\"\",\n\t\t\"### Mandatory Rules\",\n\t\t...mandatoryRuleLines,\n\t\t\"\",\n\t\t\"## Files\",\n\t\t...fileLines,\n\t\t\"\",\n\t\t\"## Done\",\n\t\t...doneLines,\n\t\t\"\",\n\t\t\"## Constraints & Preferences\",\n\t\t\"Preserve exact file paths, commands, line numbers, and error strings.\",\n\t\t\"\",\n\t\t\"## Key Decisions\",\n\t\t\"- Deterministic checkpoint used after repeated compaction retries.\",\n\t\t\"\",\n\t\t\"## Blocked / Open\",\n\t\t\"(none)\",\n\t\t\"\",\n\t\t\"## Critical Context\",\n\t\t\"- Deterministic facts-only checkpoint; no LLM summary was accepted.\",\n\t\tfactsText,\n\t].join(\"\\n\");\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n): Promise<string> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.5 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t); // Smaller budget for turn prefix\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversation(llmMessages);\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Turn prefix summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from \"@caupulican/pi-ai\";\nimport { completeSimple } from \"@caupulican/pi-ai\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { buildSessionContext, type CompactionEntry, type SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage, StreamFn, ThinkingLevel } from \"../types.ts\";\nimport { type CompactionFacts, extractCompactionFacts, renderFactsBlock } from \"./extraction.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tserializeConversation,\n} from \"./utils.ts\";\nimport { buildRetryPrompt, type VerificationReport, verifySummary } from \"./verification.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\n/** Details stored in CompactionEntry.details for file tracking */\nexport interface CompactionDetails {\n\treadFiles: string[];\n\tmodifiedFiles: string[];\n}\n\n/**\n * Extract file operations from messages and previous compaction entries.\n */\nfunction extractFileOperations(\n\tmessages: AgentMessage[],\n\tentries: SessionEntry[],\n\tprevCompactionIndex: number,\n): FileOperations {\n\tconst fileOps = createFileOps();\n\n\t// Collect from previous compaction's details (if pi-generated)\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\tif (!prevCompaction.fromHook && prevCompaction.details) {\n\t\t\t// fromHook field kept for session file compatibility\n\t\t\tconst details = prevCompaction.details as CompactionDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\tfor (const f of details.modifiedFiles) fileOps.edited.add(f);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Extract from tool calls in messages\n\tfor (const msg of messages) {\n\t\textractFileOpsFromMessage(msg, fileOps);\n\t}\n\n\treturn fileOps;\n}\n\n// ============================================================================\n// Message Extraction\n// ============================================================================\n\n/**\n * Extract AgentMessage from an entry if it produces one.\n * Returns undefined for entries that don't contribute to LLM context.\n */\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"message\") {\n\t\treturn entry.message;\n\t}\n\tif (entry.type === \"custom_message\") {\n\t\treturn createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);\n\t}\n\tif (entry.type === \"branch_summary\") {\n\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\t}\n\tif (entry.type === \"compaction\") {\n\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\t}\n\treturn undefined;\n}\n\nfunction getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\treturn getMessageFromEntry(entry);\n}\n\n/** Result from compact() - SessionManager adds uuid/parentUuid when saving */\nexport interface CompactionResult<T = unknown> {\n\tsummary: string;\n\tfirstKeptEntryId: string;\n\ttokensBefore: number;\n\t/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */\n\tdetails?: T;\n\tverification?: VerificationReport;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n\t/**\n\t * Compaction also triggers once context exceeds this fraction of the model's window — not only when\n\t * it's nearly full (`contextWindow - reserveTokens`). On large-window models, waiting until nearly\n\t * full means every turn pays a huge input cost; a fractional cap keeps per-turn input bounded\n\t * (cost guard). The effective trigger is the LOWER of the two, so small-window models keep the\n\t * reserve-based behavior while large windows compact earlier. `0`/`1`+ disables the fractional cap.\n\t */\n\ttriggerPercent?: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\ttriggerPercent: 0.7,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Minimum projected space saving for the EARLY (fractional) compaction trigger to fire. Anti-thrashing\n * (cost guard, #30): an early compaction whose summary would barely shrink the context (mostly recent,\n * protected content) just burns a summarization call for little gain — skip it and let the context grow\n * until either the saving is worthwhile or the hard (near-full) trigger forces it. Does NOT gate the\n * hard trigger, so overflow is always avoided.\n */\nexport const MIN_COMPACTION_SAVINGS = 0.12;\n\n/**\n * Check if compaction should trigger based on context usage.\n *\n * Two triggers:\n * - HARD: context exceeds `contextWindow - reserveTokens` (near-full) or an explicit `triggerTokens`\n * override — always compact (prevents overflow).\n * - EARLY (fractional, cost guard): context exceeds `contextWindow * triggerPercent` — compact only if\n * the summary would actually save enough (`MIN_COMPACTION_SAVINGS`), so we don't thrash for tiny gains.\n */\nexport function shouldCompact(\n\tcontextTokens: number,\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n\ttriggerTokens?: number,\n): boolean {\n\tif (!settings.enabled) return false;\n\n\t// Hard trigger: near-full, or a caller-supplied lower override. Always compacts (avoid overflow).\n\tconst reserveTrigger = contextWindow - settings.reserveTokens;\n\tconst hardTrigger = triggerTokens === undefined ? reserveTrigger : Math.min(reserveTrigger, triggerTokens);\n\tif (contextTokens > hardTrigger) return true;\n\n\t// Early fractional trigger: bounds per-turn input cost on large-window models, gated by anti-thrashing.\n\tconst pct = settings.triggerPercent ?? 0;\n\tif (pct > 0 && pct < 1) {\n\t\tconst fractionalTrigger = Math.floor(contextWindow * pct);\n\t\tif (contextTokens > fractionalTrigger) {\n\t\t\t// Projected saving ≈ the non-protected fraction (everything but the recent tail we keep).\n\t\t\tconst projectedSavings = contextTokens > 0 ? 1 - settings.keepRecentTokens / contextTokens : 0;\n\t\t\treturn projectedSavings >= MIN_COMPACTION_SAVINGS;\n\t\t}\n\t}\n\treturn false;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\":\n\t\tcase \"compactionSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tswitch (entry.type) {\n\t\t\tcase \"message\": {\n\t\t\t\tconst role = entry.message.role;\n\t\t\t\tswitch (role) {\n\t\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\tcase \"custom\":\n\t\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\tcase \"user\":\n\t\t\t\t\tcase \"assistant\":\n\t\t\t\t\t\tcutPoints.push(i);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"toolResult\":\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"thinking_level_change\":\n\t\t\tcase \"model_change\":\n\t\t\tcase \"compaction\":\n\t\t\tcase \"branch_summary\":\n\t\t\tcase \"custom\":\n\t\t\tcase \"custom_message\":\n\t\t\tcase \"label\":\n\t\t\tcase \"session_info\":\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// branch_summary and custom_message are user-role messages, valid cut points\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\t// branch_summary and custom_message are user-role messages, can start a turn\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\treturn i;\n\t\t}\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = cutPoints[0]; // Default: keep from first message (not header)\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at session header or compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `Checkpoint the conversation above. Format from your instructions, sections in this order:\n## Active Task\n### Mandatory Rules\n## Files\n## Done\n## Constraints & Preferences\n## Key Decisions\n## Blocked / Open\n## Critical Context\n\nPre-extracted facts (verified by tooling — include ALL of them, merged with what you read):\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens. Concrete beats complete.`;\n\nconst UPDATE_SUMMARIZATION_PROMPT = `Update the checkpoint in <previous-summary> with the NEW turns above. RULES:\n- PRESERVE every existing ### Mandatory Rules bullet VERBATIM; append new ones.\n- Continue the ## Done numbering; move finished work from Blocked/Open.\n- Update ## Active Task to the newest unfulfilled user input; apply the cancellation rule.\n- Keep ## Files current (add new, keep still-relevant, drop obsolete).\n- Preserve exact paths, commands, errors.\n\nSame section order. Pre-extracted facts:\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens.`;\n\nfunction createSummarizationOptions(\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n): SimpleStreamOptions {\n\tconst options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };\n\tif (model.reasoning && thinkingLevel && thinkingLevel !== \"off\") {\n\t\toptions.reasoning = thinkingLevel;\n\t}\n\treturn options;\n}\n\nasync function completeSummarization(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (!streamFn) {\n\t\treturn completeSimple(model, context, options);\n\t}\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\n/**\n * Generate a summary of the conversation using the LLM.\n * If previousSummary is provided, uses the update prompt to merge.\n */\nexport async function generateSummary(\n\tcurrentMessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n\tpreviousSummary?: string,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\tfactsBlock = \"files:\\nactions:\\nprohibitions:\",\n\tchunked = false,\n): Promise<string> {\n\tconst summaryBudget = getSummaryBudget(reserveTokens, model, factsBlock);\n\tconst maxTokens = summaryBudget;\n\n\tlet promptSuffix = fillPromptTemplate(\n\t\tpreviousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT,\n\t\tfactsBlock,\n\t\tsummaryBudget,\n\t);\n\tif (customInstructions) {\n\t\tpromptSuffix = `${promptSuffix}\\n\\nAdditional focus: ${customInstructions}`;\n\t}\n\n\tconst llmMessages = convertToLlm(currentMessages);\n\tlet conversationText = serializeConversation(llmMessages);\n\tif (preDigest) {\n\t\ttry {\n\t\t\tconversationText = await preDigest(conversationText, signal);\n\t\t} catch {\n\t\t\t// Keep the verbatim conversation when an optional pre-digest fails.\n\t\t}\n\t}\n\n\tconst inputBound = getSummarizerInputBound(model, maxTokens);\n\tconst initialPromptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tif (estimateStringTokens(initialPromptText) > inputBound) {\n\t\tif (!chunked) {\n\t\t\tthrow new Error(\"input-overflow: summarization request exceeds summarizer window\");\n\t\t}\n\t\tconversationText = await summarizeChunks(\n\t\t\tconversationText,\n\t\t\tmodel,\n\t\t\tmaxTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t\tinputBound,\n\t\t);\n\t}\n\n\tconst promptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{\n\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\t// A length-stopped checkpoint silently lost its tail sections — gating it as if complete\n\t// guarantees a verification failure. Fail loudly so the compaction ladder escalates instead.\n\tif (response.stopReason === \"length\") {\n\t\tthrow new Error(\"summary-length-stop: summarizer hit its output cap before completing the checkpoint\");\n\t}\n\n\treturn truncateSummaryToBudget(extractTextContent(response), summaryBudget);\n}\n\nfunction fillPromptTemplate(template: string, factsBlock: string, budget: number): string {\n\treturn template.replaceAll(\"{FACTS_BLOCK}\", factsBlock).replaceAll(\"{BUDGET}\", String(budget));\n}\n\nconst SUMMARY_BUDGET_BASE_TOKENS = 1_500;\n/** Hard ceiling for the facts-scaled summary budget; also the worst case selection must assume. */\nexport const SUMMARY_BUDGET_MAX_TOKENS = 4_000;\n/** Prompt-side margin beyond the raw conversation input (system prompt, tags, instructions). */\nconst SUMMARIZER_PROMPT_MARGIN_TOKENS = 2_000;\n\nfunction getSummaryBudget(reserveTokens: number, model: Model<any>, factsBlock?: string): number {\n\tconst modelMaxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;\n\t// The verification gate demands the summary restate every extracted fact (modified files,\n\t// actions, rules). A fixed budget makes large spans structurally fail: the model length-stops\n\t// and the gated sections are the casualties. Scale the budget with the demand, bounded.\n\tconst factsTokens = factsBlock ? estimateStringTokens(factsBlock) : 0;\n\tconst demandBudget = Math.min(SUMMARY_BUDGET_MAX_TOKENS, Math.max(SUMMARY_BUDGET_BASE_TOKENS, factsTokens + 500));\n\treturn Math.max(1, Math.min(demandBudget, Math.floor(0.8 * reserveTokens), modelMaxTokens));\n}\n\nfunction getSummarizerInputBound(model: Model<any>, maxTokens: number): number {\n\tconst contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;\n\treturn contextWindow === Number.POSITIVE_INFINITY\n\t\t? contextWindow\n\t\t: Math.max(1, contextWindow - maxTokens - SUMMARIZER_PROMPT_MARGIN_TOKENS);\n}\n\n/**\n * Whether a candidate summarizer can ingest a summarization input of the given size in ONE\n * request (unchunked), using the same window arithmetic as {@link getSummarizerInputBound} with\n * the worst-case (facts-scaled) summary budget. Hosts use this at SELECTION time: a model that\n * fails this must not be handed the job — chunking cannot rescue recall-gated summarization, and\n * local servers silently truncate over-window prompts instead of erroring.\n */\nexport function summarizerCanIngest(model: Model<any>, estimatedInputTokens: number): boolean {\n\tconst contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;\n\tif (contextWindow === Number.POSITIVE_INFINITY) return true;\n\treturn estimatedInputTokens <= contextWindow - SUMMARY_BUDGET_MAX_TOKENS - SUMMARIZER_PROMPT_MARGIN_TOKENS;\n}\n\nfunction buildSummarizationPrompt(\n\tconversationText: string,\n\tpreviousSummary: string | undefined,\n\tpromptSuffix: string,\n): string {\n\tlet promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n`;\n\tif (previousSummary) {\n\t\tpromptText += `<previous-summary>\\n${previousSummary}\\n</previous-summary>\\n\\n`;\n\t}\n\treturn promptText + promptSuffix;\n}\n\nasync function summarizeChunks(\n\tconversationText: string,\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n\tstreamFn: StreamFn | undefined,\n\tinputBound: number,\n): Promise<string> {\n\tconst maxChunkTokens = Math.max(1, inputBound);\n\tconst maxChunkChars = Math.max(1, maxChunkTokens * 4);\n\tconst chunks = splitText(conversationText, maxChunkChars);\n\tconst retainedChunks = chunks.slice(-4);\n\tconst omittedChunks = chunks.length - retainedChunks.length;\n\tconst summaries: string[] = [];\n\n\tfor (let i = 0; i < retainedChunks.length; i++) {\n\t\tconst promptText = `<conversation-chunk index=\"${i + 1}\" total=\"${retainedChunks.length}\">\\n${retainedChunks[i]}\\n</conversation-chunk>\\n\\nSummarize this chunk for a later checkpoint merge. Preserve exact file paths, commands, errors, user prohibitions, and active work. Output concise notes only.`;\n\t\tconst response = await completeSummarization(\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\t\tstreamFn,\n\t\t);\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t\t}\n\t\tsummaries.push(extractTextContent(response));\n\t}\n\n\tconst omittedNote =\n\t\tomittedChunks > 0\n\t\t\t? `## Critical Context\\n${omittedChunks} older oversized chunks omitted; deterministic facts supplied separately.\\n\\n`\n\t\t\t: \"\";\n\treturn `${omittedNote}${summaries.join(\"\\n\\n\")}`;\n}\n\nfunction splitText(text: string, maxChars: number): string[] {\n\tconst chunks: string[] = [];\n\tfor (let start = 0; start < text.length; start += maxChars) {\n\t\tchunks.push(text.slice(start, start + maxChars));\n\t}\n\treturn chunks.length > 0 ? chunks : [\"\"];\n}\n\nfunction extractTextContent(message: AssistantMessage): string {\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n\nfunction truncateSummaryToBudget(summary: string, budget: number): string {\n\tconst maxTokens = Math.floor(budget * 1.3);\n\tif (estimateStringTokens(summary) <= maxTokens) {\n\t\treturn summary;\n\t}\n\n\tlet current = summary;\n\t// Never drop \"Files\" or \"Done\" here: the verification gate checks exactly those sections\n\t// (files-modified/read-recall, actions-overlap), so deleting them guarantees gate failure.\n\tfor (const heading of [\"Critical Context\", \"Blocked / Open\", \"Key Decisions\", \"Constraints & Preferences\"]) {\n\t\tconst next = removeSummarySection(current, heading);\n\t\tif (next === current) {\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent = next;\n\t\tif (estimateStringTokens(current) <= maxTokens) {\n\t\t\treturn current;\n\t\t}\n\t}\n\treturn current;\n}\n\nfunction removeSummarySection(summary: string, heading: string): string {\n\tconst lines = summary.split(/\\r?\\n/);\n\tconst kept: string[] = [];\n\tlet skipping = false;\n\tfor (const line of lines) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tskipping = match[1].trim().toLowerCase() === heading.toLowerCase();\n\t\t\tif (skipping) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!skipping) {\n\t\t\tkept.push(line);\n\t\t}\n\t}\n\treturn kept.join(\"\\n\").trim();\n}\n\nfunction estimateStringTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\n// ============================================================================\n// Compaction Preparation (for extensions)\n// ============================================================================\n\nexport interface CompactionPreparation {\n\t/** UUID of first entry to keep */\n\tfirstKeptEntryId: string;\n\t/** Messages that will be summarized and discarded */\n\tmessagesToSummarize: AgentMessage[];\n\t/** Messages that will be turned into turn prefix summary (if splitting) */\n\tturnPrefixMessages: AgentMessage[];\n\t/** Whether this is a split turn (cut point in middle of turn) */\n\tisSplitTurn: boolean;\n\ttokensBefore: number;\n\t/** Summary from previous compaction, for iterative update */\n\tpreviousSummary?: string;\n\t/** File operations extracted from messagesToSummarize */\n\tfileOps: FileOperations;\n\t/** Facts extracted from the compacted span for verification gating */\n\tfacts?: CompactionFacts;\n\t/** Compaction settions from settings.jsonl\t*/\n\tsettings: CompactionSettings;\n}\n\nexport function prepareCompaction(\n\tpathEntries: SessionEntry[],\n\tsettings: CompactionSettings,\n): CompactionPreparation | undefined {\n\tif (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = pathEntries.length - 1; i >= 0; i--) {\n\t\tif (pathEntries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet previousSummary: string | undefined;\n\tlet boundaryStart = 0;\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;\n\t\tpreviousSummary = prevCompaction.summary;\n\t\tconst firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);\n\t\tboundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;\n\t}\n\tconst boundaryEnd = pathEntries.length;\n\n\tconst tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;\n\n\tconst cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Get UUID of first kept entry\n\tconst firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];\n\tif (!firstKeptEntry?.id) {\n\t\treturn undefined; // Session needs migration\n\t}\n\tconst firstKeptEntryId = firstKeptEntry.id;\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\n\t// Messages to summarize (will be discarded after summary)\n\tconst messagesToSummarize: AgentMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\tif (msg) messagesToSummarize.push(msg);\n\t}\n\n\t// Messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AgentMessage[] = [];\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {\n\t\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\t\tif (msg) turnPrefixMessages.push(msg);\n\t\t}\n\t}\n\n\t// Extract file operations from messages and previous compaction\n\tconst fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);\n\n\t// Also extract file ops from turn prefix if splitting\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (const msg of turnPrefixMessages) {\n\t\t\textractFileOpsFromMessage(msg, fileOps);\n\t\t}\n\t}\n\n\tconst facts = extractCompactionFacts(pathEntries, boundaryStart, historyEnd);\n\n\treturn {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn: cutPoint.isSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tfacts,\n\t\tsettings,\n\t};\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.`;\n\n/**\n * Generate summaries for compaction using prepared data.\n * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.\n *\n * @param preparation - Pre-calculated preparation from prepareCompaction()\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tpreparation: CompactionPreparation,\n\tmodel: Model<any>,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tcustomInstructions?: string,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\texecutionOptions?: { chunked?: boolean; allowVerificationFailure?: boolean },\n): Promise<CompactionResult> {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t\tfacts: factsFromPreparation,\n\t} = preparation;\n\n\tconst facts = factsFromPreparation ?? {\n\t\tfiles: [],\n\t\tactions: [],\n\t\tprohibitions: [],\n\t\tcancelledText: \"\",\n\t\tactiveTaskSource: \"\",\n\t};\n\tconst factsBlock = renderFactsBlock(facts);\n\tlet verification: VerificationReport | undefined;\n\tlet summary = \"\";\n\n\tif (isSplitTurn && messagesToSummarize.length > 0) {\n\t\tlet historySummary = \"No prior history.\";\n\t\tlet historyInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\thistorySummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\thistoryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(historySummary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\thistoryInstructions = buildRetryPrompt(verification, historySummary);\n\t\t}\n\n\t\tconst turnPrefixSummary = await generateTurnPrefixSummary(\n\t\t\tturnPrefixMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t);\n\t\tsummary = `${historySummary}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixSummary}`;\n\t} else {\n\t\tlet customSummaryInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\tsummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\tcustomSummaryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(summary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\tcustomSummaryInstructions = buildRetryPrompt(verification, summary);\n\t\t}\n\t}\n\n\t// Compute file lists and append to summary\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t\tverification,\n\t};\n}\n\nfunction formatVerificationFailures(verification: VerificationReport): string {\n\treturn verification.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(\", \");\n}\n\nexport function createDeterministicCompaction(preparation: CompactionPreparation): CompactionResult {\n\tconst { firstKeptEntryId, tokensBefore, fileOps, facts } = preparation;\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tconst factsText = renderFactsBlock(\n\t\tfacts ?? {\n\t\t\tfiles: [],\n\t\t\tactions: [],\n\t\t\tprohibitions: [],\n\t\t\tcancelledText: \"\",\n\t\t\tactiveTaskSource: \"\",\n\t\t},\n\t);\n\tconst fileLines = facts?.files.length\n\t\t? facts.files.map((file) => `- ${file.path} — ${file.note || file.kind} (${file.kind})`)\n\t\t: [`- read: ${readFiles.length}`, `- modified: ${modifiedFiles.length}`];\n\tconst mandatoryRuleLines = facts?.prohibitions.length ? facts.prohibitions.map((rule) => `- ${rule}`) : [\"(none)\"];\n\tconst doneLines = facts?.actions.length\n\t\t? facts.actions.map((action, index) => `${index + 1}. ${action}`)\n\t\t: [\"1. CHECKPOINT deterministic fallback — repeated compaction retries exhausted\"];\n\tconst summary = [\n\t\t\"## Active Task\",\n\t\tfacts?.activeTaskSource ? `User: ${facts.activeTaskSource}` : \"Continue from the deterministic compact snapshot.\",\n\t\t\"\",\n\t\t\"### Mandatory Rules\",\n\t\t...mandatoryRuleLines,\n\t\t\"\",\n\t\t\"## Files\",\n\t\t...fileLines,\n\t\t\"\",\n\t\t\"## Done\",\n\t\t...doneLines,\n\t\t\"\",\n\t\t\"## Constraints & Preferences\",\n\t\t\"Preserve exact file paths, commands, line numbers, and error strings.\",\n\t\t\"\",\n\t\t\"## Key Decisions\",\n\t\t\"- Deterministic checkpoint used after repeated compaction retries.\",\n\t\t\"\",\n\t\t\"## Blocked / Open\",\n\t\t\"(none)\",\n\t\t\"\",\n\t\t\"## Critical Context\",\n\t\t\"- Deterministic facts-only checkpoint; no LLM summary was accepted.\",\n\t\tfactsText,\n\t].join(\"\\n\");\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n): Promise<string> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.5 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t); // Smaller budget for turn prefix\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversation(llmMessages);\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Turn prefix summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
@@ -429,7 +429,7 @@ async function completeSummarization(model, context, options, streamFn) {
429
429
  * If previousSummary is provided, uses the update prompt to merge.
430
430
  */
431
431
  export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock = "files:\nactions:\nprohibitions:", chunked = false) {
432
- const summaryBudget = getSummaryBudget(reserveTokens, model);
432
+ const summaryBudget = getSummaryBudget(reserveTokens, model, factsBlock);
433
433
  const maxTokens = summaryBudget;
434
434
  let promptSuffix = fillPromptTemplate(previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT, factsBlock, summaryBudget);
435
435
  if (customInstructions) {
@@ -467,18 +467,48 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
467
467
  if (response.stopReason === "error") {
468
468
  throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
469
469
  }
470
+ // A length-stopped checkpoint silently lost its tail sections — gating it as if complete
471
+ // guarantees a verification failure. Fail loudly so the compaction ladder escalates instead.
472
+ if (response.stopReason === "length") {
473
+ throw new Error("summary-length-stop: summarizer hit its output cap before completing the checkpoint");
474
+ }
470
475
  return truncateSummaryToBudget(extractTextContent(response), summaryBudget);
471
476
  }
472
477
  function fillPromptTemplate(template, factsBlock, budget) {
473
478
  return template.replaceAll("{FACTS_BLOCK}", factsBlock).replaceAll("{BUDGET}", String(budget));
474
479
  }
475
- function getSummaryBudget(reserveTokens, model) {
480
+ const SUMMARY_BUDGET_BASE_TOKENS = 1_500;
481
+ /** Hard ceiling for the facts-scaled summary budget; also the worst case selection must assume. */
482
+ export const SUMMARY_BUDGET_MAX_TOKENS = 4_000;
483
+ /** Prompt-side margin beyond the raw conversation input (system prompt, tags, instructions). */
484
+ const SUMMARIZER_PROMPT_MARGIN_TOKENS = 2_000;
485
+ function getSummaryBudget(reserveTokens, model, factsBlock) {
476
486
  const modelMaxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;
477
- return Math.max(1, Math.min(1_500, Math.floor(0.8 * reserveTokens), modelMaxTokens));
487
+ // The verification gate demands the summary restate every extracted fact (modified files,
488
+ // actions, rules). A fixed budget makes large spans structurally fail: the model length-stops
489
+ // and the gated sections are the casualties. Scale the budget with the demand, bounded.
490
+ const factsTokens = factsBlock ? estimateStringTokens(factsBlock) : 0;
491
+ const demandBudget = Math.min(SUMMARY_BUDGET_MAX_TOKENS, Math.max(SUMMARY_BUDGET_BASE_TOKENS, factsTokens + 500));
492
+ return Math.max(1, Math.min(demandBudget, Math.floor(0.8 * reserveTokens), modelMaxTokens));
478
493
  }
479
494
  function getSummarizerInputBound(model, maxTokens) {
480
495
  const contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;
481
- return contextWindow === Number.POSITIVE_INFINITY ? contextWindow : Math.max(1, contextWindow - maxTokens - 2_000);
496
+ return contextWindow === Number.POSITIVE_INFINITY
497
+ ? contextWindow
498
+ : Math.max(1, contextWindow - maxTokens - SUMMARIZER_PROMPT_MARGIN_TOKENS);
499
+ }
500
+ /**
501
+ * Whether a candidate summarizer can ingest a summarization input of the given size in ONE
502
+ * request (unchunked), using the same window arithmetic as {@link getSummarizerInputBound} with
503
+ * the worst-case (facts-scaled) summary budget. Hosts use this at SELECTION time: a model that
504
+ * fails this must not be handed the job — chunking cannot rescue recall-gated summarization, and
505
+ * local servers silently truncate over-window prompts instead of erroring.
506
+ */
507
+ export function summarizerCanIngest(model, estimatedInputTokens) {
508
+ const contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;
509
+ if (contextWindow === Number.POSITIVE_INFINITY)
510
+ return true;
511
+ return estimatedInputTokens <= contextWindow - SUMMARY_BUDGET_MAX_TOKENS - SUMMARIZER_PROMPT_MARGIN_TOKENS;
482
512
  }
483
513
  function buildSummarizationPrompt(conversationText, previousSummary, promptSuffix) {
484
514
  let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
@@ -535,14 +565,9 @@ function truncateSummaryToBudget(summary, budget) {
535
565
  return summary;
536
566
  }
537
567
  let current = summary;
538
- for (const heading of [
539
- "Critical Context",
540
- "Blocked / Open",
541
- "Key Decisions",
542
- "Constraints & Preferences",
543
- "Done",
544
- "Files",
545
- ]) {
568
+ // Never drop "Files" or "Done" here: the verification gate checks exactly those sections
569
+ // (files-modified/read-recall, actions-overlap), so deleting them guarantees gate failure.
570
+ for (const heading of ["Critical Context", "Blocked / Open", "Key Decisions", "Constraints & Preferences"]) {
546
571
  const next = removeSummarySection(current, heading);
547
572
  if (next === current) {
548
573
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from \"@caupulican/pi-ai\";\nimport { completeSimple } from \"@caupulican/pi-ai\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { buildSessionContext, type CompactionEntry, type SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage, StreamFn, ThinkingLevel } from \"../types.ts\";\nimport { type CompactionFacts, extractCompactionFacts, renderFactsBlock } from \"./extraction.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tserializeConversation,\n} from \"./utils.ts\";\nimport { buildRetryPrompt, type VerificationReport, verifySummary } from \"./verification.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\n/** Details stored in CompactionEntry.details for file tracking */\nexport interface CompactionDetails {\n\treadFiles: string[];\n\tmodifiedFiles: string[];\n}\n\n/**\n * Extract file operations from messages and previous compaction entries.\n */\nfunction extractFileOperations(\n\tmessages: AgentMessage[],\n\tentries: SessionEntry[],\n\tprevCompactionIndex: number,\n): FileOperations {\n\tconst fileOps = createFileOps();\n\n\t// Collect from previous compaction's details (if pi-generated)\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\tif (!prevCompaction.fromHook && prevCompaction.details) {\n\t\t\t// fromHook field kept for session file compatibility\n\t\t\tconst details = prevCompaction.details as CompactionDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\tfor (const f of details.modifiedFiles) fileOps.edited.add(f);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Extract from tool calls in messages\n\tfor (const msg of messages) {\n\t\textractFileOpsFromMessage(msg, fileOps);\n\t}\n\n\treturn fileOps;\n}\n\n// ============================================================================\n// Message Extraction\n// ============================================================================\n\n/**\n * Extract AgentMessage from an entry if it produces one.\n * Returns undefined for entries that don't contribute to LLM context.\n */\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"message\") {\n\t\treturn entry.message;\n\t}\n\tif (entry.type === \"custom_message\") {\n\t\treturn createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);\n\t}\n\tif (entry.type === \"branch_summary\") {\n\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\t}\n\tif (entry.type === \"compaction\") {\n\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\t}\n\treturn undefined;\n}\n\nfunction getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\treturn getMessageFromEntry(entry);\n}\n\n/** Result from compact() - SessionManager adds uuid/parentUuid when saving */\nexport interface CompactionResult<T = unknown> {\n\tsummary: string;\n\tfirstKeptEntryId: string;\n\ttokensBefore: number;\n\t/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */\n\tdetails?: T;\n\tverification?: VerificationReport;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n\t/**\n\t * Compaction also triggers once context exceeds this fraction of the model's window — not only when\n\t * it's nearly full (`contextWindow - reserveTokens`). On large-window models, waiting until nearly\n\t * full means every turn pays a huge input cost; a fractional cap keeps per-turn input bounded\n\t * (cost guard). The effective trigger is the LOWER of the two, so small-window models keep the\n\t * reserve-based behavior while large windows compact earlier. `0`/`1`+ disables the fractional cap.\n\t */\n\ttriggerPercent?: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\ttriggerPercent: 0.7,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Minimum projected space saving for the EARLY (fractional) compaction trigger to fire. Anti-thrashing\n * (cost guard, #30): an early compaction whose summary would barely shrink the context (mostly recent,\n * protected content) just burns a summarization call for little gain — skip it and let the context grow\n * until either the saving is worthwhile or the hard (near-full) trigger forces it. Does NOT gate the\n * hard trigger, so overflow is always avoided.\n */\nexport const MIN_COMPACTION_SAVINGS = 0.12;\n\n/**\n * Check if compaction should trigger based on context usage.\n *\n * Two triggers:\n * - HARD: context exceeds `contextWindow - reserveTokens` (near-full) or an explicit `triggerTokens`\n * override — always compact (prevents overflow).\n * - EARLY (fractional, cost guard): context exceeds `contextWindow * triggerPercent` — compact only if\n * the summary would actually save enough (`MIN_COMPACTION_SAVINGS`), so we don't thrash for tiny gains.\n */\nexport function shouldCompact(\n\tcontextTokens: number,\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n\ttriggerTokens?: number,\n): boolean {\n\tif (!settings.enabled) return false;\n\n\t// Hard trigger: near-full, or a caller-supplied lower override. Always compacts (avoid overflow).\n\tconst reserveTrigger = contextWindow - settings.reserveTokens;\n\tconst hardTrigger = triggerTokens === undefined ? reserveTrigger : Math.min(reserveTrigger, triggerTokens);\n\tif (contextTokens > hardTrigger) return true;\n\n\t// Early fractional trigger: bounds per-turn input cost on large-window models, gated by anti-thrashing.\n\tconst pct = settings.triggerPercent ?? 0;\n\tif (pct > 0 && pct < 1) {\n\t\tconst fractionalTrigger = Math.floor(contextWindow * pct);\n\t\tif (contextTokens > fractionalTrigger) {\n\t\t\t// Projected saving ≈ the non-protected fraction (everything but the recent tail we keep).\n\t\t\tconst projectedSavings = contextTokens > 0 ? 1 - settings.keepRecentTokens / contextTokens : 0;\n\t\t\treturn projectedSavings >= MIN_COMPACTION_SAVINGS;\n\t\t}\n\t}\n\treturn false;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\":\n\t\tcase \"compactionSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tswitch (entry.type) {\n\t\t\tcase \"message\": {\n\t\t\t\tconst role = entry.message.role;\n\t\t\t\tswitch (role) {\n\t\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\tcase \"custom\":\n\t\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\tcase \"user\":\n\t\t\t\t\tcase \"assistant\":\n\t\t\t\t\t\tcutPoints.push(i);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"toolResult\":\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"thinking_level_change\":\n\t\t\tcase \"model_change\":\n\t\t\tcase \"compaction\":\n\t\t\tcase \"branch_summary\":\n\t\t\tcase \"custom\":\n\t\t\tcase \"custom_message\":\n\t\t\tcase \"label\":\n\t\t\tcase \"session_info\":\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// branch_summary and custom_message are user-role messages, valid cut points\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\t// branch_summary and custom_message are user-role messages, can start a turn\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\treturn i;\n\t\t}\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = cutPoints[0]; // Default: keep from first message (not header)\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at session header or compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `Checkpoint the conversation above. Format from your instructions, sections in this order:\n## Active Task\n### Mandatory Rules\n## Files\n## Done\n## Constraints & Preferences\n## Key Decisions\n## Blocked / Open\n## Critical Context\n\nPre-extracted facts (verified by tooling — include ALL of them, merged with what you read):\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens. Concrete beats complete.`;\n\nconst UPDATE_SUMMARIZATION_PROMPT = `Update the checkpoint in <previous-summary> with the NEW turns above. RULES:\n- PRESERVE every existing ### Mandatory Rules bullet VERBATIM; append new ones.\n- Continue the ## Done numbering; move finished work from Blocked/Open.\n- Update ## Active Task to the newest unfulfilled user input; apply the cancellation rule.\n- Keep ## Files current (add new, keep still-relevant, drop obsolete).\n- Preserve exact paths, commands, errors.\n\nSame section order. Pre-extracted facts:\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens.`;\n\nfunction createSummarizationOptions(\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n): SimpleStreamOptions {\n\tconst options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };\n\tif (model.reasoning && thinkingLevel && thinkingLevel !== \"off\") {\n\t\toptions.reasoning = thinkingLevel;\n\t}\n\treturn options;\n}\n\nasync function completeSummarization(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (!streamFn) {\n\t\treturn completeSimple(model, context, options);\n\t}\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\n/**\n * Generate a summary of the conversation using the LLM.\n * If previousSummary is provided, uses the update prompt to merge.\n */\nexport async function generateSummary(\n\tcurrentMessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n\tpreviousSummary?: string,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\tfactsBlock = \"files:\\nactions:\\nprohibitions:\",\n\tchunked = false,\n): Promise<string> {\n\tconst summaryBudget = getSummaryBudget(reserveTokens, model);\n\tconst maxTokens = summaryBudget;\n\n\tlet promptSuffix = fillPromptTemplate(\n\t\tpreviousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT,\n\t\tfactsBlock,\n\t\tsummaryBudget,\n\t);\n\tif (customInstructions) {\n\t\tpromptSuffix = `${promptSuffix}\\n\\nAdditional focus: ${customInstructions}`;\n\t}\n\n\tconst llmMessages = convertToLlm(currentMessages);\n\tlet conversationText = serializeConversation(llmMessages);\n\tif (preDigest) {\n\t\ttry {\n\t\t\tconversationText = await preDigest(conversationText, signal);\n\t\t} catch {\n\t\t\t// Keep the verbatim conversation when an optional pre-digest fails.\n\t\t}\n\t}\n\n\tconst inputBound = getSummarizerInputBound(model, maxTokens);\n\tconst initialPromptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tif (estimateStringTokens(initialPromptText) > inputBound) {\n\t\tif (!chunked) {\n\t\t\tthrow new Error(\"input-overflow: summarization request exceeds summarizer window\");\n\t\t}\n\t\tconversationText = await summarizeChunks(\n\t\t\tconversationText,\n\t\t\tmodel,\n\t\t\tmaxTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t\tinputBound,\n\t\t);\n\t}\n\n\tconst promptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{\n\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\n\treturn truncateSummaryToBudget(extractTextContent(response), summaryBudget);\n}\n\nfunction fillPromptTemplate(template: string, factsBlock: string, budget: number): string {\n\treturn template.replaceAll(\"{FACTS_BLOCK}\", factsBlock).replaceAll(\"{BUDGET}\", String(budget));\n}\n\nfunction getSummaryBudget(reserveTokens: number, model: Model<any>): number {\n\tconst modelMaxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;\n\treturn Math.max(1, Math.min(1_500, Math.floor(0.8 * reserveTokens), modelMaxTokens));\n}\n\nfunction getSummarizerInputBound(model: Model<any>, maxTokens: number): number {\n\tconst contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;\n\treturn contextWindow === Number.POSITIVE_INFINITY ? contextWindow : Math.max(1, contextWindow - maxTokens - 2_000);\n}\n\nfunction buildSummarizationPrompt(\n\tconversationText: string,\n\tpreviousSummary: string | undefined,\n\tpromptSuffix: string,\n): string {\n\tlet promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n`;\n\tif (previousSummary) {\n\t\tpromptText += `<previous-summary>\\n${previousSummary}\\n</previous-summary>\\n\\n`;\n\t}\n\treturn promptText + promptSuffix;\n}\n\nasync function summarizeChunks(\n\tconversationText: string,\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n\tstreamFn: StreamFn | undefined,\n\tinputBound: number,\n): Promise<string> {\n\tconst maxChunkTokens = Math.max(1, inputBound);\n\tconst maxChunkChars = Math.max(1, maxChunkTokens * 4);\n\tconst chunks = splitText(conversationText, maxChunkChars);\n\tconst retainedChunks = chunks.slice(-4);\n\tconst omittedChunks = chunks.length - retainedChunks.length;\n\tconst summaries: string[] = [];\n\n\tfor (let i = 0; i < retainedChunks.length; i++) {\n\t\tconst promptText = `<conversation-chunk index=\"${i + 1}\" total=\"${retainedChunks.length}\">\\n${retainedChunks[i]}\\n</conversation-chunk>\\n\\nSummarize this chunk for a later checkpoint merge. Preserve exact file paths, commands, errors, user prohibitions, and active work. Output concise notes only.`;\n\t\tconst response = await completeSummarization(\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\t\tstreamFn,\n\t\t);\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t\t}\n\t\tsummaries.push(extractTextContent(response));\n\t}\n\n\tconst omittedNote =\n\t\tomittedChunks > 0\n\t\t\t? `## Critical Context\\n${omittedChunks} older oversized chunks omitted; deterministic facts supplied separately.\\n\\n`\n\t\t\t: \"\";\n\treturn `${omittedNote}${summaries.join(\"\\n\\n\")}`;\n}\n\nfunction splitText(text: string, maxChars: number): string[] {\n\tconst chunks: string[] = [];\n\tfor (let start = 0; start < text.length; start += maxChars) {\n\t\tchunks.push(text.slice(start, start + maxChars));\n\t}\n\treturn chunks.length > 0 ? chunks : [\"\"];\n}\n\nfunction extractTextContent(message: AssistantMessage): string {\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n\nfunction truncateSummaryToBudget(summary: string, budget: number): string {\n\tconst maxTokens = Math.floor(budget * 1.3);\n\tif (estimateStringTokens(summary) <= maxTokens) {\n\t\treturn summary;\n\t}\n\n\tlet current = summary;\n\tfor (const heading of [\n\t\t\"Critical Context\",\n\t\t\"Blocked / Open\",\n\t\t\"Key Decisions\",\n\t\t\"Constraints & Preferences\",\n\t\t\"Done\",\n\t\t\"Files\",\n\t]) {\n\t\tconst next = removeSummarySection(current, heading);\n\t\tif (next === current) {\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent = next;\n\t\tif (estimateStringTokens(current) <= maxTokens) {\n\t\t\treturn current;\n\t\t}\n\t}\n\treturn current;\n}\n\nfunction removeSummarySection(summary: string, heading: string): string {\n\tconst lines = summary.split(/\\r?\\n/);\n\tconst kept: string[] = [];\n\tlet skipping = false;\n\tfor (const line of lines) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tskipping = match[1].trim().toLowerCase() === heading.toLowerCase();\n\t\t\tif (skipping) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!skipping) {\n\t\t\tkept.push(line);\n\t\t}\n\t}\n\treturn kept.join(\"\\n\").trim();\n}\n\nfunction estimateStringTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\n// ============================================================================\n// Compaction Preparation (for extensions)\n// ============================================================================\n\nexport interface CompactionPreparation {\n\t/** UUID of first entry to keep */\n\tfirstKeptEntryId: string;\n\t/** Messages that will be summarized and discarded */\n\tmessagesToSummarize: AgentMessage[];\n\t/** Messages that will be turned into turn prefix summary (if splitting) */\n\tturnPrefixMessages: AgentMessage[];\n\t/** Whether this is a split turn (cut point in middle of turn) */\n\tisSplitTurn: boolean;\n\ttokensBefore: number;\n\t/** Summary from previous compaction, for iterative update */\n\tpreviousSummary?: string;\n\t/** File operations extracted from messagesToSummarize */\n\tfileOps: FileOperations;\n\t/** Facts extracted from the compacted span for verification gating */\n\tfacts?: CompactionFacts;\n\t/** Compaction settions from settings.jsonl\t*/\n\tsettings: CompactionSettings;\n}\n\nexport function prepareCompaction(\n\tpathEntries: SessionEntry[],\n\tsettings: CompactionSettings,\n): CompactionPreparation | undefined {\n\tif (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = pathEntries.length - 1; i >= 0; i--) {\n\t\tif (pathEntries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet previousSummary: string | undefined;\n\tlet boundaryStart = 0;\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;\n\t\tpreviousSummary = prevCompaction.summary;\n\t\tconst firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);\n\t\tboundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;\n\t}\n\tconst boundaryEnd = pathEntries.length;\n\n\tconst tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;\n\n\tconst cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Get UUID of first kept entry\n\tconst firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];\n\tif (!firstKeptEntry?.id) {\n\t\treturn undefined; // Session needs migration\n\t}\n\tconst firstKeptEntryId = firstKeptEntry.id;\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\n\t// Messages to summarize (will be discarded after summary)\n\tconst messagesToSummarize: AgentMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\tif (msg) messagesToSummarize.push(msg);\n\t}\n\n\t// Messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AgentMessage[] = [];\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {\n\t\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\t\tif (msg) turnPrefixMessages.push(msg);\n\t\t}\n\t}\n\n\t// Extract file operations from messages and previous compaction\n\tconst fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);\n\n\t// Also extract file ops from turn prefix if splitting\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (const msg of turnPrefixMessages) {\n\t\t\textractFileOpsFromMessage(msg, fileOps);\n\t\t}\n\t}\n\n\tconst facts = extractCompactionFacts(pathEntries, boundaryStart, historyEnd);\n\n\treturn {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn: cutPoint.isSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tfacts,\n\t\tsettings,\n\t};\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.`;\n\n/**\n * Generate summaries for compaction using prepared data.\n * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.\n *\n * @param preparation - Pre-calculated preparation from prepareCompaction()\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tpreparation: CompactionPreparation,\n\tmodel: Model<any>,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tcustomInstructions?: string,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\texecutionOptions?: { chunked?: boolean; allowVerificationFailure?: boolean },\n): Promise<CompactionResult> {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t\tfacts: factsFromPreparation,\n\t} = preparation;\n\n\tconst facts = factsFromPreparation ?? {\n\t\tfiles: [],\n\t\tactions: [],\n\t\tprohibitions: [],\n\t\tcancelledText: \"\",\n\t\tactiveTaskSource: \"\",\n\t};\n\tconst factsBlock = renderFactsBlock(facts);\n\tlet verification: VerificationReport | undefined;\n\tlet summary = \"\";\n\n\tif (isSplitTurn && messagesToSummarize.length > 0) {\n\t\tlet historySummary = \"No prior history.\";\n\t\tlet historyInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\thistorySummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\thistoryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(historySummary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\thistoryInstructions = buildRetryPrompt(verification, historySummary);\n\t\t}\n\n\t\tconst turnPrefixSummary = await generateTurnPrefixSummary(\n\t\t\tturnPrefixMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t);\n\t\tsummary = `${historySummary}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixSummary}`;\n\t} else {\n\t\tlet customSummaryInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\tsummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\tcustomSummaryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(summary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\tcustomSummaryInstructions = buildRetryPrompt(verification, summary);\n\t\t}\n\t}\n\n\t// Compute file lists and append to summary\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t\tverification,\n\t};\n}\n\nfunction formatVerificationFailures(verification: VerificationReport): string {\n\treturn verification.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(\", \");\n}\n\nexport function createDeterministicCompaction(preparation: CompactionPreparation): CompactionResult {\n\tconst { firstKeptEntryId, tokensBefore, fileOps, facts } = preparation;\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tconst factsText = renderFactsBlock(\n\t\tfacts ?? {\n\t\t\tfiles: [],\n\t\t\tactions: [],\n\t\t\tprohibitions: [],\n\t\t\tcancelledText: \"\",\n\t\t\tactiveTaskSource: \"\",\n\t\t},\n\t);\n\tconst fileLines = facts?.files.length\n\t\t? facts.files.map((file) => `- ${file.path} — ${file.note || file.kind} (${file.kind})`)\n\t\t: [`- read: ${readFiles.length}`, `- modified: ${modifiedFiles.length}`];\n\tconst mandatoryRuleLines = facts?.prohibitions.length ? facts.prohibitions.map((rule) => `- ${rule}`) : [\"(none)\"];\n\tconst doneLines = facts?.actions.length\n\t\t? facts.actions.map((action, index) => `${index + 1}. ${action}`)\n\t\t: [\"1. CHECKPOINT deterministic fallback — repeated compaction retries exhausted\"];\n\tconst summary = [\n\t\t\"## Active Task\",\n\t\tfacts?.activeTaskSource ? `User: ${facts.activeTaskSource}` : \"Continue from the deterministic compact snapshot.\",\n\t\t\"\",\n\t\t\"### Mandatory Rules\",\n\t\t...mandatoryRuleLines,\n\t\t\"\",\n\t\t\"## Files\",\n\t\t...fileLines,\n\t\t\"\",\n\t\t\"## Done\",\n\t\t...doneLines,\n\t\t\"\",\n\t\t\"## Constraints & Preferences\",\n\t\t\"Preserve exact file paths, commands, line numbers, and error strings.\",\n\t\t\"\",\n\t\t\"## Key Decisions\",\n\t\t\"- Deterministic checkpoint used after repeated compaction retries.\",\n\t\t\"\",\n\t\t\"## Blocked / Open\",\n\t\t\"(none)\",\n\t\t\"\",\n\t\t\"## Critical Context\",\n\t\t\"- Deterministic facts-only checkpoint; no LLM summary was accepted.\",\n\t\tfactsText,\n\t].join(\"\\n\");\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n): Promise<string> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.5 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t); // Smaller budget for turn prefix\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversation(llmMessages);\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Turn prefix summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from \"@caupulican/pi-ai\";\nimport { completeSimple } from \"@caupulican/pi-ai\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { buildSessionContext, type CompactionEntry, type SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage, StreamFn, ThinkingLevel } from \"../types.ts\";\nimport { type CompactionFacts, extractCompactionFacts, renderFactsBlock } from \"./extraction.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tserializeConversation,\n} from \"./utils.ts\";\nimport { buildRetryPrompt, type VerificationReport, verifySummary } from \"./verification.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\n/** Details stored in CompactionEntry.details for file tracking */\nexport interface CompactionDetails {\n\treadFiles: string[];\n\tmodifiedFiles: string[];\n}\n\n/**\n * Extract file operations from messages and previous compaction entries.\n */\nfunction extractFileOperations(\n\tmessages: AgentMessage[],\n\tentries: SessionEntry[],\n\tprevCompactionIndex: number,\n): FileOperations {\n\tconst fileOps = createFileOps();\n\n\t// Collect from previous compaction's details (if pi-generated)\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\tif (!prevCompaction.fromHook && prevCompaction.details) {\n\t\t\t// fromHook field kept for session file compatibility\n\t\t\tconst details = prevCompaction.details as CompactionDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\tfor (const f of details.modifiedFiles) fileOps.edited.add(f);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Extract from tool calls in messages\n\tfor (const msg of messages) {\n\t\textractFileOpsFromMessage(msg, fileOps);\n\t}\n\n\treturn fileOps;\n}\n\n// ============================================================================\n// Message Extraction\n// ============================================================================\n\n/**\n * Extract AgentMessage from an entry if it produces one.\n * Returns undefined for entries that don't contribute to LLM context.\n */\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"message\") {\n\t\treturn entry.message;\n\t}\n\tif (entry.type === \"custom_message\") {\n\t\treturn createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);\n\t}\n\tif (entry.type === \"branch_summary\") {\n\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\t}\n\tif (entry.type === \"compaction\") {\n\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\t}\n\treturn undefined;\n}\n\nfunction getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined {\n\tif (entry.type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\treturn getMessageFromEntry(entry);\n}\n\n/** Result from compact() - SessionManager adds uuid/parentUuid when saving */\nexport interface CompactionResult<T = unknown> {\n\tsummary: string;\n\tfirstKeptEntryId: string;\n\ttokensBefore: number;\n\t/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */\n\tdetails?: T;\n\tverification?: VerificationReport;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n\t/**\n\t * Compaction also triggers once context exceeds this fraction of the model's window — not only when\n\t * it's nearly full (`contextWindow - reserveTokens`). On large-window models, waiting until nearly\n\t * full means every turn pays a huge input cost; a fractional cap keeps per-turn input bounded\n\t * (cost guard). The effective trigger is the LOWER of the two, so small-window models keep the\n\t * reserve-based behavior while large windows compact earlier. `0`/`1`+ disables the fractional cap.\n\t */\n\ttriggerPercent?: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\ttriggerPercent: 0.7,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Minimum projected space saving for the EARLY (fractional) compaction trigger to fire. Anti-thrashing\n * (cost guard, #30): an early compaction whose summary would barely shrink the context (mostly recent,\n * protected content) just burns a summarization call for little gain — skip it and let the context grow\n * until either the saving is worthwhile or the hard (near-full) trigger forces it. Does NOT gate the\n * hard trigger, so overflow is always avoided.\n */\nexport const MIN_COMPACTION_SAVINGS = 0.12;\n\n/**\n * Check if compaction should trigger based on context usage.\n *\n * Two triggers:\n * - HARD: context exceeds `contextWindow - reserveTokens` (near-full) or an explicit `triggerTokens`\n * override — always compact (prevents overflow).\n * - EARLY (fractional, cost guard): context exceeds `contextWindow * triggerPercent` — compact only if\n * the summary would actually save enough (`MIN_COMPACTION_SAVINGS`), so we don't thrash for tiny gains.\n */\nexport function shouldCompact(\n\tcontextTokens: number,\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n\ttriggerTokens?: number,\n): boolean {\n\tif (!settings.enabled) return false;\n\n\t// Hard trigger: near-full, or a caller-supplied lower override. Always compacts (avoid overflow).\n\tconst reserveTrigger = contextWindow - settings.reserveTokens;\n\tconst hardTrigger = triggerTokens === undefined ? reserveTrigger : Math.min(reserveTrigger, triggerTokens);\n\tif (contextTokens > hardTrigger) return true;\n\n\t// Early fractional trigger: bounds per-turn input cost on large-window models, gated by anti-thrashing.\n\tconst pct = settings.triggerPercent ?? 0;\n\tif (pct > 0 && pct < 1) {\n\t\tconst fractionalTrigger = Math.floor(contextWindow * pct);\n\t\tif (contextTokens > fractionalTrigger) {\n\t\t\t// Projected saving ≈ the non-protected fraction (everything but the recent tail we keep).\n\t\t\tconst projectedSavings = contextTokens > 0 ? 1 - settings.keepRecentTokens / contextTokens : 0;\n\t\t\treturn projectedSavings >= MIN_COMPACTION_SAVINGS;\n\t\t}\n\t}\n\treturn false;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\":\n\t\tcase \"compactionSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tswitch (entry.type) {\n\t\t\tcase \"message\": {\n\t\t\t\tconst role = entry.message.role;\n\t\t\t\tswitch (role) {\n\t\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\tcase \"custom\":\n\t\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\tcase \"user\":\n\t\t\t\t\tcase \"assistant\":\n\t\t\t\t\t\tcutPoints.push(i);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"toolResult\":\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"thinking_level_change\":\n\t\t\tcase \"model_change\":\n\t\t\tcase \"compaction\":\n\t\t\tcase \"branch_summary\":\n\t\t\tcase \"custom\":\n\t\t\tcase \"custom_message\":\n\t\t\tcase \"label\":\n\t\t\tcase \"session_info\":\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// branch_summary and custom_message are user-role messages, valid cut points\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\t// branch_summary and custom_message are user-role messages, can start a turn\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\treturn i;\n\t\t}\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = cutPoints[0]; // Default: keep from first message (not header)\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at session header or compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `Checkpoint the conversation above. Format from your instructions, sections in this order:\n## Active Task\n### Mandatory Rules\n## Files\n## Done\n## Constraints & Preferences\n## Key Decisions\n## Blocked / Open\n## Critical Context\n\nPre-extracted facts (verified by tooling — include ALL of them, merged with what you read):\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens. Concrete beats complete.`;\n\nconst UPDATE_SUMMARIZATION_PROMPT = `Update the checkpoint in <previous-summary> with the NEW turns above. RULES:\n- PRESERVE every existing ### Mandatory Rules bullet VERBATIM; append new ones.\n- Continue the ## Done numbering; move finished work from Blocked/Open.\n- Update ## Active Task to the newest unfulfilled user input; apply the cancellation rule.\n- Keep ## Files current (add new, keep still-relevant, drop obsolete).\n- Preserve exact paths, commands, errors.\n\nSame section order. Pre-extracted facts:\n<facts>\n{FACTS_BLOCK}\n</facts>\n\nBudget: ~{BUDGET} tokens.`;\n\nfunction createSummarizationOptions(\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n): SimpleStreamOptions {\n\tconst options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };\n\tif (model.reasoning && thinkingLevel && thinkingLevel !== \"off\") {\n\t\toptions.reasoning = thinkingLevel;\n\t}\n\treturn options;\n}\n\nasync function completeSummarization(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (!streamFn) {\n\t\treturn completeSimple(model, context, options);\n\t}\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\n/**\n * Generate a summary of the conversation using the LLM.\n * If previousSummary is provided, uses the update prompt to merge.\n */\nexport async function generateSummary(\n\tcurrentMessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n\tpreviousSummary?: string,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\tfactsBlock = \"files:\\nactions:\\nprohibitions:\",\n\tchunked = false,\n): Promise<string> {\n\tconst summaryBudget = getSummaryBudget(reserveTokens, model, factsBlock);\n\tconst maxTokens = summaryBudget;\n\n\tlet promptSuffix = fillPromptTemplate(\n\t\tpreviousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT,\n\t\tfactsBlock,\n\t\tsummaryBudget,\n\t);\n\tif (customInstructions) {\n\t\tpromptSuffix = `${promptSuffix}\\n\\nAdditional focus: ${customInstructions}`;\n\t}\n\n\tconst llmMessages = convertToLlm(currentMessages);\n\tlet conversationText = serializeConversation(llmMessages);\n\tif (preDigest) {\n\t\ttry {\n\t\t\tconversationText = await preDigest(conversationText, signal);\n\t\t} catch {\n\t\t\t// Keep the verbatim conversation when an optional pre-digest fails.\n\t\t}\n\t}\n\n\tconst inputBound = getSummarizerInputBound(model, maxTokens);\n\tconst initialPromptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tif (estimateStringTokens(initialPromptText) > inputBound) {\n\t\tif (!chunked) {\n\t\t\tthrow new Error(\"input-overflow: summarization request exceeds summarizer window\");\n\t\t}\n\t\tconversationText = await summarizeChunks(\n\t\t\tconversationText,\n\t\t\tmodel,\n\t\t\tmaxTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t\tinputBound,\n\t\t);\n\t}\n\n\tconst promptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{\n\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\t// A length-stopped checkpoint silently lost its tail sections — gating it as if complete\n\t// guarantees a verification failure. Fail loudly so the compaction ladder escalates instead.\n\tif (response.stopReason === \"length\") {\n\t\tthrow new Error(\"summary-length-stop: summarizer hit its output cap before completing the checkpoint\");\n\t}\n\n\treturn truncateSummaryToBudget(extractTextContent(response), summaryBudget);\n}\n\nfunction fillPromptTemplate(template: string, factsBlock: string, budget: number): string {\n\treturn template.replaceAll(\"{FACTS_BLOCK}\", factsBlock).replaceAll(\"{BUDGET}\", String(budget));\n}\n\nconst SUMMARY_BUDGET_BASE_TOKENS = 1_500;\n/** Hard ceiling for the facts-scaled summary budget; also the worst case selection must assume. */\nexport const SUMMARY_BUDGET_MAX_TOKENS = 4_000;\n/** Prompt-side margin beyond the raw conversation input (system prompt, tags, instructions). */\nconst SUMMARIZER_PROMPT_MARGIN_TOKENS = 2_000;\n\nfunction getSummaryBudget(reserveTokens: number, model: Model<any>, factsBlock?: string): number {\n\tconst modelMaxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;\n\t// The verification gate demands the summary restate every extracted fact (modified files,\n\t// actions, rules). A fixed budget makes large spans structurally fail: the model length-stops\n\t// and the gated sections are the casualties. Scale the budget with the demand, bounded.\n\tconst factsTokens = factsBlock ? estimateStringTokens(factsBlock) : 0;\n\tconst demandBudget = Math.min(SUMMARY_BUDGET_MAX_TOKENS, Math.max(SUMMARY_BUDGET_BASE_TOKENS, factsTokens + 500));\n\treturn Math.max(1, Math.min(demandBudget, Math.floor(0.8 * reserveTokens), modelMaxTokens));\n}\n\nfunction getSummarizerInputBound(model: Model<any>, maxTokens: number): number {\n\tconst contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;\n\treturn contextWindow === Number.POSITIVE_INFINITY\n\t\t? contextWindow\n\t\t: Math.max(1, contextWindow - maxTokens - SUMMARIZER_PROMPT_MARGIN_TOKENS);\n}\n\n/**\n * Whether a candidate summarizer can ingest a summarization input of the given size in ONE\n * request (unchunked), using the same window arithmetic as {@link getSummarizerInputBound} with\n * the worst-case (facts-scaled) summary budget. Hosts use this at SELECTION time: a model that\n * fails this must not be handed the job — chunking cannot rescue recall-gated summarization, and\n * local servers silently truncate over-window prompts instead of erroring.\n */\nexport function summarizerCanIngest(model: Model<any>, estimatedInputTokens: number): boolean {\n\tconst contextWindow = model.contextWindow > 0 ? model.contextWindow : Number.POSITIVE_INFINITY;\n\tif (contextWindow === Number.POSITIVE_INFINITY) return true;\n\treturn estimatedInputTokens <= contextWindow - SUMMARY_BUDGET_MAX_TOKENS - SUMMARIZER_PROMPT_MARGIN_TOKENS;\n}\n\nfunction buildSummarizationPrompt(\n\tconversationText: string,\n\tpreviousSummary: string | undefined,\n\tpromptSuffix: string,\n): string {\n\tlet promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n`;\n\tif (previousSummary) {\n\t\tpromptText += `<previous-summary>\\n${previousSummary}\\n</previous-summary>\\n\\n`;\n\t}\n\treturn promptText + promptSuffix;\n}\n\nasync function summarizeChunks(\n\tconversationText: string,\n\tmodel: Model<any>,\n\tmaxTokens: number,\n\tapiKey: string | undefined,\n\theaders: Record<string, string> | undefined,\n\tsignal: AbortSignal | undefined,\n\tthinkingLevel: ThinkingLevel | undefined,\n\tstreamFn: StreamFn | undefined,\n\tinputBound: number,\n): Promise<string> {\n\tconst maxChunkTokens = Math.max(1, inputBound);\n\tconst maxChunkChars = Math.max(1, maxChunkTokens * 4);\n\tconst chunks = splitText(conversationText, maxChunkChars);\n\tconst retainedChunks = chunks.slice(-4);\n\tconst omittedChunks = chunks.length - retainedChunks.length;\n\tconst summaries: string[] = [];\n\n\tfor (let i = 0; i < retainedChunks.length; i++) {\n\t\tconst promptText = `<conversation-chunk index=\"${i + 1}\" total=\"${retainedChunks.length}\">\\n${retainedChunks[i]}\\n</conversation-chunk>\\n\\nSummarize this chunk for a later checkpoint merge. Preserve exact file paths, commands, errors, user prohibitions, and active work. Output concise notes only.`;\n\t\tconst response = await completeSummarization(\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tsystemPrompt: SUMMARIZATION_SYSTEM_PROMPT,\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: promptText }],\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\t\tstreamFn,\n\t\t);\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthrow new Error(`Summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t\t}\n\t\tsummaries.push(extractTextContent(response));\n\t}\n\n\tconst omittedNote =\n\t\tomittedChunks > 0\n\t\t\t? `## Critical Context\\n${omittedChunks} older oversized chunks omitted; deterministic facts supplied separately.\\n\\n`\n\t\t\t: \"\";\n\treturn `${omittedNote}${summaries.join(\"\\n\\n\")}`;\n}\n\nfunction splitText(text: string, maxChars: number): string[] {\n\tconst chunks: string[] = [];\n\tfor (let start = 0; start < text.length; start += maxChars) {\n\t\tchunks.push(text.slice(start, start + maxChars));\n\t}\n\treturn chunks.length > 0 ? chunks : [\"\"];\n}\n\nfunction extractTextContent(message: AssistantMessage): string {\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n\nfunction truncateSummaryToBudget(summary: string, budget: number): string {\n\tconst maxTokens = Math.floor(budget * 1.3);\n\tif (estimateStringTokens(summary) <= maxTokens) {\n\t\treturn summary;\n\t}\n\n\tlet current = summary;\n\t// Never drop \"Files\" or \"Done\" here: the verification gate checks exactly those sections\n\t// (files-modified/read-recall, actions-overlap), so deleting them guarantees gate failure.\n\tfor (const heading of [\"Critical Context\", \"Blocked / Open\", \"Key Decisions\", \"Constraints & Preferences\"]) {\n\t\tconst next = removeSummarySection(current, heading);\n\t\tif (next === current) {\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent = next;\n\t\tif (estimateStringTokens(current) <= maxTokens) {\n\t\t\treturn current;\n\t\t}\n\t}\n\treturn current;\n}\n\nfunction removeSummarySection(summary: string, heading: string): string {\n\tconst lines = summary.split(/\\r?\\n/);\n\tconst kept: string[] = [];\n\tlet skipping = false;\n\tfor (const line of lines) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tskipping = match[1].trim().toLowerCase() === heading.toLowerCase();\n\t\t\tif (skipping) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!skipping) {\n\t\t\tkept.push(line);\n\t\t}\n\t}\n\treturn kept.join(\"\\n\").trim();\n}\n\nfunction estimateStringTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\n// ============================================================================\n// Compaction Preparation (for extensions)\n// ============================================================================\n\nexport interface CompactionPreparation {\n\t/** UUID of first entry to keep */\n\tfirstKeptEntryId: string;\n\t/** Messages that will be summarized and discarded */\n\tmessagesToSummarize: AgentMessage[];\n\t/** Messages that will be turned into turn prefix summary (if splitting) */\n\tturnPrefixMessages: AgentMessage[];\n\t/** Whether this is a split turn (cut point in middle of turn) */\n\tisSplitTurn: boolean;\n\ttokensBefore: number;\n\t/** Summary from previous compaction, for iterative update */\n\tpreviousSummary?: string;\n\t/** File operations extracted from messagesToSummarize */\n\tfileOps: FileOperations;\n\t/** Facts extracted from the compacted span for verification gating */\n\tfacts?: CompactionFacts;\n\t/** Compaction settions from settings.jsonl\t*/\n\tsettings: CompactionSettings;\n}\n\nexport function prepareCompaction(\n\tpathEntries: SessionEntry[],\n\tsettings: CompactionSettings,\n): CompactionPreparation | undefined {\n\tif (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = pathEntries.length - 1; i >= 0; i--) {\n\t\tif (pathEntries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet previousSummary: string | undefined;\n\tlet boundaryStart = 0;\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;\n\t\tpreviousSummary = prevCompaction.summary;\n\t\tconst firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);\n\t\tboundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;\n\t}\n\tconst boundaryEnd = pathEntries.length;\n\n\tconst tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;\n\n\tconst cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Get UUID of first kept entry\n\tconst firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];\n\tif (!firstKeptEntry?.id) {\n\t\treturn undefined; // Session needs migration\n\t}\n\tconst firstKeptEntryId = firstKeptEntry.id;\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\n\t// Messages to summarize (will be discarded after summary)\n\tconst messagesToSummarize: AgentMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\tif (msg) messagesToSummarize.push(msg);\n\t}\n\n\t// Messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AgentMessage[] = [];\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {\n\t\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\t\tif (msg) turnPrefixMessages.push(msg);\n\t\t}\n\t}\n\n\t// Extract file operations from messages and previous compaction\n\tconst fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);\n\n\t// Also extract file ops from turn prefix if splitting\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (const msg of turnPrefixMessages) {\n\t\t\textractFileOpsFromMessage(msg, fileOps);\n\t\t}\n\t}\n\n\tconst facts = extractCompactionFacts(pathEntries, boundaryStart, historyEnd);\n\n\treturn {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn: cutPoint.isSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tfacts,\n\t\tsettings,\n\t};\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.`;\n\n/**\n * Generate summaries for compaction using prepared data.\n * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.\n *\n * @param preparation - Pre-calculated preparation from prepareCompaction()\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tpreparation: CompactionPreparation,\n\tmodel: Model<any>,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tcustomInstructions?: string,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n\tpreDigest?: (conversationText: string, signal?: AbortSignal) => Promise<string>,\n\texecutionOptions?: { chunked?: boolean; allowVerificationFailure?: boolean },\n): Promise<CompactionResult> {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t\tfacts: factsFromPreparation,\n\t} = preparation;\n\n\tconst facts = factsFromPreparation ?? {\n\t\tfiles: [],\n\t\tactions: [],\n\t\tprohibitions: [],\n\t\tcancelledText: \"\",\n\t\tactiveTaskSource: \"\",\n\t};\n\tconst factsBlock = renderFactsBlock(facts);\n\tlet verification: VerificationReport | undefined;\n\tlet summary = \"\";\n\n\tif (isSplitTurn && messagesToSummarize.length > 0) {\n\t\tlet historySummary = \"No prior history.\";\n\t\tlet historyInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\thistorySummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\thistoryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(historySummary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\thistoryInstructions = buildRetryPrompt(verification, historySummary);\n\t\t}\n\n\t\tconst turnPrefixSummary = await generateTurnPrefixSummary(\n\t\t\tturnPrefixMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tthinkingLevel,\n\t\t\tstreamFn,\n\t\t);\n\t\tsummary = `${historySummary}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixSummary}`;\n\t} else {\n\t\tlet customSummaryInstructions = customInstructions;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\tsummary = await generateSummary(\n\t\t\t\tmessagesToSummarize,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\tcustomSummaryInstructions,\n\t\t\t\tpreviousSummary,\n\t\t\t\tthinkingLevel,\n\t\t\t\tstreamFn,\n\t\t\t\tpreDigest,\n\t\t\t\tfactsBlock,\n\t\t\t\texecutionOptions?.chunked ?? false,\n\t\t\t);\n\n\t\t\tverification = verifySummary(summary, facts);\n\t\t\tif (verification.ok) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (attempt >= 1) {\n\t\t\t\tif (executionOptions?.allowVerificationFailure) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`gate-failed: ${formatVerificationFailures(verification)}`);\n\t\t\t}\n\n\t\t\tcustomSummaryInstructions = buildRetryPrompt(verification, summary);\n\t\t}\n\t}\n\n\t// Compute file lists and append to summary\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t\tverification,\n\t};\n}\n\nfunction formatVerificationFailures(verification: VerificationReport): string {\n\treturn verification.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(\", \");\n}\n\nexport function createDeterministicCompaction(preparation: CompactionPreparation): CompactionResult {\n\tconst { firstKeptEntryId, tokensBefore, fileOps, facts } = preparation;\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tconst factsText = renderFactsBlock(\n\t\tfacts ?? {\n\t\t\tfiles: [],\n\t\t\tactions: [],\n\t\t\tprohibitions: [],\n\t\t\tcancelledText: \"\",\n\t\t\tactiveTaskSource: \"\",\n\t\t},\n\t);\n\tconst fileLines = facts?.files.length\n\t\t? facts.files.map((file) => `- ${file.path} — ${file.note || file.kind} (${file.kind})`)\n\t\t: [`- read: ${readFiles.length}`, `- modified: ${modifiedFiles.length}`];\n\tconst mandatoryRuleLines = facts?.prohibitions.length ? facts.prohibitions.map((rule) => `- ${rule}`) : [\"(none)\"];\n\tconst doneLines = facts?.actions.length\n\t\t? facts.actions.map((action, index) => `${index + 1}. ${action}`)\n\t\t: [\"1. CHECKPOINT deterministic fallback — repeated compaction retries exhausted\"];\n\tconst summary = [\n\t\t\"## Active Task\",\n\t\tfacts?.activeTaskSource ? `User: ${facts.activeTaskSource}` : \"Continue from the deterministic compact snapshot.\",\n\t\t\"\",\n\t\t\"### Mandatory Rules\",\n\t\t...mandatoryRuleLines,\n\t\t\"\",\n\t\t\"## Files\",\n\t\t...fileLines,\n\t\t\"\",\n\t\t\"## Done\",\n\t\t...doneLines,\n\t\t\"\",\n\t\t\"## Constraints & Preferences\",\n\t\t\"Preserve exact file paths, commands, line numbers, and error strings.\",\n\t\t\"\",\n\t\t\"## Key Decisions\",\n\t\t\"- Deterministic checkpoint used after repeated compaction retries.\",\n\t\t\"\",\n\t\t\"## Blocked / Open\",\n\t\t\"(none)\",\n\t\t\"\",\n\t\t\"## Critical Context\",\n\t\t\"- Deterministic facts-only checkpoint; no LLM summary was accepted.\",\n\t\tfactsText,\n\t].join(\"\\n\");\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string | undefined,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tstreamFn?: StreamFn,\n): Promise<string> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.5 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t); // Smaller budget for turn prefix\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversation(llmMessages);\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await completeSummarization(\n\t\tmodel,\n\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\tcreateSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),\n\t\tstreamFn,\n\t);\n\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(`Turn prefix summarization failed: ${response.errorMessage || \"Unknown error\"}`);\n\t}\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"extraction.d.ts","sourceRoot":"","sources":["../../src/compaction/extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAGlE,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CACzB;AA0LD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,CAmM3G;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAc/D","sourcesContent":["import type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage } from \"../types.ts\";\n\nexport interface CompactionFacts {\n\tfiles: Array<{ path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>;\n\tactions: string[];\n\tprohibitions: string[];\n\tcancelledText: string;\n\tactiveTaskSource: string;\n}\n\ninterface ToolCallFact {\n\tid: string | undefined;\n\tname: string;\n\tpath: string | undefined;\n\tbaseKind: \"modified\" | \"created\" | \"read\" | null;\n\tfinalKind: \"modified\" | \"created\" | \"read\" | null;\n\tverb: string;\n\toutcome: string;\n\tresolved: boolean;\n}\n\nconst PROHIBITION_PATTERN = /\\b(do not|don't|never|stop (?:doing|using|changing)|no more)\\b/i;\nconst REVERSAL_PATTERN = /\\b(stop|undo|revert|roll back|never mind|scrap that|forget (?:it|that))\\b/i;\n\nconst FILE_KIND_PRIORITY: Record<NonNullable<ToolCallFact[\"finalKind\"]>, number> = {\n\tread: 1,\n\tcreated: 2,\n\tmodified: 3,\n};\n\nconst TOOL_KIND_BY_NAME: Record<string, \"modified\" | \"created\" | \"read\" | null> = {\n\tread: \"read\",\n\tgrep: \"read\",\n\tfind: \"read\",\n\twrite: \"modified\",\n\tedit: \"modified\",\n\tbash: null,\n};\n\nconst TOOL_VERB_BY_NAME: Record<string, string> = {\n\twrite: \"WRITE\",\n\tedit: \"EDIT\",\n\tbash: \"RUN\",\n\tread: \"READ\",\n\tgrep: \"READ\",\n\tfind: \"READ\",\n};\n\nfunction messageToText(message: AgentMessage): string {\n\tconst rawContent = (message as { content?: unknown }).content;\n\tif (typeof rawContent === \"string\") {\n\t\treturn rawContent;\n\t}\n\tif (!Array.isArray(rawContent)) {\n\t\treturn \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tfor (const block of rawContent) {\n\t\tif (!block || typeof block !== \"object\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ((block as { type?: unknown }).type === \"text\" && typeof (block as { text?: unknown }).text === \"string\") {\n\t\t\tparts.push((block as { text: string }).text);\n\t\t}\n\t}\n\treturn parts.join(\" \").trim();\n}\n\nfunction assistantToolCallTarget(name: string, rawArgs: unknown): string | undefined {\n\tif (!rawArgs || typeof rawArgs !== \"object\") {\n\t\treturn undefined;\n\t}\n\tconst args = rawArgs as Record<string, unknown>;\n\n\tif (\n\t\t(name === \"read\" || name === \"grep\" || name === \"find\" || name === \"write\" || name === \"edit\") &&\n\t\ttypeof args.path === \"string\" &&\n\t\targs.path.length > 0\n\t) {\n\t\treturn args.path;\n\t}\n\n\tif (name === \"bash\" && typeof args.command === \"string\" && args.command.length > 0) {\n\t\treturn args.command;\n\t}\n\n\treturn undefined;\n}\n\nfunction clampText(text: string, maxLen: number): string {\n\treturn text.length > maxLen ? text.slice(0, maxLen) : text;\n}\n\nfunction firstLine(text: string): string {\n\tfor (const line of text.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\treturn trimmed;\n\t\t}\n\t}\n\treturn \"\";\n}\n\nfunction firstOutcome(message: AgentMessage): string {\n\tif (message.role !== \"toolResult\") {\n\t\treturn \"\";\n\t}\n\treturn firstLine(messageToText(message));\n}\n\nfunction appearsCreated(message: AgentMessage): boolean {\n\tif (message.role !== \"toolResult\") {\n\t\treturn false;\n\t}\n\n\tconst text = messageToText(message).toLowerCase();\n\tif (/(\\bnew file\\b|\\bcreated\\b|\\bcreated file\\b)/.test(text)) {\n\t\treturn true;\n\t}\n\n\tconst details = (message as { details?: unknown }).details;\n\tif (!details || typeof details !== \"object\") {\n\t\treturn false;\n\t}\n\n\tif (typeof (details as { created?: unknown }).created === \"boolean\") {\n\t\treturn (details as { created: boolean }).created;\n\t}\n\tif (typeof (details as { isCreated?: unknown }).isCreated === \"boolean\") {\n\t\treturn (details as { isCreated: boolean }).isCreated;\n\t}\n\tif (typeof (details as { isNewFile?: unknown }).isNewFile === \"boolean\") {\n\t\treturn (details as { isNewFile: boolean }).isNewFile;\n\t}\n\n\treturn false;\n}\n\nfunction splitSentenceLines(text: string): string[] {\n\treturn text\n\t\t.split(/[.!?\\n]+/)\n\t\t.map((line) => line.trim())\n\t\t.filter(Boolean);\n}\n\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tswitch (entry.type) {\n\t\tcase \"message\":\n\t\t\treturn entry.message;\n\t\tcase \"custom_message\":\n\t\t\treturn {\n\t\t\t\trole: \"custom\",\n\t\t\t\tcustomType: entry.customType,\n\t\t\t\tcontent: entry.content,\n\t\t\t\tdetails: entry.details,\n\t\t\t\tdisplay: entry.display,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"branch_summary\":\n\t\t\treturn {\n\t\t\t\trole: \"branchSummary\",\n\t\t\t\tsummary: entry.summary,\n\t\t\t\tfromId: entry.fromId,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"compaction\":\n\t\t\treturn undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction isToolCallBlock(block: unknown): block is { type: \"toolCall\"; id?: string; name: string; arguments: unknown } {\n\tif (!block || typeof block !== \"object\") {\n\t\treturn false;\n\t}\n\tif ((block as { type?: unknown }).type !== \"toolCall\") {\n\t\treturn false;\n\t}\n\tif (typeof (block as { name?: unknown }).name !== \"string\") {\n\t\treturn false;\n\t}\n\tif (!(block as { arguments?: unknown }).arguments) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction toolCallVerb(name: string): string {\n\treturn TOOL_VERB_BY_NAME[name] ?? name.toUpperCase();\n}\n\nexport function extractCompactionFacts(entries: SessionEntry[], start: number, end: number): CompactionFacts {\n\tconst rangeStart = Math.max(0, start);\n\tconst rangeEnd = Math.min(entries.length, Math.max(rangeStart, end));\n\n\tif (rangeStart >= rangeEnd) {\n\t\treturn { files: [], actions: [], prohibitions: [], cancelledText: \"\", activeTaskSource: \"\" };\n\t}\n\n\tconst filesByPath = new Map<string, { path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>();\n\tconst filesNotes = new Map<string, string>();\n\tconst seenProhibitions = new Set<string>();\n\tconst actionFacts: ToolCallFact[] = [];\n\tconst pendingById = new Map<string, number[]>();\n\tconst pendingByOrder: number[] = [];\n\n\tconst actions: string[] = [];\n\tconst prohibitions: string[] = [];\n\tlet activeTaskSource = \"\";\n\tconst cancelledParts: string[] = [];\n\tconst sinceLastUser: string[] = [];\n\n\tfor (let i = rangeStart; i < rangeEnd; i++) {\n\t\tconst message = getMessageFromEntry(entries[i]);\n\t\tif (!message) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"user\") {\n\t\t\tconst userText = messageToText(message);\n\t\t\tif (REVERSAL_PATTERN.test(userText)) {\n\t\t\t\tcancelledParts.push(...sinceLastUser);\n\t\t\t}\n\t\t\tsinceLastUser.length = 0;\n\n\t\t\tif (userText) {\n\t\t\t\tactiveTaskSource = userText;\n\t\t\t\tfor (const sentence of splitSentenceLines(userText)) {\n\t\t\t\t\tif (!PROHIBITION_PATTERN.test(sentence)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst normalized = clampText(sentence, 160);\n\t\t\t\t\tconst dedupeKey = normalized.toLowerCase();\n\t\t\t\t\tif (!seenProhibitions.has(dedupeKey)) {\n\t\t\t\t\t\tseenProhibitions.add(dedupeKey);\n\t\t\t\t\t\tprohibitions.push(normalized);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"assistant\" || message.role === \"toolResult\") {\n\t\t\tconst messageText = messageToText(message);\n\t\t\tif (messageText) {\n\t\t\t\tsinceLastUser.push(messageText);\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"assistant\" && Array.isArray(message.content)) {\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (!isToolCallBlock(block)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst name = block.name;\n\t\t\t\tconst verb = toolCallVerb(name);\n\t\t\t\tconst path = assistantToolCallTarget(name, block.arguments);\n\t\t\t\tconst baseKind = TOOL_KIND_BY_NAME[name] ?? null;\n\t\t\t\tconst fact: ToolCallFact = {\n\t\t\t\t\tid: block.id,\n\t\t\t\t\tname,\n\t\t\t\t\tpath,\n\t\t\t\t\tbaseKind,\n\t\t\t\t\tfinalKind: baseKind,\n\t\t\t\t\tverb,\n\t\t\t\t\toutcome: \"\",\n\t\t\t\t\tresolved: false,\n\t\t\t\t};\n\t\t\t\tactionFacts.push(fact);\n\t\t\t\tconst index = actionFacts.length - 1;\n\t\t\t\tpendingByOrder.push(index);\n\t\t\t\tif (fact.id) {\n\t\t\t\t\tconst bucket = pendingById.get(fact.id) ?? [];\n\t\t\t\t\tbucket.push(index);\n\t\t\t\t\tpendingById.set(fact.id, bucket);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"toolResult\") {\n\t\t\tconst toolCallId = (message as { toolCallId?: string }).toolCallId;\n\t\t\tconst toolName = (message as { toolName?: string }).toolName;\n\t\t\tlet matchedIndex: number | undefined;\n\n\t\t\tif (toolCallId) {\n\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\tif (bucket && bucket.length > 0) {\n\t\t\t\t\tmatchedIndex = bucket.shift();\n\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex === undefined && toolName) {\n\t\t\t\tfor (const index of pendingByOrder) {\n\t\t\t\t\tconst candidate = actionFacts[index];\n\t\t\t\t\tif (!candidate.resolved && candidate.name === toolName) {\n\t\t\t\t\t\tmatchedIndex = index;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex !== undefined) {\n\t\t\t\tconst fact = actionFacts[matchedIndex];\n\t\t\t\tconst outcome = firstOutcome(message);\n\t\t\t\tfact.outcome = outcome;\n\t\t\t\tfact.resolved = true;\n\t\t\t\tif (fact.baseKind === \"modified\" && appearsCreated(message)) {\n\t\t\t\t\tfact.finalKind = \"created\";\n\t\t\t\t}\n\n\t\t\t\tconst finalOutcome = clampText(fact.outcome, 80);\n\t\t\t\tfact.outcome = finalOutcome;\n\t\t\t\tif (fact.path && fact.finalKind) {\n\t\t\t\t\tconst existing = filesByPath.get(fact.path);\n\t\t\t\t\tconst nextKind = fact.finalKind;\n\t\t\t\t\tconst note = fact.verb;\n\t\t\t\t\tif (!existing || FILE_KIND_PRIORITY[nextKind] > FILE_KIND_PRIORITY[existing.kind]) {\n\t\t\t\t\t\tfilesByPath.set(fact.path, { path: fact.path, kind: nextKind, note });\n\t\t\t\t\t} else if (existing.kind === nextKind) {\n\t\t\t\t\t\texisting.note = `${note}: ${finalOutcome}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst pendingPos = pendingByOrder.indexOf(matchedIndex);\n\t\t\t\tif (pendingPos >= 0) {\n\t\t\t\t\tpendingByOrder.splice(pendingPos, 1);\n\t\t\t\t}\n\t\t\t\tif (toolCallId) {\n\t\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\t\tif (bucket) {\n\t\t\t\t\t\tconst pos = bucket.indexOf(matchedIndex);\n\t\t\t\t\t\tif (pos >= 0) {\n\t\t\t\t\t\t\tbucket.splice(pos, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const action of actionFacts) {\n\t\tif (action.finalKind && action.path && !filesByPath.has(action.path)) {\n\t\t\tconst note = action.outcome ? `${action.verb}: ${action.outcome}` : action.verb;\n\t\t\tfilesByPath.set(action.path, { path: action.path, kind: action.finalKind, note });\n\t\t}\n\n\t\tactions.push(`${action.verb} ${action.path ?? \"(unknown)\"} — ${action.outcome}`);\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tif (!file.note && filesNotes.has(file.path)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note && filesNotes.get(file.path) === file.note) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note) {\n\t\t\tfilesNotes.set(file.path, file.note);\n\t\t}\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tconst suffix = filesNotes.get(file.path);\n\t\tif (suffix) {\n\t\t\tfile.note = suffix;\n\t\t}\n\t}\n\n\treturn {\n\t\tfiles: Array.from(filesByPath.values()).sort((a, b) => {\n\t\t\tif (a.path === b.path) {\n\t\t\t\treturn FILE_KIND_PRIORITY[a.kind] - FILE_KIND_PRIORITY[b.kind];\n\t\t\t}\n\t\t\treturn a.path.localeCompare(b.path);\n\t\t}),\n\t\tactions,\n\t\tprohibitions,\n\t\tcancelledText: cancelledParts.join(\"\\n\"),\n\t\tactiveTaskSource,\n\t};\n}\n\nexport function renderFactsBlock(facts: CompactionFacts): string {\n\tconst lines: string[] = [\"files:\"];\n\tfor (const file of facts.files) {\n\t\tlines.push(`${file.kind}: ${file.path} — ${file.note}`);\n\t}\n\tlines.push(\"actions:\");\n\tfor (const action of facts.actions) {\n\t\tlines.push(action);\n\t}\n\tlines.push(\"prohibitions:\");\n\tfor (const prohibition of facts.prohibitions) {\n\t\tlines.push(prohibition);\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"extraction.d.ts","sourceRoot":"","sources":["../../src/compaction/extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAGlE,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CACzB;AA0LD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,CAmM3G;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAqB/D","sourcesContent":["import type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage } from \"../types.ts\";\n\nexport interface CompactionFacts {\n\tfiles: Array<{ path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>;\n\tactions: string[];\n\tprohibitions: string[];\n\tcancelledText: string;\n\tactiveTaskSource: string;\n}\n\ninterface ToolCallFact {\n\tid: string | undefined;\n\tname: string;\n\tpath: string | undefined;\n\tbaseKind: \"modified\" | \"created\" | \"read\" | null;\n\tfinalKind: \"modified\" | \"created\" | \"read\" | null;\n\tverb: string;\n\toutcome: string;\n\tresolved: boolean;\n}\n\nconst PROHIBITION_PATTERN = /\\b(do not|don't|never|stop (?:doing|using|changing)|no more)\\b/i;\nconst REVERSAL_PATTERN = /\\b(stop|undo|revert|roll back|never mind|scrap that|forget (?:it|that))\\b/i;\n\nconst FILE_KIND_PRIORITY: Record<NonNullable<ToolCallFact[\"finalKind\"]>, number> = {\n\tread: 1,\n\tcreated: 2,\n\tmodified: 3,\n};\n\nconst TOOL_KIND_BY_NAME: Record<string, \"modified\" | \"created\" | \"read\" | null> = {\n\tread: \"read\",\n\tgrep: \"read\",\n\tfind: \"read\",\n\twrite: \"modified\",\n\tedit: \"modified\",\n\tbash: null,\n};\n\nconst TOOL_VERB_BY_NAME: Record<string, string> = {\n\twrite: \"WRITE\",\n\tedit: \"EDIT\",\n\tbash: \"RUN\",\n\tread: \"READ\",\n\tgrep: \"READ\",\n\tfind: \"READ\",\n};\n\nfunction messageToText(message: AgentMessage): string {\n\tconst rawContent = (message as { content?: unknown }).content;\n\tif (typeof rawContent === \"string\") {\n\t\treturn rawContent;\n\t}\n\tif (!Array.isArray(rawContent)) {\n\t\treturn \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tfor (const block of rawContent) {\n\t\tif (!block || typeof block !== \"object\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ((block as { type?: unknown }).type === \"text\" && typeof (block as { text?: unknown }).text === \"string\") {\n\t\t\tparts.push((block as { text: string }).text);\n\t\t}\n\t}\n\treturn parts.join(\" \").trim();\n}\n\nfunction assistantToolCallTarget(name: string, rawArgs: unknown): string | undefined {\n\tif (!rawArgs || typeof rawArgs !== \"object\") {\n\t\treturn undefined;\n\t}\n\tconst args = rawArgs as Record<string, unknown>;\n\n\tif (\n\t\t(name === \"read\" || name === \"grep\" || name === \"find\" || name === \"write\" || name === \"edit\") &&\n\t\ttypeof args.path === \"string\" &&\n\t\targs.path.length > 0\n\t) {\n\t\treturn args.path;\n\t}\n\n\tif (name === \"bash\" && typeof args.command === \"string\" && args.command.length > 0) {\n\t\treturn args.command;\n\t}\n\n\treturn undefined;\n}\n\nfunction clampText(text: string, maxLen: number): string {\n\treturn text.length > maxLen ? text.slice(0, maxLen) : text;\n}\n\nfunction firstLine(text: string): string {\n\tfor (const line of text.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\treturn trimmed;\n\t\t}\n\t}\n\treturn \"\";\n}\n\nfunction firstOutcome(message: AgentMessage): string {\n\tif (message.role !== \"toolResult\") {\n\t\treturn \"\";\n\t}\n\treturn firstLine(messageToText(message));\n}\n\nfunction appearsCreated(message: AgentMessage): boolean {\n\tif (message.role !== \"toolResult\") {\n\t\treturn false;\n\t}\n\n\tconst text = messageToText(message).toLowerCase();\n\tif (/(\\bnew file\\b|\\bcreated\\b|\\bcreated file\\b)/.test(text)) {\n\t\treturn true;\n\t}\n\n\tconst details = (message as { details?: unknown }).details;\n\tif (!details || typeof details !== \"object\") {\n\t\treturn false;\n\t}\n\n\tif (typeof (details as { created?: unknown }).created === \"boolean\") {\n\t\treturn (details as { created: boolean }).created;\n\t}\n\tif (typeof (details as { isCreated?: unknown }).isCreated === \"boolean\") {\n\t\treturn (details as { isCreated: boolean }).isCreated;\n\t}\n\tif (typeof (details as { isNewFile?: unknown }).isNewFile === \"boolean\") {\n\t\treturn (details as { isNewFile: boolean }).isNewFile;\n\t}\n\n\treturn false;\n}\n\nfunction splitSentenceLines(text: string): string[] {\n\treturn text\n\t\t.split(/[.!?\\n]+/)\n\t\t.map((line) => line.trim())\n\t\t.filter(Boolean);\n}\n\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tswitch (entry.type) {\n\t\tcase \"message\":\n\t\t\treturn entry.message;\n\t\tcase \"custom_message\":\n\t\t\treturn {\n\t\t\t\trole: \"custom\",\n\t\t\t\tcustomType: entry.customType,\n\t\t\t\tcontent: entry.content,\n\t\t\t\tdetails: entry.details,\n\t\t\t\tdisplay: entry.display,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"branch_summary\":\n\t\t\treturn {\n\t\t\t\trole: \"branchSummary\",\n\t\t\t\tsummary: entry.summary,\n\t\t\t\tfromId: entry.fromId,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"compaction\":\n\t\t\treturn undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction isToolCallBlock(block: unknown): block is { type: \"toolCall\"; id?: string; name: string; arguments: unknown } {\n\tif (!block || typeof block !== \"object\") {\n\t\treturn false;\n\t}\n\tif ((block as { type?: unknown }).type !== \"toolCall\") {\n\t\treturn false;\n\t}\n\tif (typeof (block as { name?: unknown }).name !== \"string\") {\n\t\treturn false;\n\t}\n\tif (!(block as { arguments?: unknown }).arguments) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction toolCallVerb(name: string): string {\n\treturn TOOL_VERB_BY_NAME[name] ?? name.toUpperCase();\n}\n\nexport function extractCompactionFacts(entries: SessionEntry[], start: number, end: number): CompactionFacts {\n\tconst rangeStart = Math.max(0, start);\n\tconst rangeEnd = Math.min(entries.length, Math.max(rangeStart, end));\n\n\tif (rangeStart >= rangeEnd) {\n\t\treturn { files: [], actions: [], prohibitions: [], cancelledText: \"\", activeTaskSource: \"\" };\n\t}\n\n\tconst filesByPath = new Map<string, { path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>();\n\tconst filesNotes = new Map<string, string>();\n\tconst seenProhibitions = new Set<string>();\n\tconst actionFacts: ToolCallFact[] = [];\n\tconst pendingById = new Map<string, number[]>();\n\tconst pendingByOrder: number[] = [];\n\n\tconst actions: string[] = [];\n\tconst prohibitions: string[] = [];\n\tlet activeTaskSource = \"\";\n\tconst cancelledParts: string[] = [];\n\tconst sinceLastUser: string[] = [];\n\n\tfor (let i = rangeStart; i < rangeEnd; i++) {\n\t\tconst message = getMessageFromEntry(entries[i]);\n\t\tif (!message) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"user\") {\n\t\t\tconst userText = messageToText(message);\n\t\t\tif (REVERSAL_PATTERN.test(userText)) {\n\t\t\t\tcancelledParts.push(...sinceLastUser);\n\t\t\t}\n\t\t\tsinceLastUser.length = 0;\n\n\t\t\tif (userText) {\n\t\t\t\tactiveTaskSource = userText;\n\t\t\t\tfor (const sentence of splitSentenceLines(userText)) {\n\t\t\t\t\tif (!PROHIBITION_PATTERN.test(sentence)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst normalized = clampText(sentence, 160);\n\t\t\t\t\tconst dedupeKey = normalized.toLowerCase();\n\t\t\t\t\tif (!seenProhibitions.has(dedupeKey)) {\n\t\t\t\t\t\tseenProhibitions.add(dedupeKey);\n\t\t\t\t\t\tprohibitions.push(normalized);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"assistant\" || message.role === \"toolResult\") {\n\t\t\tconst messageText = messageToText(message);\n\t\t\tif (messageText) {\n\t\t\t\tsinceLastUser.push(messageText);\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"assistant\" && Array.isArray(message.content)) {\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (!isToolCallBlock(block)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst name = block.name;\n\t\t\t\tconst verb = toolCallVerb(name);\n\t\t\t\tconst path = assistantToolCallTarget(name, block.arguments);\n\t\t\t\tconst baseKind = TOOL_KIND_BY_NAME[name] ?? null;\n\t\t\t\tconst fact: ToolCallFact = {\n\t\t\t\t\tid: block.id,\n\t\t\t\t\tname,\n\t\t\t\t\tpath,\n\t\t\t\t\tbaseKind,\n\t\t\t\t\tfinalKind: baseKind,\n\t\t\t\t\tverb,\n\t\t\t\t\toutcome: \"\",\n\t\t\t\t\tresolved: false,\n\t\t\t\t};\n\t\t\t\tactionFacts.push(fact);\n\t\t\t\tconst index = actionFacts.length - 1;\n\t\t\t\tpendingByOrder.push(index);\n\t\t\t\tif (fact.id) {\n\t\t\t\t\tconst bucket = pendingById.get(fact.id) ?? [];\n\t\t\t\t\tbucket.push(index);\n\t\t\t\t\tpendingById.set(fact.id, bucket);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"toolResult\") {\n\t\t\tconst toolCallId = (message as { toolCallId?: string }).toolCallId;\n\t\t\tconst toolName = (message as { toolName?: string }).toolName;\n\t\t\tlet matchedIndex: number | undefined;\n\n\t\t\tif (toolCallId) {\n\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\tif (bucket && bucket.length > 0) {\n\t\t\t\t\tmatchedIndex = bucket.shift();\n\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex === undefined && toolName) {\n\t\t\t\tfor (const index of pendingByOrder) {\n\t\t\t\t\tconst candidate = actionFacts[index];\n\t\t\t\t\tif (!candidate.resolved && candidate.name === toolName) {\n\t\t\t\t\t\tmatchedIndex = index;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex !== undefined) {\n\t\t\t\tconst fact = actionFacts[matchedIndex];\n\t\t\t\tconst outcome = firstOutcome(message);\n\t\t\t\tfact.outcome = outcome;\n\t\t\t\tfact.resolved = true;\n\t\t\t\tif (fact.baseKind === \"modified\" && appearsCreated(message)) {\n\t\t\t\t\tfact.finalKind = \"created\";\n\t\t\t\t}\n\n\t\t\t\tconst finalOutcome = clampText(fact.outcome, 80);\n\t\t\t\tfact.outcome = finalOutcome;\n\t\t\t\tif (fact.path && fact.finalKind) {\n\t\t\t\t\tconst existing = filesByPath.get(fact.path);\n\t\t\t\t\tconst nextKind = fact.finalKind;\n\t\t\t\t\tconst note = fact.verb;\n\t\t\t\t\tif (!existing || FILE_KIND_PRIORITY[nextKind] > FILE_KIND_PRIORITY[existing.kind]) {\n\t\t\t\t\t\tfilesByPath.set(fact.path, { path: fact.path, kind: nextKind, note });\n\t\t\t\t\t} else if (existing.kind === nextKind) {\n\t\t\t\t\t\texisting.note = `${note}: ${finalOutcome}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst pendingPos = pendingByOrder.indexOf(matchedIndex);\n\t\t\t\tif (pendingPos >= 0) {\n\t\t\t\t\tpendingByOrder.splice(pendingPos, 1);\n\t\t\t\t}\n\t\t\t\tif (toolCallId) {\n\t\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\t\tif (bucket) {\n\t\t\t\t\t\tconst pos = bucket.indexOf(matchedIndex);\n\t\t\t\t\t\tif (pos >= 0) {\n\t\t\t\t\t\t\tbucket.splice(pos, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const action of actionFacts) {\n\t\tif (action.finalKind && action.path && !filesByPath.has(action.path)) {\n\t\t\tconst note = action.outcome ? `${action.verb}: ${action.outcome}` : action.verb;\n\t\t\tfilesByPath.set(action.path, { path: action.path, kind: action.finalKind, note });\n\t\t}\n\n\t\tactions.push(`${action.verb} ${action.path ?? \"(unknown)\"} — ${action.outcome}`);\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tif (!file.note && filesNotes.has(file.path)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note && filesNotes.get(file.path) === file.note) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note) {\n\t\t\tfilesNotes.set(file.path, file.note);\n\t\t}\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tconst suffix = filesNotes.get(file.path);\n\t\tif (suffix) {\n\t\t\tfile.note = suffix;\n\t\t}\n\t}\n\n\treturn {\n\t\tfiles: Array.from(filesByPath.values()).sort((a, b) => {\n\t\t\tif (a.path === b.path) {\n\t\t\t\treturn FILE_KIND_PRIORITY[a.kind] - FILE_KIND_PRIORITY[b.kind];\n\t\t\t}\n\t\t\treturn a.path.localeCompare(b.path);\n\t\t}),\n\t\tactions,\n\t\tprohibitions,\n\t\tcancelledText: cancelledParts.join(\"\\n\"),\n\t\tactiveTaskSource,\n\t};\n}\n\nexport function renderFactsBlock(facts: CompactionFacts): string {\n\tconst lines: string[] = [\"files:\"];\n\tfor (const file of facts.files) {\n\t\tlines.push(`${file.kind}: ${file.path} — ${file.note}`);\n\t}\n\tlines.push(\"actions:\");\n\tfor (const action of facts.actions) {\n\t\tlines.push(action);\n\t}\n\tlines.push(\"prohibitions:\");\n\tfor (const prohibition of facts.prohibitions) {\n\t\tlines.push(prohibition);\n\t}\n\t// The active-task gate demands near-verbatim recall of this text, but the conversation the\n\t// summarizer sees may be pre-digested or truncated — the facts block is the one channel\n\t// guaranteed to reach the prompt, so the gated text must ride in it (bounded).\n\tlines.push(\"active task:\");\n\tif (facts.activeTaskSource) {\n\t\tlines.push(clampText(facts.activeTaskSource, 4000));\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -337,6 +337,13 @@ export function renderFactsBlock(facts) {
337
337
  for (const prohibition of facts.prohibitions) {
338
338
  lines.push(prohibition);
339
339
  }
340
+ // The active-task gate demands near-verbatim recall of this text, but the conversation the
341
+ // summarizer sees may be pre-digested or truncated — the facts block is the one channel
342
+ // guaranteed to reach the prompt, so the gated text must ride in it (bounded).
343
+ lines.push("active task:");
344
+ if (facts.activeTaskSource) {
345
+ lines.push(clampText(facts.activeTaskSource, 4000));
346
+ }
340
347
  return lines.join("\n");
341
348
  }
342
349
  //# sourceMappingURL=extraction.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extraction.js","sourceRoot":"","sources":["../../src/compaction/extraction.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage } from \"../types.ts\";\n\nexport interface CompactionFacts {\n\tfiles: Array<{ path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>;\n\tactions: string[];\n\tprohibitions: string[];\n\tcancelledText: string;\n\tactiveTaskSource: string;\n}\n\ninterface ToolCallFact {\n\tid: string | undefined;\n\tname: string;\n\tpath: string | undefined;\n\tbaseKind: \"modified\" | \"created\" | \"read\" | null;\n\tfinalKind: \"modified\" | \"created\" | \"read\" | null;\n\tverb: string;\n\toutcome: string;\n\tresolved: boolean;\n}\n\nconst PROHIBITION_PATTERN = /\\b(do not|don't|never|stop (?:doing|using|changing)|no more)\\b/i;\nconst REVERSAL_PATTERN = /\\b(stop|undo|revert|roll back|never mind|scrap that|forget (?:it|that))\\b/i;\n\nconst FILE_KIND_PRIORITY: Record<NonNullable<ToolCallFact[\"finalKind\"]>, number> = {\n\tread: 1,\n\tcreated: 2,\n\tmodified: 3,\n};\n\nconst TOOL_KIND_BY_NAME: Record<string, \"modified\" | \"created\" | \"read\" | null> = {\n\tread: \"read\",\n\tgrep: \"read\",\n\tfind: \"read\",\n\twrite: \"modified\",\n\tedit: \"modified\",\n\tbash: null,\n};\n\nconst TOOL_VERB_BY_NAME: Record<string, string> = {\n\twrite: \"WRITE\",\n\tedit: \"EDIT\",\n\tbash: \"RUN\",\n\tread: \"READ\",\n\tgrep: \"READ\",\n\tfind: \"READ\",\n};\n\nfunction messageToText(message: AgentMessage): string {\n\tconst rawContent = (message as { content?: unknown }).content;\n\tif (typeof rawContent === \"string\") {\n\t\treturn rawContent;\n\t}\n\tif (!Array.isArray(rawContent)) {\n\t\treturn \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tfor (const block of rawContent) {\n\t\tif (!block || typeof block !== \"object\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ((block as { type?: unknown }).type === \"text\" && typeof (block as { text?: unknown }).text === \"string\") {\n\t\t\tparts.push((block as { text: string }).text);\n\t\t}\n\t}\n\treturn parts.join(\" \").trim();\n}\n\nfunction assistantToolCallTarget(name: string, rawArgs: unknown): string | undefined {\n\tif (!rawArgs || typeof rawArgs !== \"object\") {\n\t\treturn undefined;\n\t}\n\tconst args = rawArgs as Record<string, unknown>;\n\n\tif (\n\t\t(name === \"read\" || name === \"grep\" || name === \"find\" || name === \"write\" || name === \"edit\") &&\n\t\ttypeof args.path === \"string\" &&\n\t\targs.path.length > 0\n\t) {\n\t\treturn args.path;\n\t}\n\n\tif (name === \"bash\" && typeof args.command === \"string\" && args.command.length > 0) {\n\t\treturn args.command;\n\t}\n\n\treturn undefined;\n}\n\nfunction clampText(text: string, maxLen: number): string {\n\treturn text.length > maxLen ? text.slice(0, maxLen) : text;\n}\n\nfunction firstLine(text: string): string {\n\tfor (const line of text.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\treturn trimmed;\n\t\t}\n\t}\n\treturn \"\";\n}\n\nfunction firstOutcome(message: AgentMessage): string {\n\tif (message.role !== \"toolResult\") {\n\t\treturn \"\";\n\t}\n\treturn firstLine(messageToText(message));\n}\n\nfunction appearsCreated(message: AgentMessage): boolean {\n\tif (message.role !== \"toolResult\") {\n\t\treturn false;\n\t}\n\n\tconst text = messageToText(message).toLowerCase();\n\tif (/(\\bnew file\\b|\\bcreated\\b|\\bcreated file\\b)/.test(text)) {\n\t\treturn true;\n\t}\n\n\tconst details = (message as { details?: unknown }).details;\n\tif (!details || typeof details !== \"object\") {\n\t\treturn false;\n\t}\n\n\tif (typeof (details as { created?: unknown }).created === \"boolean\") {\n\t\treturn (details as { created: boolean }).created;\n\t}\n\tif (typeof (details as { isCreated?: unknown }).isCreated === \"boolean\") {\n\t\treturn (details as { isCreated: boolean }).isCreated;\n\t}\n\tif (typeof (details as { isNewFile?: unknown }).isNewFile === \"boolean\") {\n\t\treturn (details as { isNewFile: boolean }).isNewFile;\n\t}\n\n\treturn false;\n}\n\nfunction splitSentenceLines(text: string): string[] {\n\treturn text\n\t\t.split(/[.!?\\n]+/)\n\t\t.map((line) => line.trim())\n\t\t.filter(Boolean);\n}\n\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tswitch (entry.type) {\n\t\tcase \"message\":\n\t\t\treturn entry.message;\n\t\tcase \"custom_message\":\n\t\t\treturn {\n\t\t\t\trole: \"custom\",\n\t\t\t\tcustomType: entry.customType,\n\t\t\t\tcontent: entry.content,\n\t\t\t\tdetails: entry.details,\n\t\t\t\tdisplay: entry.display,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"branch_summary\":\n\t\t\treturn {\n\t\t\t\trole: \"branchSummary\",\n\t\t\t\tsummary: entry.summary,\n\t\t\t\tfromId: entry.fromId,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"compaction\":\n\t\t\treturn undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction isToolCallBlock(block: unknown): block is { type: \"toolCall\"; id?: string; name: string; arguments: unknown } {\n\tif (!block || typeof block !== \"object\") {\n\t\treturn false;\n\t}\n\tif ((block as { type?: unknown }).type !== \"toolCall\") {\n\t\treturn false;\n\t}\n\tif (typeof (block as { name?: unknown }).name !== \"string\") {\n\t\treturn false;\n\t}\n\tif (!(block as { arguments?: unknown }).arguments) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction toolCallVerb(name: string): string {\n\treturn TOOL_VERB_BY_NAME[name] ?? name.toUpperCase();\n}\n\nexport function extractCompactionFacts(entries: SessionEntry[], start: number, end: number): CompactionFacts {\n\tconst rangeStart = Math.max(0, start);\n\tconst rangeEnd = Math.min(entries.length, Math.max(rangeStart, end));\n\n\tif (rangeStart >= rangeEnd) {\n\t\treturn { files: [], actions: [], prohibitions: [], cancelledText: \"\", activeTaskSource: \"\" };\n\t}\n\n\tconst filesByPath = new Map<string, { path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>();\n\tconst filesNotes = new Map<string, string>();\n\tconst seenProhibitions = new Set<string>();\n\tconst actionFacts: ToolCallFact[] = [];\n\tconst pendingById = new Map<string, number[]>();\n\tconst pendingByOrder: number[] = [];\n\n\tconst actions: string[] = [];\n\tconst prohibitions: string[] = [];\n\tlet activeTaskSource = \"\";\n\tconst cancelledParts: string[] = [];\n\tconst sinceLastUser: string[] = [];\n\n\tfor (let i = rangeStart; i < rangeEnd; i++) {\n\t\tconst message = getMessageFromEntry(entries[i]);\n\t\tif (!message) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"user\") {\n\t\t\tconst userText = messageToText(message);\n\t\t\tif (REVERSAL_PATTERN.test(userText)) {\n\t\t\t\tcancelledParts.push(...sinceLastUser);\n\t\t\t}\n\t\t\tsinceLastUser.length = 0;\n\n\t\t\tif (userText) {\n\t\t\t\tactiveTaskSource = userText;\n\t\t\t\tfor (const sentence of splitSentenceLines(userText)) {\n\t\t\t\t\tif (!PROHIBITION_PATTERN.test(sentence)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst normalized = clampText(sentence, 160);\n\t\t\t\t\tconst dedupeKey = normalized.toLowerCase();\n\t\t\t\t\tif (!seenProhibitions.has(dedupeKey)) {\n\t\t\t\t\t\tseenProhibitions.add(dedupeKey);\n\t\t\t\t\t\tprohibitions.push(normalized);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"assistant\" || message.role === \"toolResult\") {\n\t\t\tconst messageText = messageToText(message);\n\t\t\tif (messageText) {\n\t\t\t\tsinceLastUser.push(messageText);\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"assistant\" && Array.isArray(message.content)) {\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (!isToolCallBlock(block)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst name = block.name;\n\t\t\t\tconst verb = toolCallVerb(name);\n\t\t\t\tconst path = assistantToolCallTarget(name, block.arguments);\n\t\t\t\tconst baseKind = TOOL_KIND_BY_NAME[name] ?? null;\n\t\t\t\tconst fact: ToolCallFact = {\n\t\t\t\t\tid: block.id,\n\t\t\t\t\tname,\n\t\t\t\t\tpath,\n\t\t\t\t\tbaseKind,\n\t\t\t\t\tfinalKind: baseKind,\n\t\t\t\t\tverb,\n\t\t\t\t\toutcome: \"\",\n\t\t\t\t\tresolved: false,\n\t\t\t\t};\n\t\t\t\tactionFacts.push(fact);\n\t\t\t\tconst index = actionFacts.length - 1;\n\t\t\t\tpendingByOrder.push(index);\n\t\t\t\tif (fact.id) {\n\t\t\t\t\tconst bucket = pendingById.get(fact.id) ?? [];\n\t\t\t\t\tbucket.push(index);\n\t\t\t\t\tpendingById.set(fact.id, bucket);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"toolResult\") {\n\t\t\tconst toolCallId = (message as { toolCallId?: string }).toolCallId;\n\t\t\tconst toolName = (message as { toolName?: string }).toolName;\n\t\t\tlet matchedIndex: number | undefined;\n\n\t\t\tif (toolCallId) {\n\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\tif (bucket && bucket.length > 0) {\n\t\t\t\t\tmatchedIndex = bucket.shift();\n\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex === undefined && toolName) {\n\t\t\t\tfor (const index of pendingByOrder) {\n\t\t\t\t\tconst candidate = actionFacts[index];\n\t\t\t\t\tif (!candidate.resolved && candidate.name === toolName) {\n\t\t\t\t\t\tmatchedIndex = index;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex !== undefined) {\n\t\t\t\tconst fact = actionFacts[matchedIndex];\n\t\t\t\tconst outcome = firstOutcome(message);\n\t\t\t\tfact.outcome = outcome;\n\t\t\t\tfact.resolved = true;\n\t\t\t\tif (fact.baseKind === \"modified\" && appearsCreated(message)) {\n\t\t\t\t\tfact.finalKind = \"created\";\n\t\t\t\t}\n\n\t\t\t\tconst finalOutcome = clampText(fact.outcome, 80);\n\t\t\t\tfact.outcome = finalOutcome;\n\t\t\t\tif (fact.path && fact.finalKind) {\n\t\t\t\t\tconst existing = filesByPath.get(fact.path);\n\t\t\t\t\tconst nextKind = fact.finalKind;\n\t\t\t\t\tconst note = fact.verb;\n\t\t\t\t\tif (!existing || FILE_KIND_PRIORITY[nextKind] > FILE_KIND_PRIORITY[existing.kind]) {\n\t\t\t\t\t\tfilesByPath.set(fact.path, { path: fact.path, kind: nextKind, note });\n\t\t\t\t\t} else if (existing.kind === nextKind) {\n\t\t\t\t\t\texisting.note = `${note}: ${finalOutcome}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst pendingPos = pendingByOrder.indexOf(matchedIndex);\n\t\t\t\tif (pendingPos >= 0) {\n\t\t\t\t\tpendingByOrder.splice(pendingPos, 1);\n\t\t\t\t}\n\t\t\t\tif (toolCallId) {\n\t\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\t\tif (bucket) {\n\t\t\t\t\t\tconst pos = bucket.indexOf(matchedIndex);\n\t\t\t\t\t\tif (pos >= 0) {\n\t\t\t\t\t\t\tbucket.splice(pos, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const action of actionFacts) {\n\t\tif (action.finalKind && action.path && !filesByPath.has(action.path)) {\n\t\t\tconst note = action.outcome ? `${action.verb}: ${action.outcome}` : action.verb;\n\t\t\tfilesByPath.set(action.path, { path: action.path, kind: action.finalKind, note });\n\t\t}\n\n\t\tactions.push(`${action.verb} ${action.path ?? \"(unknown)\"} — ${action.outcome}`);\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tif (!file.note && filesNotes.has(file.path)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note && filesNotes.get(file.path) === file.note) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note) {\n\t\t\tfilesNotes.set(file.path, file.note);\n\t\t}\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tconst suffix = filesNotes.get(file.path);\n\t\tif (suffix) {\n\t\t\tfile.note = suffix;\n\t\t}\n\t}\n\n\treturn {\n\t\tfiles: Array.from(filesByPath.values()).sort((a, b) => {\n\t\t\tif (a.path === b.path) {\n\t\t\t\treturn FILE_KIND_PRIORITY[a.kind] - FILE_KIND_PRIORITY[b.kind];\n\t\t\t}\n\t\t\treturn a.path.localeCompare(b.path);\n\t\t}),\n\t\tactions,\n\t\tprohibitions,\n\t\tcancelledText: cancelledParts.join(\"\\n\"),\n\t\tactiveTaskSource,\n\t};\n}\n\nexport function renderFactsBlock(facts: CompactionFacts): string {\n\tconst lines: string[] = [\"files:\"];\n\tfor (const file of facts.files) {\n\t\tlines.push(`${file.kind}: ${file.path} — ${file.note}`);\n\t}\n\tlines.push(\"actions:\");\n\tfor (const action of facts.actions) {\n\t\tlines.push(action);\n\t}\n\tlines.push(\"prohibitions:\");\n\tfor (const prohibition of facts.prohibitions) {\n\t\tlines.push(prohibition);\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"extraction.js","sourceRoot":"","sources":["../../src/compaction/extraction.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage } from \"../types.ts\";\n\nexport interface CompactionFacts {\n\tfiles: Array<{ path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>;\n\tactions: string[];\n\tprohibitions: string[];\n\tcancelledText: string;\n\tactiveTaskSource: string;\n}\n\ninterface ToolCallFact {\n\tid: string | undefined;\n\tname: string;\n\tpath: string | undefined;\n\tbaseKind: \"modified\" | \"created\" | \"read\" | null;\n\tfinalKind: \"modified\" | \"created\" | \"read\" | null;\n\tverb: string;\n\toutcome: string;\n\tresolved: boolean;\n}\n\nconst PROHIBITION_PATTERN = /\\b(do not|don't|never|stop (?:doing|using|changing)|no more)\\b/i;\nconst REVERSAL_PATTERN = /\\b(stop|undo|revert|roll back|never mind|scrap that|forget (?:it|that))\\b/i;\n\nconst FILE_KIND_PRIORITY: Record<NonNullable<ToolCallFact[\"finalKind\"]>, number> = {\n\tread: 1,\n\tcreated: 2,\n\tmodified: 3,\n};\n\nconst TOOL_KIND_BY_NAME: Record<string, \"modified\" | \"created\" | \"read\" | null> = {\n\tread: \"read\",\n\tgrep: \"read\",\n\tfind: \"read\",\n\twrite: \"modified\",\n\tedit: \"modified\",\n\tbash: null,\n};\n\nconst TOOL_VERB_BY_NAME: Record<string, string> = {\n\twrite: \"WRITE\",\n\tedit: \"EDIT\",\n\tbash: \"RUN\",\n\tread: \"READ\",\n\tgrep: \"READ\",\n\tfind: \"READ\",\n};\n\nfunction messageToText(message: AgentMessage): string {\n\tconst rawContent = (message as { content?: unknown }).content;\n\tif (typeof rawContent === \"string\") {\n\t\treturn rawContent;\n\t}\n\tif (!Array.isArray(rawContent)) {\n\t\treturn \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tfor (const block of rawContent) {\n\t\tif (!block || typeof block !== \"object\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ((block as { type?: unknown }).type === \"text\" && typeof (block as { text?: unknown }).text === \"string\") {\n\t\t\tparts.push((block as { text: string }).text);\n\t\t}\n\t}\n\treturn parts.join(\" \").trim();\n}\n\nfunction assistantToolCallTarget(name: string, rawArgs: unknown): string | undefined {\n\tif (!rawArgs || typeof rawArgs !== \"object\") {\n\t\treturn undefined;\n\t}\n\tconst args = rawArgs as Record<string, unknown>;\n\n\tif (\n\t\t(name === \"read\" || name === \"grep\" || name === \"find\" || name === \"write\" || name === \"edit\") &&\n\t\ttypeof args.path === \"string\" &&\n\t\targs.path.length > 0\n\t) {\n\t\treturn args.path;\n\t}\n\n\tif (name === \"bash\" && typeof args.command === \"string\" && args.command.length > 0) {\n\t\treturn args.command;\n\t}\n\n\treturn undefined;\n}\n\nfunction clampText(text: string, maxLen: number): string {\n\treturn text.length > maxLen ? text.slice(0, maxLen) : text;\n}\n\nfunction firstLine(text: string): string {\n\tfor (const line of text.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\treturn trimmed;\n\t\t}\n\t}\n\treturn \"\";\n}\n\nfunction firstOutcome(message: AgentMessage): string {\n\tif (message.role !== \"toolResult\") {\n\t\treturn \"\";\n\t}\n\treturn firstLine(messageToText(message));\n}\n\nfunction appearsCreated(message: AgentMessage): boolean {\n\tif (message.role !== \"toolResult\") {\n\t\treturn false;\n\t}\n\n\tconst text = messageToText(message).toLowerCase();\n\tif (/(\\bnew file\\b|\\bcreated\\b|\\bcreated file\\b)/.test(text)) {\n\t\treturn true;\n\t}\n\n\tconst details = (message as { details?: unknown }).details;\n\tif (!details || typeof details !== \"object\") {\n\t\treturn false;\n\t}\n\n\tif (typeof (details as { created?: unknown }).created === \"boolean\") {\n\t\treturn (details as { created: boolean }).created;\n\t}\n\tif (typeof (details as { isCreated?: unknown }).isCreated === \"boolean\") {\n\t\treturn (details as { isCreated: boolean }).isCreated;\n\t}\n\tif (typeof (details as { isNewFile?: unknown }).isNewFile === \"boolean\") {\n\t\treturn (details as { isNewFile: boolean }).isNewFile;\n\t}\n\n\treturn false;\n}\n\nfunction splitSentenceLines(text: string): string[] {\n\treturn text\n\t\t.split(/[.!?\\n]+/)\n\t\t.map((line) => line.trim())\n\t\t.filter(Boolean);\n}\n\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tswitch (entry.type) {\n\t\tcase \"message\":\n\t\t\treturn entry.message;\n\t\tcase \"custom_message\":\n\t\t\treturn {\n\t\t\t\trole: \"custom\",\n\t\t\t\tcustomType: entry.customType,\n\t\t\t\tcontent: entry.content,\n\t\t\t\tdetails: entry.details,\n\t\t\t\tdisplay: entry.display,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"branch_summary\":\n\t\t\treturn {\n\t\t\t\trole: \"branchSummary\",\n\t\t\t\tsummary: entry.summary,\n\t\t\t\tfromId: entry.fromId,\n\t\t\t\ttimestamp: new Date(entry.timestamp).getTime(),\n\t\t\t};\n\t\tcase \"compaction\":\n\t\t\treturn undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction isToolCallBlock(block: unknown): block is { type: \"toolCall\"; id?: string; name: string; arguments: unknown } {\n\tif (!block || typeof block !== \"object\") {\n\t\treturn false;\n\t}\n\tif ((block as { type?: unknown }).type !== \"toolCall\") {\n\t\treturn false;\n\t}\n\tif (typeof (block as { name?: unknown }).name !== \"string\") {\n\t\treturn false;\n\t}\n\tif (!(block as { arguments?: unknown }).arguments) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction toolCallVerb(name: string): string {\n\treturn TOOL_VERB_BY_NAME[name] ?? name.toUpperCase();\n}\n\nexport function extractCompactionFacts(entries: SessionEntry[], start: number, end: number): CompactionFacts {\n\tconst rangeStart = Math.max(0, start);\n\tconst rangeEnd = Math.min(entries.length, Math.max(rangeStart, end));\n\n\tif (rangeStart >= rangeEnd) {\n\t\treturn { files: [], actions: [], prohibitions: [], cancelledText: \"\", activeTaskSource: \"\" };\n\t}\n\n\tconst filesByPath = new Map<string, { path: string; kind: \"modified\" | \"created\" | \"read\"; note: string }>();\n\tconst filesNotes = new Map<string, string>();\n\tconst seenProhibitions = new Set<string>();\n\tconst actionFacts: ToolCallFact[] = [];\n\tconst pendingById = new Map<string, number[]>();\n\tconst pendingByOrder: number[] = [];\n\n\tconst actions: string[] = [];\n\tconst prohibitions: string[] = [];\n\tlet activeTaskSource = \"\";\n\tconst cancelledParts: string[] = [];\n\tconst sinceLastUser: string[] = [];\n\n\tfor (let i = rangeStart; i < rangeEnd; i++) {\n\t\tconst message = getMessageFromEntry(entries[i]);\n\t\tif (!message) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"user\") {\n\t\t\tconst userText = messageToText(message);\n\t\t\tif (REVERSAL_PATTERN.test(userText)) {\n\t\t\t\tcancelledParts.push(...sinceLastUser);\n\t\t\t}\n\t\t\tsinceLastUser.length = 0;\n\n\t\t\tif (userText) {\n\t\t\t\tactiveTaskSource = userText;\n\t\t\t\tfor (const sentence of splitSentenceLines(userText)) {\n\t\t\t\t\tif (!PROHIBITION_PATTERN.test(sentence)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst normalized = clampText(sentence, 160);\n\t\t\t\t\tconst dedupeKey = normalized.toLowerCase();\n\t\t\t\t\tif (!seenProhibitions.has(dedupeKey)) {\n\t\t\t\t\t\tseenProhibitions.add(dedupeKey);\n\t\t\t\t\t\tprohibitions.push(normalized);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"assistant\" || message.role === \"toolResult\") {\n\t\t\tconst messageText = messageToText(message);\n\t\t\tif (messageText) {\n\t\t\t\tsinceLastUser.push(messageText);\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"assistant\" && Array.isArray(message.content)) {\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (!isToolCallBlock(block)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst name = block.name;\n\t\t\t\tconst verb = toolCallVerb(name);\n\t\t\t\tconst path = assistantToolCallTarget(name, block.arguments);\n\t\t\t\tconst baseKind = TOOL_KIND_BY_NAME[name] ?? null;\n\t\t\t\tconst fact: ToolCallFact = {\n\t\t\t\t\tid: block.id,\n\t\t\t\t\tname,\n\t\t\t\t\tpath,\n\t\t\t\t\tbaseKind,\n\t\t\t\t\tfinalKind: baseKind,\n\t\t\t\t\tverb,\n\t\t\t\t\toutcome: \"\",\n\t\t\t\t\tresolved: false,\n\t\t\t\t};\n\t\t\t\tactionFacts.push(fact);\n\t\t\t\tconst index = actionFacts.length - 1;\n\t\t\t\tpendingByOrder.push(index);\n\t\t\t\tif (fact.id) {\n\t\t\t\t\tconst bucket = pendingById.get(fact.id) ?? [];\n\t\t\t\t\tbucket.push(index);\n\t\t\t\t\tpendingById.set(fact.id, bucket);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (message.role === \"toolResult\") {\n\t\t\tconst toolCallId = (message as { toolCallId?: string }).toolCallId;\n\t\t\tconst toolName = (message as { toolName?: string }).toolName;\n\t\t\tlet matchedIndex: number | undefined;\n\n\t\t\tif (toolCallId) {\n\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\tif (bucket && bucket.length > 0) {\n\t\t\t\t\tmatchedIndex = bucket.shift();\n\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex === undefined && toolName) {\n\t\t\t\tfor (const index of pendingByOrder) {\n\t\t\t\t\tconst candidate = actionFacts[index];\n\t\t\t\t\tif (!candidate.resolved && candidate.name === toolName) {\n\t\t\t\t\t\tmatchedIndex = index;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matchedIndex !== undefined) {\n\t\t\t\tconst fact = actionFacts[matchedIndex];\n\t\t\t\tconst outcome = firstOutcome(message);\n\t\t\t\tfact.outcome = outcome;\n\t\t\t\tfact.resolved = true;\n\t\t\t\tif (fact.baseKind === \"modified\" && appearsCreated(message)) {\n\t\t\t\t\tfact.finalKind = \"created\";\n\t\t\t\t}\n\n\t\t\t\tconst finalOutcome = clampText(fact.outcome, 80);\n\t\t\t\tfact.outcome = finalOutcome;\n\t\t\t\tif (fact.path && fact.finalKind) {\n\t\t\t\t\tconst existing = filesByPath.get(fact.path);\n\t\t\t\t\tconst nextKind = fact.finalKind;\n\t\t\t\t\tconst note = fact.verb;\n\t\t\t\t\tif (!existing || FILE_KIND_PRIORITY[nextKind] > FILE_KIND_PRIORITY[existing.kind]) {\n\t\t\t\t\t\tfilesByPath.set(fact.path, { path: fact.path, kind: nextKind, note });\n\t\t\t\t\t} else if (existing.kind === nextKind) {\n\t\t\t\t\t\texisting.note = `${note}: ${finalOutcome}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst pendingPos = pendingByOrder.indexOf(matchedIndex);\n\t\t\t\tif (pendingPos >= 0) {\n\t\t\t\t\tpendingByOrder.splice(pendingPos, 1);\n\t\t\t\t}\n\t\t\t\tif (toolCallId) {\n\t\t\t\t\tconst bucket = pendingById.get(toolCallId);\n\t\t\t\t\tif (bucket) {\n\t\t\t\t\t\tconst pos = bucket.indexOf(matchedIndex);\n\t\t\t\t\t\tif (pos >= 0) {\n\t\t\t\t\t\t\tbucket.splice(pos, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (bucket.length === 0) {\n\t\t\t\t\t\t\tpendingById.delete(toolCallId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const action of actionFacts) {\n\t\tif (action.finalKind && action.path && !filesByPath.has(action.path)) {\n\t\t\tconst note = action.outcome ? `${action.verb}: ${action.outcome}` : action.verb;\n\t\t\tfilesByPath.set(action.path, { path: action.path, kind: action.finalKind, note });\n\t\t}\n\n\t\tactions.push(`${action.verb} ${action.path ?? \"(unknown)\"} — ${action.outcome}`);\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tif (!file.note && filesNotes.has(file.path)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note && filesNotes.get(file.path) === file.note) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (file.note) {\n\t\t\tfilesNotes.set(file.path, file.note);\n\t\t}\n\t}\n\n\tfor (const file of filesByPath.values()) {\n\t\tconst suffix = filesNotes.get(file.path);\n\t\tif (suffix) {\n\t\t\tfile.note = suffix;\n\t\t}\n\t}\n\n\treturn {\n\t\tfiles: Array.from(filesByPath.values()).sort((a, b) => {\n\t\t\tif (a.path === b.path) {\n\t\t\t\treturn FILE_KIND_PRIORITY[a.kind] - FILE_KIND_PRIORITY[b.kind];\n\t\t\t}\n\t\t\treturn a.path.localeCompare(b.path);\n\t\t}),\n\t\tactions,\n\t\tprohibitions,\n\t\tcancelledText: cancelledParts.join(\"\\n\"),\n\t\tactiveTaskSource,\n\t};\n}\n\nexport function renderFactsBlock(facts: CompactionFacts): string {\n\tconst lines: string[] = [\"files:\"];\n\tfor (const file of facts.files) {\n\t\tlines.push(`${file.kind}: ${file.path} — ${file.note}`);\n\t}\n\tlines.push(\"actions:\");\n\tfor (const action of facts.actions) {\n\t\tlines.push(action);\n\t}\n\tlines.push(\"prohibitions:\");\n\tfor (const prohibition of facts.prohibitions) {\n\t\tlines.push(prohibition);\n\t}\n\t// The active-task gate demands near-verbatim recall of this text, but the conversation the\n\t// summarizer sees may be pre-digested or truncated — the facts block is the one channel\n\t// guaranteed to reach the prompt, so the gated text must ride in it (bounded).\n\tlines.push(\"active task:\");\n\tif (facts.activeTaskSource) {\n\t\tlines.push(clampText(facts.activeTaskSource, 4000));\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/compaction/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IAClC,iBAAiB,IAAI,MAAM,CAAC;IAC5B,mBAAmB,IAAI,MAAM,CAAC;IAC9B,SAAS,IAAI,MAAM,CAAC;IACpB,SAAS,IAAI,YAAY,EAAE,CAAC;IAC5B,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC1F,kBAAkB,CACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,YAAY,EAAE,GACpB,OAAO,CAAC;QAAE,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC;IACzC,4BAA4B,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC,GAAG;QAAE,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC;IACrG,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtD,YAAY,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,uBAAuB,CAAC,IAAI,MAAM,CAAC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAMtD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAgGhG","sourcesContent":["import type { Model } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { CompactionResult } from \"./compaction.ts\";\n\nexport interface CompactionCycleParams {\n\tmodelTier: \"cheap\" | \"session\";\n\tkeepRecentTokens: number;\n\tchunked: boolean;\n\tdeterministicOnly: boolean;\n}\n\nexport interface ModelAndAuth {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tfailure?: string;\n}\n\nexport interface CompactionLoopDeps {\n\tmeasureLiveTokens(): number;\n\tgetTriggerThreshold(): number;\n\tgetMargin(): number;\n\tgetBranch(): SessionEntry[];\n\tresolveModelAndAuth(modelTier: CompactionCycleParams[\"modelTier\"]): Promise<ModelAndAuth>;\n\tsummarizeAndVerify(\n\t\tparams: CompactionCycleParams,\n\t\tmodel: Model<any>,\n\t\tapiKey: string | undefined,\n\t\theaders: Record<string, string> | undefined,\n\t\tbranch: SessionEntry[],\n\t): Promise<{ result: CompactionResult }>;\n\tbuildDeterministicCheckpoint(): Promise<{ result: CompactionResult }> | { result: CompactionResult };\n\tapply(result: CompactionResult): Promise<void> | void;\n\tonTransition(info: { cycle: number; from: string; cause: string }): void;\n\tgetBaseKeepRecentTokens?(): number;\n\tsignal?: AbortSignal;\n}\n\nexport type CompactionLoopOutcome =\n\t| { kind: \"success\"; result: CompactionResult; cycles: number }\n\t| { kind: \"skip\"; reason: string }\n\t| { kind: \"failed\"; reason: string; cycles: number };\n\nconst MAX_CYCLES = 4;\nconst MAX_LLM_CYCLES = 3;\nconst DEFAULT_KEEP_RECENT = 20_000;\n\nexport async function runCompactionLoop(deps: CompactionLoopDeps): Promise<CompactionLoopOutcome> {\n\tlet lastCause = \"start\";\n\tlet lastParams: CompactionCycleParams | undefined;\n\tlet lastObservedTokens: number | undefined;\n\tlet appliedResult: CompactionResult | undefined;\n\tlet baseKeepRecent = deps.getBaseKeepRecentTokens ? deps.getBaseKeepRecentTokens() : DEFAULT_KEEP_RECENT;\n\tif (!Number.isFinite(baseKeepRecent) || baseKeepRecent <= 0) {\n\t\tbaseKeepRecent = DEFAULT_KEEP_RECENT;\n\t}\n\n\tfor (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t}\n\n\t\tconst branch = deps.getBranch();\n\t\tif (branch.length > 0 && branch[branch.length - 1]?.type === \"compaction\") {\n\t\t\tif (appliedResult) {\n\t\t\t\treturn { kind: \"success\", result: appliedResult, cycles: cycle - 1 };\n\t\t\t}\n\t\t\treturn { kind: \"skip\", reason: \"already compacted\" };\n\t\t}\n\n\t\tconst observedTokens = deps.measureLiveTokens();\n\t\tif (observedTokens <= deps.getTriggerThreshold()) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\treason:\n\t\t\t\t\tbranch.length > 0 && branch[branch.length - 1]?.type === \"compaction\"\n\t\t\t\t\t\t? \"already compacted\"\n\t\t\t\t\t\t: \"within threshold\",\n\t\t\t};\n\t\t}\n\n\t\tconst selectedParams = selectCycleParams(cycle, lastCause, lastParams, baseKeepRecent);\n\t\tconst params = enforceMonotonicProgress(selectedParams, lastParams, observedTokens, lastObservedTokens);\n\t\tlastObservedTokens = observedTokens;\n\t\tlastParams = params;\n\n\t\tif (params.deterministicOnly || cycle > MAX_LLM_CYCLES) {\n\t\t\tif (deps.signal?.aborted) {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t\t}\n\t\t\tconst { result } = await Promise.resolve(deps.buildDeterministicCheckpoint());\n\t\t\tawait deps.apply(result);\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlet modelInfo: ModelAndAuth;\n\t\ttry {\n\t\t\tmodelInfo = await deps.resolveModelAndAuth(params.modelTier);\n\t\t} catch {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\t\tif (modelInfo.failure) {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet result: CompactionResult;\n\t\ttry {\n\t\t\t({ result } = await deps.summarizeAndVerify(\n\t\t\t\tparams,\n\t\t\t\tmodelInfo.model,\n\t\t\t\tmodelInfo.apiKey,\n\t\t\t\tmodelInfo.headers,\n\t\t\t\tbranch,\n\t\t\t));\n\t\t} catch (error) {\n\t\t\tlastCause = mapFailureCause(error);\n\t\t\tif (lastCause === \"aborted\") {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t\t}\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step3\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t}\n\t\tawait deps.apply(result);\n\t\tappliedResult = result;\n\n\t\tconst measuredAfter = deps.measureLiveTokens();\n\t\tif (measuredAfter <= deps.getTriggerThreshold() - deps.getMargin()) {\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlastCause = \"effect-not-restored\";\n\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step5\", cause: lastCause });\n\t}\n\n\treturn { kind: \"failed\", reason: \"exhausted-compaction-cycles\", cycles: MAX_CYCLES };\n}\n\nfunction selectCycleParams(\n\tcycle: number,\n\tcause: string,\n\tlastParams: CompactionCycleParams | undefined,\n\tbaseKeepRecent: number,\n): CompactionCycleParams {\n\tif (cycle >= MAX_CYCLES) {\n\t\treturn {\n\t\t\tmodelTier: lastParams?.modelTier ?? \"session\",\n\t\t\tkeepRecentTokens: Math.max(1, Math.floor((lastParams?.keepRecentTokens ?? baseKeepRecent) / 2)),\n\t\t\tchunked: true,\n\t\t\tdeterministicOnly: true,\n\t\t};\n\t}\n\n\tif (!lastParams) {\n\t\treturn {\n\t\t\tmodelTier: \"cheap\",\n\t\t\tkeepRecentTokens: Math.max(1, baseKeepRecent),\n\t\t\tchunked: false,\n\t\t\tdeterministicOnly: false,\n\t\t};\n\t}\n\n\tconst params: CompactionCycleParams = {\n\t\t...lastParams,\n\t\tdeterministicOnly: false,\n\t};\n\n\tif (cause === \"gate-failed\" || cause === \"auth-failed\") {\n\t\tparams.modelTier = \"session\";\n\t} else if (cause === \"input-overflow\") {\n\t\tparams.chunked = true;\n\t} else if (cause === \"effect-not-restored\") {\n\t\tparams.chunked = true;\n\t\tparams.keepRecentTokens = Math.max(1, Math.floor(lastParams.keepRecentTokens / 2));\n\t}\n\n\treturn params;\n}\n\nfunction enforceMonotonicProgress(\n\tparams: CompactionCycleParams,\n\tlastParams: CompactionCycleParams | undefined,\n\tobservedTokens: number,\n\tlastObservedTokens: number | undefined,\n): CompactionCycleParams {\n\tif (\n\t\t!lastParams ||\n\t\tparams.deterministicOnly ||\n\t\t(lastObservedTokens !== undefined && observedTokens < lastObservedTokens)\n\t) {\n\t\treturn params;\n\t}\n\tif (!sameParams(params, lastParams)) {\n\t\treturn params;\n\t}\n\n\tconst keepRecentTokens = Math.max(1, Math.floor(params.keepRecentTokens / 2));\n\tif (keepRecentTokens !== params.keepRecentTokens) {\n\t\treturn { ...params, chunked: true, keepRecentTokens };\n\t}\n\tif (!params.chunked) {\n\t\treturn { ...params, chunked: true };\n\t}\n\treturn { ...params, modelTier: params.modelTier === \"cheap\" ? \"session\" : \"cheap\" };\n}\n\nfunction sameParams(a: CompactionCycleParams, b: CompactionCycleParams): boolean {\n\treturn (\n\t\ta.modelTier === b.modelTier &&\n\t\ta.keepRecentTokens === b.keepRecentTokens &&\n\t\ta.chunked === b.chunked &&\n\t\ta.deterministicOnly === b.deterministicOnly\n\t);\n}\n\nfunction mapFailureCause(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : typeof error === \"string\" ? error : \"\";\n\tif (message.includes(\"gate-failed\")) return \"gate-failed\";\n\tif (message.includes(\"input-overflow\")) return \"input-overflow\";\n\tif (message.includes(\"auto-compaction-cancelled\") || message.includes(\"aborted\")) return \"aborted\";\n\tif (message.includes(\"auth\") || message.includes(\"api key\") || message.includes(\"not compacted\"))\n\t\treturn \"auth-failed\";\n\treturn \"unknown-failure\";\n}\n"]}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/compaction/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IAClC,iBAAiB,IAAI,MAAM,CAAC;IAC5B,mBAAmB,IAAI,MAAM,CAAC;IAC9B,SAAS,IAAI,MAAM,CAAC;IACpB,SAAS,IAAI,YAAY,EAAE,CAAC;IAC5B,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC1F,kBAAkB,CACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,YAAY,EAAE,GACpB,OAAO,CAAC;QAAE,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC;IACzC,4BAA4B,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC,GAAG;QAAE,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC;IACrG,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtD,YAAY,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,uBAAuB,CAAC,IAAI,MAAM,CAAC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAMtD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAgGhG","sourcesContent":["import type { Model } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { CompactionResult } from \"./compaction.ts\";\n\nexport interface CompactionCycleParams {\n\tmodelTier: \"cheap\" | \"session\";\n\tkeepRecentTokens: number;\n\tchunked: boolean;\n\tdeterministicOnly: boolean;\n}\n\nexport interface ModelAndAuth {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tfailure?: string;\n}\n\nexport interface CompactionLoopDeps {\n\tmeasureLiveTokens(): number;\n\tgetTriggerThreshold(): number;\n\tgetMargin(): number;\n\tgetBranch(): SessionEntry[];\n\tresolveModelAndAuth(modelTier: CompactionCycleParams[\"modelTier\"]): Promise<ModelAndAuth>;\n\tsummarizeAndVerify(\n\t\tparams: CompactionCycleParams,\n\t\tmodel: Model<any>,\n\t\tapiKey: string | undefined,\n\t\theaders: Record<string, string> | undefined,\n\t\tbranch: SessionEntry[],\n\t): Promise<{ result: CompactionResult }>;\n\tbuildDeterministicCheckpoint(): Promise<{ result: CompactionResult }> | { result: CompactionResult };\n\tapply(result: CompactionResult): Promise<void> | void;\n\tonTransition(info: { cycle: number; from: string; cause: string }): void;\n\tgetBaseKeepRecentTokens?(): number;\n\tsignal?: AbortSignal;\n}\n\nexport type CompactionLoopOutcome =\n\t| { kind: \"success\"; result: CompactionResult; cycles: number }\n\t| { kind: \"skip\"; reason: string }\n\t| { kind: \"failed\"; reason: string; cycles: number };\n\nconst MAX_CYCLES = 4;\nconst MAX_LLM_CYCLES = 3;\nconst DEFAULT_KEEP_RECENT = 20_000;\n\nexport async function runCompactionLoop(deps: CompactionLoopDeps): Promise<CompactionLoopOutcome> {\n\tlet lastCause = \"start\";\n\tlet lastParams: CompactionCycleParams | undefined;\n\tlet lastObservedTokens: number | undefined;\n\tlet appliedResult: CompactionResult | undefined;\n\tlet baseKeepRecent = deps.getBaseKeepRecentTokens ? deps.getBaseKeepRecentTokens() : DEFAULT_KEEP_RECENT;\n\tif (!Number.isFinite(baseKeepRecent) || baseKeepRecent <= 0) {\n\t\tbaseKeepRecent = DEFAULT_KEEP_RECENT;\n\t}\n\n\tfor (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t}\n\n\t\tconst branch = deps.getBranch();\n\t\tif (branch.length > 0 && branch[branch.length - 1]?.type === \"compaction\") {\n\t\t\tif (appliedResult) {\n\t\t\t\treturn { kind: \"success\", result: appliedResult, cycles: cycle - 1 };\n\t\t\t}\n\t\t\treturn { kind: \"skip\", reason: \"already compacted\" };\n\t\t}\n\n\t\tconst observedTokens = deps.measureLiveTokens();\n\t\tif (observedTokens <= deps.getTriggerThreshold()) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\treason:\n\t\t\t\t\tbranch.length > 0 && branch[branch.length - 1]?.type === \"compaction\"\n\t\t\t\t\t\t? \"already compacted\"\n\t\t\t\t\t\t: \"within threshold\",\n\t\t\t};\n\t\t}\n\n\t\tconst selectedParams = selectCycleParams(cycle, lastCause, lastParams, baseKeepRecent);\n\t\tconst params = enforceMonotonicProgress(selectedParams, lastParams, observedTokens, lastObservedTokens);\n\t\tlastObservedTokens = observedTokens;\n\t\tlastParams = params;\n\n\t\tif (params.deterministicOnly || cycle > MAX_LLM_CYCLES) {\n\t\t\tif (deps.signal?.aborted) {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t\t}\n\t\t\tconst { result } = await Promise.resolve(deps.buildDeterministicCheckpoint());\n\t\t\tawait deps.apply(result);\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlet modelInfo: ModelAndAuth;\n\t\ttry {\n\t\t\tmodelInfo = await deps.resolveModelAndAuth(params.modelTier);\n\t\t} catch {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\t\tif (modelInfo.failure) {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet result: CompactionResult;\n\t\ttry {\n\t\t\t({ result } = await deps.summarizeAndVerify(\n\t\t\t\tparams,\n\t\t\t\tmodelInfo.model,\n\t\t\t\tmodelInfo.apiKey,\n\t\t\t\tmodelInfo.headers,\n\t\t\t\tbranch,\n\t\t\t));\n\t\t} catch (error) {\n\t\t\tlastCause = mapFailureCause(error);\n\t\t\tif (lastCause === \"aborted\") {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t\t}\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step3\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t}\n\t\tawait deps.apply(result);\n\t\tappliedResult = result;\n\n\t\tconst measuredAfter = deps.measureLiveTokens();\n\t\tif (measuredAfter <= deps.getTriggerThreshold() - deps.getMargin()) {\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlastCause = \"effect-not-restored\";\n\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step5\", cause: lastCause });\n\t}\n\n\treturn { kind: \"failed\", reason: \"exhausted-compaction-cycles\", cycles: MAX_CYCLES };\n}\n\nfunction selectCycleParams(\n\tcycle: number,\n\tcause: string,\n\tlastParams: CompactionCycleParams | undefined,\n\tbaseKeepRecent: number,\n): CompactionCycleParams {\n\tif (cycle >= MAX_CYCLES) {\n\t\treturn {\n\t\t\tmodelTier: lastParams?.modelTier ?? \"session\",\n\t\t\tkeepRecentTokens: Math.max(1, Math.floor((lastParams?.keepRecentTokens ?? baseKeepRecent) / 2)),\n\t\t\tchunked: true,\n\t\t\tdeterministicOnly: true,\n\t\t};\n\t}\n\n\tif (!lastParams) {\n\t\treturn {\n\t\t\tmodelTier: \"cheap\",\n\t\t\tkeepRecentTokens: Math.max(1, baseKeepRecent),\n\t\t\tchunked: false,\n\t\t\tdeterministicOnly: false,\n\t\t};\n\t}\n\n\tconst params: CompactionCycleParams = {\n\t\t...lastParams,\n\t\tdeterministicOnly: false,\n\t};\n\n\tif (cause === \"gate-failed\" || cause === \"auth-failed\" || cause === \"length-stop\") {\n\t\tparams.modelTier = \"session\";\n\t} else if (cause === \"input-overflow\") {\n\t\tparams.chunked = true;\n\t} else if (cause === \"effect-not-restored\") {\n\t\tparams.chunked = true;\n\t\tparams.keepRecentTokens = Math.max(1, Math.floor(lastParams.keepRecentTokens / 2));\n\t}\n\n\treturn params;\n}\n\nfunction enforceMonotonicProgress(\n\tparams: CompactionCycleParams,\n\tlastParams: CompactionCycleParams | undefined,\n\tobservedTokens: number,\n\tlastObservedTokens: number | undefined,\n): CompactionCycleParams {\n\tif (\n\t\t!lastParams ||\n\t\tparams.deterministicOnly ||\n\t\t(lastObservedTokens !== undefined && observedTokens < lastObservedTokens)\n\t) {\n\t\treturn params;\n\t}\n\tif (!sameParams(params, lastParams)) {\n\t\treturn params;\n\t}\n\n\tconst keepRecentTokens = Math.max(1, Math.floor(params.keepRecentTokens / 2));\n\tif (keepRecentTokens !== params.keepRecentTokens) {\n\t\treturn { ...params, chunked: true, keepRecentTokens };\n\t}\n\tif (!params.chunked) {\n\t\treturn { ...params, chunked: true };\n\t}\n\treturn { ...params, modelTier: params.modelTier === \"cheap\" ? \"session\" : \"cheap\" };\n}\n\nfunction sameParams(a: CompactionCycleParams, b: CompactionCycleParams): boolean {\n\treturn (\n\t\ta.modelTier === b.modelTier &&\n\t\ta.keepRecentTokens === b.keepRecentTokens &&\n\t\ta.chunked === b.chunked &&\n\t\ta.deterministicOnly === b.deterministicOnly\n\t);\n}\n\nfunction mapFailureCause(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : typeof error === \"string\" ? error : \"\";\n\tif (message.includes(\"gate-failed\")) return \"gate-failed\";\n\tif (message.includes(\"input-overflow\")) return \"input-overflow\";\n\t// A length-stopped summary lost gated sections; escalating the tier buys a larger output cap.\n\tif (message.includes(\"summary-length-stop\")) return \"length-stop\";\n\tif (message.includes(\"auto-compaction-cancelled\") || message.includes(\"aborted\")) return \"aborted\";\n\tif (message.includes(\"auth\") || message.includes(\"api key\") || message.includes(\"not compacted\"))\n\t\treturn \"auth-failed\";\n\treturn \"unknown-failure\";\n}\n"]}
@@ -103,7 +103,7 @@ function selectCycleParams(cycle, cause, lastParams, baseKeepRecent) {
103
103
  ...lastParams,
104
104
  deterministicOnly: false,
105
105
  };
106
- if (cause === "gate-failed" || cause === "auth-failed") {
106
+ if (cause === "gate-failed" || cause === "auth-failed" || cause === "length-stop") {
107
107
  params.modelTier = "session";
108
108
  }
109
109
  else if (cause === "input-overflow") {
@@ -145,6 +145,9 @@ function mapFailureCause(error) {
145
145
  return "gate-failed";
146
146
  if (message.includes("input-overflow"))
147
147
  return "input-overflow";
148
+ // A length-stopped summary lost gated sections; escalating the tier buys a larger output cap.
149
+ if (message.includes("summary-length-stop"))
150
+ return "length-stop";
148
151
  if (message.includes("auto-compaction-cancelled") || message.includes("aborted"))
149
152
  return "aborted";
150
153
  if (message.includes("auth") || message.includes("api key") || message.includes("not compacted"))
@@ -1 +1 @@
1
- {"version":3,"file":"loop.js","sourceRoot":"","sources":["../../src/compaction/loop.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Model } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { CompactionResult } from \"./compaction.ts\";\n\nexport interface CompactionCycleParams {\n\tmodelTier: \"cheap\" | \"session\";\n\tkeepRecentTokens: number;\n\tchunked: boolean;\n\tdeterministicOnly: boolean;\n}\n\nexport interface ModelAndAuth {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tfailure?: string;\n}\n\nexport interface CompactionLoopDeps {\n\tmeasureLiveTokens(): number;\n\tgetTriggerThreshold(): number;\n\tgetMargin(): number;\n\tgetBranch(): SessionEntry[];\n\tresolveModelAndAuth(modelTier: CompactionCycleParams[\"modelTier\"]): Promise<ModelAndAuth>;\n\tsummarizeAndVerify(\n\t\tparams: CompactionCycleParams,\n\t\tmodel: Model<any>,\n\t\tapiKey: string | undefined,\n\t\theaders: Record<string, string> | undefined,\n\t\tbranch: SessionEntry[],\n\t): Promise<{ result: CompactionResult }>;\n\tbuildDeterministicCheckpoint(): Promise<{ result: CompactionResult }> | { result: CompactionResult };\n\tapply(result: CompactionResult): Promise<void> | void;\n\tonTransition(info: { cycle: number; from: string; cause: string }): void;\n\tgetBaseKeepRecentTokens?(): number;\n\tsignal?: AbortSignal;\n}\n\nexport type CompactionLoopOutcome =\n\t| { kind: \"success\"; result: CompactionResult; cycles: number }\n\t| { kind: \"skip\"; reason: string }\n\t| { kind: \"failed\"; reason: string; cycles: number };\n\nconst MAX_CYCLES = 4;\nconst MAX_LLM_CYCLES = 3;\nconst DEFAULT_KEEP_RECENT = 20_000;\n\nexport async function runCompactionLoop(deps: CompactionLoopDeps): Promise<CompactionLoopOutcome> {\n\tlet lastCause = \"start\";\n\tlet lastParams: CompactionCycleParams | undefined;\n\tlet lastObservedTokens: number | undefined;\n\tlet appliedResult: CompactionResult | undefined;\n\tlet baseKeepRecent = deps.getBaseKeepRecentTokens ? deps.getBaseKeepRecentTokens() : DEFAULT_KEEP_RECENT;\n\tif (!Number.isFinite(baseKeepRecent) || baseKeepRecent <= 0) {\n\t\tbaseKeepRecent = DEFAULT_KEEP_RECENT;\n\t}\n\n\tfor (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t}\n\n\t\tconst branch = deps.getBranch();\n\t\tif (branch.length > 0 && branch[branch.length - 1]?.type === \"compaction\") {\n\t\t\tif (appliedResult) {\n\t\t\t\treturn { kind: \"success\", result: appliedResult, cycles: cycle - 1 };\n\t\t\t}\n\t\t\treturn { kind: \"skip\", reason: \"already compacted\" };\n\t\t}\n\n\t\tconst observedTokens = deps.measureLiveTokens();\n\t\tif (observedTokens <= deps.getTriggerThreshold()) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\treason:\n\t\t\t\t\tbranch.length > 0 && branch[branch.length - 1]?.type === \"compaction\"\n\t\t\t\t\t\t? \"already compacted\"\n\t\t\t\t\t\t: \"within threshold\",\n\t\t\t};\n\t\t}\n\n\t\tconst selectedParams = selectCycleParams(cycle, lastCause, lastParams, baseKeepRecent);\n\t\tconst params = enforceMonotonicProgress(selectedParams, lastParams, observedTokens, lastObservedTokens);\n\t\tlastObservedTokens = observedTokens;\n\t\tlastParams = params;\n\n\t\tif (params.deterministicOnly || cycle > MAX_LLM_CYCLES) {\n\t\t\tif (deps.signal?.aborted) {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t\t}\n\t\t\tconst { result } = await Promise.resolve(deps.buildDeterministicCheckpoint());\n\t\t\tawait deps.apply(result);\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlet modelInfo: ModelAndAuth;\n\t\ttry {\n\t\t\tmodelInfo = await deps.resolveModelAndAuth(params.modelTier);\n\t\t} catch {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\t\tif (modelInfo.failure) {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet result: CompactionResult;\n\t\ttry {\n\t\t\t({ result } = await deps.summarizeAndVerify(\n\t\t\t\tparams,\n\t\t\t\tmodelInfo.model,\n\t\t\t\tmodelInfo.apiKey,\n\t\t\t\tmodelInfo.headers,\n\t\t\t\tbranch,\n\t\t\t));\n\t\t} catch (error) {\n\t\t\tlastCause = mapFailureCause(error);\n\t\t\tif (lastCause === \"aborted\") {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t\t}\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step3\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t}\n\t\tawait deps.apply(result);\n\t\tappliedResult = result;\n\n\t\tconst measuredAfter = deps.measureLiveTokens();\n\t\tif (measuredAfter <= deps.getTriggerThreshold() - deps.getMargin()) {\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlastCause = \"effect-not-restored\";\n\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step5\", cause: lastCause });\n\t}\n\n\treturn { kind: \"failed\", reason: \"exhausted-compaction-cycles\", cycles: MAX_CYCLES };\n}\n\nfunction selectCycleParams(\n\tcycle: number,\n\tcause: string,\n\tlastParams: CompactionCycleParams | undefined,\n\tbaseKeepRecent: number,\n): CompactionCycleParams {\n\tif (cycle >= MAX_CYCLES) {\n\t\treturn {\n\t\t\tmodelTier: lastParams?.modelTier ?? \"session\",\n\t\t\tkeepRecentTokens: Math.max(1, Math.floor((lastParams?.keepRecentTokens ?? baseKeepRecent) / 2)),\n\t\t\tchunked: true,\n\t\t\tdeterministicOnly: true,\n\t\t};\n\t}\n\n\tif (!lastParams) {\n\t\treturn {\n\t\t\tmodelTier: \"cheap\",\n\t\t\tkeepRecentTokens: Math.max(1, baseKeepRecent),\n\t\t\tchunked: false,\n\t\t\tdeterministicOnly: false,\n\t\t};\n\t}\n\n\tconst params: CompactionCycleParams = {\n\t\t...lastParams,\n\t\tdeterministicOnly: false,\n\t};\n\n\tif (cause === \"gate-failed\" || cause === \"auth-failed\") {\n\t\tparams.modelTier = \"session\";\n\t} else if (cause === \"input-overflow\") {\n\t\tparams.chunked = true;\n\t} else if (cause === \"effect-not-restored\") {\n\t\tparams.chunked = true;\n\t\tparams.keepRecentTokens = Math.max(1, Math.floor(lastParams.keepRecentTokens / 2));\n\t}\n\n\treturn params;\n}\n\nfunction enforceMonotonicProgress(\n\tparams: CompactionCycleParams,\n\tlastParams: CompactionCycleParams | undefined,\n\tobservedTokens: number,\n\tlastObservedTokens: number | undefined,\n): CompactionCycleParams {\n\tif (\n\t\t!lastParams ||\n\t\tparams.deterministicOnly ||\n\t\t(lastObservedTokens !== undefined && observedTokens < lastObservedTokens)\n\t) {\n\t\treturn params;\n\t}\n\tif (!sameParams(params, lastParams)) {\n\t\treturn params;\n\t}\n\n\tconst keepRecentTokens = Math.max(1, Math.floor(params.keepRecentTokens / 2));\n\tif (keepRecentTokens !== params.keepRecentTokens) {\n\t\treturn { ...params, chunked: true, keepRecentTokens };\n\t}\n\tif (!params.chunked) {\n\t\treturn { ...params, chunked: true };\n\t}\n\treturn { ...params, modelTier: params.modelTier === \"cheap\" ? \"session\" : \"cheap\" };\n}\n\nfunction sameParams(a: CompactionCycleParams, b: CompactionCycleParams): boolean {\n\treturn (\n\t\ta.modelTier === b.modelTier &&\n\t\ta.keepRecentTokens === b.keepRecentTokens &&\n\t\ta.chunked === b.chunked &&\n\t\ta.deterministicOnly === b.deterministicOnly\n\t);\n}\n\nfunction mapFailureCause(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : typeof error === \"string\" ? error : \"\";\n\tif (message.includes(\"gate-failed\")) return \"gate-failed\";\n\tif (message.includes(\"input-overflow\")) return \"input-overflow\";\n\tif (message.includes(\"auto-compaction-cancelled\") || message.includes(\"aborted\")) return \"aborted\";\n\tif (message.includes(\"auth\") || message.includes(\"api key\") || message.includes(\"not compacted\"))\n\t\treturn \"auth-failed\";\n\treturn \"unknown-failure\";\n}\n"]}
1
+ {"version":3,"file":"loop.js","sourceRoot":"","sources":["../../src/compaction/loop.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Model } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { CompactionResult } from \"./compaction.ts\";\n\nexport interface CompactionCycleParams {\n\tmodelTier: \"cheap\" | \"session\";\n\tkeepRecentTokens: number;\n\tchunked: boolean;\n\tdeterministicOnly: boolean;\n}\n\nexport interface ModelAndAuth {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tfailure?: string;\n}\n\nexport interface CompactionLoopDeps {\n\tmeasureLiveTokens(): number;\n\tgetTriggerThreshold(): number;\n\tgetMargin(): number;\n\tgetBranch(): SessionEntry[];\n\tresolveModelAndAuth(modelTier: CompactionCycleParams[\"modelTier\"]): Promise<ModelAndAuth>;\n\tsummarizeAndVerify(\n\t\tparams: CompactionCycleParams,\n\t\tmodel: Model<any>,\n\t\tapiKey: string | undefined,\n\t\theaders: Record<string, string> | undefined,\n\t\tbranch: SessionEntry[],\n\t): Promise<{ result: CompactionResult }>;\n\tbuildDeterministicCheckpoint(): Promise<{ result: CompactionResult }> | { result: CompactionResult };\n\tapply(result: CompactionResult): Promise<void> | void;\n\tonTransition(info: { cycle: number; from: string; cause: string }): void;\n\tgetBaseKeepRecentTokens?(): number;\n\tsignal?: AbortSignal;\n}\n\nexport type CompactionLoopOutcome =\n\t| { kind: \"success\"; result: CompactionResult; cycles: number }\n\t| { kind: \"skip\"; reason: string }\n\t| { kind: \"failed\"; reason: string; cycles: number };\n\nconst MAX_CYCLES = 4;\nconst MAX_LLM_CYCLES = 3;\nconst DEFAULT_KEEP_RECENT = 20_000;\n\nexport async function runCompactionLoop(deps: CompactionLoopDeps): Promise<CompactionLoopOutcome> {\n\tlet lastCause = \"start\";\n\tlet lastParams: CompactionCycleParams | undefined;\n\tlet lastObservedTokens: number | undefined;\n\tlet appliedResult: CompactionResult | undefined;\n\tlet baseKeepRecent = deps.getBaseKeepRecentTokens ? deps.getBaseKeepRecentTokens() : DEFAULT_KEEP_RECENT;\n\tif (!Number.isFinite(baseKeepRecent) || baseKeepRecent <= 0) {\n\t\tbaseKeepRecent = DEFAULT_KEEP_RECENT;\n\t}\n\n\tfor (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t}\n\n\t\tconst branch = deps.getBranch();\n\t\tif (branch.length > 0 && branch[branch.length - 1]?.type === \"compaction\") {\n\t\t\tif (appliedResult) {\n\t\t\t\treturn { kind: \"success\", result: appliedResult, cycles: cycle - 1 };\n\t\t\t}\n\t\t\treturn { kind: \"skip\", reason: \"already compacted\" };\n\t\t}\n\n\t\tconst observedTokens = deps.measureLiveTokens();\n\t\tif (observedTokens <= deps.getTriggerThreshold()) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\treason:\n\t\t\t\t\tbranch.length > 0 && branch[branch.length - 1]?.type === \"compaction\"\n\t\t\t\t\t\t? \"already compacted\"\n\t\t\t\t\t\t: \"within threshold\",\n\t\t\t};\n\t\t}\n\n\t\tconst selectedParams = selectCycleParams(cycle, lastCause, lastParams, baseKeepRecent);\n\t\tconst params = enforceMonotonicProgress(selectedParams, lastParams, observedTokens, lastObservedTokens);\n\t\tlastObservedTokens = observedTokens;\n\t\tlastParams = params;\n\n\t\tif (params.deterministicOnly || cycle > MAX_LLM_CYCLES) {\n\t\t\tif (deps.signal?.aborted) {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t\t}\n\t\t\tconst { result } = await Promise.resolve(deps.buildDeterministicCheckpoint());\n\t\t\tawait deps.apply(result);\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlet modelInfo: ModelAndAuth;\n\t\ttry {\n\t\t\tmodelInfo = await deps.resolveModelAndAuth(params.modelTier);\n\t\t} catch {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\t\tif (modelInfo.failure) {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet result: CompactionResult;\n\t\ttry {\n\t\t\t({ result } = await deps.summarizeAndVerify(\n\t\t\t\tparams,\n\t\t\t\tmodelInfo.model,\n\t\t\t\tmodelInfo.apiKey,\n\t\t\t\tmodelInfo.headers,\n\t\t\t\tbranch,\n\t\t\t));\n\t\t} catch (error) {\n\t\t\tlastCause = mapFailureCause(error);\n\t\t\tif (lastCause === \"aborted\") {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t\t}\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step3\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t}\n\t\tawait deps.apply(result);\n\t\tappliedResult = result;\n\n\t\tconst measuredAfter = deps.measureLiveTokens();\n\t\tif (measuredAfter <= deps.getTriggerThreshold() - deps.getMargin()) {\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlastCause = \"effect-not-restored\";\n\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step5\", cause: lastCause });\n\t}\n\n\treturn { kind: \"failed\", reason: \"exhausted-compaction-cycles\", cycles: MAX_CYCLES };\n}\n\nfunction selectCycleParams(\n\tcycle: number,\n\tcause: string,\n\tlastParams: CompactionCycleParams | undefined,\n\tbaseKeepRecent: number,\n): CompactionCycleParams {\n\tif (cycle >= MAX_CYCLES) {\n\t\treturn {\n\t\t\tmodelTier: lastParams?.modelTier ?? \"session\",\n\t\t\tkeepRecentTokens: Math.max(1, Math.floor((lastParams?.keepRecentTokens ?? baseKeepRecent) / 2)),\n\t\t\tchunked: true,\n\t\t\tdeterministicOnly: true,\n\t\t};\n\t}\n\n\tif (!lastParams) {\n\t\treturn {\n\t\t\tmodelTier: \"cheap\",\n\t\t\tkeepRecentTokens: Math.max(1, baseKeepRecent),\n\t\t\tchunked: false,\n\t\t\tdeterministicOnly: false,\n\t\t};\n\t}\n\n\tconst params: CompactionCycleParams = {\n\t\t...lastParams,\n\t\tdeterministicOnly: false,\n\t};\n\n\tif (cause === \"gate-failed\" || cause === \"auth-failed\" || cause === \"length-stop\") {\n\t\tparams.modelTier = \"session\";\n\t} else if (cause === \"input-overflow\") {\n\t\tparams.chunked = true;\n\t} else if (cause === \"effect-not-restored\") {\n\t\tparams.chunked = true;\n\t\tparams.keepRecentTokens = Math.max(1, Math.floor(lastParams.keepRecentTokens / 2));\n\t}\n\n\treturn params;\n}\n\nfunction enforceMonotonicProgress(\n\tparams: CompactionCycleParams,\n\tlastParams: CompactionCycleParams | undefined,\n\tobservedTokens: number,\n\tlastObservedTokens: number | undefined,\n): CompactionCycleParams {\n\tif (\n\t\t!lastParams ||\n\t\tparams.deterministicOnly ||\n\t\t(lastObservedTokens !== undefined && observedTokens < lastObservedTokens)\n\t) {\n\t\treturn params;\n\t}\n\tif (!sameParams(params, lastParams)) {\n\t\treturn params;\n\t}\n\n\tconst keepRecentTokens = Math.max(1, Math.floor(params.keepRecentTokens / 2));\n\tif (keepRecentTokens !== params.keepRecentTokens) {\n\t\treturn { ...params, chunked: true, keepRecentTokens };\n\t}\n\tif (!params.chunked) {\n\t\treturn { ...params, chunked: true };\n\t}\n\treturn { ...params, modelTier: params.modelTier === \"cheap\" ? \"session\" : \"cheap\" };\n}\n\nfunction sameParams(a: CompactionCycleParams, b: CompactionCycleParams): boolean {\n\treturn (\n\t\ta.modelTier === b.modelTier &&\n\t\ta.keepRecentTokens === b.keepRecentTokens &&\n\t\ta.chunked === b.chunked &&\n\t\ta.deterministicOnly === b.deterministicOnly\n\t);\n}\n\nfunction mapFailureCause(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : typeof error === \"string\" ? error : \"\";\n\tif (message.includes(\"gate-failed\")) return \"gate-failed\";\n\tif (message.includes(\"input-overflow\")) return \"input-overflow\";\n\t// A length-stopped summary lost gated sections; escalating the tier buys a larger output cap.\n\tif (message.includes(\"summary-length-stop\")) return \"length-stop\";\n\tif (message.includes(\"auto-compaction-cancelled\") || message.includes(\"aborted\")) return \"aborted\";\n\tif (message.includes(\"auth\") || message.includes(\"api key\") || message.includes(\"not compacted\"))\n\t\treturn \"auth-failed\";\n\treturn \"unknown-failure\";\n}\n"]}
@@ -2,8 +2,10 @@ import type { FailureReason } from "./classifier.ts";
2
2
  export interface ProviderSignature {
3
3
  reason: FailureReason;
4
4
  pattern: RegExp;
5
- /** Where this pattern was observed: adapter file:line, or "incident YYYY-MM-DD". */
5
+ /** Evidence citation: sdk package+version+file, corpus capture, or adapter file:line. */
6
6
  source: string;
7
+ /** True when the SDK-rendered fixture uses a vendor body shape that still awaits corpus confirmation. */
8
+ provisional?: boolean;
7
9
  }
8
10
  /** Provider-thrown-message signatures checked before the generic ladder. */
9
11
  export declare const PROVIDER_FAILURE_SIGNATURES: Record<string, readonly ProviderSignature[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"provider-signatures.d.ts","sourceRoot":"","sources":["../../src/reliability/provider-signatures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC;CACf;AAED,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAQpF,CAAC","sourcesContent":["import type { FailureReason } from \"./classifier.ts\";\n\nexport interface ProviderSignature {\n\treason: FailureReason;\n\tpattern: RegExp;\n\t/** Where this pattern was observed: adapter file:line, or \"incident YYYY-MM-DD\". */\n\tsource: string;\n}\n\n/** Provider-thrown-message signatures checked before the generic ladder. */\nexport const PROVIDER_FAILURE_SIGNATURES: Record<string, readonly ProviderSignature[]> = {\n\t\"openai-codex\": [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /You have hit your ChatGPT usage limit/i,\n\t\t\tsource: \"packages/ai/src/providers/openai-codex-responses.ts:1402\",\n\t\t},\n\t],\n};\n"]}
1
+ {"version":3,"file":"provider-signatures.d.ts","sourceRoot":"","sources":["../../src/reliability/provider-signatures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CA+BpF,CAAC","sourcesContent":["import type { FailureReason } from \"./classifier.ts\";\n\nexport interface ProviderSignature {\n\treason: FailureReason;\n\tpattern: RegExp;\n\t/** Evidence citation: sdk package+version+file, corpus capture, or adapter file:line. */\n\tsource: string;\n\t/** True when the SDK-rendered fixture uses a vendor body shape that still awaits corpus confirmation. */\n\tprovisional?: boolean;\n}\n\n/** Provider-thrown-message signatures checked before the generic ladder. */\nexport const PROVIDER_FAILURE_SIGNATURES: Record<string, readonly ProviderSignature[]> = {\n\tanthropic: [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /credit balance is too low/i,\n\t\t\tsource: \"sdk:@anthropic-ai/sdk@0.91.1 node_modules/@anthropic-ai/sdk/core/error.js\",\n\t\t},\n\t],\n\tmistral: [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /insufficient credits/i,\n\t\t\tsource: \"sdk:@mistralai/mistralai@2.2.1 node_modules/@mistralai/mistralai/esm/models/errors/sdkerror.js\",\n\t\t\tprovisional: true,\n\t\t},\n\t],\n\topenrouter: [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /insufficient credits/i,\n\t\t\tsource: \"sdk:openai@6.26.0 node_modules/openai/core/error.js\",\n\t\t\tprovisional: true,\n\t\t},\n\t],\n\t\"openai-codex\": [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /You have hit your ChatGPT usage limit/i,\n\t\t\tsource: \"packages/ai/src/providers/openai-codex-responses.ts:1402\",\n\t\t},\n\t],\n};\n"]}
@@ -1,5 +1,28 @@
1
1
  /** Provider-thrown-message signatures checked before the generic ladder. */
2
2
  export const PROVIDER_FAILURE_SIGNATURES = {
3
+ anthropic: [
4
+ {
5
+ reason: "billing_or_quota",
6
+ pattern: /credit balance is too low/i,
7
+ source: "sdk:@anthropic-ai/sdk@0.91.1 node_modules/@anthropic-ai/sdk/core/error.js",
8
+ },
9
+ ],
10
+ mistral: [
11
+ {
12
+ reason: "billing_or_quota",
13
+ pattern: /insufficient credits/i,
14
+ source: "sdk:@mistralai/mistralai@2.2.1 node_modules/@mistralai/mistralai/esm/models/errors/sdkerror.js",
15
+ provisional: true,
16
+ },
17
+ ],
18
+ openrouter: [
19
+ {
20
+ reason: "billing_or_quota",
21
+ pattern: /insufficient credits/i,
22
+ source: "sdk:openai@6.26.0 node_modules/openai/core/error.js",
23
+ provisional: true,
24
+ },
25
+ ],
3
26
  "openai-codex": [
4
27
  {
5
28
  reason: "billing_or_quota",
@@ -1 +1 @@
1
- {"version":3,"file":"provider-signatures.js","sourceRoot":"","sources":["../../src/reliability/provider-signatures.ts"],"names":[],"mappings":"AASA,4EAA4E;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAAiD;IACxF,cAAc,EAAE;QACf;YACC,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,wCAAwC;YACjD,MAAM,EAAE,0DAA0D;SAClE;KACD;CACD,CAAC","sourcesContent":["import type { FailureReason } from \"./classifier.ts\";\n\nexport interface ProviderSignature {\n\treason: FailureReason;\n\tpattern: RegExp;\n\t/** Where this pattern was observed: adapter file:line, or \"incident YYYY-MM-DD\". */\n\tsource: string;\n}\n\n/** Provider-thrown-message signatures checked before the generic ladder. */\nexport const PROVIDER_FAILURE_SIGNATURES: Record<string, readonly ProviderSignature[]> = {\n\t\"openai-codex\": [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /You have hit your ChatGPT usage limit/i,\n\t\t\tsource: \"packages/ai/src/providers/openai-codex-responses.ts:1402\",\n\t\t},\n\t],\n};\n"]}
1
+ {"version":3,"file":"provider-signatures.js","sourceRoot":"","sources":["../../src/reliability/provider-signatures.ts"],"names":[],"mappings":"AAWA,4EAA4E;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAAiD;IACxF,SAAS,EAAE;QACV;YACC,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,4BAA4B;YACrC,MAAM,EAAE,2EAA2E;SACnF;KACD;IACD,OAAO,EAAE;QACR;YACC,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,uBAAuB;YAChC,MAAM,EAAE,gGAAgG;YACxG,WAAW,EAAE,IAAI;SACjB;KACD;IACD,UAAU,EAAE;QACX;YACC,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,uBAAuB;YAChC,MAAM,EAAE,qDAAqD;YAC7D,WAAW,EAAE,IAAI;SACjB;KACD;IACD,cAAc,EAAE;QACf;YACC,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,wCAAwC;YACjD,MAAM,EAAE,0DAA0D;SAClE;KACD;CACD,CAAC","sourcesContent":["import type { FailureReason } from \"./classifier.ts\";\n\nexport interface ProviderSignature {\n\treason: FailureReason;\n\tpattern: RegExp;\n\t/** Evidence citation: sdk package+version+file, corpus capture, or adapter file:line. */\n\tsource: string;\n\t/** True when the SDK-rendered fixture uses a vendor body shape that still awaits corpus confirmation. */\n\tprovisional?: boolean;\n}\n\n/** Provider-thrown-message signatures checked before the generic ladder. */\nexport const PROVIDER_FAILURE_SIGNATURES: Record<string, readonly ProviderSignature[]> = {\n\tanthropic: [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /credit balance is too low/i,\n\t\t\tsource: \"sdk:@anthropic-ai/sdk@0.91.1 node_modules/@anthropic-ai/sdk/core/error.js\",\n\t\t},\n\t],\n\tmistral: [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /insufficient credits/i,\n\t\t\tsource: \"sdk:@mistralai/mistralai@2.2.1 node_modules/@mistralai/mistralai/esm/models/errors/sdkerror.js\",\n\t\t\tprovisional: true,\n\t\t},\n\t],\n\topenrouter: [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /insufficient credits/i,\n\t\t\tsource: \"sdk:openai@6.26.0 node_modules/openai/core/error.js\",\n\t\t\tprovisional: true,\n\t\t},\n\t],\n\t\"openai-codex\": [\n\t\t{\n\t\t\treason: \"billing_or_quota\",\n\t\t\tpattern: /You have hit your ChatGPT usage limit/i,\n\t\t\tsource: \"packages/ai/src/providers/openai-codex-responses.ts:1402\",\n\t\t},\n\t],\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"retry-controller.d.ts","sourceRoot":"","sources":["../../src/reliability/retry-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAuB,KAAK,WAAW,EAAkB,MAAM,YAAY,CAAC;AAEnF,0FAA0F;AAC1F,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;CAC7C;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC3B,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED,iGAAiG;AACjG,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvE,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAe;IAEhD,YACC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,qBAAqB,EACtC,MAAM,EAAE,WAAW,EACnB,gBAAgB,EAAE,MAAM,MAAM,EAM9B;IAED,0EAA0E;IAC1E,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+FAA+F;IAC/F,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,oGAAkG;IAClG,KAAK,IAAI,IAAI,CAEZ;IAED,8EAA8E;IAC9E,KAAK,IAAI,IAAI,CAEZ;IAED;;;;OAIG;IACG,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAyD9D;CACD","sourcesContent":["/**\n * Host-agnostic auto-retry driver.\n *\n * Owns the retry attempt counter and the abortable backoff for one agent run. It reads the\n * failure verdict from {@link classifyFailure} (fed a host-computed context-overflow flag), so\n * billing/auth are terminal and overflow routes to compaction — never to a pointless retry.\n * The controller only ever touches `agent.state.messages` (it drops the trailing assistant error\n * before retrying); durable history stays the host session's responsibility.\n *\n * Ported from AgentSession._prepareRetry / _isRetryableError so the exact event ordering and\n * exhaustion semantics carry over: the retry window is marked active (isRetrying) before the\n * start event fires, so prompts arriving inside start handlers queue as steering instead of\n * racing the retry continuation.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\nimport { classifyFailure } from \"./classifier.ts\";\nimport { computeRetryDelayMs, type RetryPolicy, sleepAbortable } from \"./retry.ts\";\n\n/** The slice of an Agent the retry driver reads and mutates: just the live transcript. */\nexport interface RetryAgent {\n\treadonly state: { messages: AgentMessage[] };\n}\n\nexport interface RetryStartInfo {\n\tattempt: number;\n\tmaxAttempts: number;\n\tdelayMs: number;\n\terrorMessage: string;\n}\n\nexport interface RetryEndInfo {\n\tsuccess: boolean;\n\tattempt: number;\n\tfinalError?: string;\n}\n\nexport interface RetryEvents {\n\tonRetryStart(info: RetryStartInfo): void;\n\tonRetryEnd(info: RetryEndInfo): void;\n}\n\n/** Runtime retry policy: the backoff shape plus the on/off switch the host resolves per call. */\nexport type RetryControllerPolicy = RetryPolicy & { enabled: boolean };\n\nexport class RetryController {\n\tprivate _attempt = 0;\n\tprivate _abortController: AbortController | undefined;\n\tprivate readonly agent: RetryAgent;\n\tprivate readonly getPolicy: () => RetryControllerPolicy;\n\tprivate readonly events: RetryEvents;\n\tprivate readonly getContextWindow: () => number;\n\n\tconstructor(\n\t\tagent: RetryAgent,\n\t\tgetPolicy: () => RetryControllerPolicy,\n\t\tevents: RetryEvents,\n\t\tgetContextWindow: () => number,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.getPolicy = getPolicy;\n\t\tthis.events = events;\n\t\tthis.getContextWindow = getContextWindow;\n\t}\n\n\t/** Completed retry attempts for the current run (0 when not retrying). */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** True from the instant onRetryStart fires until the backoff sleep resolves or is aborted. */\n\tget isRetrying(): boolean {\n\t\treturn this._abortController !== undefined;\n\t}\n\n\t/** Clear the attempt counter — the host calls this after a successful turn or a final failure. */\n\treset(): void {\n\t\tthis._attempt = 0;\n\t}\n\n\t/** Cancel an in-progress backoff; the pending prepareRetry resolves false. */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t}\n\n\t/**\n\t * Classify `message`; if it is retryable and attempts remain, drop the trailing assistant\n\t * error from agent state, emit the start event, and wait out the backoff (abortable).\n\t * @returns true if the caller should continue the agent, false otherwise.\n\t */\n\tasync prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst policy = this.getPolicy();\n\t\tif (!policy.enabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// The classifier is the single source of the retry verdict: context overflow (host-computed\n\t\t// from the live window) routes to compaction, billing/auth are terminal, transient failures retry.\n\t\tconst classified = classifyFailure({\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tcontextOverflow: isContextOverflow(message, this.getContextWindow()),\n\t\t\tprovider: message.provider,\n\t\t});\n\t\tif (!classified.retryable) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis._attempt++;\n\t\tif (this._attempt > policy.maxAttempts) {\n\t\t\t// Preserve the completed attempt count so the host can emit the final failure.\n\t\t\tthis._attempt--;\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = computeRetryDelayMs(policy, this._attempt);\n\n\t\t// The retry window counts as active work from the instant listeners hear about it:\n\t\t// isRetrying must already be true inside onRetryStart handlers so prompts arriving there\n\t\t// queue as steering instead of racing the retry continuation.\n\t\tthis._abortController = new AbortController();\n\n\t\tthis.events.onRetryStart({\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: policy.maxAttempts,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove the trailing assistant error from live agent state (the host session keeps it in history).\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleepAbortable(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted mid-backoff: report the cancellation and reset so the next turn starts clean.\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis.events.onRetryEnd({ success: false, attempt, finalError: \"Retry cancelled\" });\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._abortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"retry-controller.d.ts","sourceRoot":"","sources":["../../src/reliability/retry-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAuB,KAAK,WAAW,EAAkB,MAAM,YAAY,CAAC;AAEnF,0FAA0F;AAC1F,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;CAC7C;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC3B,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED,iGAAiG;AACjG,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvE,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAe;IAEhD,YACC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,qBAAqB,EACtC,MAAM,EAAE,WAAW,EACnB,gBAAgB,EAAE,MAAM,MAAM,EAM9B;IAED,0EAA0E;IAC1E,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+FAA+F;IAC/F,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,oGAAkG;IAClG,KAAK,IAAI,IAAI,CAEZ;IAED,8EAA8E;IAC9E,KAAK,IAAI,IAAI,CAEZ;IAED;;;;OAIG;IACG,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAyD9D;CACD","sourcesContent":["/**\n * Host-agnostic auto-retry driver.\n *\n * Owns the retry attempt counter and the abortable backoff for one agent run. It reads the\n * failure verdict from {@link classifyFailure} (fed a host-computed context-overflow flag), so\n * billing/auth are terminal and overflow routes to compaction — never to a pointless retry.\n * The controller only ever touches `agent.state.messages` (it drops the trailing assistant error\n * before retrying); durable history stays the host session's responsibility.\n *\n * Ported from AgentSession._prepareRetry / _isRetryableError so the exact event ordering and\n * exhaustion semantics carry over: the retry window is marked active (isRetrying) before the\n * start event fires, so prompts arriving inside start handlers queue as steering instead of\n * racing the retry continuation.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\nimport { classifyFailure } from \"./classifier.ts\";\nimport { computeRetryDelayMs, type RetryPolicy, sleepAbortable } from \"./retry.ts\";\n\n/** The slice of an Agent the retry driver reads and mutates: just the live transcript. */\nexport interface RetryAgent {\n\treadonly state: { messages: AgentMessage[] };\n}\n\nexport interface RetryStartInfo {\n\tattempt: number;\n\tmaxAttempts: number;\n\tdelayMs: number;\n\terrorMessage: string;\n}\n\nexport interface RetryEndInfo {\n\tsuccess: boolean;\n\tattempt: number;\n\tfinalError?: string;\n}\n\nexport interface RetryEvents {\n\tonRetryStart(info: RetryStartInfo): void;\n\tonRetryEnd(info: RetryEndInfo): void;\n}\n\n/** Runtime retry policy: the backoff shape plus the on/off switch the host resolves per call. */\nexport type RetryControllerPolicy = RetryPolicy & { enabled: boolean };\n\nexport class RetryController {\n\tprivate _attempt = 0;\n\tprivate _abortController: AbortController | undefined;\n\tprivate readonly agent: RetryAgent;\n\tprivate readonly getPolicy: () => RetryControllerPolicy;\n\tprivate readonly events: RetryEvents;\n\tprivate readonly getContextWindow: () => number;\n\n\tconstructor(\n\t\tagent: RetryAgent,\n\t\tgetPolicy: () => RetryControllerPolicy,\n\t\tevents: RetryEvents,\n\t\tgetContextWindow: () => number,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.getPolicy = getPolicy;\n\t\tthis.events = events;\n\t\tthis.getContextWindow = getContextWindow;\n\t}\n\n\t/** Completed retry attempts for the current run (0 when not retrying). */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** True from the instant onRetryStart fires until the backoff sleep resolves or is aborted. */\n\tget isRetrying(): boolean {\n\t\treturn this._abortController !== undefined;\n\t}\n\n\t/** Clear the attempt counter — the host calls this after a successful turn or a final failure. */\n\treset(): void {\n\t\tthis._attempt = 0;\n\t}\n\n\t/** Cancel an in-progress backoff; the pending prepareRetry resolves false. */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t}\n\n\t/**\n\t * Classify `message`; if it is retryable and attempts remain, drop the trailing assistant\n\t * error from agent state, emit the start event, and wait out the backoff (abortable).\n\t * @returns true if the caller should continue the agent, false otherwise.\n\t */\n\tasync prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst policy = this.getPolicy();\n\t\tif (!policy.enabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// The classifier is the single source of the retry verdict: context overflow (host-computed\n\t\t// from the live window) routes to compaction, billing/auth are terminal, transient failures retry.\n\t\tconst classified = classifyFailure({\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tcontextOverflow: isContextOverflow(message, this.getContextWindow()),\n\t\t\tprovider: message.provider,\n\t\t});\n\t\tif (!classified.retryable) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis._attempt++;\n\t\tif (this._attempt > policy.maxAttempts) {\n\t\t\t// Preserve the completed attempt count so the host can emit the final failure.\n\t\t\tthis._attempt--;\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = computeRetryDelayMs(policy, this._attempt, { retryAfterMs: classified.retryAfterMs });\n\n\t\t// The retry window counts as active work from the instant listeners hear about it:\n\t\t// isRetrying must already be true inside onRetryStart handlers so prompts arriving there\n\t\t// queue as steering instead of racing the retry continuation.\n\t\tthis._abortController = new AbortController();\n\n\t\tthis.events.onRetryStart({\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: policy.maxAttempts,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove the trailing assistant error from live agent state (the host session keeps it in history).\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleepAbortable(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted mid-backoff: report the cancellation and reset so the next turn starts clean.\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis.events.onRetryEnd({ success: false, attempt, finalError: \"Retry cancelled\" });\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._abortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
@@ -70,7 +70,7 @@ export class RetryController {
70
70
  this._attempt--;
71
71
  return false;
72
72
  }
73
- const delayMs = computeRetryDelayMs(policy, this._attempt);
73
+ const delayMs = computeRetryDelayMs(policy, this._attempt, { retryAfterMs: classified.retryAfterMs });
74
74
  // The retry window counts as active work from the instant listeners hear about it:
75
75
  // isRetrying must already be true inside onRetryStart handlers so prompts arriving there
76
76
  // queue as steering instead of racing the retry continuation.
@@ -1 +1 @@
1
- {"version":3,"file":"retry-controller.js","sourceRoot":"","sources":["../../src/reliability/retry-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Host-agnostic auto-retry driver.\n *\n * Owns the retry attempt counter and the abortable backoff for one agent run. It reads the\n * failure verdict from {@link classifyFailure} (fed a host-computed context-overflow flag), so\n * billing/auth are terminal and overflow routes to compaction — never to a pointless retry.\n * The controller only ever touches `agent.state.messages` (it drops the trailing assistant error\n * before retrying); durable history stays the host session's responsibility.\n *\n * Ported from AgentSession._prepareRetry / _isRetryableError so the exact event ordering and\n * exhaustion semantics carry over: the retry window is marked active (isRetrying) before the\n * start event fires, so prompts arriving inside start handlers queue as steering instead of\n * racing the retry continuation.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\nimport { classifyFailure } from \"./classifier.ts\";\nimport { computeRetryDelayMs, type RetryPolicy, sleepAbortable } from \"./retry.ts\";\n\n/** The slice of an Agent the retry driver reads and mutates: just the live transcript. */\nexport interface RetryAgent {\n\treadonly state: { messages: AgentMessage[] };\n}\n\nexport interface RetryStartInfo {\n\tattempt: number;\n\tmaxAttempts: number;\n\tdelayMs: number;\n\terrorMessage: string;\n}\n\nexport interface RetryEndInfo {\n\tsuccess: boolean;\n\tattempt: number;\n\tfinalError?: string;\n}\n\nexport interface RetryEvents {\n\tonRetryStart(info: RetryStartInfo): void;\n\tonRetryEnd(info: RetryEndInfo): void;\n}\n\n/** Runtime retry policy: the backoff shape plus the on/off switch the host resolves per call. */\nexport type RetryControllerPolicy = RetryPolicy & { enabled: boolean };\n\nexport class RetryController {\n\tprivate _attempt = 0;\n\tprivate _abortController: AbortController | undefined;\n\tprivate readonly agent: RetryAgent;\n\tprivate readonly getPolicy: () => RetryControllerPolicy;\n\tprivate readonly events: RetryEvents;\n\tprivate readonly getContextWindow: () => number;\n\n\tconstructor(\n\t\tagent: RetryAgent,\n\t\tgetPolicy: () => RetryControllerPolicy,\n\t\tevents: RetryEvents,\n\t\tgetContextWindow: () => number,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.getPolicy = getPolicy;\n\t\tthis.events = events;\n\t\tthis.getContextWindow = getContextWindow;\n\t}\n\n\t/** Completed retry attempts for the current run (0 when not retrying). */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** True from the instant onRetryStart fires until the backoff sleep resolves or is aborted. */\n\tget isRetrying(): boolean {\n\t\treturn this._abortController !== undefined;\n\t}\n\n\t/** Clear the attempt counter — the host calls this after a successful turn or a final failure. */\n\treset(): void {\n\t\tthis._attempt = 0;\n\t}\n\n\t/** Cancel an in-progress backoff; the pending prepareRetry resolves false. */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t}\n\n\t/**\n\t * Classify `message`; if it is retryable and attempts remain, drop the trailing assistant\n\t * error from agent state, emit the start event, and wait out the backoff (abortable).\n\t * @returns true if the caller should continue the agent, false otherwise.\n\t */\n\tasync prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst policy = this.getPolicy();\n\t\tif (!policy.enabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// The classifier is the single source of the retry verdict: context overflow (host-computed\n\t\t// from the live window) routes to compaction, billing/auth are terminal, transient failures retry.\n\t\tconst classified = classifyFailure({\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tcontextOverflow: isContextOverflow(message, this.getContextWindow()),\n\t\t\tprovider: message.provider,\n\t\t});\n\t\tif (!classified.retryable) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis._attempt++;\n\t\tif (this._attempt > policy.maxAttempts) {\n\t\t\t// Preserve the completed attempt count so the host can emit the final failure.\n\t\t\tthis._attempt--;\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = computeRetryDelayMs(policy, this._attempt);\n\n\t\t// The retry window counts as active work from the instant listeners hear about it:\n\t\t// isRetrying must already be true inside onRetryStart handlers so prompts arriving there\n\t\t// queue as steering instead of racing the retry continuation.\n\t\tthis._abortController = new AbortController();\n\n\t\tthis.events.onRetryStart({\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: policy.maxAttempts,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove the trailing assistant error from live agent state (the host session keeps it in history).\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleepAbortable(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted mid-backoff: report the cancellation and reset so the next turn starts clean.\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis.events.onRetryEnd({ success: false, attempt, finalError: \"Retry cancelled\" });\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._abortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"retry-controller.js","sourceRoot":"","sources":["../../src/reliability/retry-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Host-agnostic auto-retry driver.\n *\n * Owns the retry attempt counter and the abortable backoff for one agent run. It reads the\n * failure verdict from {@link classifyFailure} (fed a host-computed context-overflow flag), so\n * billing/auth are terminal and overflow routes to compaction — never to a pointless retry.\n * The controller only ever touches `agent.state.messages` (it drops the trailing assistant error\n * before retrying); durable history stays the host session's responsibility.\n *\n * Ported from AgentSession._prepareRetry / _isRetryableError so the exact event ordering and\n * exhaustion semantics carry over: the retry window is marked active (isRetrying) before the\n * start event fires, so prompts arriving inside start handlers queue as steering instead of\n * racing the retry continuation.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\nimport { classifyFailure } from \"./classifier.ts\";\nimport { computeRetryDelayMs, type RetryPolicy, sleepAbortable } from \"./retry.ts\";\n\n/** The slice of an Agent the retry driver reads and mutates: just the live transcript. */\nexport interface RetryAgent {\n\treadonly state: { messages: AgentMessage[] };\n}\n\nexport interface RetryStartInfo {\n\tattempt: number;\n\tmaxAttempts: number;\n\tdelayMs: number;\n\terrorMessage: string;\n}\n\nexport interface RetryEndInfo {\n\tsuccess: boolean;\n\tattempt: number;\n\tfinalError?: string;\n}\n\nexport interface RetryEvents {\n\tonRetryStart(info: RetryStartInfo): void;\n\tonRetryEnd(info: RetryEndInfo): void;\n}\n\n/** Runtime retry policy: the backoff shape plus the on/off switch the host resolves per call. */\nexport type RetryControllerPolicy = RetryPolicy & { enabled: boolean };\n\nexport class RetryController {\n\tprivate _attempt = 0;\n\tprivate _abortController: AbortController | undefined;\n\tprivate readonly agent: RetryAgent;\n\tprivate readonly getPolicy: () => RetryControllerPolicy;\n\tprivate readonly events: RetryEvents;\n\tprivate readonly getContextWindow: () => number;\n\n\tconstructor(\n\t\tagent: RetryAgent,\n\t\tgetPolicy: () => RetryControllerPolicy,\n\t\tevents: RetryEvents,\n\t\tgetContextWindow: () => number,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.getPolicy = getPolicy;\n\t\tthis.events = events;\n\t\tthis.getContextWindow = getContextWindow;\n\t}\n\n\t/** Completed retry attempts for the current run (0 when not retrying). */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** True from the instant onRetryStart fires until the backoff sleep resolves or is aborted. */\n\tget isRetrying(): boolean {\n\t\treturn this._abortController !== undefined;\n\t}\n\n\t/** Clear the attempt counter — the host calls this after a successful turn or a final failure. */\n\treset(): void {\n\t\tthis._attempt = 0;\n\t}\n\n\t/** Cancel an in-progress backoff; the pending prepareRetry resolves false. */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t}\n\n\t/**\n\t * Classify `message`; if it is retryable and attempts remain, drop the trailing assistant\n\t * error from agent state, emit the start event, and wait out the backoff (abortable).\n\t * @returns true if the caller should continue the agent, false otherwise.\n\t */\n\tasync prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst policy = this.getPolicy();\n\t\tif (!policy.enabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// The classifier is the single source of the retry verdict: context overflow (host-computed\n\t\t// from the live window) routes to compaction, billing/auth are terminal, transient failures retry.\n\t\tconst classified = classifyFailure({\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tcontextOverflow: isContextOverflow(message, this.getContextWindow()),\n\t\t\tprovider: message.provider,\n\t\t});\n\t\tif (!classified.retryable) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis._attempt++;\n\t\tif (this._attempt > policy.maxAttempts) {\n\t\t\t// Preserve the completed attempt count so the host can emit the final failure.\n\t\t\tthis._attempt--;\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = computeRetryDelayMs(policy, this._attempt, { retryAfterMs: classified.retryAfterMs });\n\n\t\t// The retry window counts as active work from the instant listeners hear about it:\n\t\t// isRetrying must already be true inside onRetryStart handlers so prompts arriving there\n\t\t// queue as steering instead of racing the retry continuation.\n\t\tthis._abortController = new AbortController();\n\n\t\tthis.events.onRetryStart({\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: policy.maxAttempts,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove the trailing assistant error from live agent state (the host session keeps it in history).\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleepAbortable(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted mid-backoff: report the cancellation and reset so the next turn starts clean.\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis.events.onRetryEnd({ success: false, attempt, finalError: \"Retry cancelled\" });\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._abortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caupulican/pi-agent-core",
3
- "version": "0.81.3",
3
+ "version": "0.81.4",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run clean && npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "@caupulican/pi-ai": "^0.81.3",
36
+ "@caupulican/pi-ai": "^0.81.4",
37
37
  "ignore": "7.0.5",
38
38
  "typebox": "1.1.38",
39
39
  "yaml": "2.9.0"