@bastani/atomic 0.8.29 → 0.8.30-alpha.2

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +4 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +4 -0
  5. package/dist/builtin/intercom/package.json +2 -2
  6. package/dist/builtin/mcp/CHANGELOG.md +4 -0
  7. package/dist/builtin/mcp/package.json +3 -3
  8. package/dist/builtin/subagents/CHANGELOG.md +9 -0
  9. package/dist/builtin/subagents/README.md +10 -30
  10. package/dist/builtin/subagents/package.json +4 -4
  11. package/dist/builtin/subagents/skills/subagent/SKILL.md +5 -11
  12. package/dist/builtin/subagents/src/agents/agent-management.ts +0 -5
  13. package/dist/builtin/subagents/src/agents/agent-serializer.ts +7 -3
  14. package/dist/builtin/subagents/src/agents/agents.ts +4 -29
  15. package/dist/builtin/subagents/src/agents/chain-serializer.ts +27 -25
  16. package/dist/builtin/subagents/src/extension/schemas.ts +0 -75
  17. package/dist/builtin/subagents/src/runs/background/async-execution.ts +0 -29
  18. package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -2
  19. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +134 -239
  20. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +1 -52
  21. package/dist/builtin/subagents/src/runs/foreground/execution.ts +103 -94
  22. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +0 -10
  23. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +16 -8
  24. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +0 -1
  25. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +0 -3
  26. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +67 -2
  27. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +6 -20
  28. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -1
  29. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +2 -6
  30. package/dist/builtin/subagents/src/shared/settings.ts +1 -4
  31. package/dist/builtin/subagents/src/shared/types.ts +1 -156
  32. package/dist/builtin/subagents/src/tui/render.ts +0 -1
  33. package/dist/builtin/web-access/CHANGELOG.md +4 -0
  34. package/dist/builtin/web-access/package.json +2 -2
  35. package/dist/builtin/workflows/CHANGELOG.md +11 -0
  36. package/dist/builtin/workflows/README.md +2 -2
  37. package/dist/builtin/workflows/package.json +2 -2
  38. package/dist/builtin/workflows/src/extension/index.ts +8 -1
  39. package/dist/builtin/workflows/src/extension/wiring.ts +66 -10
  40. package/dist/builtin/workflows/src/runs/foreground/executor.ts +70 -19
  41. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +98 -14
  42. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +0 -1
  43. package/dist/builtin/workflows/src/shared/persistence-restore.ts +4 -0
  44. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  45. package/dist/builtin/workflows/src/shared/store.ts +2 -0
  46. package/dist/config.d.ts.map +1 -1
  47. package/dist/config.js +18 -4
  48. package/dist/config.js.map +1 -1
  49. package/dist/core/agent-session.d.ts +2 -2
  50. package/dist/core/agent-session.d.ts.map +1 -1
  51. package/dist/core/agent-session.js +21 -9
  52. package/dist/core/agent-session.js.map +1 -1
  53. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  54. package/dist/core/compaction/branch-summarization.js +2 -2
  55. package/dist/core/compaction/branch-summarization.js.map +1 -1
  56. package/dist/core/compaction/compaction.d.ts +6 -0
  57. package/dist/core/compaction/compaction.d.ts.map +1 -1
  58. package/dist/core/compaction/compaction.js +2 -0
  59. package/dist/core/compaction/compaction.js.map +1 -1
  60. package/dist/core/compaction/context-compaction.d.ts +43 -16
  61. package/dist/core/compaction/context-compaction.d.ts.map +1 -1
  62. package/dist/core/compaction/context-compaction.js +561 -189
  63. package/dist/core/compaction/context-compaction.js.map +1 -1
  64. package/dist/core/extensions/loader.d.ts +4 -2
  65. package/dist/core/extensions/loader.d.ts.map +1 -1
  66. package/dist/core/extensions/loader.js +11 -7
  67. package/dist/core/extensions/loader.js.map +1 -1
  68. package/dist/core/extensions/types.d.ts +14 -3
  69. package/dist/core/extensions/types.d.ts.map +1 -1
  70. package/dist/core/extensions/types.js.map +1 -1
  71. package/dist/core/model-registry.d.ts.map +1 -1
  72. package/dist/core/model-registry.js +2 -1
  73. package/dist/core/model-registry.js.map +1 -1
  74. package/dist/core/package-manager.d.ts +1 -1
  75. package/dist/core/package-manager.d.ts.map +1 -1
  76. package/dist/core/package-manager.js +52 -18
  77. package/dist/core/package-manager.js.map +1 -1
  78. package/dist/core/resource-loader.d.ts +20 -0
  79. package/dist/core/resource-loader.d.ts.map +1 -1
  80. package/dist/core/resource-loader.js +89 -24
  81. package/dist/core/resource-loader.js.map +1 -1
  82. package/dist/core/session-manager.d.ts +14 -1
  83. package/dist/core/session-manager.d.ts.map +1 -1
  84. package/dist/core/session-manager.js +145 -3
  85. package/dist/core/session-manager.js.map +1 -1
  86. package/dist/core/settings-manager.d.ts +9 -0
  87. package/dist/core/settings-manager.d.ts.map +1 -1
  88. package/dist/core/settings-manager.js +16 -0
  89. package/dist/core/settings-manager.js.map +1 -1
  90. package/dist/core/thinking-blocks.d.ts +7 -0
  91. package/dist/core/thinking-blocks.d.ts.map +1 -0
  92. package/dist/core/thinking-blocks.js +16 -0
  93. package/dist/core/thinking-blocks.js.map +1 -0
  94. package/dist/core/tools/bash.d.ts.map +1 -1
  95. package/dist/core/tools/bash.js +4 -0
  96. package/dist/core/tools/bash.js.map +1 -1
  97. package/dist/index.d.ts +2 -2
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/index.js.map +1 -1
  101. package/dist/main.d.ts.map +1 -1
  102. package/dist/main.js +30 -0
  103. package/dist/main.js.map +1 -1
  104. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/tree-selector.js +87 -12
  106. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  107. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  108. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.js +37 -18
  110. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/dist/modes/interactive/theme/theme.d.ts +24 -1
  112. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  113. package/dist/modes/interactive/theme/theme.js +58 -13
  114. package/dist/modes/interactive/theme/theme.js.map +1 -1
  115. package/dist/utils/child-process.d.ts +9 -4
  116. package/dist/utils/child-process.d.ts.map +1 -1
  117. package/dist/utils/child-process.js +42 -10
  118. package/dist/utils/child-process.js.map +1 -1
  119. package/dist/utils/version-check.d.ts.map +1 -1
  120. package/dist/utils/version-check.js +4 -27
  121. package/dist/utils/version-check.js.map +1 -1
  122. package/docs/compaction.md +470 -51
  123. package/docs/containerization.md +37 -37
  124. package/docs/extensions.md +23 -14
  125. package/docs/models.md +6 -4
  126. package/docs/packages.md +2 -0
  127. package/docs/providers.md +1 -1
  128. package/docs/subagents.md +11 -4
  129. package/docs/workflows.md +4 -2
  130. package/examples/extensions/README.md +2 -2
  131. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  132. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  133. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  134. package/examples/extensions/gondolin/package-lock.json +2 -2
  135. package/examples/extensions/gondolin/package.json +1 -1
  136. package/examples/extensions/question.ts +39 -18
  137. package/examples/extensions/questionnaire.ts +49 -28
  138. package/examples/extensions/sandbox/package-lock.json +2 -2
  139. package/examples/extensions/sandbox/package.json +1 -1
  140. package/examples/extensions/with-deps/package-lock.json +2 -2
  141. package/examples/extensions/with-deps/package.json +1 -1
  142. package/package.json +7 -5
  143. package/dist/builtin/subagents/src/runs/shared/acceptance.ts +0 -612
  144. package/dist/builtin/subagents/src/runs/shared/completion-guard.ts +0 -147
@@ -1,24 +1,28 @@
1
1
  import { Agent } from "@earendil-works/pi-agent-core";
2
- import { createAssistantMessageEventStream, isContextOverflow, streamSimple, StringEnum, } from "@earendil-works/pi-ai";
2
+ import { createAssistantMessageEventStream, isContextOverflow, streamSimple, StringEnum } from "@earendil-works/pi-ai";
3
3
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { Type } from "typebox";
7
7
  import { createBranchSummaryMessage, createCustomMessage } from "../messages.js";
8
- import { buildContextDeletionFilteredPath, buildContextDeletionFilters, } from "../session-manager.js";
8
+ import { isAssistantThinkingBlockType, messageHasAssistantThinkingContentBlock, } from "../thinking-blocks.js";
9
+ import { buildContextDeletionFilteredPath, buildEffectiveContextDeletionFilters, } from "../session-manager.js";
9
10
  import { estimateTokens } from "./compaction.js";
10
11
  export const CONTEXT_COMPACTION_PROMPT_VERSION = 1;
11
12
  const CONTEXT_DELETE_TOOL_NAME = "context_delete";
12
13
  const CONTEXT_GREP_DELETE_TOOL_NAME = "context_grep_delete";
13
14
  const CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME = "context_search_transcript";
14
15
  const CONTEXT_READ_ENTRY_TOOL_NAME = "context_read_entry";
15
- export const CONTEXT_COMPACTION_MAX_TURNS = 50;
16
+ const CONTEXT_COMPACTION_BUDGET_TOOL_NAME = "context_compaction_budget";
17
+ export const CONTEXT_COMPACTION_DEFAULT_COMPRESSION_RATIO = 0.5;
18
+ export const CONTEXT_COMPACTION_TARGET_REDUCTION_PERCENT = 50;
19
+ export const CONTEXT_COMPACTION_DEFAULT_PRESERVE_RECENT = 2;
20
+ export const CONTEXT_COMPACTION_AUTO_QUERY = "auto-detected";
16
21
  const CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES = 50;
