@bastani/atomic 0.8.29-alpha.4 → 0.8.30-alpha.1

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 +27 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +6 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +7 -1
  5. package/dist/builtin/intercom/package.json +2 -2
  6. package/dist/builtin/mcp/CHANGELOG.md +7 -1
  7. package/dist/builtin/mcp/package.json +3 -3
  8. package/dist/builtin/subagents/CHANGELOG.md +11 -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 +7 -1
  34. package/dist/builtin/web-access/package.json +2 -2
  35. package/dist/builtin/workflows/CHANGELOG.md +13 -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 +518 -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 +469 -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 any reasoning steps, EXCEPT thinking or redacted_thinking blocks in the latest assistant message.
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,98 @@ 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) {
539
+ for (let index = transcript.entries.length - 1; index >= 0; index--) {
540
+ const entry = transcript.entries[index];
541
+ if (entry.role === "assistant")
542
+ return entry;
543
+ }
544
+ return undefined;
545
+ }
546
+ function assertNoLatestAssistantThinkingDeletionTargets(transcript, targets) {
547
+ const latestAssistant = latestAssistantEntry(transcript);
548
+ if (!latestAssistant || !assistantEntryHasThinkingContentBlock(latestAssistant))
549
+ return;
550
+ for (const target of targets) {
551
+ if (target.entryId !== latestAssistant.entryId)
552
+ continue;
553
+ if (target.kind === "entry") {
554
+ throw new Error(`Cannot delete assistant entry ${target.entryId} because it is the latest assistant message and contains thinking/redacted_thinking content blocks`);
555
+ }
556
+ const block = latestAssistant.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
557
+ if (block && isAssistantThinkingBlockType(block.type)) {
558
+ throw new Error(`Cannot delete content block ${target.entryId}:${target.blockIndex} because it is a thinking/redacted_thinking block in the latest assistant message`);
559
+ }
560
+ }
561
+ }
377
562
  function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBlocks) {
378
563
  if (deletedEntryIds.has(entry.entryId))
379
564
  return true;
@@ -404,15 +589,6 @@ function deleteEntryTarget(targets, entryId) {
404
589
  }
405
590
  return addTarget(targets, { kind: "entry", entryId }) || changed;
406
591
  }
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
592
  function mergeContextDeletionTargets(baseTargets, additionalTargets) {
417
593
  const targets = [...baseTargets];
418
594
  for (const target of additionalTargets) {
@@ -426,8 +602,10 @@ function mergeContextDeletionTargets(baseTargets, additionalTargets) {
426
602
  }
427
603
  return targets;
428
604
  }
429
- function canonicalizeEntryTargets(targets, entry) {
430
- if (entry.protected || getDeletedEntryIds(targets).has(entry.entryId))
605
+ function canonicalizeEntryTargets(transcript, targets, entry) {
606
+ if (!canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }))
607
+ return false;
608
+ if (getDeletedEntryIds(targets).has(entry.entryId))
431
609
  return false;
432
610
  const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
433
611
  if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
@@ -436,28 +614,17 @@ function canonicalizeEntryTargets(targets, entry) {
436
614
  // request every block individually stay invalid so the assistant must choose explicit entry deletion.
437
615
  return deleteEntryTarget(targets, entry.entryId);
438
616
  }
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;
617
+ function addToolCallDeletion(transcript, targets, entry, callId) {
454
618
  let changed = false;
455
619
  for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
620
+ const target = { kind: "content_block", entryId: entry.entryId, blockIndex };
621
+ if (!canDeleteTarget(transcript, target))
622
+ continue;
456
623
  if (!getDeletedEntryIds(targets).has(entry.entryId)) {
457
- changed = addTarget(targets, { kind: "content_block", entryId: entry.entryId, blockIndex }) || changed;
624
+ changed = addTarget(targets, target) || changed;
458
625
  }
459
626
  }
460
- return canonicalizeEntryTargets(targets, entry) || changed;
627
+ return canonicalizeEntryTargets(transcript, targets, entry) || changed;
461
628
  }
462
629
  let warnedReconciliationNonConvergence = false;
463
630
  function reconcileToolDependencies(transcript, initialTargets) {
@@ -496,9 +663,14 @@ function reconcileToolDependencies(transcript, initialTargets) {
496
663
  const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
497
664
  const results = resultEntries.get(callId) ?? [];
498
665
  if (callDeleted) {
499
- const retainedProtectedResult = results.find((entry) => entry.protected && !deletedEntryIds.has(entry.entryId));
666
+ const retainedProtectedResult = results.find((entry) => !deletedEntryIds.has(entry.entryId) &&
667
+ !canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }));
500
668
  if (retainedProtectedResult) {
501
- recordChange(removeToolCallDeletion(targets, callEntry, callId));
669
+ const retainedResultTarget = { kind: "entry", entryId: retainedProtectedResult.entryId };
670
+ if (isRecentTarget(transcript, retainedResultTarget)) {
671
+ throw new Error(formatRecentContextDeletionError(transcript, retainedResultTarget));
672
+ }
673
+ throw new Error(formatProtectedToolDependencyError(transcript, retainedResultTarget, `Cannot delete tool call ${callId} because its paired tool result entry ${retainedProtectedResult.entryId} is protected.`));
502
674
  }
503
675
  else {
504
676
  for (const result of results) {
@@ -512,15 +684,19 @@ function reconcileToolDependencies(transcript, initialTargets) {
512
684
  if (!deletedEntryIds.has(result.entryId))
513
685
  continue;
514
686
  recordChange(deleteEntryTarget(targets, result.entryId));
515
- if (callEntry.protected) {
516
- recordChange(removeEntryDeletion(targets, result.entryId));
517
- continue;
687
+ const callEntryTarget = { kind: "entry", entryId: callEntry.entryId };
688
+ const callBlockTarget = firstToolCallBlockTarget(callEntry, callId) ?? callEntryTarget;
689
+ if (!canDeleteTarget(transcript, callBlockTarget)) {
690
+ if (isRecentTarget(transcript, callBlockTarget)) {
691
+ throw new Error(formatRecentContextDeletionError(transcript, callBlockTarget));
692
+ }
693
+ 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
694
  }
519
- recordChange(addToolCallDeletion(targets, callEntry, callId));
695
+ recordChange(addToolCallDeletion(transcript, targets, callEntry, callId));
520
696
  }
521
697
  }
522
698
  for (const entry of entriesWithToolCalls) {
523
- recordChange(canonicalizeEntryTargets(targets, entry));
699
+ recordChange(canonicalizeEntryTargets(transcript, targets, entry));
524
700
  }
525
701
  }
526
702
  if (changed && !warnedReconciliationNonConvergence) {
@@ -602,11 +778,7 @@ function computeContextCompactionStats(transcript, targets) {
602
778
  * message, an extension-injected `custom` message, or a branch summary (`branchSummary` role /
603
779
  * `branch_summary` entry type) that recaps an earlier branch's task.
604
780
  *
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.
781
+ * Verbatim compaction must always leave at least one task-bearing entry in context.
610
782
  */
611
783
  function isTaskBearingEntry(entry) {
612
784
  return (entry.role === "user" ||
@@ -614,37 +786,31 @@ function isTaskBearingEntry(entry) {
614
786
  entry.role === "branchSummary" ||
615
787
  entry.entryType === "branch_summary");
616
788
  }
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);
789
+ function isRecentTarget(transcript, target) {
790
+ const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
791
+ return entry !== undefined && isRecentContextEntry(entry, transcript);
630
792
  }
631
- function canDeleteProtectedTargetInMode(transcript, target, mode) {
632
- if (mode !== "critical_overflow")
633
- return false;
793
+ function canDeleteTarget(transcript, target) {
634
794
  const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
635
- if (!entry || !isCriticalOverflowProtectedEntryDeletable(entry, transcript))
795
+ if (!entry)
796
+ return false;
797
+ if (isRecentTarget(transcript, target))
798
+ return false;
799
+ if (entry.protected)
636
800
  return false;
637
801
  if (target.kind === "entry")
638
802
  return true;
639
803
  const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
640
- return block !== undefined;
804
+ if (!block)
805
+ return false;
806
+ return !block.protected;
641
807
  }
642
- export function validateContextDeletionRequest(request, transcript, options = {}) {
643
- const mode = options.mode ?? "standard";
808
+ export function validateContextDeletionRequest(request, transcript) {
644
809
  if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
645
810
  throw new Error("Context deletion request must be an object with a deletions array");
646
811
  }
647
812
  const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
813
+ const recentEntryIds = getRecentContextEntryIds(transcript);
648
814
  const seen = new Set();
649
815
  const deletedTargets = [];
650
816
  for (const deletion of request.deletions) {
@@ -654,6 +820,7 @@ export function validateContextDeletionRequest(request, transcript, options = {}
654
820
  if (deletion.kind !== "entry" && deletion.kind !== "content_block") {
655
821
  throw new Error(`Unsupported deletion target kind: ${String(deletion.kind)}`);
656
822
  }
823
+ assertIdOnlyDeletionTarget(deletion);
657
824
  if (typeof deletion.entryId !== "string" || deletion.entryId.length === 0) {
658
825
  throw new Error("Deletion target entryId must be a non-empty string");
659
826
  }
@@ -661,19 +828,31 @@ export function validateContextDeletionRequest(request, transcript, options = {}
661
828
  if (!entry) {
662
829
  throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
663
830
  }
664
- if (entry.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
665
- throw new Error(`Deletion target ${deletion.entryId} is protected`);
831
+ const normalized = normalizeRawTarget(deletion);
832
+ if (deletion.kind === "entry") {
833
+ if (recentEntryIds.has(deletion.entryId)) {
834
+ throw new Error(formatRecentContextDeletionError(transcript, normalized));
835
+ }
836
+ if (entry.protected) {
837
+ throw new Error(formatProtectedDeletionError(transcript, normalized));
838
+ }
666
839
  }
667
840
  if (deletion.kind === "content_block") {
668
- if (!Number.isInteger(deletion.blockIndex) || deletion.blockIndex === undefined || deletion.blockIndex < 0) {
841
+ if (typeof deletion.blockIndex !== "number" || !Number.isInteger(deletion.blockIndex) || deletion.blockIndex < 0) {
669
842
  throw new Error(`Invalid content block index for entry ${deletion.entryId}`);
670
843
  }
844
+ if (recentEntryIds.has(deletion.entryId)) {
845
+ throw new Error(formatRecentContextDeletionError(transcript, normalized));
846
+ }
847
+ if (entry.protected) {
848
+ throw new Error(formatProtectedDeletionError(transcript, normalized));
849
+ }
671
850
  const block = entry.contentBlocks.find((item) => item.blockIndex === deletion.blockIndex);
672
851
  if (!block) {
673
852
  throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
674
853
  }
675
- if (block.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
676
- throw new Error(`Content block ${deletion.entryId}:${deletion.blockIndex} is protected`);
854
+ if (block.protected) {
855
+ throw new Error(formatProtectedDeletionError(transcript, normalized));
677
856
  }
678
857
  if (entry.contentBlocks.length <= 1) {
679
858
  throw new Error(`Deleting the only content block of ${deletion.entryId} must be an entry deletion`);
@@ -684,10 +863,13 @@ export function validateContextDeletionRequest(request, transcript, options = {}
684
863
  throw new Error(`Duplicate deletion target: ${key}`);
685
864
  }
686
865
  seen.add(key);
687
- const normalized = normalizeRawTarget(deletion);
688
866
  deletedTargets.push(normalized);
689
867
  }
690
868
  const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
869
+ // Tool reconciliation can add targets after the per-request checks above, so
870
+ // these post-reconcile assertions remain authoritative.
871
+ assertNoRecentContextDeletionTargets(transcript, reconciledTargets);
872
+ assertNoLatestAssistantThinkingDeletionTargets(transcript, reconciledTargets);
691
873
  const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
692
874
  for (const target of reconciledTargets) {
693
875
  if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
@@ -716,18 +898,6 @@ export function validateContextDeletionRequest(request, transcript, options = {}
716
898
  stats: computeContextCompactionStats(transcript, reconciledTargets),
717
899
  };
718
900
  }
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
901
  function contextDeletionRequestFromObject(value, source) {
732
902
  if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
733
903
  throw new Error(`${source} must contain a deletions array`);
@@ -743,6 +913,69 @@ function formatErrorMessage(error) {
743
913
  function createContextDeletionToolResult(text, details) {
744
914
  return { content: [{ type: "text", text }], details, terminate: false };
745
915
  }
916
+ function roundPercent(value) {
917
+ return Math.round(value * 10) / 10;
918
+ }
919
+ function percentOf(part, total) {
920
+ return total > 0 ? roundPercent((part / total) * 100) : 0;
921
+ }
922
+ function finitePositiveNumber(value) {
923
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
924
+ }
925
+ function createContextCompactionBudgetDetails(stats, callCount, contextWindow, parameters) {
926
+ const targetTokensAfter = Math.max(0, Math.floor(stats.tokensBefore * parameters.compression_ratio));
927
+ const targetReductionPercent = contextCompactionTargetReductionPercent(parameters);
928
+ const details = {
929
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
930
+ compression_ratio: parameters.compression_ratio,
931
+ tokensBefore: stats.tokensBefore,
932
+ currentTokensAfter: stats.tokensAfter,
933
+ deletedTokens: Math.max(0, stats.tokensBefore - stats.tokensAfter),
934
+ currentReductionPercent: stats.percentReduction,
935
+ targetReductionPercent,
936
+ targetTokensAfter,
937
+ tokensToDeleteForTarget: Math.max(0, stats.tokensAfter - targetTokensAfter),
938
+ ...(contextWindow !== undefined
939
+ ? {
940
+ contextWindowBeforePercent: percentOf(stats.tokensBefore, contextWindow),
941
+ contextWindowAfterPercent: percentOf(stats.tokensAfter, contextWindow),
942
+ }
943
+ : {}),
944
+ callCount,
945
+ };
946
+ return details;
947
+ }
948
+ function contextCompactionTargetMet(result, parameters) {
949
+ return (result !== undefined &&
950
+ result.deletedTargets.length > 0 &&
951
+ result.stats.percentReduction >= contextCompactionTargetReductionPercent(parameters));
952
+ }
953
+ function contextCompactionProgressKey(result) {
954
+ if (!result)
955
+ return "none:0";
956
+ return `${result.deletedTargets.length}:${result.stats.percentReduction}:${result.stats.tokensAfter}`;
957
+ }
958
+ function contextCompactionProgressPercent(result) {
959
+ return result?.stats.percentReduction ?? 0;
960
+ }
961
+ function createContextCompactionTargetNudgeMessage(result, parameters) {
962
+ const currentReductionPercent = contextCompactionProgressPercent(result);
963
+ const targetLabel = contextCompactionTargetLabel(parameters);
964
+ const tokensToDelete = result
965
+ ? createContextCompactionBudgetDetails(result.stats, 0, undefined, parameters).tokensToDeleteForTarget
966
+ : undefined;
967
+ const remainingText = tokensToDelete !== undefined ? ` Delete about ${tokensToDelete} more token(s) if safe candidates exist.` : "";
968
+ return {
969
+ role: "user",
970
+ content: [
971
+ {
972
+ type: "text",
973
+ 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}.`,
974
+ },
975
+ ],
976
+ timestamp: Date.now(),
977
+ };
978
+ }
746
979
  function assertSafeRegexPattern(pattern) {
747
980
  if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
748
981
  throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
@@ -809,6 +1042,41 @@ function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
809
1042
  candidates.push(candidate);
810
1043
  matches.push(match);
811
1044
  }
1045
+ function pushProtectedGrepSkip(skipped, match) {
1046
+ skipped.push({
1047
+ entryId: match.entryId,
1048
+ target: match.target,
1049
+ ...(match.blockIndex === undefined ? {} : { blockIndex: match.blockIndex }),
1050
+ reason: match.target === "content_block" ? "protected_block" : "protected_entry",
1051
+ text: match.text,
1052
+ });
1053
+ }
1054
+ function filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped) {
1055
+ const eligibleCandidates = [];
1056
+ const eligibleMatches = [];
1057
+ for (let index = 0; index < candidates.length; index++) {
1058
+ const candidate = candidates[index];
1059
+ const match = matches[index];
1060
+ if (!candidate || !match)
1061
+ continue;
1062
+ try {
1063
+ const mergedTargets = mergeContextDeletionTargets(currentTargets, [candidate]);
1064
+ validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
1065
+ eligibleCandidates.push(candidate);
1066
+ eligibleMatches.push(match);
1067
+ }
1068
+ catch (error) {
1069
+ const message = formatErrorMessage(error);
1070
+ if (isProtectedContextDeletionErrorMessage(message)) {
1071
+ pushProtectedGrepSkip(skipped, match);
1072
+ continue;
1073
+ }
1074
+ eligibleCandidates.push(candidate);
1075
+ eligibleMatches.push(match);
1076
+ }
1077
+ }
1078
+ return { candidates: eligibleCandidates, matches: eligibleMatches };
1079
+ }
812
1080
  function copyDeletionTarget(target) {
813
1081
  return target.kind === "entry"
814
1082
  ? { kind: "entry", entryId: target.entryId }
@@ -829,30 +1097,36 @@ class ContextDeletionMemoryStore {
829
1097
  entryId: entry.entryId,
830
1098
  role: entry.role,
831
1099
  protected: entry.protected,
1100
+ hasAssistantThinkingBlocks: assistantEntryHasThinkingContentBlock(entry),
832
1101
  tokenEstimate: entry.tokenEstimate,
833
1102
  text: entry.text,
834
1103
  };
835
1104
  });
836
1105
  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
- }));
1106
+ this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => {
1107
+ const hasAssistantThinkingBlocks = assistantEntryHasThinkingContentBlock(entry);
1108
+ return entry.contentBlocks.map((block) => {
1109
+ if (block.entryId !== entry.entryId) {
1110
+ throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`);
1111
+ }
1112
+ const blockKey = `${block.entryId}:${block.blockIndex}`;
1113
+ if (blockKeys.has(blockKey)) {
1114
+ throw new Error(`Duplicate transcript content block: ${blockKey}`);
1115
+ }
1116
+ blockKeys.add(blockKey);
1117
+ return {
1118
+ entryPosition,
1119
+ entryId: block.entryId,
1120
+ blockIndex: block.blockIndex,
1121
+ role: entry.role,
1122
+ type: block.type,
1123
+ protected: block.protected,
1124
+ hasAssistantThinkingBlocks,
1125
+ tokenEstimate: block.tokenEstimate,
1126
+ text: block.text,
1127
+ };
1128
+ });
1129
+ });
856
1130
  this.contentBlockCountByEntryId = new Map();
857
1131
  for (const block of this.contentBlocks) {
858
1132
  this.contentBlockCountByEntryId.set(block.entryId, (this.contentBlockCountByEntryId.get(block.entryId) ?? 0) + 1);
@@ -879,6 +1153,7 @@ class ContextDeletionMemoryStore {
879
1153
  entry_id: entry.entryId,
880
1154
  text: entry.text,
881
1155
  is_protected: entry.protected ? 1 : 0,
1156
+ has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
882
1157
  }));
883
1158
  }
884
1159
  listContentBlocksForGrep() {
@@ -887,10 +1162,13 @@ class ContextDeletionMemoryStore {
887
1162
  .map((block) => ({
888
1163
  entry_id: block.entryId,
889
1164
  block_index: block.blockIndex,
1165
+ role: block.role,
1166
+ type: block.type,
890
1167
  text: block.text,
891
1168
  entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
892
1169
  block_protected: block.protected ? 1 : 0,
893
1170
  block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
1171
+ has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
894
1172
  }));
895
1173
  }
896
1174
  getEntryForRead(entryId) {
@@ -901,6 +1179,7 @@ class ContextDeletionMemoryStore {
901
1179
  entry_id: entry.entryId,
902
1180
  role: entry.role,
903
1181
  is_protected: entry.protected ? 1 : 0,
1182
+ has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
904
1183
  token_estimate: entry.tokenEstimate,
905
1184
  text: entry.text,
906
1185
  };
@@ -912,12 +1191,14 @@ class ContextDeletionMemoryStore {
912
1191
  return {
913
1192
  entry_id: block.entryId,
914
1193
  block_index: block.blockIndex,
1194
+ role: block.role,
915
1195
  type: block.type,
916
1196
  token_estimate: block.tokenEstimate,
917
1197
  text: block.text,
918
1198
  entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
919
1199
  block_protected: block.protected ? 1 : 0,
920
1200
  block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
1201
+ has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
921
1202
  };
922
1203
  }
923
1204
  getGrepScanTextLength(target) {
@@ -956,8 +1237,10 @@ class ContextDeletionMemoryStore {
956
1237
  function createContextDeletionStore(transcript) {
957
1238
  return new ContextDeletionMemoryStore(transcript);
958
1239
  }
959
- export function createContextDeletionTool(transcript, options = {}) {
960
- const mode = options.mode ?? "standard";
1240
+ export function createContextDeletionTool(inputTranscript, options = {}) {
1241
+ const contextWindow = finitePositiveNumber(options.contextWindow);
1242
+ const parameters = normalizeContextCompactionParameters({ ...getTranscriptCompactionParameters(inputTranscript), ...options }, inputTranscript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
1243
+ const transcript = { ...inputTranscript, parameters };
961
1244
  const store = createContextDeletionStore(transcript);
962
1245
  let validatedResult;
963
1246
  function readTargets() {
@@ -965,7 +1248,7 @@ export function createContextDeletionTool(transcript, options = {}) {
965
1248
  }
966
1249
  function applyValidatedTargets(additionalTargets) {
967
1250
  const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
968
- validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript, { mode });
1251
+ validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
969
1252
  store.replaceTargets(validatedResult.deletedTargets);
970
1253
  return validatedResult;
971
1254
  }
@@ -973,7 +1256,7 @@ export function createContextDeletionTool(transcript, options = {}) {
973
1256
  return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
974
1257
  }
975
1258
  function canDeleteProtectedTarget(target) {
976
- return canDeleteProtectedTargetInMode(transcript, target, mode);
1259
+ return canDeleteTarget(transcript, target);
977
1260
  }
978
1261
  const tool = {
979
1262
  ...CONTEXT_DELETE_TOOL,
@@ -984,7 +1267,7 @@ export function createContextDeletionTool(transcript, options = {}) {
984
1267
  const callCount = store.incrementCallCount();
985
1268
  try {
986
1269
  const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
987
- const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript, { mode });
1270
+ const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript);
988
1271
  const applied = applyValidatedTargets(incomingValidated.deletedTargets);
989
1272
  store.clearLastError();
990
1273
  const deletedTargets = readTargets();
@@ -1027,6 +1310,7 @@ export function createContextDeletionTool(transcript, options = {}) {
1027
1310
  const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
1028
1311
  const candidates = [];
1029
1312
  const matches = [];
1313
+ let reportedMatches = matches;
1030
1314
  const skipped = [];
1031
1315
  const seenTargets = new Set();
1032
1316
  try {
@@ -1035,11 +1319,16 @@ export function createContextDeletionTool(transcript, options = {}) {
1035
1319
  }
1036
1320
  const matcher = createGrepMatcher(pattern, regex, caseSensitive);
1037
1321
  const currentTargets = readTargets();
1322
+ const recentEntryIds = getRecentContextEntryIds(transcript);
1038
1323
  if (target === "entry") {
1039
1324
  for (const entry of store.listEntriesForGrep()) {
1040
1325
  if (!matcher.test(entry.text))
1041
1326
  continue;
1042
1327
  const candidate = { kind: "entry", entryId: entry.entry_id };
1328
+ if (recentEntryIds.has(candidate.entryId)) {
1329
+ skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
1330
+ continue;
1331
+ }
1043
1332
  if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1044
1333
  skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
1045
1334
  continue;
@@ -1062,6 +1351,16 @@ export function createContextDeletionTool(transcript, options = {}) {
1062
1351
  const candidate = block.block_count <= 1
1063
1352
  ? { kind: "entry", entryId: block.entry_id }
1064
1353
  : { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
1354
+ if (recentEntryIds.has(candidate.entryId)) {
1355
+ skipped.push({
1356
+ entryId: block.entry_id,
1357
+ target: candidate.kind,
1358
+ ...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
1359
+ reason: "protected_entry",
1360
+ text: block.text,
1361
+ });
1362
+ continue;
1363
+ }
1065
1364
  if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1066
1365
  skipped.push({
1067
1366
  entryId: block.entry_id,
@@ -1100,15 +1399,17 @@ export function createContextDeletionTool(transcript, options = {}) {
1100
1399
  });
1101
1400
  }
1102
1401
  }
1402
+ const eligible = filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped);
1403
+ reportedMatches = eligible.matches;
1103
1404
  let applied;
1104
- if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
1405
+ if (params.expectedMatchCount !== undefined && eligible.candidates.length !== params.expectedMatchCount) {
1105
1406
  skipped.push({ reason: "expected_match_count_mismatch" });
1106
1407
  }
1107
- else if (candidates.length > maxMatches) {
1408
+ else if (eligible.candidates.length > maxMatches) {
1108
1409
  skipped.push({ reason: "max_matches_exceeded" });
1109
1410
  }
1110
- else if (candidates.length > 0) {
1111
- applied = applyValidatedTargets(candidates);
1411
+ else if (eligible.candidates.length > 0) {
1412
+ applied = applyValidatedTargets(eligible.candidates);
1112
1413
  }
1113
1414
  store.clearLastError();
1114
1415
  const deletedTargets = readTargets();
@@ -1117,13 +1418,13 @@ export function createContextDeletionTool(transcript, options = {}) {
1117
1418
  regex,
1118
1419
  caseSensitive,
1119
1420
  target,
1120
- matches,
1421
+ matches: eligible.matches,
1121
1422
  skipped,
1122
1423
  deletedTargets,
1123
1424
  stats: applied?.stats ?? currentStats(),
1124
1425
  callCount,
1125
1426
  };
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}.`;
1427
+ 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
1428
  return createContextDeletionToolResult(text, details);
1128
1429
  }
1129
1430
  catch (error) {
@@ -1135,7 +1436,7 @@ export function createContextDeletionTool(transcript, options = {}) {
1135
1436
  regex,
1136
1437
  caseSensitive,
1137
1438
  target,
1138
- matches,
1439
+ matches: reportedMatches,
1139
1440
  skipped,
1140
1441
  deletedTargets,
1141
1442
  stats: currentStats(),
@@ -1290,53 +1591,38 @@ export function createContextDeletionTool(transcript, options = {}) {
1290
1591
  });
1291
1592
  },
1292
1593
  };
1594
+ const budgetTool = {
1595
+ ...CONTEXT_COMPACTION_BUDGET_TOOL,
1596
+ label: "context compaction budget",
1597
+ executionMode: "parallel",
1598
+ async execute(_toolCallId) {
1599
+ return store.transaction(() => {
1600
+ const callCount = store.incrementCallCount();
1601
+ store.clearLastError();
1602
+ const details = createContextCompactionBudgetDetails(currentStats(), callCount, contextWindow, parameters);
1603
+ const windowText = details.contextWindowBeforePercent !== undefined
1604
+ ? ` Context window fullness: ${details.contextWindowBeforePercent}% before selected deletions, ${details.contextWindowAfterPercent}% after selected deletions.`
1605
+ : " Context window size is unknown for this model, so fullness percentages are unavailable.";
1606
+ const targetText = details.tokensToDeleteForTarget > 0
1607
+ ? ` Delete about ${details.tokensToDeleteForTarget} more token(s) to reach the ${details.targetReductionPercent}% reduction target.`
1608
+ : ` The selected deletions meet or exceed the ${details.targetReductionPercent}% reduction target.`;
1609
+ 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);
1610
+ });
1611
+ },
1612
+ };
1293
1613
  return {
1294
1614
  tool,
1295
1615
  grepTool,
1296
1616
  searchTool,
1297
1617
  readEntryTool,
1298
- tools: [tool, grepTool, searchTool, readEntryTool],
1618
+ budgetTool,
1619
+ tools: [tool, grepTool, searchTool, readEntryTool, budgetTool],
1299
1620
  getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
1300
1621
  getValidatedResult: () => validatedResult,
1301
1622
  getLastError: () => store.getLastError(),
1302
1623
  getCallCount: () => store.getCallCount(),
1303
1624
  };
1304
1625
  }
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
1626
  function truncateForPrompt(text, maxChars) {
1341
1627
  if (text.length <= maxChars)
1342
1628
  return text;
@@ -1420,14 +1706,16 @@ function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
1420
1706
  })),
1421
1707
  };
1422
1708
  }
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>`;
1709
+ function contextCompactionParametersPrompt(parameters) {
1710
+ return `\n<compaction-parameters>\n${JSON.stringify({
1711
+ compression_ratio: parameters.compression_ratio,
1712
+ preserve_recent: parameters.preserve_recent,
1713
+ query: parameters.query,
1714
+ target_reduction_percent: contextCompactionTargetReductionPercent(parameters),
1715
+ }, null, 2)}\n</compaction-parameters>`;
1428
1716
  }
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>`;
1717
+ export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", parameters = getTranscriptCompactionParameters(transcript)) {
1718
+ 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
1719
  }
1432
1720
  function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
1433
1721
  return {
@@ -1461,7 +1749,8 @@ function createContextCompactionStopStream(model, text) {
1461
1749
  function isContextCompactionOverflowError(model, errorMessage) {
1462
1750
  return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
1463
1751
  }
1464
- async function runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel = "off", mode = "standard") {
1752
+ async function runContextDeletionAssistant(inputTranscript, model, apiKey, headers, signal, thinkingLevel = "off", parameters = getTranscriptCompactionParameters(inputTranscript)) {
1753
+ const transcript = { ...inputTranscript, parameters };
1465
1754
  const maxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;
1466
1755
  if (signal?.aborted) {
1467
1756
  throw new Error("Context compaction failed: Request was aborted");
@@ -1469,11 +1758,10 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1469
1758
  const transcriptFile = writeContextCompactionTranscriptFile(transcript);
1470
1759
  const promptMessage = {
1471
1760
  role: "user",
1472
- content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, mode) }],
1761
+ content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, parameters) }],
1473
1762
  timestamp: Date.now(),
1474
1763
  };
1475
- const deletionTool = createContextDeletionTool(transcript, { mode });
1476
- let compactionTurnCount = 0;
1764
+ const deletionTool = createContextDeletionTool(transcript, { contextWindow: model.contextWindow, ...parameters });
1477
1765
  const agent = new Agent({
1478
1766
  initialState: {
1479
1767
  systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
@@ -1483,9 +1771,9 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1483
1771
  },
1484
1772
  toolExecution: "parallel",
1485
1773
  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.`);
1774
+ const currentResult = deletionTool.getValidatedResult();
1775
+ if (contextCompactionTargetMet(currentResult, parameters)) {
1776
+ return createContextCompactionStopStream(requestModel, `Reached the strict ${contextCompactionTargetLabel(parameters)} context-reduction requirement (${currentResult.stats.percentReduction}%); using the validated deletions recorded so far.`);
1489
1777
  }
1490
1778
  return streamSimple(requestModel, context, {
1491
1779
  ...streamOptions,
@@ -1495,6 +1783,25 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1495
1783
  });
1496
1784
  },
1497
1785
  });
1786
+ let lastNudgedProgressKey;
1787
+ const unsubscribeNudge = agent.subscribe((event, eventSignal) => {
1788
+ if (event.type !== "turn_end" || signal?.aborted || eventSignal.aborted)
1789
+ return;
1790
+ if (event.message.role !== "assistant")
1791
+ return;
1792
+ if (event.message.stopReason === "error" || event.message.stopReason === "aborted")
1793
+ return;
1794
+ if (event.message.content.some((content) => content.type === "toolCall"))
1795
+ return;
1796
+ const currentResult = deletionTool.getValidatedResult();
1797
+ if (contextCompactionTargetMet(currentResult, parameters))
1798
+ return;
1799
+ const progressKey = contextCompactionProgressKey(currentResult);
1800
+ if (progressKey === lastNudgedProgressKey)
1801
+ return;
1802
+ lastNudgedProgressKey = progressKey;
1803
+ agent.followUp(createContextCompactionTargetNudgeMessage(currentResult, parameters));
1804
+ });
1498
1805
  const abortOnSignal = () => agent.abort();
1499
1806
  signal?.addEventListener("abort", abortOnSignal, { once: true });
1500
1807
  try {
@@ -1502,6 +1809,7 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1502
1809
  }
1503
1810
  finally {
1504
1811
  signal?.removeEventListener("abort", abortOnSignal);
1812
+ unsubscribeNudge();
1505
1813
  transcriptFile.cleanup();
1506
1814
  }
1507
1815
  if (signal?.aborted) {
@@ -1517,18 +1825,39 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
1517
1825
  throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
1518
1826
  }
1519
1827
  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})`);
1828
+ 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
1829
  }
1522
1830
  return {
1523
1831
  validatedResult: deletionTool.getValidatedResult(),
1524
1832
  lastToolError: deletionTool.getLastError(),
1525
1833
  };
1526
1834
  }
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");
1835
+ function hasMetContextCompactionTarget(run, parameters) {
1836
+ return contextCompactionTargetMet(run.validatedResult, parameters);
1837
+ }
1838
+ function formatContextCompactionTargetFailureMessage(attempts, parameters) {
1839
+ const targetLabel = contextCompactionTargetLabel(parameters);
1840
+ if (attempts.length === 0) {
1841
+ return `Context compaction did not meet the strict ${targetLabel} reduction requirement`;
1531
1842
  }
1532
- return validatedResult;
1843
+ const attemptDetails = attempts
1844
+ .map((attempt) => {
1845
+ const reduction = contextCompactionProgressPercent(attempt.validatedResult);
1846
+ const deletionCount = attempt.validatedResult?.deletedTargets.length ?? 0;
1847
+ const errorText = attempt.lastToolError ? `; last deletion tool error: ${attempt.lastToolError}` : "";
1848
+ return `attempt reached ${reduction}% with ${deletionCount} validated deletion target(s)${errorText}`;
1849
+ })
1850
+ .join("; ");
1851
+ return `Context compaction did not meet the strict ${targetLabel} reduction requirement; ${attemptDetails}`;
1852
+ }
1853
+ export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel = "off") {
1854
+ const parameters = normalizeContextCompactionParameters(preparation.parameters ?? preparation.transcript.parameters, preparation.parameters?.query ?? preparation.transcript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
1855
+ const transcript = { ...preparation.transcript, parameters };
1856
+ const attempts = [];
1857
+ const standardRun = await runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel, parameters);
1858
+ if (hasMetContextCompactionTarget(standardRun, parameters))
1859
+ return standardRun.validatedResult;
1860
+ attempts.push({ ...standardRun });
1861
+ throw new Error(formatContextCompactionTargetFailureMessage(attempts, parameters));
1533
1862
  }
1534
1863
  //# sourceMappingURL=context-compaction.js.map