17
22
  const CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS = 512;
18
23
  const CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS = 250_000;
19
24
  const CONTEXT_MANIFEST_MAX_ENTRIES = 80;
20
25
  const CONTEXT_MANIFEST_PREVIEW_CHARS = 240;
21
- const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
22
26
  const CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS = 4000;
23
27
  const CONTEXT_READ_ENTRY_MAX_CHARS = 12_000;
24
28
  const CONTEXT_SEARCH_DEFAULT_MAX_MATCHES = 20;
@@ -35,7 +39,9 @@ const ContextDeleteToolParameters = Type.Object({
35
39
  minimum: 0,
36
40
  description: "Required when kind is content_block; omit when kind is entry.",
37
41
  })),
38
- }, { additionalProperties: false }), { description: "Deletion targets only. Protected entries and recent active context must not be included." }),
42
+ }, { additionalProperties: false }), {
43
+ description: "ID-only deletion targets. Include only kind, entryId, and blockIndex when needed; do not include transcript text, block contents, summaries, or replacement content. Invalid targets are rejected by the tool with correction guidance.",
44
+ }),
39
45
  }, { additionalProperties: false });
40
46
  const ContextGrepDeleteToolParameters = Type.Object({
41
47
  pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to match in transcript text." }),
@@ -47,7 +53,7 @@ const ContextGrepDeleteToolParameters = Type.Object({
47
53
  maxMatches: Type.Optional(Type.Integer({
48
54
  minimum: 1,
49
55
  maximum: 200,
50
- description: "Safety cap. If more unprotected, not-yet-deleted candidate targets are found, no deletions are applied. Defaults to 50.",
56
+ description: "Per-call safety cap. If more not-yet-deleted candidate targets are found in this tool call, no deletions are applied. Defaults to 50. This is not a cumulative compaction cap; call the tool again for additional batches.",
51
57
  })),
52
58
  expectedMatchCount: Type.Optional(Type.Integer({
53
59
  minimum: 0,
@@ -78,6 +84,7 @@ const ContextReadEntryToolParameters = Type.Object({
78
84
  description: "Maximum characters to return. Defaults to 4000; keep reads small to avoid overflowing context.",
79
85
  })),
80
86
  }, { additionalProperties: false });
87
+ const ContextCompactionBudgetToolParameters = Type.Object({}, { additionalProperties: false });
81
88
  const CONTEXT_DELETE_TOOL = {
82
89
  name: CONTEXT_DELETE_TOOL_NAME,
83
90
  description: "Record context compaction deletion targets directly against the transcript.",
@@ -98,16 +105,36 @@ const CONTEXT_READ_ENTRY_TOOL = {
98
105
  description: "Read a small slice of one transcript entry or content block from the full transcript working copy.",
99
106
  parameters: ContextReadEntryToolParameters,
100
107
  };
108
+ const CONTEXT_COMPACTION_BUDGET_TOOL = {
109
+ name: CONTEXT_COMPACTION_BUDGET_TOOL_NAME,
110
+ description: "Report current context-window fullness and reduction progress for the selected deletion targets without mutating deletion state.",
111
+ parameters: ContextCompactionBudgetToolParameters,
112
+ };
101
113
  const CONTEXT_COMPACTION_SYSTEM_PROMPT = `You are a context compaction assistant.
102
114
 
103
115
  Your task is to read relevant parts of a conversation between a user and an AI assistant provided via a transcript file, then run a series of tools to apply deletion-only verbatim compaction using the exact context_delete or context_grep_delete format specified.`;
104
- const CONTEXT_COMPACTION_FIXED_PROMPT = `Reference the provided transcript file transcript and use your search/read tools for small inspections, then use context_delete or context_grep_delete for deletions.
116
+ function contextCompactionFixedPrompt(parameters) {
117
+ const targetLabel = contextCompactionTargetLabel(parameters);
118
+ return `Reference the provided transcript file transcript and use your search/read tools for small inspections, then use context_delete or context_grep_delete for deletions.
119
+
120
+ Compaction records deletion targets, not replacement content.
121
+ For context_delete, use id-only targets: stable entryId values and optional blockIndex values only.
122
+ For context_grep_delete, use a concise literal or regex pattern to select matching entries or blocks; do not paste full transcript entries or content-block bodies.
123
+ Do not summarize, paraphrase, or generate replacement context; those are not accepted compaction outputs.
124
+ Do not mutate retained transcript objects or content.
125
+ Deletion tool calls are the compaction action.
105
126
 
106
- You MUST NOT summarize.
107
- You MUST NOT paraphrase.
108
- You MUST NOT generate replacement context.
109
- You MUST NOT mutate retained transcript objects or content.
110
- Deletion tool calls are the compaction action; record only deletion targets by stable ID.
127
+ Strategy:
128
+ - Start by calling context_compaction_budget to see how much of the context window is full and how much reduction is needed.
129
+ - Spend a few turns exploring with search/read tools to gain high confidence of candidate blocks to remove.
130
+ - Prefer high-confidence exploit actions after that: delete obvious low-value entries via context_grep_delete or context_delete.
131
+ - Use grep deletion for repeated low-value patterns.
132
+ - Use exact id deletion for inspected one-off stale entries.
133
+ - Check context_compaction_budget after deletion batches to track progress.
134
+ - Strict requirement: reduce current context by at least ${targetLabel} before finishing. This is a hard completion gate, not a loose goal.
135
+ - Do not send a final plain-text completion message until context_compaction_budget reports at least ${targetLabel} currentReductionPercent.
136
+ - If the strict ${targetLabel} reduction is not met yet, continue removing low-value message entries or content blocks with context_delete/context_grep_delete.
137
+ - Use the focus query to preserve relevant context: ${JSON.stringify(parameters.query)}.
111
138
 
112
139
  What Gets Deleted:
113
140
  - Redundant tool outputs: file reads already acted on, grep/search results already processed, passing test output no longer needed.
@@ -117,11 +144,14 @@ What Gets Deleted:
117
144
 
118
145
  What Survives:
119
146
  - Active file paths and line numbers: Any reference the agent might need to navigate.
120
- - Current error messages: Unresolved bugss and their exact text.
147
+ - Current error messages: Unresolved bugs and their exact text.
121
148
  - Reasoning decisions: Why the agent chose approach A over B. An agent's chain of thought (why it chose this file, what pattern it noticed, what fix it decided on) carries more information-per-token than the raw grep output or file content that informed those decisions.
122
149
  - Recent tool calls and their results: The last 3-5 operations.
123
150
  - User instructions: The original task and any clarifications.
124
151
 
152
+ Conditionally Deleted:
153
+ - Old Reasoning decisions: If there is nothing else to remove and the target reduction is not met, you can remove reasoning steps, EXCEPT do not delete any content block from the latest assistant message when that message contains thinking or redacted_thinking blocks.
154
+
125
155
  <output_format>
126
156
  Call the context_delete tool one or more times with deletion targets in this shape:
127
157
  { "deletions": [{ "kind": "entry", "entryId": "..." }] }
@@ -131,12 +161,13 @@ For content-block deletions, use:
131
161
 
132
162
  The tool applies and validates deletion targets immediately. You can continue calling it for additional deletions if useful.
133
163
 
134
- For guarded bulk deletion by text match, call context_grep_delete with a literal pattern or regex. It skips protected context, enforces maxMatches and expectedMatchCount, and validates through the same tool-call/tool-result safety rules.
164
+ For guarded bulk deletion by text match, call context_grep_delete with a literal pattern or regex. It removes valid matching context, silently ignores candidates that validation does not allow so they are not counted as removals, enforces a per-call maxMatches safety cap and optional expectedMatchCount, and validates through the same tool-call/tool-result safety rules. maxMatches only limits one tool call; there is no cumulative cap across corrected or repeated deletion calls.
135
165
 
136
166
  The full transcript is available as a JSONL file path in the prompt, but do NOT try to load the whole file into context. Use context_search_transcript to find candidate entry IDs and context_read_entry to read only small slices (for example maxChars 1000-4000) before deleting.
137
167
 
138
- When you are done, reply with a brief plain-text completion message. Do not write deletion JSON or deletion target IDs outside tool calls.
168
+ When the strict ${targetLabel} reduction requirement is met, reply with a brief plain-text completion message. Do not include deletion target IDs outside tool calls.
139
169
  </output_format>`;
170
+ }
140
171
  function getMessageFromEntry(entry) {
141
172
  if (entry.type === "message") {
142
173
  return entry.message;
@@ -190,6 +221,11 @@ function textFromContentBlock(block) {
190
221
  return "[image]";
191
222
  return JSON.stringify(record);
192
223
  }
224
+ function assistantEntryHasThinkingContentBlock(entry) {
225
+ return (entry.role === "assistant" &&
226
+ (entry.contentBlocks.some((block) => isAssistantThinkingBlockType(block.type)) ||
227
+ messageHasAssistantThinkingContentBlock(entry.message)));
228
+ }
193
229
  const IMAGE_BLOCK_CHAR_ESTIMATE = 4800;
194
230
  const IMAGE_BLOCK_TOKEN_ESTIMATE = Math.ceil(IMAGE_BLOCK_CHAR_ESTIMATE / 4);
195
231
  function estimateTextTokens(text) {
@@ -221,15 +257,17 @@ function contentBlocksForEntry(entryId, message, protectedEntry, existingDeleted
221
257
  return [];
222
258
  return content
223
259
  .map((block, blockIndex) => {
224
- if (existingDeletedBlocks?.has(blockIndex))
260
+ if (existingDeletedBlocks?.has(blockIndex)) {
225
261
  return undefined;
262
+ }
263
+ const type = block && typeof block === "object" && typeof block.type === "string"
264
+ ? (block.type)
265
+ : "unknown";
226
266
  const text = textFromContentBlock(block);
227
267
  return {
228
268
  entryId,
229
269
  blockIndex,
230
- type: block && typeof block === "object" && typeof block.type === "string"
231
- ? (block.type)
232
- : "unknown",
270
+ type,
233
271
  text,
234
272
  tokenEstimate: estimateContentBlockTokens(block, text),
235
273
  protected: protectedEntry,
@@ -272,6 +310,51 @@ function hasToolResultError(message) {
272
310
  function hasFailedBashExecution(message) {
273
311
  return message.role === "bashExecution" && typeof message.exitCode === "number" && message.exitCode !== 0;
274
312
  }
313
+ const CONTEXT_COMPACTION_QUERY_MAX_CHARS = 1000;
314
+ function normalizeCompressionRatio(value) {
315
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value < 1
316
+ ? value
317
+ : CONTEXT_COMPACTION_DEFAULT_COMPRESSION_RATIO;
318
+ }
319
+ function normalizePreserveRecent(value) {
320
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : CONTEXT_COMPACTION_DEFAULT_PRESERVE_RECENT;
321
+ }
322
+ function normalizeQuery(value, fallbackQuery) {
323
+ const query = value?.trim() || fallbackQuery.trim() || CONTEXT_COMPACTION_AUTO_QUERY;
324
+ return query.length > CONTEXT_COMPACTION_QUERY_MAX_CHARS
325
+ ? `${query.slice(0, CONTEXT_COMPACTION_QUERY_MAX_CHARS)}\n[... ${query.length - CONTEXT_COMPACTION_QUERY_MAX_CHARS} more characters omitted from compaction query]`
326
+ : query;
327
+ }
328
+ export function autoDetectContextCompactionQuery(pathEntries) {
329
+ for (let index = pathEntries.length - 1; index >= 0; index--) {
330
+ const entry = pathEntries[index];
331
+ if (entry.type === "context_compaction")
332
+ continue;
333
+ const message = getContextEligibleMessageFromEntry(entry);
334
+ if (!message || message.role !== "user")
335
+ continue;
336
+ const text = messageText(message).trim();
337
+ if (text.length > 0)
338
+ return normalizeQuery(text, CONTEXT_COMPACTION_AUTO_QUERY);
339
+ }
340
+ return CONTEXT_COMPACTION_AUTO_QUERY;
341
+ }
342
+ export function normalizeContextCompactionParameters(input = {}, fallbackQuery = CONTEXT_COMPACTION_AUTO_QUERY) {
343
+ return {
344
+ compression_ratio: normalizeCompressionRatio(input.compression_ratio),
345
+ preserve_recent: normalizePreserveRecent(input.preserve_recent),
346
+ query: normalizeQuery(input.query, fallbackQuery),
347
+ };
348
+ }
349
+ function getTranscriptCompactionParameters(transcript) {
350
+ return normalizeContextCompactionParameters(transcript.parameters ?? transcript.settings, transcript.parameters?.query ?? transcript.settings.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
351
+ }
352
+ function contextCompactionTargetReductionPercent(parameters) {
353
+ return roundPercent((1 - parameters.compression_ratio) * 100);
354
+ }
355
+ function contextCompactionTargetLabel(parameters) {
356
+ return `${contextCompactionTargetReductionPercent(parameters)}%`;
357
+ }
275
358
  function isProtectedEntry(entry, message, recentEntryIds) {
276
359
  if (recentEntryIds.has(entry.id))
277
360
  return true;
@@ -292,13 +375,14 @@ function isProtectedEntry(entry, message, recentEntryIds) {
292
375
  export function prepareContextCompaction(pathEntries, settings, options = {}) {
293
376
  if (pathEntries.length === 0)
294
377
  return undefined;
295
- const deletionFilters = buildContextDeletionFilters(pathEntries);
296
- const filteredPathEntries = buildContextDeletionFilteredPath(pathEntries, deletionFilters);
378
+ const effectiveDeletionFilters = buildEffectiveContextDeletionFilters(pathEntries);
379
+ const filteredPathEntries = buildContextDeletionFilteredPath(pathEntries, effectiveDeletionFilters);
380
+ const parameters = normalizeContextCompactionParameters({ ...settings, ...options }, autoDetectContextCompactionQuery(filteredPathEntries));
297
381
  const rawEntryById = new Map(pathEntries.map((entry) => [entry.id, entry]));
298
382
  const messageEntryIds = filteredPathEntries
299
383
  .filter((entry) => entry.type !== "context_compaction" && getContextEligibleMessageFromEntry(entry) !== undefined)
300
384
  .map((entry) => entry.id);
301
- const recentEntryIds = new Set(messageEntryIds.slice(-CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT));
385
+ const recentEntryIds = new Set(parameters.preserve_recent > 0 ? messageEntryIds.slice(-parameters.preserve_recent) : []);
302
386
  const protectedEntryIds = new Set();
303
387
  const entries = [];
304
388
  for (const entry of filteredPathEntries) {
@@ -312,7 +396,7 @@ export function prepareContextCompaction(pathEntries, settings, options = {}) {
312
396
  if (protectedEntry)
313
397
  protectedEntryIds.add(entry.id);
314
398
  const rawMessage = getContextEligibleMessageFromEntry(rawEntry) ?? message;
315
- const contentBlocks = contentBlocksForEntry(entry.id, rawMessage, protectedEntry, deletionFilters.deletedContentBlocks.get(entry.id));
399
+ const contentBlocks = contentBlocksForEntry(entry.id, rawMessage, protectedEntry, effectiveDeletionFilters.deletedContentBlocks.get(entry.id));
316
400
  const toolCallIds = contentBlocks.map((block) => block.toolCallId).filter((id) => id !== undefined);
317
401
  const text = contentBlocks.length > 0 ? contentBlocks.map((block) => block.text).join("\n") : messageText(message);
318
402
  entries.push({
@@ -332,12 +416,13 @@ export function prepareContextCompaction(pathEntries, settings, options = {}) {
332
416
  return undefined;
333
417
  return {
334
418
  branchEntries: pathEntries,
335
- mode: options.mode ?? "standard",
419
+ parameters,
336
420
  transcript: {
337
421
  entries,
338
422
  protectedEntryIds: [...protectedEntryIds],
339
423
  tokensBefore: entries.reduce((total, entry) => total + entry.tokenEstimate, 0),
340
424
  settings,
425
+ parameters,
341
426
  },
342
427
  };
343
428
  }
@@ -352,6 +437,14 @@ function normalizeRawTarget(target) {
352
437
  return { kind: "entry", entryId: target.entryId };
353
438
  return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
354
439
  }
440
+ function assertIdOnlyDeletionTarget(target) {
441
+ const allowedKeys = target.kind === "content_block" ? new Set(["kind", "entryId", "blockIndex"]) : new Set(["kind", "entryId"]);
442
+ for (const key of Object.keys(target)) {
443
+ if (!allowedKeys.has(key)) {
444
+ throw new Error(`Deletion target includes unsupported property ${JSON.stringify(key)}; context deletion targets are id-only and must contain only kind, entryId${target.kind === "content_block" ? ", and blockIndex" : ""}`);
445
+ }
446
+ }
447
+ }
355
448
  function rawDeletionFromTarget(target) {
356
449
  if (target.kind === "entry")
357
450
  return { kind: "entry", entryId: target.entryId };
@@ -374,6 +467,113 @@ function getDeletedContentBlocks(targets) {
374
467
  }
375
468
  return blocksByEntry;
376
469
  }
470
+ function recentContextEntryBoundary(transcript) {
471
+ const { preserve_recent } = getTranscriptCompactionParameters(transcript);
472
+ return preserve_recent > 0 ? Math.max(0, transcript.entries.length - preserve_recent) : transcript.entries.length;
473
+ }
474
+ function getRecentContextEntryIds(transcript) {
475
+ const { preserve_recent } = getTranscriptCompactionParameters(transcript);
476
+ if (preserve_recent <= 0)
477
+ return new Set();
478
+ return new Set(transcript.entries.slice(recentContextEntryBoundary(transcript)).map((entry) => entry.entryId));
479
+ }
480
+ function isRecentContextEntry(entry, transcript) {
481
+ const { preserve_recent } = getTranscriptCompactionParameters(transcript);
482
+ if (preserve_recent <= 0)
483
+ return false;
484
+ const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
485
+ return entryIndex >= 0 && entryIndex >= recentContextEntryBoundary(transcript);
486
+ }
487
+ function formatRecentContextDeletionError(transcript, target) {
488
+ const { preserve_recent } = getTranscriptCompactionParameters(transcript);
489
+ const recentWindow = `last ${preserve_recent} context ${preserve_recent === 1 ? "entry" : "entries"}`;
490
+ if (target.kind === "entry") {
491
+ return `Cannot delete recent context entry ${target.entryId} because the ${recentWindow} must remain available for active continuity. Choose an older entry.`;
492
+ }
493
+ return `Cannot delete content block ${target.entryId}:${target.blockIndex} because entry ${target.entryId} is one of the ${recentWindow} that must remain available for active continuity. Choose an older entry or content block.`;
494
+ }
495
+ function deletionGuidance() {
496
+ return "Choose another deletion candidate.";
497
+ }
498
+ function findTranscriptEntry(transcript, entryId) {
499
+ return transcript.entries.find((entry) => entry.entryId === entryId);
500
+ }
501
+ function findTranscriptContentBlock(transcript, target) {
502
+ if (target.kind !== "content_block")
503
+ return undefined;
504
+ return findTranscriptEntry(transcript, target.entryId)?.contentBlocks.find((block) => block.blockIndex === target.blockIndex);
505
+ }
506
+ function firstToolCallBlockTarget(entry, callId) {
507
+ const blockIndex = toolCallBlockIndexes(entry, callId)[0];
508
+ return blockIndex === undefined ? undefined : { kind: "content_block", entryId: entry.entryId, blockIndex };
509
+ }
510
+ function formatProtectedDeletionError(transcript, target) {
511
+ const entry = findTranscriptEntry(transcript, target.entryId);
512
+ if (target.kind === "entry") {
513
+ const toolResultSuffix = entry?.toolResultFor ? ` for tool call ${entry.toolResultFor}` : "";
514
+ const toolCallSuffix = entry && entry.toolCallIds.length > 0 ? ` containing tool call ${entry.toolCallIds.join(", ")}` : "";
515
+ return `Deletion target ${target.entryId}${toolResultSuffix}${toolCallSuffix} is protected. ${deletionGuidance()}`;
516
+ }
517
+ const block = findTranscriptContentBlock(transcript, target);
518
+ const toolBlockSuffix = block?.toolCallId ? ` It is a protected tool block for tool call ${block.toolCallId}.` : "";
519
+ return `Content block ${target.entryId}:${target.blockIndex} is protected.${toolBlockSuffix} ${deletionGuidance()}`;
520
+ }
521
+ function formatProtectedToolDependencyError(transcript, blockedTarget, context) {
522
+ const protectedMessage = formatProtectedDeletionError(transcript, blockedTarget);
523
+ return `${context} ${protectedMessage}`;
524
+ }
525
+ function isProtectedContextDeletionErrorMessage(message) {
526
+ return (/\bprotected\b/i.test(message) ||
527
+ /Cannot delete (?:recent context entry|content block .* because entry .* is one of the last)/u.test(message) ||
528
+ /latest assistant message|thinking\/redacted_thinking block in the latest assistant message/u.test(message));
529
+ }
530
+ function assertNoRecentContextDeletionTargets(transcript, targets) {
531
+ const recentEntryIds = getRecentContextEntryIds(transcript);
532
+ for (const target of targets) {
533
+ if (recentEntryIds.has(target.entryId)) {
534
+ throw new Error(formatRecentContextDeletionError(transcript, target));
535
+ }
536
+ }
537
+ }
538
+ function latestAssistantEntry(transcript, deletedEntryIds = new Set()) {
539
+ for (let index = transcript.entries.length - 1; index >= 0; index--) {
540
+ const entry = transcript.entries[index];
541
+ if (entry.role === "assistant" && !deletedEntryIds.has(entry.entryId))
542
+ return entry;
543
+ }
544
+ return undefined;
545
+ }
546
+ function findLatestAssistantThinkingDeletionViolation(transcript, targets) {
547
+ const deletedEntryIds = getDeletedEntryIds(targets);
548
+ const latestRetainedAssistant = latestAssistantEntry(transcript, deletedEntryIds);
549
+ for (const target of targets) {
550
+ if (target.kind === "entry") {
551
+ const entry = findTranscriptEntry(transcript, target.entryId);
552
+ if (!entry || !assistantEntryHasThinkingContentBlock(entry))
553
+ continue;
554
+ const deletedEntryIdsIfTargetWereKept = new Set(deletedEntryIds);
555
+ deletedEntryIdsIfTargetWereKept.delete(target.entryId);
556
+ if (latestAssistantEntry(transcript, deletedEntryIdsIfTargetWereKept)?.entryId === target.entryId) {
557
+ return target;
558
+ }
559
+ continue;
560
+ }
561
+ if (latestRetainedAssistant?.entryId === target.entryId &&
562
+ assistantEntryHasThinkingContentBlock(latestRetainedAssistant)) {
563
+ return target;
564
+ }
565
+ }
566
+ return undefined;
567
+ }
568
+ function assertNoLatestAssistantThinkingDeletionTargets(transcript, targets) {
569
+ const violation = findLatestAssistantThinkingDeletionViolation(transcript, targets);
570
+ if (!violation)
571
+ return;
572
+ if (violation.kind === "entry") {
573
+ throw new Error(`Cannot delete assistant entry ${violation.entryId} because it is the latest assistant message retained after other deletions and contains thinking/redacted_thinking content blocks`);
574
+ }
575
+ throw new Error(`Cannot delete content block ${violation.entryId}:${violation.blockIndex} because a thinking/redacted_thinking block in the latest assistant message must remain unmodified; the latest retained assistant message contains thinking/redacted_thinking content blocks`);
576
+ }
377
577
  function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBlocks) {
378
578
  if (deletedEntryIds.has(entry.entryId))
379
579
  return true;
@@ -404,15 +604,6 @@ function deleteEntryTarget(targets, entryId) {
404
604
  }
405
605
  return addTarget(targets, { kind: "entry", entryId }) || changed;
406
606
  }
407
- function removeEntryDeletion(targets, entryId) {
408
- const originalLength = targets.length;
409
- for (let index = targets.length - 1; index >= 0; index--) {
410
- const target = targets[index];
411
- if (target.kind === "entry" && target.entryId === entryId)
412
- targets.splice(index, 1);
413
- }
414
- return targets.length !== originalLength;
415
- }
416
607
  function mergeContextDeletionTargets(baseTargets, additionalTargets) {
417
608
  const targets = [...baseTargets];
418
609
  for (const target of additionalTargets) {
@@ -426,8 +617,10 @@ function mergeContextDeletionTargets(baseTargets, additionalTargets) {
426
617
  }
427
618
  return targets;
428
619
  }
429
- function canonicalizeEntryTargets(targets, entry) {
430
- if (entry.protected || getDeletedEntryIds(targets).has(entry.entryId))
620
+ function canonicalizeEntryTargets(transcript, targets, entry) {
621
+ if (!canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }))
622
+ return false;
623
+ if (getDeletedEntryIds(targets).has(entry.entryId))
431
624
  return false;
432
625
  const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
433
626
  if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
@@ -436,28 +629,17 @@ function canonicalizeEntryTargets(targets, entry) {
436
629
  // request every block individually stay invalid so the assistant must choose explicit entry deletion.
437
630
  return deleteEntryTarget(targets, entry.entryId);
438
631
  }
439
- function removeToolCallDeletion(targets, entry, callId) {
440
- let changed = removeEntryDeletion(targets, entry.entryId);
441
- const blockIndexes = new Set(toolCallBlockIndexes(entry, callId));
442
- for (let index = targets.length - 1; index >= 0; index--) {
443
- const target = targets[index];
444
- if (target.kind === "content_block" && target.entryId === entry.entryId && blockIndexes.has(target.blockIndex)) {
445
- targets.splice(index, 1);
446
- changed = true;
447
- }
448
- }
449
- return changed;
450
- }
451
- function addToolCallDeletion(targets, entry, callId) {
452
- if (entry.protected)
453
- return false;
632
+ function addToolCallDeletion(transcript, targets, entry, callId) {
454
633
  let changed = false;
455
634
  for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
635
+ const target = { kind: "content_block", entryId: entry.entryId, blockIndex };
636
+ if (!canDeleteTarget(transcript, target))
637
+ continue;
456
638
  if (!getDeletedEntryIds(targets).has(entry.entryId)) {
457
- changed = addTarget(targets, { kind: "content_block", entryId: entry.entryId, blockIndex }) || changed;
639
+ changed = addTarget(targets, target) || changed;
458
640
  }
459
641
  }
460
- return canonicalizeEntryTargets(targets, entry) || changed;
642
+ return canonicalizeEntryTargets(transcript, targets, entry) || changed;
461
643
  }
462
644
  let warnedReconciliationNonConvergence = false;
463
645
  function reconcileToolDependencies(transcript, initialTargets) {
@@ -496,9 +678,14 @@ function reconcileToolDependencies(transcript, initialTargets) {
496
678
  const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
497
679
  const results = resultEntries.get(callId) ?? [];
498
680
  if (callDeleted) {
499
- const retainedProtectedResult = results.find((entry) => entry.protected && !deletedEntryIds.has(entry.entryId));
681
+ const retainedProtectedResult = results.find((entry) => !deletedEntryIds.has(entry.entryId) &&
682
+ !canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }));
500
683
  if (retainedProtectedResult) {
501
- recordChange(removeToolCallDeletion(targets, callEntry, callId));
684
+ const retainedResultTarget = { kind: "entry", entryId: retainedProtectedResult.entryId };
685
+ if (isRecentTarget(transcript, retainedResultTarget)) {
686
+ throw new Error(formatRecentContextDeletionError(transcript, retainedResultTarget));
687
+ }
688
+ throw new Error(formatProtectedToolDependencyError(transcript, retainedResultTarget, `Cannot delete tool call ${callId} because its paired tool result entry ${retainedProtectedResult.entryId} is protected.`));
502
689
  }
503
690
  else {
504
691
  for (const result of results) {
@@ -512,15 +699,19 @@ function reconcileToolDependencies(transcript, initialTargets) {
512
699
  if (!deletedEntryIds.has(result.entryId))
513
700
  continue;
514
701
  recordChange(deleteEntryTarget(targets, result.entryId));
515
- if (callEntry.protected) {
516
- recordChange(removeEntryDeletion(targets, result.entryId));
517
- continue;
702
+ const callEntryTarget = { kind: "entry", entryId: callEntry.entryId };
703
+ const callBlockTarget = firstToolCallBlockTarget(callEntry, callId) ?? callEntryTarget;
704
+ if (!canDeleteTarget(transcript, callBlockTarget)) {
705
+ if (isRecentTarget(transcript, callBlockTarget)) {
706
+ throw new Error(formatRecentContextDeletionError(transcript, callBlockTarget));
707
+ }
708
+ throw new Error(formatProtectedToolDependencyError(transcript, callBlockTarget, `Cannot delete tool result entry ${result.entryId} because that would require deleting protected tool block for tool call ${callId}.`));
518
709
  }
519
- recordChange(addToolCallDeletion(targets, callEntry, callId));
710
+ recordChange(addToolCallDeletion(transcript, targets, callEntry, callId));
520
711
  }
521
712
  }
522
713
  for (const entry of entriesWithToolCalls) {
523
- recordChange(canonicalizeEntryTargets(targets, entry));
714
+ recordChange(canonicalizeEntryTargets(transcript, targets, entry));
524
715
  }
525
716
  }
526
717
  if (changed && !warnedReconciliationNonConvergence) {
@@ -602,11 +793,7 @@ function computeContextCompactionStats(transcript, targets) {
602
793
  * message, an extension-injected `custom` message, or a branch summary (`branchSummary` role /
603
794
  * `branch_summary` entry type) that recaps an earlier branch's task.
604
795
  *
605
- * Verbatim compaction must always leave at least one task-bearing entry in context. The same set
606
- * also defines which protected entries `critical_overflow` may delete, because the intent each one
607
- * carries is recoverable from any other surviving task-bearing entry. As a deliberate consequence,
608
- * `critical_overflow` MAY delete every literal `user` message as long as a branch summary or custom
609
- * entry survives — branch summaries intentionally carry the task forward.
796
+ * Verbatim compaction must always leave at least one task-bearing entry in context.
610
797
  */
611
798
  function isTaskBearingEntry(entry) {
612
799
  return (entry.role === "user" ||
@@ -614,37 +801,31 @@ function isTaskBearingEntry(entry) {
614
801
  entry.role === "branchSummary" ||
615
802
  entry.entryType === "branch_summary");
616
803
  }
617
- function isCriticalOverflowProtectedEntryDeletable(entry, transcript) {
618
- if (!entry.protected)
619
- return true;
620
- const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
621
- if (entryIndex < 0)
622
- return false;
623
- const recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);
624
- if (entryIndex >= recentBoundary)
625
- return false;
626
- if (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {
627
- return false;
628
- }
629
- return isTaskBearingEntry(entry);
804
+ function isRecentTarget(transcript, target) {
805
+ const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
806
+ return entry !== undefined && isRecentContextEntry(entry, transcript);
630
807
  }
631
- function canDeleteProtectedTargetInMode(transcript, target, mode) {
632
- if (mode !== "critical_overflow")
633
- return false;
808
+ function canDeleteTarget(transcript, target) {
634
809
  const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
635
- if (!entry || !isCriticalOverflowProtectedEntryDeletable(entry, transcript))
810
+ if (!entry)
811
+ return false;
812
+ if (isRecentTarget(transcript, target))
813
+ return false;
814
+ if (entry.protected)
636
815
  return false;
637
816
  if (target.kind === "entry")
638
817
  return true;
639
818
  const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
640
- return block !== undefined;
819
+ if (!block)
820
+ return false;
821
+ return !block.protected;
641
822
  }
642
- export function validateContextDeletionRequest(request, transcript, options = {}) {
643
- const mode = options.mode ?? "standard";
823
+ export function validateContextDeletionRequest(request, transcript) {
644
824
  if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
645
825
  throw new Error("Context deletion request must be an object with a deletions array");
646
826
  }
647
827
  const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
828
+ const recentEntryIds = getRecentContextEntryIds(transcript);
648
829
  const seen = new Set();
649
830
  const deletedTargets = [];
650
831
  for (const deletion of request.deletions) {
@@ -654,6 +835,7 @@ export function validateContextDeletionRequest(request, transcript, options = {}
654
835
  if (deletion.kind !== "entry" && deletion.kind !== "content_block") {
655
836
  throw new Error(`Unsupported deletion target kind: ${String(deletion.kind)}`);
656
837
  }
838
+ assertIdOnlyDeletionTarget(deletion);
657
839
  if (typeof deletion.entryId !== "string" || deletion.entryId.length === 0) {
658
840
  throw new Error("Deletion target entryId must be a non-empty string");
659
841
  }
@@ -661,19 +843,31 @@ export function validateContextDeletionRequest(request, transcript, options = {}
661
843
  if (!entry) {
662
844
  throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
663
845
  }
664
- if (entry.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
665
- throw new Error(`Deletion target ${deletion.entryId} is protected`);
846
+ const normalized = normalizeRawTarget(deletion);
847
+ if (deletion.kind === "entry") {
848
+ if (recentEntryIds.has(deletion.entryId)) {
849
+ throw new Error(formatRecentContextDeletionError(transcript, normalized));
850
+ }
851
+ if (entry.protected) {
852
+ throw new Error(formatProtectedDeletionError(transcript, normalized));
853
+ }
666
854
  }
667
855
  if (deletion.kind === "content_block") {
668
- if (!Number.isInteger(deletion.blockIndex) || deletion.blockIndex === undefined || deletion.blockIndex < 0) {
856
+ if (typeof deletion.blockIndex !== "number" || !Number.isInteger(deletion.blockIndex) || deletion.blockIndex < 0) {
669
857
  throw new Error(`Invalid content block index for entry ${deletion.entryId}`);
670
858
  }
859
+ if (recentEntryIds.has(deletion.entryId)) {
860
+ throw new Error(formatRecentContextDeletionError(transcript, normalized));
861
+ }
862
+ if (entry.protected) {
863
+ throw new Error(formatProtectedDeletionError(transcript, normalized));
864
+ }
671
865
  const block = entry.contentBlocks.find((item) => item.blockIndex === deletion.blockIndex);
672
866
  if (!block) {
673
867
  throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
674
868
  }
675
- if (block.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
676
- throw new Error(`Content block ${deletion.entryId}:${deletion.blockIndex} is protected`);
869
+ if (block.protected) {
870
+ throw new Error(formatProtectedDeletionError(transcript, normalized));
677
871
  }
678
872
  if (entry.contentBlocks.length <= 1) {
679
873
  throw new Error(`Deleting the only content block of ${deletion.entryId} must be an entry deletion`);
@@ -684,10 +878,13 @@ export function validateContextDeletionRequest(request, transcript, options = {}
684
878
  throw new Error(`Duplicate deletion target: ${key}`);
685
879
  }
686
880
  seen.add(key);
687
- const normalized = normalizeRawTarget(deletion);
688
881
  deletedTargets.push(normalized);
689
882
  }
690
883
  const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
884
+ // Tool reconciliation can add targets after the per-request checks above, so
885
+ // these post-reconcile assertions remain authoritative.
886
+ assertNoRecentContextDeletionTargets(transcript, reconciledTargets);
887
+ assertNoLatestAssistantThinkingDeletionTargets(transcript, reconciledTargets);
691
888
  const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
692
889
  for (const target of reconciledTargets) {
693
890
  if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
@@ -716,18 +913,6 @@ export function validateContextDeletionRequest(request, transcript, options = {}
716
913
  stats: computeContextCompactionStats(transcript, reconciledTargets),
717
914
  };
718
915
  }
719
- function stripJsonFence(text) {
720
- const trimmed = text.trim();
721
- if (!trimmed.startsWith("```") || !trimmed.endsWith("```"))
722
- return trimmed;
723
- const firstLineEnd = trimmed.indexOf("\n");
724
- if (firstLineEnd < 0)
725
- return trimmed;
726
- const fenceInfo = trimmed.slice(3, firstLineEnd).trim().toLowerCase();
727
- if (fenceInfo !== "" && fenceInfo !== "json")
728
- return trimmed;
729
- return trimmed.slice(firstLineEnd + 1, -3).trim();
730
- }
731
916
  function contextDeletionRequestFromObject(value, source) {
732
917
  if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
733
918
  throw new Error(`${source} must contain a deletions array`);
@@ -743,6 +928,69 @@ function formatErrorMessage(error) {
743
928
  function createContextDeletionToolResult(text, details) {
744
929
  return { content: [{ type: "text", text }], details, terminate: false };
745
930
  }
931
+ function roundPercent(value) {
932
+ return Math.round(value * 10) / 10;
933
+ }
934
+ function percentOf(part, total) {
935
+ return total > 0 ? roundPercent((part / total) * 100) : 0;
936
+ }
937
+ function finitePositiveNumber(value) {
938
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
939
+ }
940
+ function createContextCompactionBudgetDetails(stats, callCount, contextWindow, parameters) {
941
+ const targetTokensAfter = Math.max(0, Math.floor(stats.tokensBefore * parameters.compression_ratio));
942
+ const targetReductionPercent = contextCompactionTargetReductionPercent(parameters);
943
+ const details = {
944
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
945
+ compression_ratio: parameters.compression_ratio,
946
+ tokensBefore: stats.tokensBefore,
947
+ currentTokensAfter: stats.tokensAfter,
948
+ deletedTokens: Math.max(0, stats.tokensBefore - stats.tokensAfter),
949
+ currentReductionPercent: stats.percentReduction,
950
+ targetReductionPercent,
951
+ targetTokensAfter,
952
+ tokensToDeleteForTarget: Math.max(0, stats.tokensAfter - targetTokensAfter),
953
+ ...(contextWindow !== undefined
954
+ ? {
955
+ contextWindowBeforePercent: percentOf(stats.tokensBefore, contextWindow),
956
+ contextWindowAfterPercent: percentOf(stats.tokensAfter, contextWindow),
957
+ }
958
+ : {}),
959
+ callCount,
960
+ };
961
+ return details;
962
+ }
963
+ function contextCompactionTargetMet(result, parameters) {
964
+ return (result !== undefined &&
965
+ result.deletedTargets.length > 0 &&
966
+ result.stats.percentReduction >= contextCompactionTargetReductionPercent(parameters));
967
+ }
968
+ function contextCompactionProgressKey(result) {
969
+ if (!result)
970
+ return "none:0";
971
+ return `${result.deletedTargets.length}:${result.stats.percentReduction}:${result.stats.tokensAfter}`;
972
+ }
973
+ function contextCompactionProgressPercent(result) {
974
+ return result?.stats.percentReduction ?? 0;
975
+ }
976
+ function createContextCompactionTargetNudgeMessage(result, parameters) {
977
+ const currentReductionPercent = contextCompactionProgressPercent(result);
978
+ const targetLabel = contextCompactionTargetLabel(parameters);
979
+ const tokensToDelete = result
980
+ ? createContextCompactionBudgetDetails(result.stats, 0, undefined, parameters).tokensToDeleteForTarget
981
+ : undefined;
982
+ const remainingText = tokensToDelete !== undefined ? ` Delete about ${tokensToDelete} more token(s) if safe candidates exist.` : "";
983
+ return {
984
+ role: "user",
985
+ content: [
986
+ {
987
+ type: "text",
988
+ text: `The strict ${targetLabel} context-reduction requirement is not met yet; current validated reduction is ${currentReductionPercent}%.${remainingText} Continue removing low-value message entries or message content blocks using ${CONTEXT_DELETE_TOOL_NAME} or ${CONTEXT_GREP_DELETE_TOOL_NAME}. Use the focus query ${JSON.stringify(parameters.query)} to preserve relevant context. Call ${CONTEXT_COMPACTION_BUDGET_TOOL_NAME} to verify progress, and do not provide a final answer until the validated reduction is at least ${targetLabel}.`,
989
+ },
990
+ ],
991
+ timestamp: Date.now(),
992
+ };
993
+ }
746
994
  function assertSafeRegexPattern(pattern) {
747
995
  if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
748
996
  throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
@@ -809,6 +1057,69 @@ function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
809
1057
  candidates.push(candidate);
810
1058
  matches.push(match);
811
1059
  }
1060
+ function pushProtectedGrepSkip(skipped, match) {
1061
+ skipped.push({
1062
+ entryId: match.entryId,
1063
+ target: match.target,
1064
+ ...(match.blockIndex === undefined ? {} : { blockIndex: match.blockIndex }),
1065
+ reason: match.target === "content_block" ? "protected_block" : "protected_entry",
1066
+ text: match.text,
1067
+ });
1068
+ }
1069
+ function filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped) {
1070
+ const eligibleCandidates = [];
1071
+ const eligibleMatches = [];
1072
+ for (let index = 0; index < candidates.length; index++) {
1073
+ const candidate = candidates[index];
1074
+ const match = matches[index];
1075
+ if (!candidate || !match)
1076
+ continue;
1077
+ try {
1078
+ const mergedTargets = mergeContextDeletionTargets(currentTargets, [candidate]);
1079
+ validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
1080
+ eligibleCandidates.push(candidate);
1081
+ eligibleMatches.push(match);
1082
+ }
1083
+ catch (error) {
1084
+ const message = formatErrorMessage(error);
1085
+ if (isProtectedContextDeletionErrorMessage(message)) {
1086
+ pushProtectedGrepSkip(skipped, match);
1087
+ continue;
1088
+ }
1089
+ eligibleCandidates.push(candidate);
1090
+ eligibleMatches.push(match);
1091
+ }
1092
+ }
1093
+ // Some latest-assistant thinking violations only become visible after a grep batch also
1094
+ // deletes newer assistant entries. Classify the newly-unsafe grep candidates as
1095
+ // protected/skipped before maxMatches, expectedMatchCount, stats, or removals are computed.
1096
+ let changed = true;
1097
+ while (changed) {
1098
+ changed = false;
1099
+ const mergedTargets = mergeContextDeletionTargets(currentTargets, eligibleCandidates);
1100
+ const violation = findLatestAssistantThinkingDeletionViolation(transcript, mergedTargets);
1101
+ if (!violation)
1102
+ continue;
1103
+ const violationKey = targetKey(violation);
1104
+ let violationIndex = eligibleCandidates.findIndex((candidate) => targetKey(candidate) === violationKey);
1105
+ if (violationIndex < 0) {
1106
+ violationIndex = eligibleCandidates.findIndex((_candidate, candidateIndex) => {
1107
+ const remainingCandidates = eligibleCandidates.filter((_candidateToKeep, index) => index !== candidateIndex);
1108
+ const remainingTargets = mergeContextDeletionTargets(currentTargets, remainingCandidates);
1109
+ const remainingViolation = findLatestAssistantThinkingDeletionViolation(transcript, remainingTargets);
1110
+ return !remainingViolation || targetKey(remainingViolation) !== violationKey;
1111
+ });
1112
+ }
1113
+ if (violationIndex < 0)
1114
+ continue;
1115
+ const [skippedMatch] = eligibleMatches.splice(violationIndex, 1);
1116
+ eligibleCandidates.splice(violationIndex, 1);
1117
+ if (skippedMatch)
1118
+ pushProtectedGrepSkip(skipped, skippedMatch);
1119
+ changed = true;
1120
+ }
1121
+ return { candidates: eligibleCandidates, matches: eligibleMatches };
1122
+ }
812
1123
  function copyDeletionTarget(target) {
813
1124
  return target.kind === "entry"
814
1125
  ? { kind: "entry", entryId: target.entryId }
@@ -829,30 +1140,36 @@ class ContextDeletionMemoryStore {
829
1140
  entryId: entry.entryId,
830
1141
  role: entry.role,
831
1142
  protected: entry.protected,
1143
+ hasAssistantThinkingBlocks: assistantEntryHasThinkingContentBlock(entry),
832
1144
  tokenEstimate: entry.tokenEstimate,
833
1145
  text: entry.text,
834
1146
  };
835
1147
  });
836
1148
  this.entriesById = new Map(this.entries.map((entry) => [entry.entryId, entry]));
837
- this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => entry.contentBlocks.map((block) => {
838
- if (block.entryId !== entry.entryId) {
839
- throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`);
840
- }
841
- const blockKey = `${block.entryId}:${block.blockIndex}`;
842
- if (blockKeys.has(blockKey)) {
843
- throw new Error(`Duplicate transcript content block: ${blockKey}`);
844
- }
845
- blockKeys.add(blockKey);
846
- return {
847
- entryPosition,
848
- entryId: block.entryId,
849
- blockIndex: block.blockIndex,
850
- type: block.type,
851
- protected: block.protected,
852
- tokenEstimate: block.tokenEstimate,
853
- text: block.text,
854
- };
855
- }));
1149
+ this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => {
1150
+ const hasAssistantThinkingBlocks = assistantEntryHasThinkingContentBlock(entry);
1151
+ return entry.contentBlocks.map((block) => {
1152
+ if (block.entryId !== entry.entryId) {
1153
+ throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`);
1154
+ }
1155
+ const blockKey = `${block.entryId}:${block.blockIndex}`;
1156
+ if (blockKeys.has(blockKey)) {
1157
+ throw new Error(`Duplicate transcript content block: ${blockKey}`);
1158
+ }
1159
+ blockKeys.add(blockKey);
1160
+ return {
1161
+ entryPosition,
1162
+ entryId: block.entryId,
1163
+ blockIndex: block.blockIndex,
1164
+ role: entry.role,
1165
+ type: block.type,
1166
+ protected: block.protected,
1167
+ hasAssistantThinkingBlocks,
1168
+ tokenEstimate: block.tokenEstimate,
1169
+ text: block.text,
1170
+ };
1171
+ });
1172
+ });
856
1173
  this.contentBlockCountByEntryId = new Map();
857
1174
  for (const block of this.contentBlocks) {
858
1175
  this.contentBlockCountByEntryId.set(block.entryId, (this.contentBlockCountByEntryId.get(block.entryId) ?? 0) + 1);
@@ -879,6 +1196,7 @@ class ContextDeletionMemoryStore {
879
1196
  entry_id: entry.entryId,
880
1197
  text: entry.text,
881
1198
  is_protected: entry.protected ? 1 : 0,
1199
+ has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
882
1200
  }));
883
1201
  }
884
1202
  listContentBlocksForGrep() {
@@ -887,10 +1205,13 @@ class ContextDeletionMemoryStore {
887
1205
  .map((block) => ({
888
1206
  entry_id: block.entryId,
889
1207
  block_index: block.blockIndex,
1208
+ role: block.role,
1209
+ type: block.type,
890
1210
  text: block.text,
891
1211
  entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
892
1212
  block_protected: block.protected ? 1 : 0,
893
1213
  block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
1214
+ has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
894
1215
  }));
895
1216
  }
896
1217
  getEntryForRead(entryId) {
@@ -901,6 +1222,7 @@ class ContextDeletionMemoryStore {
901
1222
  entry_id: entry.entryId,
902
1223
  role: entry.role,
903
1224
  is_protected: entry.protected ? 1 : 0,
1225
+ has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
904
1226
  token_estimate: entry.tokenEstimate,
905
1227
  text: entry.text,
906
1228
  };
@@ -912,12 +1234,14 @@ class ContextDeletionMemoryStore {
912
1234
  return {
913
1235
  entry_id: block.entryId,
914
1236
  block_index: block.blockIndex,
1237
+ role: block.role,
915
1238
  type: block.type,
916
1239
  token_estimate: block.tokenEstimate,
917
1240
  text: block.text,
918
1241
  entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
919
1242
  block_protected: block.protected ? 1 : 0,
920
1243
  block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
1244
+ has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
921
1245
  };
922
1246
  }
923
1247
  getGrepScanTextLength(target) {
@@ -956,8 +1280,10 @@ class ContextDeletionMemoryStore {
956
1280
  function createContextDeletionStore(transcript) {
957
1281
  return new ContextDeletionMemoryStore(transcript);
958
1282
  }
959
- export function createContextDeletionTool(transcript, options = {}) {
960
- const mode = options.mode ?? "standard";
1283
+ export function createContextDeletionTool(inputTranscript, options = {}) {
1284
+ const contextWindow = finitePositiveNumber(options.contextWindow);
1285
+ const parameters = normalizeContextCompactionParameters({ ...getTranscriptCompactionParameters(inputTranscript), ...options }, inputTranscript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
1286
+ const transcript = { ...inputTranscript, parameters };
961
1287
  const store = createContextDeletionStore(transcript);
962
1288
  let validatedResult;
963
1289
  function readTargets() {
@@ -965,7 +1291,7 @@ export function createContextDeletionTool(transcript, options = {}) {
965
1291
  }
966
1292
  function applyValidatedTargets(additionalTargets) {
967
1293
  const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
968
- validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript, { mode });
1294
+ validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
969
1295
  store.replaceTargets(validatedResult.deletedTargets);
970
1296
  return validatedResult;
971
1297
  }
@@ -973,7 +1299,7 @@ export function createContextDeletionTool(transcript, options = {}) {
973
1299
  return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
974
1300
  }
975
1301
  function canDeleteProtectedTarget(target) {
976
- return canDeleteProtectedTargetInMode(transcript, target, mode);
1302
+ return canDeleteTarget(transcript, target);
977
1303
  }
978
1304
  const tool = {
979
1305
  ...CONTEXT_DELETE_TOOL,
@@ -984,7 +1310,7 @@ export function createContextDeletionTool(transcript, options = {}) {
984
1310
  const callCount = store.incrementCallCount();
985
1311
  try {
986
1312
  const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
987
- const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript, { mode });
1313
+ const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript);
988
1314
  const applied = applyValidatedTargets(incomingValidated.deletedTargets);
989
1315
  store.clearLastError();
990
1316
  const deletedTargets = readTargets();
@@ -1027,6 +1353,7 @@ export function createContextDeletionTool(transcript, options = {}) {
1027
1353
  const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
1028
1354
  const candidates = [];
1029
1355
  const matches = [];
1356
+ let reportedMatches = matches;
1030
1357
  const skipped = [];
1031
1358
  const seenTargets = new Set();
1032
1359
  try {
@@ -1035,11 +1362,16 @@ export function createContextDeletionTool(transcript, options = {}) {
1035
1362
  }
1036
1363
  const matcher = createGrepMatcher(pattern, regex, caseSensitive);
1037
1364
  const currentTargets = readTargets();
1365
+ const recentEntryIds = getRecentContextEntryIds(transcript);
1038
1366
  if (target === "entry") {
1039
1367
  for (const entry of store.listEntriesForGrep()) {
1040
1368
  if (!matcher.test(entry.text))
1041
1369
  continue;
1042
1370
  const candidate = { kind: "entry", entryId: entry.entry_id };
1371
+ if (recentEntryIds.has(candidate.entryId)) {
1372
+ skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
1373
+ continue;
1374
+ }
1043
1375
  if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1044
1376
  skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
1045
1377
  continue;
@@ -1062,6 +1394,16 @@ export function createContextDeletionTool(transcript, options = {}) {
1062
1394
  const candidate = block.block_count <= 1
1063
1395
  ? { kind: "entry", entryId: block.entry_id }
1064
1396
  : { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
1397
+ if (recentEntryIds.has(candidate.entryId)) {
1398
+ skipped.push({
1399
+ entryId: block.entry_id,
1400
+ target: candidate.kind,
1401
+ ...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
1402
+ reason: "protected_entry",
1403
+ text: block.text,
1404
+ });
1405
+ continue;
1406
+ }
1065
1407
  if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1066
1408
  skipped.push({
1067
1409
  entryId: block.entry_id,
@@ -1100,15 +1442,17 @@ export function createContextDeletionTool(transcript, options = {}) {
1100
1442
  });
1101
1443
  }
1102
1444
  }
1445
+ const eligible = filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped);
1446
+ reportedMatches = eligible.matches;
1103
1447
  let applied;
1104
- if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
1448
+ if (params.expectedMatchCount !== undefined && eligible.candidates.length !== params.expectedMatchCount) {
1105
1449
  skipped.push({ reason: "expected_match_count_mismatch" });
1106
1450
  }
1107
- else if (candidates.length > maxMatches) {
1451
+ else if (eligible.candidates.length > maxMatches) {
1108
1452
  skipped.push({ reason: "max_matches_exceeded" });
1109
1453
  }
1110
- else if (candidates.length > 0) {
1111
- applied = applyValidatedTargets(candidates);
1454
+ else if (eligible.candidates.length > 0) {
1455
+ applied = applyValidatedTargets(eligible.candidates);
1112
1456
  }
1113
1457
  store.clearLastError();
1114
1458
  const deletedTargets = readTargets();
@@ -1117,13 +1461,13 @@ export function createContextDeletionTool(transcript, options = {}) {
1117
1461
  regex,
1118
1462
  caseSensitive,
1119
1463
  target,
1120
- matches,
1464
+ matches: eligible.matches,
1121
1465
  skipped,
1122
1466
  deletedTargets,
1123
1467
  stats: applied?.stats ?? currentStats(),
1124
1468
  callCount,
1125
1469
  };
1126
- const text = `Matched ${matches.length} deletion target(s), skipped ${skipped.length}, and ${applied ? "applied" : "did not apply"} grep deletion for pattern ${JSON.stringify(pattern)}. Total validated deletion target(s): ${deletedTargets.length}.`;
1470
+ const text = `Matched ${eligible.matches.length} deletion target(s), skipped ${skipped.length}, and ${applied ? "applied" : "did not apply"} grep deletion for pattern ${JSON.stringify(pattern)}. Total validated deletion target(s): ${deletedTargets.length}.`;
1127
1471
  return createContextDeletionToolResult(text, details);
1128
1472
  }
1129
1473
  catch (error) {
@@ -1135,7 +1479,7 @@ export function createContextDeletionTool(transcript, options = {}) {
1135
1479
  regex,
1136
1480
  caseSensitive,
1137
1481
  target,
1138
- matches,
1482
+ matches: reportedMatches,
1139
1483
  skipped,
1140
1484
  deletedTargets,
1141
1485
  stats: currentStats(),
@@ -1290,53 +1634,38 @@ export function createContextDeletionTool(transcript, options = {}) {
1290
1634
  });
1291
1635
  },
1292
1636
  };
1637
+ const budgetTool = {
1638
+ ...CONTEXT_COMPACTION_BUDGET_TOOL,
1639
+ label: "context compaction budget",
1640
+ executionMode: "parallel",
1641
+ async execute(_toolCallId) {
1642
+ return store.transaction(() => {
1643
+ const callCount = store.incrementCallCount();
1644
+ store.clearLastError();
1645
+ const details = createContextCompactionBudgetDetails(currentStats(), callCount, contextWindow, parameters);
1646
+ const windowText = details.contextWindowBeforePercent !== undefined
1647
+ ? ` Context window fullness: ${details.contextWindowBeforePercent}% before selected deletions, ${details.contextWindowAfterPercent}% after selected deletions.`
1648
+ : " Context window size is unknown for this model, so fullness percentages are unavailable.";
1649
+ const targetText = details.tokensToDeleteForTarget > 0
1650
+ ? ` Delete about ${details.tokensToDeleteForTarget} more token(s) to reach the ${details.targetReductionPercent}% reduction target.`
1651
+ : ` The selected deletions meet or exceed the ${details.targetReductionPercent}% reduction target.`;
1652
+ return createContextDeletionToolResult(`Current selected deletions reduce context by ${details.currentReductionPercent}% (${details.deletedTokens} token(s)); tokens after selected deletions: ${details.currentTokensAfter}/${details.tokensBefore}.${windowText}${targetText} Keep maximizing useful retained context while aggressively removing low-value blocks.`, details);
1653
+ });
1654
+ },
1655
+ };
1293
1656
  return {
1294
1657
  tool,
1295
1658
  grepTool,
1296
1659
  searchTool,
1297
1660
  readEntryTool,
1298
- tools: [tool, grepTool, searchTool, readEntryTool],
1661
+ budgetTool,
1662
+ tools: [tool, grepTool, searchTool, readEntryTool, budgetTool],
1299
1663
  getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
1300
1664
  getValidatedResult: () => validatedResult,
1301
1665
  getLastError: () => store.getLastError(),
1302
1666
  getCallCount: () => store.getCallCount(),
1303
1667
  };
1304
1668
  }
1305
- export function parseContextDeletionRequest(text) {
1306
- const stripped = stripJsonFence(text);
1307
- let parsed;
1308
- try {
1309
- parsed = JSON.parse(stripped);
1310
- }
1311
- catch (error) {
1312
- throw new Error(`Failed to parse context deletion request JSON: ${error instanceof Error ? error.message : String(error)}`);
1313
- }
1314
- return contextDeletionRequestFromObject(parsed, "Context deletion request JSON");
1315
- }
1316
- function isContextDeleteToolCall(content) {
1317
- return content.type === "toolCall" && content.name === CONTEXT_DELETE_TOOL_NAME;
1318
- }
1319
- function textContentFromResponse(response) {
1320
- return response.content
1321
- .filter((content) => content.type === "text")
1322
- .map((content) => content.text)
1323
- .join("\n");
1324
- }
1325
- export function parseContextDeletionResponse(response) {
1326
- const toolCalls = response.content.filter(isContextDeleteToolCall);
1327
- if (toolCalls.length > 1) {
1328
- throw new Error(`Context compaction assistant called ${CONTEXT_DELETE_TOOL_NAME} more than once`);
1329
- }
1330
- const toolCall = toolCalls[0];
1331
- if (toolCall) {
1332
- return contextDeletionRequestFromObject(toolCall.arguments, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
1333
- }
1334
- const textContent = textContentFromResponse(response);
1335
- if (textContent.trim().length === 0) {
1336
- throw new Error(`Context compaction assistant did not call ${CONTEXT_DELETE_TOOL_NAME}`);
1337
- }
1338
- return parseContextDeletionRequest(textContent);
1339
- }
1340
1669
  function truncateForPrompt(text, maxChars) {
1341
1670
  if (text.length <= maxChars)
1342
1671
  return text;
@@ -1420,14 +1749,16 @@ function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
1420
1749
  })),
1421
1750
  };
1422
1751
  }
1423
- function contextCompactionModePrompt(mode) {
1424
- if (mode === "critical_overflow") {
1425
- return `\n<critical-overflow-mode>\nThe previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest protected entries or protected content shown in the manifest. Evict in priority order: remove old reasoning traces first, then old user/custom/summary context, while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.\n</critical-overflow-mode>`;
1426
- }
1427
- return `\n<standard-mode>\nDo not delete entries or content blocks marked protected. Protected context is only eligible during critical overflow recovery, not during standard compaction.\n</standard-mode>`;
1752
+ function contextCompactionParametersPrompt(parameters) {
1753
+ return `\n<compaction-parameters>\n${JSON.stringify({
1754
+ compression_ratio: parameters.compression_ratio,
1755
+ preserve_recent: parameters.preserve_recent,
1756
+ query: parameters.query,
1757
+ target_reduction_percent: contextCompactionTargetReductionPercent(parameters),
1758
+ }, null, 2)}\n</compaction-parameters>`;
1428
1759
  }
1429
- export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", mode = "standard") {
1430
- return `${CONTEXT_COMPACTION_FIXED_PROMPT}${contextCompactionModePrompt(mode)}\n\n<transcript-file>\n${transcriptFilePath}\n</transcript-file>\n\n<context-manifest>\n${JSON.stringify(contextCompactionTranscriptManifest(transcript, transcriptFilePath), null, 2)}\n</context-manifest>`;
1760
+ export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", parameters = getTranscriptCompactionParameters(transcript)) {
1761
+ return `${contextCompactionFixedPrompt(parameters)}${contextCompactionParametersPrompt(parameters)}\n\n<transcript-file>\n${transcriptFilePath}\n</transcript-file>\n\n<context-manifest>\n${JSON.stringify(contextCompactionTranscriptManifest(transcript, transcriptFilePath), null, 2)}\n</context-manifest>`;
1431
1762
  }
1432
1763
  function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
1433
1764
  return {
@@ -1461,7 +1792,8 @@ function createContextCompactionStopStream(model, text) {
1461
1792
  function isContextCompactionOverflowError(model, errorMessage) {
1462
1793
  return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
1463
1794
  }
1464
- async function runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel = "off", mode = "standard") {
1795
+ async function runContextDeletionAssistant(inputTranscript, model, apiKey, headers, signal, thinkingLevel = "off", parameters = getTranscriptCompactionParameters(inputTranscript)) {
1796
+ const transcript = { ...inputTranscript, parameters };
1465
1797
  const maxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;
1466
1798
  if (signal?.aborted) {
1467
1799
  throw new Error("Context compaction failed: Request was aborted");
@@ -1469,11 +1801,10 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1469
1801
  const transcriptFile = writeContextCompactionTranscriptFile(transcript);
1470
1802
  const promptMessage = {
1471
1803
  role: "user",
1472
- content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, mode) }],
1804
+ content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, parameters) }],
1473
1805
  timestamp: Date.now(),
1474
1806
  };
1475
- const deletionTool = createContextDeletionTool(transcript, { mode });
1476
- let compactionTurnCount = 0;
1807
+ const deletionTool = createContextDeletionTool(transcript, { contextWindow: model.contextWindow, ...parameters });
1477
1808
  const agent = new Agent({
1478
1809
  initialState: {
1479
1810
  systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
@@ -1483,9 +1814,9 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1483
1814
  },
1484
1815
  toolExecution: "parallel",
1485
1816
  streamFn: async (requestModel, context, streamOptions) => {
1486
- compactionTurnCount += 1;
1487
- if (compactionTurnCount > CONTEXT_COMPACTION_MAX_TURNS) {
1488
- return createContextCompactionStopStream(requestModel, `Reached the context compaction turn cap (${CONTEXT_COMPACTION_MAX_TURNS}); using the deletions recorded so far.`);
1817
+ const currentResult = deletionTool.getValidatedResult();
1818
+ if (contextCompactionTargetMet(currentResult, parameters)) {
1819
+ return createContextCompactionStopStream(requestModel, `Reached the strict ${contextCompactionTargetLabel(parameters)} context-reduction requirement (${currentResult.stats.percentReduction}%); using the validated deletions recorded so far.`);
1489
1820
  }
1490
1821
  return streamSimple(requestModel, context, {
1491
1822
  ...streamOptions,
@@ -1495,6 +1826,25 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1495
1826
  });
1496
1827
  },
1497
1828
  });
1829
+ let lastNudgedProgressKey;
1830
+ const unsubscribeNudge = agent.subscribe((event, eventSignal) => {
1831
+ if (event.type !== "turn_end" || signal?.aborted || eventSignal.aborted)
1832
+ return;
1833
+ if (event.message.role !== "assistant")
1834
+ return;
1835
+ if (event.message.stopReason === "error" || event.message.stopReason === "aborted")
1836
+ return;
1837
+ if (event.message.content.some((content) => content.type === "toolCall"))
1838
+ return;
1839
+ const currentResult = deletionTool.getValidatedResult();
1840
+ if (contextCompactionTargetMet(currentResult, parameters))
1841
+ return;
1842
+ const progressKey = contextCompactionProgressKey(currentResult);
1843
+ if (progressKey === lastNudgedProgressKey)
1844
+ return;
1845
+ lastNudgedProgressKey = progressKey;
1846
+ agent.followUp(createContextCompactionTargetNudgeMessage(currentResult, parameters));
1847
+ });
1498
1848
  const abortOnSignal = () => agent.abort();
1499
1849
  signal?.addEventListener("abort", abortOnSignal, { once: true });
1500
1850
  try {
@@ -1502,6 +1852,7 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1502
1852
  }
1503
1853
  finally {
1504
1854
  signal?.removeEventListener("abort", abortOnSignal);
1855
+ unsubscribeNudge();
1505
1856
  transcriptFile.cleanup();
1506
1857
  }
1507
1858
  if (signal?.aborted) {
@@ -1517,18 +1868,39 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1517
1868
  throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
1518
1869
  }
1519
1870
  if (deletionTool.getCallCount() === 0) {
1520
- throw new Error(`Context compaction did not call any transcript inspection or deletion tools (${CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME}, ${CONTEXT_READ_ENTRY_TOOL_NAME}, ${CONTEXT_DELETE_TOOL_NAME}, or ${CONTEXT_GREP_DELETE_TOOL_NAME})`);
1871
+ throw new Error(`Context compaction did not call any transcript inspection, budget, or deletion tools (${CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME}, ${CONTEXT_READ_ENTRY_TOOL_NAME}, ${CONTEXT_COMPACTION_BUDGET_TOOL_NAME}, ${CONTEXT_DELETE_TOOL_NAME}, or ${CONTEXT_GREP_DELETE_TOOL_NAME})`);
1521
1872
  }
1522
1873
  return {
1523
1874
  validatedResult: deletionTool.getValidatedResult(),
1524
1875
  lastToolError: deletionTool.getLastError(),
1525
1876
  };
1526
1877
  }
1527
- export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel = "off", mode = preparation.mode ?? "standard") {
1528
- const { validatedResult, lastToolError } = await runContextDeletionAssistant(preparation.transcript, model, apiKey, headers, signal, thinkingLevel, mode);
1529
- if (!validatedResult || validatedResult.deletedTargets.length === 0) {
1530
- throw new Error(lastToolError ? `No safe context deletions proposed; last deletion tool error: ${lastToolError}` : "No safe context deletions proposed");
1878
+ function hasMetContextCompactionTarget(run, parameters) {
1879
+ return contextCompactionTargetMet(run.validatedResult, parameters);
1880
+ }
1881
+ function formatContextCompactionTargetFailureMessage(attempts, parameters) {
1882
+ const targetLabel = contextCompactionTargetLabel(parameters);
1883
+ if (attempts.length === 0) {
1884
+ return `Context compaction did not meet the strict ${targetLabel} reduction requirement`;
1531
1885
  }
1532
- return validatedResult;
1886
+ const attemptDetails = attempts
1887
+ .map((attempt) => {
1888
+ const reduction = contextCompactionProgressPercent(attempt.validatedResult);
1889
+ const deletionCount = attempt.validatedResult?.deletedTargets.length ?? 0;
1890
+ const errorText = attempt.lastToolError ? `; last deletion tool error: ${attempt.lastToolError}` : "";
1891
+ return `attempt reached ${reduction}% with ${deletionCount} validated deletion target(s)${errorText}`;
1892
+ })
1893
+ .join("; ");
1894
+ return `Context compaction did not meet the strict ${targetLabel} reduction requirement; ${attemptDetails}`;
1895
+ }
1896
+ export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel = "off") {
1897
+ const parameters = normalizeContextCompactionParameters(preparation.parameters ?? preparation.transcript.parameters, preparation.parameters?.query ?? preparation.transcript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
1898
+ const transcript = { ...preparation.transcript, parameters };
1899
+ const attempts = [];
1900
+ const standardRun = await runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel, parameters);
1901
+ if (hasMetContextCompactionTarget(standardRun, parameters))
1902
+ return standardRun.validatedResult;
1903
+ attempts.push({ ...standardRun });
1904
+ throw new Error(formatContextCompactionTargetFailureMessage(attempts, parameters));
1533
1905
  }
1534
1906
  //# sourceMappingURL=context-compaction.js.map