@bastani/atomic 0.8.26-alpha.9 → 0.8.26

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 (37) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +18 -0
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/CHANGELOG.md +22 -0
  5. package/dist/builtin/mcp/package.json +1 -1
  6. package/dist/builtin/subagents/CHANGELOG.md +27 -0
  7. package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
  8. package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
  9. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
  10. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
  11. package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/web-access/CHANGELOG.md +18 -0
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/workflows/CHANGELOG.md +34 -0
  16. package/dist/builtin/workflows/package.json +1 -1
  17. package/dist/core/agent-session.d.ts.map +1 -1
  18. package/dist/core/agent-session.js +4 -2
  19. package/dist/core/agent-session.js.map +1 -1
  20. package/dist/core/compaction/context-compaction.d.ts +125 -11
  21. package/dist/core/compaction/context-compaction.d.ts.map +1 -1
  22. package/dist/core/compaction/context-compaction.js +1112 -78
  23. package/dist/core/compaction/context-compaction.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/docs/compaction.md +120 -37
  29. package/docs/extensions.md +4 -4
  30. package/docs/json.md +1 -1
  31. package/docs/rpc.md +4 -4
  32. package/docs/sdk.md +2 -0
  33. package/docs/session-format.md +1 -1
  34. package/docs/sessions.md +1 -1
  35. package/docs/settings.md +1 -1
  36. package/docs/usage.md +2 -2
  37. package/package.json +3 -1
@@ -1,11 +1,33 @@
1
- import { completeSimple, StringEnum } from "@earendil-works/pi-ai";
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { createAssistantMessageEventStream, getSupportedThinkingLevels, isContextOverflow, streamSimple, StringEnum, } from "@earendil-works/pi-ai";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
2
7
  import { Type } from "typebox";
3
8
  import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
4
9
  import { buildContextDeletionFilteredPath, buildContextDeletionFilters, } from "../session-manager.js";
5
10
  import { estimateTokens } from "./compaction.js";
6
11
  export const CONTEXT_COMPACTION_PROMPT_VERSION = 1;
7
- const CONTEXT_DELETION_PLAN_TOOL_NAME = "context_deletion_plan";
8
- const ContextDeletionPlanToolParameters = Type.Object({
12
+ const CONTEXT_COMPACTION_THINKING_LEVEL_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh"];
13
+ const CONTEXT_DELETE_TOOL_NAME = "context_delete";
14
+ const CONTEXT_GREP_DELETE_TOOL_NAME = "context_grep_delete";
15
+ const CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME = "context_search_transcript";
16
+ const CONTEXT_READ_ENTRY_TOOL_NAME = "context_read_entry";
17
+ export const CONTEXT_COMPACTION_MAX_TURNS = 50;
18
+ const CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES = 50;
19
+ const CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS = 512;
20
+ const CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS = 250_000;
21
+ const CONTEXT_MANIFEST_MAX_ENTRIES = 80;
22
+ const CONTEXT_MANIFEST_PREVIEW_CHARS = 240;
23
+ const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
24
+ const CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS = 4000;
25
+ const CONTEXT_READ_ENTRY_MAX_CHARS = 12_000;
26
+ const CONTEXT_SEARCH_DEFAULT_MAX_MATCHES = 20;
27
+ const CONTEXT_SEARCH_MAX_MATCHES = 100;
28
+ const CONTEXT_SEARCH_DEFAULT_CONTEXT_CHARS = 160;
29
+ const CONTEXT_SEARCH_MAX_CONTEXT_CHARS = 500;
30
+ const ContextDeleteToolParameters = Type.Object({
9
31
  deletions: Type.Array(Type.Object({
10
32
  kind: StringEnum(["entry", "content_block"], {
11
33
  description: "Delete an entire transcript entry or a single content block within one entry.",
@@ -17,21 +39,77 @@ const ContextDeletionPlanToolParameters = Type.Object({
17
39
  })),
18
40
  }, { additionalProperties: false }), { description: "Deletion targets only. Protected entries and recent active context must not be included." }),
19
41
  }, { additionalProperties: false });
20
- const CONTEXT_DELETION_PLAN_TOOL = {
21
- name: CONTEXT_DELETION_PLAN_TOOL_NAME,
22
- description: "Emit the final context compaction deletion plan as structured data.",
23
- parameters: ContextDeletionPlanToolParameters,
42
+ const ContextGrepDeleteToolParameters = Type.Object({
43
+ pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to match in transcript text." }),
44
+ regex: Type.Optional(Type.Boolean({ description: "Treat pattern as a JavaScript regular expression. Defaults to false." })),
45
+ caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
46
+ target: Type.Optional(StringEnum(["entry", "content_block"], {
47
+ description: "Delete whole matching entries or matching content blocks. Defaults to entry.",
48
+ })),
49
+ maxMatches: Type.Optional(Type.Integer({
50
+ minimum: 1,
51
+ maximum: 200,
52
+ description: "Safety cap. If more unprotected, not-yet-deleted candidate targets are found, no deletions are applied. Defaults to 50.",
53
+ })),
54
+ expectedMatchCount: Type.Optional(Type.Integer({
55
+ minimum: 0,
56
+ description: "Optional safety check. If the match count differs, no deletions are applied.",
57
+ })),
58
+ }, { additionalProperties: false });
59
+ const ContextSearchTranscriptToolParameters = Type.Object({
60
+ pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to search for." }),
61
+ regex: Type.Optional(Type.Boolean({ description: "Treat pattern as a JavaScript regular expression. Defaults to false." })),
62
+ caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
63
+ target: Type.Optional(StringEnum(["entry", "content_block"], {
64
+ description: "Search whole entry text or individual content-block text. Defaults to entry.",
65
+ })),
66
+ maxMatches: Type.Optional(Type.Integer({ minimum: 1, maximum: CONTEXT_SEARCH_MAX_MATCHES, description: "Maximum matches to return. Defaults to 20." })),
67
+ contextChars: Type.Optional(Type.Integer({
68
+ minimum: 0,
69
+ maximum: CONTEXT_SEARCH_MAX_CONTEXT_CHARS,
70
+ description: "Characters of context to include before and after each match. Defaults to 160.",
71
+ })),
72
+ }, { additionalProperties: false });
73
+ const ContextReadEntryToolParameters = Type.Object({
74
+ entryId: Type.String({ minLength: 1, description: "Stable transcript entry id to read." }),
75
+ blockIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Optional content block index to read instead of the whole entry text." })),
76
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset to begin reading. Defaults to 0." })),
77
+ maxChars: Type.Optional(Type.Integer({
78
+ minimum: 1,
79
+ maximum: CONTEXT_READ_ENTRY_MAX_CHARS,
80
+ description: "Maximum characters to return. Defaults to 4000; keep reads small to avoid overflowing context.",
81
+ })),
82
+ }, { additionalProperties: false });
83
+ const CONTEXT_DELETE_TOOL = {
84
+ name: CONTEXT_DELETE_TOOL_NAME,
85
+ description: "Record context compaction deletion targets directly against the transcript.",
86
+ parameters: ContextDeleteToolParameters,
87
+ };
88
+ const CONTEXT_GREP_DELETE_TOOL = {
89
+ name: CONTEXT_GREP_DELETE_TOOL_NAME,
90
+ description: "Bulk-delete transcript entries or content blocks matching a guarded grep/regex query.",
91
+ parameters: ContextGrepDeleteToolParameters,
92
+ };
93
+ const CONTEXT_SEARCH_TRANSCRIPT_TOOL = {
94
+ name: CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME,
95
+ description: "Search the full transcript working copy and return small snippets without mutating deletion state.",
96
+ parameters: ContextSearchTranscriptToolParameters,
97
+ };
98
+ const CONTEXT_READ_ENTRY_TOOL = {
99
+ name: CONTEXT_READ_ENTRY_TOOL_NAME,
100
+ description: "Read a small slice of one transcript entry or content block from the full transcript working copy.",
101
+ parameters: ContextReadEntryToolParameters,
24
102
  };
25
- const CONTEXT_COMPACTION_SYSTEM_PROMPT = "You are a context compaction planner for an AI coding assistant transcript. Call the context_deletion_plan tool with deletion targets only.";
26
- const CONTEXT_COMPACTION_FIXED_PROMPT = `You are a context compaction planner for an AI coding assistant transcript.
103
+ const CONTEXT_COMPACTION_SYSTEM_PROMPT = `You are a context compaction assistant.
27
104
 
28
- Your task is deletion-only verbatim compaction.
105
+ 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.`;
106
+ 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.
29
107
 
30
108
  You MUST NOT summarize.
31
109
  You MUST NOT paraphrase.
32
110
  You MUST NOT generate replacement context.
33
111
  You MUST NOT mutate retained transcript objects or content.
34
- Another step will apply deletions locally. Return only deletion targets by stable ID.
112
+ Deletion tool calls are the compaction action; record only deletion targets by stable ID.
35
113
 
36
114
  What Gets Deleted:
37
115
  - Redundant tool outputs: file reads already acted on, grep/search results already processed, passing test output no longer needed.
@@ -47,13 +125,19 @@ What Survives:
47
125
  - User instructions: The original task and any clarifications.
48
126
 
49
127
  <output_format>
50
- Call the context_deletion_plan tool exactly once with deletion targets in this shape:
128
+ Call the context_delete tool one or more times with deletion targets in this shape:
51
129
  { "deletions": [{ "kind": "entry", "entryId": "..." }] }
52
130
 
53
131
  For content-block deletions, use:
54
132
  { "kind": "content_block", "entryId": "...", "blockIndex": 0 }
55
133
 
56
- Do not write JSON or prose in a text response. The tool call is the final answer.
134
+ The tool applies and validates deletion targets immediately. You can continue calling it for additional deletions if useful.
135
+
136
+ 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.
137
+
138
+ 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.
139
+
140
+ When you are done, reply with a brief plain-text completion message. Do not write deletion JSON or deletion target IDs outside tool calls.
57
141
  </output_format>`;
58
142
  function getMessageFromEntry(entry) {
59
143
  if (entry.type === "message") {
@@ -239,7 +323,7 @@ function isProtectedEntry(entry, message, recentEntryIds) {
239
323
  return true;
240
324
  return false;
241
325
  }
242
- export function prepareContextCompaction(pathEntries, settings) {
326
+ export function prepareContextCompaction(pathEntries, settings, options = {}) {
243
327
  if (pathEntries.length === 0)
244
328
  return undefined;
245
329
  const latestCompactionIndex = collectLatestSummaryCompactionIndex(pathEntries);
@@ -251,7 +335,7 @@ export function prepareContextCompaction(pathEntries, settings) {
251
335
  .map((index) => filteredEntryById.get(pathEntries[index].id))
252
336
  .filter((entry) => entry !== undefined && getContextEligibleMessageFromEntry(entry) !== undefined)
253
337
  .map((entry) => entry.id);
254
- const recentEntryIds = new Set(messageEntryIds.slice(-5));
338
+ const recentEntryIds = new Set(messageEntryIds.slice(-CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT));
255
339
  const protectedEntryIds = new Set();
256
340
  const entries = [];
257
341
  if (latestCompactionIndex >= 0) {
@@ -306,6 +390,7 @@ export function prepareContextCompaction(pathEntries, settings) {
306
390
  return undefined;
307
391
  return {
308
392
  branchEntries: pathEntries,
393
+ mode: options.mode ?? "standard",
309
394
  transcript: {
310
395
  entries,
311
396
  protectedEntryIds: [...protectedEntryIds],
@@ -325,6 +410,14 @@ function normalizeRawTarget(target) {
325
410
  return { kind: "entry", entryId: target.entryId };
326
411
  return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
327
412
  }
413
+ function rawDeletionFromTarget(target) {
414
+ if (target.kind === "entry")
415
+ return { kind: "entry", entryId: target.entryId };
416
+ return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
417
+ }
418
+ function deletionRequestFromTargets(targets) {
419
+ return { deletions: targets.map(rawDeletionFromTarget) };
420
+ }
328
421
  function getDeletedEntryIds(targets) {
329
422
  return new Set(targets.filter((target) => target.kind === "entry").map((target) => target.entryId));
330
423
  }
@@ -347,6 +440,153 @@ function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBl
347
440
  return false;
348
441
  return entry.contentBlocks.some((block) => block.toolCallId === callId && deletedBlocks.has(block.blockIndex));
349
442
  }
443
+ function toolCallBlockIndexes(entry, callId) {
444
+ return entry.contentBlocks
445
+ .filter((block) => block.toolCallId === callId)
446
+ .map((block) => block.blockIndex);
447
+ }
448
+ function addTarget(targets, target) {
449
+ if (targets.some((existing) => targetKey(existing) === targetKey(target)))
450
+ return false;
451
+ targets.push(target);
452
+ return true;
453
+ }
454
+ function deleteEntryTarget(targets, entryId) {
455
+ let changed = false;
456
+ for (let index = targets.length - 1; index >= 0; index--) {
457
+ const target = targets[index];
458
+ if (target.kind === "content_block" && target.entryId === entryId) {
459
+ targets.splice(index, 1);
460
+ changed = true;
461
+ }
462
+ }
463
+ return addTarget(targets, { kind: "entry", entryId }) || changed;
464
+ }
465
+ function removeEntryDeletion(targets, entryId) {
466
+ const originalLength = targets.length;
467
+ for (let index = targets.length - 1; index >= 0; index--) {
468
+ const target = targets[index];
469
+ if (target.kind === "entry" && target.entryId === entryId)
470
+ targets.splice(index, 1);
471
+ }
472
+ return targets.length !== originalLength;
473
+ }
474
+ function mergeContextDeletionTargets(baseTargets, additionalTargets) {
475
+ const targets = [...baseTargets];
476
+ for (const target of additionalTargets) {
477
+ if (target.kind === "entry") {
478
+ deleteEntryTarget(targets, target.entryId);
479
+ continue;
480
+ }
481
+ if (!getDeletedEntryIds(targets).has(target.entryId)) {
482
+ addTarget(targets, target);
483
+ }
484
+ }
485
+ return targets;
486
+ }
487
+ function canonicalizeEntryTargets(targets, entry) {
488
+ if (entry.protected || getDeletedEntryIds(targets).has(entry.entryId))
489
+ return false;
490
+ const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
491
+ if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
492
+ return false;
493
+ // Only repair/promote when dependency reconciliation reaches this entry. Non-tool entries that
494
+ // request every block individually stay invalid so the assistant must choose explicit entry deletion.
495
+ return deleteEntryTarget(targets, entry.entryId);
496
+ }
497
+ function removeToolCallDeletion(targets, entry, callId) {
498
+ let changed = removeEntryDeletion(targets, entry.entryId);
499
+ const blockIndexes = new Set(toolCallBlockIndexes(entry, callId));
500
+ for (let index = targets.length - 1; index >= 0; index--) {
501
+ const target = targets[index];
502
+ if (target.kind === "content_block" && target.entryId === entry.entryId && blockIndexes.has(target.blockIndex)) {
503
+ targets.splice(index, 1);
504
+ changed = true;
505
+ }
506
+ }
507
+ return changed;
508
+ }
509
+ function addToolCallDeletion(targets, entry, callId) {
510
+ if (entry.protected)
511
+ return false;
512
+ let changed = false;
513
+ for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
514
+ if (!getDeletedEntryIds(targets).has(entry.entryId)) {
515
+ changed = addTarget(targets, { kind: "content_block", entryId: entry.entryId, blockIndex }) || changed;
516
+ }
517
+ }
518
+ return canonicalizeEntryTargets(targets, entry) || changed;
519
+ }
520
+ let warnedReconciliationNonConvergence = false;
521
+ function reconcileToolDependencies(transcript, initialTargets) {
522
+ const targets = [...initialTargets];
523
+ const callEntries = new Map();
524
+ const entriesWithToolCalls = new Set();
525
+ const resultEntries = new Map();
526
+ for (const entry of transcript.entries) {
527
+ for (const callId of entry.toolCallIds) {
528
+ callEntries.set(callId, entry);
529
+ entriesWithToolCalls.add(entry);
530
+ }
531
+ if (entry.toolResultFor) {
532
+ const results = resultEntries.get(entry.toolResultFor) ?? [];
533
+ results.push(entry);
534
+ resultEntries.set(entry.toolResultFor, results);
535
+ }
536
+ }
537
+ // Bounded fixpoint repair: each pass can add/remove paired call/result targets. In practice this
538
+ // converges within one or two passes; the cap protects against accidental oscillation.
539
+ let changed = true;
540
+ let remainingPasses = Math.max(1, transcript.entries.length * 2);
541
+ while (changed && remainingPasses > 0) {
542
+ changed = false;
543
+ remainingPasses -= 1;
544
+ let deletedEntryIds = getDeletedEntryIds(targets);
545
+ let deletedContentBlocks = getDeletedContentBlocks(targets);
546
+ const recordChange = (nextChanged) => {
547
+ if (!nextChanged)
548
+ return;
549
+ changed = true;
550
+ deletedEntryIds = getDeletedEntryIds(targets);
551
+ deletedContentBlocks = getDeletedContentBlocks(targets);
552
+ };
553
+ for (const [callId, callEntry] of callEntries) {
554
+ const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
555
+ const results = resultEntries.get(callId) ?? [];
556
+ if (callDeleted) {
557
+ const retainedProtectedResult = results.find((entry) => entry.protected && !deletedEntryIds.has(entry.entryId));
558
+ if (retainedProtectedResult) {
559
+ recordChange(removeToolCallDeletion(targets, callEntry, callId));
560
+ }
561
+ else {
562
+ for (const result of results) {
563
+ recordChange(deleteEntryTarget(targets, result.entryId));
564
+ }
565
+ }
566
+ }
567
+ if (isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks))
568
+ continue;
569
+ for (const result of results) {
570
+ if (!deletedEntryIds.has(result.entryId))
571
+ continue;
572
+ recordChange(deleteEntryTarget(targets, result.entryId));
573
+ if (callEntry.protected) {
574
+ recordChange(removeEntryDeletion(targets, result.entryId));
575
+ continue;
576
+ }
577
+ recordChange(addToolCallDeletion(targets, callEntry, callId));
578
+ }
579
+ }
580
+ for (const entry of entriesWithToolCalls) {
581
+ recordChange(canonicalizeEntryTargets(targets, entry));
582
+ }
583
+ }
584
+ if (changed && !warnedReconciliationNonConvergence) {
585
+ warnedReconciliationNonConvergence = true;
586
+ console.warn(`Context compaction tool dependency reconciliation did not converge within the bounded pass limit; validation will continue with the last reconciled target set. entries=${transcript.entries.length} callEntries=${callEntries.size} targets=${targets.length}`);
587
+ }
588
+ return targets;
589
+ }
350
590
  function validateToolDependencies(transcript, targets) {
351
591
  const deletedEntryIds = getDeletedEntryIds(targets);
352
592
  const deletedContentBlocks = getDeletedContentBlocks(targets);
@@ -415,15 +655,44 @@ function computeContextCompactionStats(transcript, targets) {
415
655
  percentReduction,
416
656
  };
417
657
  }
418
- export function validateContextDeletionPlan(plan, transcript) {
419
- if (!plan || typeof plan !== "object" || !Array.isArray(plan.deletions)) {
420
- throw new Error("Context deletion plan must be an object with a deletions array");
658
+ function isCriticalOverflowProtectedEntryDeletable(entry, transcript) {
659
+ if (!entry.protected)
660
+ return true;
661
+ const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
662
+ if (entryIndex < 0)
663
+ return false;
664
+ const recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);
665
+ if (entryIndex >= recentBoundary)
666
+ return false;
667
+ if (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {
668
+ return false;
669
+ }
670
+ return (entry.role === "user" ||
671
+ entry.role === "custom" ||
672
+ entry.role === "branchSummary" ||
673
+ entry.role === "compactionSummary" ||
674
+ entry.entryType === "branch_summary");
675
+ }
676
+ function canDeleteProtectedTargetInMode(transcript, target, mode) {
677
+ if (mode !== "critical_overflow")
678
+ return false;
679
+ const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
680
+ if (!entry || !isCriticalOverflowProtectedEntryDeletable(entry, transcript))
681
+ return false;
682
+ if (target.kind === "entry")
683
+ return true;
684
+ const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
685
+ return block !== undefined;
686
+ }
687
+ export function validateContextDeletionRequest(request, transcript, options = {}) {
688
+ const mode = options.mode ?? "standard";
689
+ if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
690
+ throw new Error("Context deletion request must be an object with a deletions array");
421
691
  }
422
692
  const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
423
693
  const seen = new Set();
424
- const deletedEntryIds = new Set();
425
694
  const deletedTargets = [];
426
- for (const deletion of plan.deletions) {
695
+ for (const deletion of request.deletions) {
427
696
  if (!deletion || typeof deletion !== "object") {
428
697
  throw new Error("Deletion target must be an object");
429
698
  }
@@ -437,7 +706,7 @@ export function validateContextDeletionPlan(plan, transcript) {
437
706
  if (!entry) {
438
707
  throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
439
708
  }
440
- if (entry.protected) {
709
+ if (entry.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
441
710
  throw new Error(`Deletion target ${deletion.entryId} is protected`);
442
711
  }
443
712
  if (deletion.kind === "content_block") {
@@ -448,7 +717,7 @@ export function validateContextDeletionPlan(plan, transcript) {
448
717
  if (!block) {
449
718
  throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
450
719
  }
451
- if (block.protected) {
720
+ if (block.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
452
721
  throw new Error(`Content block ${deletion.entryId}:${deletion.blockIndex} is protected`);
453
722
  }
454
723
  if (entry.contentBlocks.length <= 1) {
@@ -462,34 +731,34 @@ export function validateContextDeletionPlan(plan, transcript) {
462
731
  seen.add(key);
463
732
  const normalized = normalizeRawTarget(deletion);
464
733
  deletedTargets.push(normalized);
465
- if (normalized.kind === "entry")
466
- deletedEntryIds.add(normalized.entryId);
467
734
  }
468
- for (const target of deletedTargets) {
469
- if (target.kind === "content_block" && deletedEntryIds.has(target.entryId)) {
735
+ const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
736
+ const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
737
+ for (const target of reconciledTargets) {
738
+ if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
470
739
  throw new Error(`Deletion target ${targetKey(target)} overlaps with entry deletion`);
471
740
  }
472
741
  }
473
- const deletedContentBlocks = getDeletedContentBlocks(deletedTargets);
742
+ const deletedContentBlocks = getDeletedContentBlocks(reconciledTargets);
474
743
  for (const [entryId, blockIndexes] of deletedContentBlocks) {
475
744
  const entry = entryById.get(entryId);
476
745
  if (entry?.contentBlocks.every((block) => blockIndexes.has(block.blockIndex))) {
477
746
  throw new Error(`Content-block deletions for ${entryId} would remove every content block`);
478
747
  }
479
748
  }
480
- validateToolDependencies(transcript, deletedTargets);
481
- const remainingEntries = transcript.entries.filter((entry) => !deletedEntryIds.has(entry.entryId));
749
+ validateToolDependencies(transcript, reconciledTargets);
750
+ const remainingEntries = transcript.entries.filter((entry) => !reconciledDeletedEntryIds.has(entry.entryId));
482
751
  if (remainingEntries.length === 0) {
483
- throw new Error("Deletion plan would remove all context entries");
752
+ throw new Error("Deletion request would remove all context entries");
484
753
  }
485
754
  const hasTaskBearingContext = remainingEntries.some((entry) => entry.role === "user" || (entry.role === "compactionSummary" && entry.protected));
486
755
  if (!hasTaskBearingContext) {
487
- throw new Error("Deletion plan would leave no user task in context");
756
+ throw new Error("Deletion request would leave no user task in context");
488
757
  }
489
758
  return {
490
- deletedTargets,
759
+ deletedTargets: reconciledTargets,
491
760
  protectedEntryIds: [...transcript.protectedEntryIds],
492
- stats: computeContextCompactionStats(transcript, deletedTargets),
761
+ stats: computeContextCompactionStats(transcript, reconciledTargets),
493
762
  };
494
763
  }
495
764
  function stripJsonFence(text) {
@@ -504,25 +773,641 @@ function stripJsonFence(text) {
504
773
  return trimmed;
505
774
  return trimmed.slice(firstLineEnd + 1, -3).trim();
506
775
  }
507
- function rawContextDeletionPlanFromObject(value, source) {
776
+ function contextDeletionRequestFromObject(value, source) {
508
777
  if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
509
778
  throw new Error(`${source} must contain a deletions array`);
510
779
  }
511
780
  return value;
512
781
  }
513
- export function parseContextDeletionPlan(text) {
782
+ function escapeRegExpLiteral(text) {
783
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
784
+ }
785
+ function formatErrorMessage(error) {
786
+ return error instanceof Error ? error.message : String(error);
787
+ }
788
+ function createContextDeletionToolResult(text, details) {
789
+ return { content: [{ type: "text", text }], details, terminate: false };
790
+ }
791
+ function assertSafeRegexPattern(pattern) {
792
+ if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
793
+ throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
794
+ }
795
+ // Heuristic ReDoS guard for common catastrophic-backtracking shapes. JavaScript's RegExp engine
796
+ // does not expose a timeout, so reject nested quantified groups and backreferences instead of
797
+ // relying only on transcript scan-size caps.
798
+ const hasNestedQuantifiedGroup = /\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)\s*(?:[+*]|\{\d)/u.test(pattern);
799
+ const hasQuantifiedAlternation = /\((?:[^()\\]|\\.)*\|(?:[^()\\]|\\.)*\)\s*(?:[+*]|\{\d)/u.test(pattern);
800
+ const hasBackreference = /\\[1-9]/u.test(pattern);
801
+ if (hasNestedQuantifiedGroup || hasQuantifiedAlternation || hasBackreference) {
802
+ throw new Error("Regex pattern is not allowed because it may cause excessive backtracking; use a literal pattern or exact deletion targets instead.");
803
+ }
804
+ }
805
+ function createGrepMatcher(pattern, regex, caseSensitive) {
806
+ if (regex) {
807
+ assertSafeRegexPattern(pattern);
808
+ }
809
+ try {
810
+ return new RegExp(regex ? pattern : escapeRegExpLiteral(pattern), caseSensitive ? "u" : "iu");
811
+ }
812
+ catch (error) {
813
+ throw new Error(`Invalid grep ${regex ? "regex" : "pattern"}: ${formatErrorMessage(error)}`);
814
+ }
815
+ }
816
+ function assertSafeRegexScan(scanChars) {
817
+ if (scanChars <= CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS)
818
+ return;
819
+ throw new Error(`Regex grep would scan ${scanChars} characters; maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS}. Use a literal pattern or exact deletion targets instead.`);
820
+ }
821
+ function clampInteger(value, defaultValue, minimum, maximum) {
822
+ if (value === undefined)
823
+ return defaultValue;
824
+ return Math.max(minimum, Math.min(maximum, value));
825
+ }
826
+ function textSlice(text, offset, maxChars) {
827
+ return text.slice(offset, Math.min(text.length, offset + maxChars));
828
+ }
829
+ function findMatchIndex(matcher, text) {
830
+ const match = matcher.exec(text);
831
+ matcher.lastIndex = 0;
832
+ return match?.index ?? -1;
833
+ }
834
+ function snippetForMatch(text, matchIndex, contextChars) {
835
+ const start = Math.max(0, matchIndex - contextChars);
836
+ const end = Math.min(text.length, matchIndex + contextChars);
837
+ const prefix = start > 0 ? "…" : "";
838
+ const suffix = end < text.length ? "…" : "";
839
+ return `${prefix}${text.slice(start, end)}${suffix}`;
840
+ }
841
+ function currentTargetDeleted(targets, target) {
842
+ const deletedEntryIds = getDeletedEntryIds(targets);
843
+ if (deletedEntryIds.has(target.entryId))
844
+ return true;
845
+ if (target.kind === "entry")
846
+ return false;
847
+ return getDeletedContentBlocks(targets).get(target.entryId)?.has(target.blockIndex) === true;
848
+ }
849
+ function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
850
+ const key = targetKey(candidate);
851
+ if (seenTargets.has(key))
852
+ return;
853
+ seenTargets.add(key);
854
+ candidates.push(candidate);
855
+ matches.push(match);
856
+ }
857
+ const moduleRequire = createRequire(import.meta.url);
858
+ function createTransientSqliteDatabase() {
859
+ // better-sqlite3 is the portable/package dependency for Node-based Atomic installs.
860
+ // Bun cannot dlopen better-sqlite3 yet, so Bun runtime/tests use the API-compatible builtin.
861
+ if (process.versions.bun) {
862
+ const sqlite = moduleRequire("bun:sqlite");
863
+ return new sqlite.Database(":memory:");
864
+ }
865
+ const BetterSqliteDatabase = moduleRequire("better-sqlite3");
866
+ return new BetterSqliteDatabase(":memory:");
867
+ }
868
+ class SqliteAdapter {
869
+ constructor(db) {
870
+ this.db = db;
871
+ }
872
+ static createTransient() {
873
+ return new SqliteAdapter(createTransientSqliteDatabase());
874
+ }
875
+ exec(sql) {
876
+ this.db.exec(sql);
877
+ }
878
+ run(sql, ...params) {
879
+ this.db.prepare(sql).run(...params);
880
+ }
881
+ all(sql, ...params) {
882
+ return this.db.prepare(sql).all(...params);
883
+ }
884
+ get(sql, ...params) {
885
+ return this.db.prepare(sql).get(...params);
886
+ }
887
+ transaction(operation) {
888
+ this.exec("BEGIN IMMEDIATE");
889
+ try {
890
+ const result = operation();
891
+ this.exec("COMMIT");
892
+ return result;
893
+ }
894
+ catch (error) {
895
+ try {
896
+ this.exec("ROLLBACK");
897
+ }
898
+ catch { }
899
+ throw error;
900
+ }
901
+ }
902
+ close() {
903
+ this.db.close();
904
+ }
905
+ }
906
+ class ContextDeletionSqliteStore {
907
+ constructor(sqlite) {
908
+ this.sqlite = sqlite;
909
+ }
910
+ initialize(transcript) {
911
+ this.sqlite.transaction(() => {
912
+ this.sqlite.exec(`
913
+ PRAGMA foreign_keys = ON;
914
+ CREATE TABLE transcript_entries (
915
+ position INTEGER PRIMARY KEY,
916
+ entry_id TEXT NOT NULL UNIQUE,
917
+ role TEXT NOT NULL,
918
+ is_protected INTEGER NOT NULL,
919
+ token_estimate INTEGER NOT NULL,
920
+ text TEXT NOT NULL,
921
+ tool_result_for TEXT
922
+ );
923
+ CREATE TABLE transcript_content_blocks (
924
+ entry_id TEXT NOT NULL,
925
+ block_index INTEGER NOT NULL,
926
+ type TEXT NOT NULL,
927
+ is_protected INTEGER NOT NULL,
928
+ token_estimate INTEGER NOT NULL,
929
+ text TEXT NOT NULL,
930
+ tool_call_id TEXT,
931
+ PRIMARY KEY (entry_id, block_index),
932
+ FOREIGN KEY (entry_id) REFERENCES transcript_entries(entry_id) ON DELETE CASCADE
933
+ );
934
+ CREATE TABLE deletion_targets (
935
+ position INTEGER PRIMARY KEY AUTOINCREMENT,
936
+ target_key TEXT NOT NULL UNIQUE,
937
+ kind TEXT NOT NULL CHECK (kind IN ('entry', 'content_block')),
938
+ entry_id TEXT NOT NULL,
939
+ block_index INTEGER
940
+ );
941
+ CREATE TABLE context_compaction_state (
942
+ key TEXT PRIMARY KEY,
943
+ value TEXT NOT NULL
944
+ );
945
+ INSERT INTO context_compaction_state (key, value) VALUES ('call_count', '0');
946
+ `);
947
+ for (const [position, entry] of transcript.entries.entries()) {
948
+ this.sqlite.run("INSERT INTO transcript_entries (position, entry_id, role, is_protected, token_estimate, text, tool_result_for) VALUES (?, ?, ?, ?, ?, ?, ?)", position, entry.entryId, entry.role, entry.protected ? 1 : 0, entry.tokenEstimate, entry.text, entry.toolResultFor ?? null);
949
+ for (const block of entry.contentBlocks) {
950
+ this.sqlite.run("INSERT INTO transcript_content_blocks (entry_id, block_index, type, is_protected, token_estimate, text, tool_call_id) VALUES (?, ?, ?, ?, ?, ?, ?)", block.entryId, block.blockIndex, block.type, block.protected ? 1 : 0, block.tokenEstimate, block.text, block.toolCallId ?? null);
951
+ }
952
+ }
953
+ });
954
+ }
955
+ transaction(operation) {
956
+ return this.sqlite.transaction(operation);
957
+ }
958
+ readTargets() {
959
+ return this.sqlite
960
+ .all("SELECT kind, entry_id, block_index FROM deletion_targets ORDER BY position")
961
+ .map((row) => row.kind === "entry"
962
+ ? { kind: "entry", entryId: row.entry_id }
963
+ : { kind: "content_block", entryId: row.entry_id, blockIndex: row.block_index });
964
+ }
965
+ replaceTargets(targets) {
966
+ this.sqlite.run("DELETE FROM deletion_targets");
967
+ for (const target of targets) {
968
+ this.sqlite.run("INSERT INTO deletion_targets (target_key, kind, entry_id, block_index) VALUES (?, ?, ?, ?)", targetKey(target), target.kind, target.entryId, target.kind === "content_block" ? target.blockIndex : null);
969
+ }
970
+ }
971
+ listEntriesForGrep() {
972
+ return this.sqlite.all("SELECT entry_id, text, is_protected FROM transcript_entries ORDER BY position");
973
+ }
974
+ listContentBlocksForGrep() {
975
+ return this.sqlite.all(`
976
+ SELECT
977
+ blocks.entry_id,
978
+ blocks.block_index,
979
+ blocks.text,
980
+ entries.is_protected AS entry_protected,
981
+ blocks.is_protected AS block_protected,
982
+ (
983
+ SELECT COUNT(*)
984
+ FROM transcript_content_blocks sibling
985
+ WHERE sibling.entry_id = blocks.entry_id
986
+ ) AS block_count
987
+ FROM transcript_content_blocks blocks
988
+ JOIN transcript_entries entries ON entries.entry_id = blocks.entry_id
989
+ ORDER BY entries.position, blocks.block_index
990
+ `);
991
+ }
992
+ getEntryForRead(entryId) {
993
+ return this.sqlite.get("SELECT entry_id, role, is_protected, token_estimate, text FROM transcript_entries WHERE entry_id = ?", entryId);
994
+ }
995
+ getContentBlockForRead(entryId, blockIndex) {
996
+ return this.sqlite.get(`
997
+ SELECT
998
+ blocks.entry_id,
999
+ blocks.block_index,
1000
+ blocks.type,
1001
+ blocks.token_estimate,
1002
+ blocks.text,
1003
+ entries.is_protected AS entry_protected,
1004
+ blocks.is_protected AS block_protected,
1005
+ (
1006
+ SELECT COUNT(*)
1007
+ FROM transcript_content_blocks sibling
1008
+ WHERE sibling.entry_id = blocks.entry_id
1009
+ ) AS block_count
1010
+ FROM transcript_content_blocks blocks
1011
+ JOIN transcript_entries entries ON entries.entry_id = blocks.entry_id
1012
+ WHERE blocks.entry_id = ? AND blocks.block_index = ?
1013
+ `, entryId, blockIndex);
1014
+ }
1015
+ getGrepScanTextLength(target) {
1016
+ const table = target === "entry" ? "transcript_entries" : "transcript_content_blocks";
1017
+ const row = this.sqlite.get(`SELECT SUM(LENGTH(text)) AS scan_chars FROM ${table}`);
1018
+ return row?.scan_chars ?? 0;
1019
+ }
1020
+ incrementCallCount() {
1021
+ const next = this.getCallCount() + 1;
1022
+ this.setState("call_count", String(next));
1023
+ return next;
1024
+ }
1025
+ getCallCount() {
1026
+ return Number(this.getState("call_count") ?? "0");
1027
+ }
1028
+ setLastError(message) {
1029
+ this.setState("last_error", message);
1030
+ }
1031
+ clearLastError() {
1032
+ this.sqlite.run("DELETE FROM context_compaction_state WHERE key = ?", "last_error");
1033
+ }
1034
+ getLastError() {
1035
+ return this.getState("last_error");
1036
+ }
1037
+ getState(key) {
1038
+ return this.sqlite.get("SELECT value FROM context_compaction_state WHERE key = ?", key)?.value;
1039
+ }
1040
+ setState(key, value) {
1041
+ this.sqlite.run("INSERT INTO context_compaction_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", key, value);
1042
+ }
1043
+ close() {
1044
+ this.sqlite.close();
1045
+ }
1046
+ }
1047
+ function createContextDeletionSqliteStore(transcript) {
1048
+ const store = new ContextDeletionSqliteStore(SqliteAdapter.createTransient());
1049
+ store.initialize(transcript);
1050
+ return store;
1051
+ }
1052
+ export function createContextDeletionTool(transcript, options = {}) {
1053
+ const mode = options.mode ?? "standard";
1054
+ const store = createContextDeletionSqliteStore(transcript);
1055
+ let validatedResult;
1056
+ function readTargets() {
1057
+ return store.readTargets();
1058
+ }
1059
+ function applyValidatedTargets(additionalTargets) {
1060
+ const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
1061
+ validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript, { mode });
1062
+ store.replaceTargets(validatedResult.deletedTargets);
1063
+ return validatedResult;
1064
+ }
1065
+ function currentStats() {
1066
+ return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
1067
+ }
1068
+ function canDeleteProtectedTarget(target) {
1069
+ return canDeleteProtectedTargetInMode(transcript, target, mode);
1070
+ }
1071
+ const tool = {
1072
+ ...CONTEXT_DELETE_TOOL,
1073
+ label: "context deletion request",
1074
+ executionMode: "parallel",
1075
+ async execute(_toolCallId, params) {
1076
+ return store.transaction(() => {
1077
+ const callCount = store.incrementCallCount();
1078
+ try {
1079
+ const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
1080
+ const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript, { mode });
1081
+ const applied = applyValidatedTargets(incomingValidated.deletedTargets);
1082
+ store.clearLastError();
1083
+ const deletedTargets = readTargets();
1084
+ const details = {
1085
+ deletions: deletionRequestFromTargets(deletedTargets).deletions,
1086
+ deletedTargets,
1087
+ stats: applied.stats,
1088
+ callCount,
1089
+ };
1090
+ const text = `Recorded ${incomingValidated.deletedTargets.length} deletion target(s); ${deletedTargets.length} total validated deletion target(s) are selected. Continue calling ${CONTEXT_DELETE_TOOL_NAME} or ${CONTEXT_GREP_DELETE_TOOL_NAME} for additional deletions, or respond done when finished.`;
1091
+ return createContextDeletionToolResult(text, details);
1092
+ }
1093
+ catch (error) {
1094
+ const message = formatErrorMessage(error);
1095
+ store.setLastError(message);
1096
+ const deletedTargets = readTargets();
1097
+ const details = {
1098
+ deletions: deletionRequestFromTargets(deletedTargets).deletions,
1099
+ deletedTargets,
1100
+ stats: currentStats(),
1101
+ callCount,
1102
+ error: message,
1103
+ };
1104
+ return createContextDeletionToolResult(`Error recording context deletion targets: ${message}. No new deletion targets were applied; continue with a corrected tool call.`, details);
1105
+ }
1106
+ });
1107
+ },
1108
+ };
1109
+ const grepTool = {
1110
+ ...CONTEXT_GREP_DELETE_TOOL,
1111
+ label: "context grep delete",
1112
+ executionMode: "parallel",
1113
+ async execute(_toolCallId, params) {
1114
+ return store.transaction(() => {
1115
+ const callCount = store.incrementCallCount();
1116
+ const pattern = params.pattern;
1117
+ const regex = params.regex === true;
1118
+ const caseSensitive = params.caseSensitive === true;
1119
+ const target = params.target ?? "entry";
1120
+ const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
1121
+ const candidates = [];
1122
+ const matches = [];
1123
+ const skipped = [];
1124
+ const seenTargets = new Set();
1125
+ try {
1126
+ if (regex) {
1127
+ assertSafeRegexScan(store.getGrepScanTextLength(target));
1128
+ }
1129
+ const matcher = createGrepMatcher(pattern, regex, caseSensitive);
1130
+ const currentTargets = readTargets();
1131
+ if (target === "entry") {
1132
+ for (const entry of store.listEntriesForGrep()) {
1133
+ if (!matcher.test(entry.text))
1134
+ continue;
1135
+ const candidate = { kind: "entry", entryId: entry.entry_id };
1136
+ if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1137
+ skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
1138
+ continue;
1139
+ }
1140
+ if (currentTargetDeleted(currentTargets, candidate)) {
1141
+ skipped.push({ entryId: entry.entry_id, target, reason: "already_deleted", text: entry.text });
1142
+ continue;
1143
+ }
1144
+ addGrepCandidate(candidates, matches, seenTargets, candidate, {
1145
+ entryId: entry.entry_id,
1146
+ target,
1147
+ text: entry.text,
1148
+ });
1149
+ }
1150
+ }
1151
+ else {
1152
+ for (const block of store.listContentBlocksForGrep()) {
1153
+ if (!matcher.test(block.text))
1154
+ continue;
1155
+ const candidate = block.block_count <= 1
1156
+ ? { kind: "entry", entryId: block.entry_id }
1157
+ : { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
1158
+ if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1159
+ skipped.push({
1160
+ entryId: block.entry_id,
1161
+ target,
1162
+ blockIndex: block.block_index,
1163
+ reason: "protected_entry",
1164
+ text: block.text,
1165
+ });
1166
+ continue;
1167
+ }
1168
+ if (block.block_protected === 1 && !canDeleteProtectedTarget(candidate)) {
1169
+ skipped.push({
1170
+ entryId: block.entry_id,
1171
+ target,
1172
+ blockIndex: block.block_index,
1173
+ reason: "protected_block",
1174
+ text: block.text,
1175
+ });
1176
+ continue;
1177
+ }
1178
+ if (currentTargetDeleted(currentTargets, candidate)) {
1179
+ skipped.push({
1180
+ entryId: block.entry_id,
1181
+ target: candidate.kind,
1182
+ ...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
1183
+ reason: "already_deleted",
1184
+ text: block.text,
1185
+ });
1186
+ continue;
1187
+ }
1188
+ addGrepCandidate(candidates, matches, seenTargets, candidate, {
1189
+ entryId: block.entry_id,
1190
+ target: candidate.kind,
1191
+ ...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
1192
+ text: block.text,
1193
+ });
1194
+ }
1195
+ }
1196
+ let applied;
1197
+ if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
1198
+ skipped.push({ reason: "expected_match_count_mismatch" });
1199
+ }
1200
+ else if (candidates.length > maxMatches) {
1201
+ skipped.push({ reason: "max_matches_exceeded" });
1202
+ }
1203
+ else if (candidates.length > 0) {
1204
+ applied = applyValidatedTargets(candidates);
1205
+ }
1206
+ store.clearLastError();
1207
+ const deletedTargets = readTargets();
1208
+ const details = {
1209
+ pattern,
1210
+ regex,
1211
+ caseSensitive,
1212
+ target,
1213
+ matches,
1214
+ skipped,
1215
+ deletedTargets,
1216
+ stats: applied?.stats ?? currentStats(),
1217
+ callCount,
1218
+ };
1219
+ 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}.`;
1220
+ return createContextDeletionToolResult(text, details);
1221
+ }
1222
+ catch (error) {
1223
+ const message = formatErrorMessage(error);
1224
+ store.setLastError(message);
1225
+ const deletedTargets = readTargets();
1226
+ const details = {
1227
+ pattern,
1228
+ regex,
1229
+ caseSensitive,
1230
+ target,
1231
+ matches,
1232
+ skipped,
1233
+ deletedTargets,
1234
+ stats: currentStats(),
1235
+ callCount,
1236
+ error: message,
1237
+ };
1238
+ return createContextDeletionToolResult(`Error applying grep deletion for pattern ${JSON.stringify(pattern)}: ${message}. No new deletion targets were applied; continue with a corrected tool call.`, details);
1239
+ }
1240
+ });
1241
+ },
1242
+ };
1243
+ const searchTool = {
1244
+ ...CONTEXT_SEARCH_TRANSCRIPT_TOOL,
1245
+ label: "context transcript search",
1246
+ executionMode: "parallel",
1247
+ async execute(_toolCallId, params) {
1248
+ return store.transaction(() => {
1249
+ const callCount = store.incrementCallCount();
1250
+ const pattern = params.pattern;
1251
+ const regex = params.regex === true;
1252
+ const caseSensitive = params.caseSensitive === true;
1253
+ const target = params.target ?? "entry";
1254
+ const maxMatches = clampInteger(params.maxMatches, CONTEXT_SEARCH_DEFAULT_MAX_MATCHES, 1, CONTEXT_SEARCH_MAX_MATCHES);
1255
+ const contextChars = clampInteger(params.contextChars, CONTEXT_SEARCH_DEFAULT_CONTEXT_CHARS, 0, CONTEXT_SEARCH_MAX_CONTEXT_CHARS);
1256
+ const matches = [];
1257
+ let truncated = false;
1258
+ try {
1259
+ if (regex) {
1260
+ assertSafeRegexScan(store.getGrepScanTextLength(target));
1261
+ }
1262
+ const matcher = createGrepMatcher(pattern, regex, caseSensitive);
1263
+ if (target === "entry") {
1264
+ for (const entry of store.listEntriesForGrep()) {
1265
+ const matchIndex = findMatchIndex(matcher, entry.text);
1266
+ if (matchIndex < 0)
1267
+ continue;
1268
+ if (matches.length >= maxMatches) {
1269
+ truncated = true;
1270
+ break;
1271
+ }
1272
+ matches.push({
1273
+ entryId: entry.entry_id,
1274
+ target,
1275
+ matchIndex,
1276
+ snippet: snippetForMatch(entry.text, matchIndex, contextChars),
1277
+ protected: entry.is_protected === 1,
1278
+ });
1279
+ }
1280
+ }
1281
+ else {
1282
+ for (const block of store.listContentBlocksForGrep()) {
1283
+ const matchIndex = findMatchIndex(matcher, block.text);
1284
+ if (matchIndex < 0)
1285
+ continue;
1286
+ if (matches.length >= maxMatches) {
1287
+ truncated = true;
1288
+ break;
1289
+ }
1290
+ matches.push({
1291
+ entryId: block.entry_id,
1292
+ target,
1293
+ blockIndex: block.block_index,
1294
+ matchIndex,
1295
+ snippet: snippetForMatch(block.text, matchIndex, contextChars),
1296
+ protected: block.entry_protected === 1 || block.block_protected === 1,
1297
+ });
1298
+ }
1299
+ }
1300
+ store.clearLastError();
1301
+ const details = {
1302
+ pattern,
1303
+ regex,
1304
+ caseSensitive,
1305
+ target,
1306
+ matches,
1307
+ truncated,
1308
+ callCount,
1309
+ };
1310
+ const text = `Found ${matches.length}${truncated ? "+" : ""} ${target} match(es) for ${JSON.stringify(pattern)}. Use ${CONTEXT_READ_ENTRY_TOOL_NAME} with small maxChars to inspect exact content before deleting.`;
1311
+ return createContextDeletionToolResult(text, details);
1312
+ }
1313
+ catch (error) {
1314
+ const message = formatErrorMessage(error);
1315
+ store.setLastError(message);
1316
+ const details = {
1317
+ pattern,
1318
+ regex,
1319
+ caseSensitive,
1320
+ target,
1321
+ matches,
1322
+ truncated,
1323
+ callCount,
1324
+ error: message,
1325
+ };
1326
+ return createContextDeletionToolResult(`Error searching transcript for ${JSON.stringify(pattern)}: ${message}. Try a literal pattern or narrower query.`, details);
1327
+ }
1328
+ });
1329
+ },
1330
+ };
1331
+ const readEntryTool = {
1332
+ ...CONTEXT_READ_ENTRY_TOOL,
1333
+ label: "context read entry",
1334
+ executionMode: "parallel",
1335
+ async execute(_toolCallId, params) {
1336
+ return store.transaction(() => {
1337
+ const callCount = store.incrementCallCount();
1338
+ const offset = clampInteger(params.offset, 0, 0, Number.MAX_SAFE_INTEGER);
1339
+ const maxChars = clampInteger(params.maxChars, CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS, 1, CONTEXT_READ_ENTRY_MAX_CHARS);
1340
+ try {
1341
+ const row = params.blockIndex === undefined
1342
+ ? store.getEntryForRead(params.entryId)
1343
+ : store.getContentBlockForRead(params.entryId, params.blockIndex);
1344
+ if (!row) {
1345
+ throw new Error(params.blockIndex === undefined
1346
+ ? `Unknown transcript entry: ${params.entryId}`
1347
+ : `Unknown transcript content block: ${params.entryId}:${params.blockIndex}`);
1348
+ }
1349
+ const text = row.text;
1350
+ const slice = textSlice(text, offset, maxChars);
1351
+ store.clearLastError();
1352
+ const details = {
1353
+ entryId: params.entryId,
1354
+ ...(params.blockIndex === undefined ? {} : { blockIndex: params.blockIndex }),
1355
+ offset,
1356
+ maxChars,
1357
+ totalChars: text.length,
1358
+ text: slice,
1359
+ truncatedBefore: offset > 0,
1360
+ truncatedAfter: offset + maxChars < text.length,
1361
+ callCount,
1362
+ };
1363
+ const textResult = `Read ${slice.length} of ${text.length} characters from ${params.blockIndex === undefined ? params.entryId : `${params.entryId}:${params.blockIndex}`}. Keep reads small; increase offset for the next slice if needed.`;
1364
+ return createContextDeletionToolResult(textResult, details);
1365
+ }
1366
+ catch (error) {
1367
+ const message = formatErrorMessage(error);
1368
+ store.setLastError(message);
1369
+ const details = {
1370
+ entryId: params.entryId,
1371
+ ...(params.blockIndex === undefined ? {} : { blockIndex: params.blockIndex }),
1372
+ offset,
1373
+ maxChars,
1374
+ totalChars: 0,
1375
+ text: "",
1376
+ truncatedBefore: false,
1377
+ truncatedAfter: false,
1378
+ callCount,
1379
+ error: message,
1380
+ };
1381
+ return createContextDeletionToolResult(`Error reading transcript entry: ${message}`, details);
1382
+ }
1383
+ });
1384
+ },
1385
+ };
1386
+ return {
1387
+ tool,
1388
+ grepTool,
1389
+ searchTool,
1390
+ readEntryTool,
1391
+ tools: [tool, grepTool, searchTool, readEntryTool],
1392
+ getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
1393
+ getValidatedResult: () => validatedResult,
1394
+ getLastError: () => store.getLastError(),
1395
+ getCallCount: () => store.getCallCount(),
1396
+ };
1397
+ }
1398
+ export function parseContextDeletionRequest(text) {
514
1399
  const stripped = stripJsonFence(text);
515
1400
  let parsed;
516
1401
  try {
517
1402
  parsed = JSON.parse(stripped);
518
1403
  }
519
1404
  catch (error) {
520
- throw new Error(`Failed to parse context deletion plan JSON: ${error instanceof Error ? error.message : String(error)}`);
1405
+ throw new Error(`Failed to parse context deletion request JSON: ${error instanceof Error ? error.message : String(error)}`);
521
1406
  }
522
- return rawContextDeletionPlanFromObject(parsed, "Context deletion plan JSON");
1407
+ return contextDeletionRequestFromObject(parsed, "Context deletion request JSON");
523
1408
  }
524
- function isContextDeletionPlanToolCall(content) {
525
- return content.type === "toolCall" && content.name === CONTEXT_DELETION_PLAN_TOOL_NAME;
1409
+ function isContextDeleteToolCall(content) {
1410
+ return content.type === "toolCall" && content.name === CONTEXT_DELETE_TOOL_NAME;
526
1411
  }
527
1412
  function textContentFromResponse(response) {
528
1413
  return response.content
@@ -530,73 +1415,222 @@ function textContentFromResponse(response) {
530
1415
  .map((content) => content.text)
531
1416
  .join("\n");
532
1417
  }
533
- export function parseContextDeletionPlanResponse(response) {
534
- const toolCalls = response.content.filter(isContextDeletionPlanToolCall);
1418
+ export function parseContextDeletionResponse(response) {
1419
+ const toolCalls = response.content.filter(isContextDeleteToolCall);
535
1420
  if (toolCalls.length > 1) {
536
- throw new Error(`Context compaction planner called ${CONTEXT_DELETION_PLAN_TOOL_NAME} more than once`);
1421
+ throw new Error(`Context compaction assistant called ${CONTEXT_DELETE_TOOL_NAME} more than once`);
537
1422
  }
538
1423
  const toolCall = toolCalls[0];
539
1424
  if (toolCall) {
540
- return rawContextDeletionPlanFromObject(toolCall.arguments, `${CONTEXT_DELETION_PLAN_TOOL_NAME} arguments`);
1425
+ return contextDeletionRequestFromObject(toolCall.arguments, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
541
1426
  }
542
1427
  const textContent = textContentFromResponse(response);
543
1428
  if (textContent.trim().length === 0) {
544
- throw new Error(`Context compaction planner did not call ${CONTEXT_DELETION_PLAN_TOOL_NAME}`);
1429
+ throw new Error(`Context compaction assistant did not call ${CONTEXT_DELETE_TOOL_NAME}`);
545
1430
  }
546
- return parseContextDeletionPlan(textContent);
1431
+ return parseContextDeletionRequest(textContent);
547
1432
  }
548
1433
  function truncateForPrompt(text, maxChars) {
549
1434
  if (text.length <= maxChars)
550
1435
  return text;
551
- return `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} more characters omitted from planner prompt]`;
1436
+ return `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} more characters omitted from context compaction prompt]`;
552
1437
  }
553
- function plannerTranscriptPayload(transcript) {
554
- return transcript.entries
555
- .filter((entry) => !isExcludedFromLlmContext(entry.message))
556
- .map((entry) => ({
1438
+ function transcriptEntryFilePayload(entry) {
1439
+ return {
557
1440
  entryId: entry.entryId,
1441
+ entryType: entry.entryType,
558
1442
  role: entry.role,
559
1443
  protected: entry.protected,
560
1444
  tokenEstimate: entry.tokenEstimate,
561
1445
  toolCallIds: entry.toolCallIds,
562
1446
  toolResultFor: entry.toolResultFor,
1447
+ text: entry.text,
563
1448
  contentBlocks: entry.contentBlocks.map((block) => ({
564
1449
  blockIndex: block.blockIndex,
565
1450
  type: block.type,
566
1451
  protected: block.protected,
567
1452
  toolCallId: block.toolCallId,
568
- text: truncateForPrompt(block.text, 2000),
1453
+ tokenEstimate: block.tokenEstimate,
1454
+ text: block.text,
569
1455
  })),
570
- text: truncateForPrompt(entry.text, 4000),
571
- }));
1456
+ };
1457
+ }
1458
+ function writeContextCompactionTranscriptFile(transcript) {
1459
+ const directory = mkdtempSync(join(tmpdir(), "atomic-context-transcript-"));
1460
+ const path = join(directory, "transcript.jsonl");
1461
+ const lines = transcript.entries
1462
+ .filter((entry) => !isExcludedFromLlmContext(entry.message))
1463
+ .map((entry) => JSON.stringify(transcriptEntryFilePayload(entry)));
1464
+ writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
1465
+ return {
1466
+ path,
1467
+ cleanup: () => rmSync(directory, { recursive: true, force: true }),
1468
+ };
1469
+ }
1470
+ function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
1471
+ const eligibleEntries = transcript.entries.filter((entry) => !isExcludedFromLlmContext(entry.message));
1472
+ const selectedEntryIds = new Set();
1473
+ const selectedEntries = [];
1474
+ const addEntry = (entry) => {
1475
+ if (selectedEntryIds.has(entry.entryId) || selectedEntries.length >= CONTEXT_MANIFEST_MAX_ENTRIES)
1476
+ return;
1477
+ selectedEntryIds.add(entry.entryId);
1478
+ selectedEntries.push(entry);
1479
+ };
1480
+ for (const entry of eligibleEntries.filter((entry) => entry.protected)) {
1481
+ addEntry(entry);
1482
+ }
1483
+ for (const entry of [...eligibleEntries]
1484
+ .filter((entry) => !entry.protected)
1485
+ .sort((left, right) => right.tokenEstimate - left.tokenEstimate)) {
1486
+ addEntry(entry);
1487
+ }
1488
+ selectedEntries.sort((left, right) => eligibleEntries.indexOf(left) - eligibleEntries.indexOf(right));
1489
+ return {
1490
+ transcriptFilePath,
1491
+ transcriptFileFormat: "jsonl: one compactable transcript entry per line with full text and contentBlocks text",
1492
+ totalEntries: eligibleEntries.length,
1493
+ manifestEntries: selectedEntries.length,
1494
+ omittedEntries: Math.max(0, eligibleEntries.length - selectedEntries.length),
1495
+ tokensBefore: transcript.tokensBefore,
1496
+ protectedEntryIds: transcript.protectedEntryIds,
1497
+ entries: selectedEntries.map((entry) => ({
1498
+ entryId: entry.entryId,
1499
+ role: entry.role,
1500
+ protected: entry.protected,
1501
+ tokenEstimate: entry.tokenEstimate,
1502
+ toolCallIds: entry.toolCallIds,
1503
+ toolResultFor: entry.toolResultFor,
1504
+ contentBlockCount: entry.contentBlocks.length,
1505
+ contentBlocks: entry.contentBlocks.map((block) => ({
1506
+ blockIndex: block.blockIndex,
1507
+ type: block.type,
1508
+ protected: block.protected,
1509
+ toolCallId: block.toolCallId,
1510
+ tokenEstimate: block.tokenEstimate,
1511
+ })),
1512
+ preview: truncateForPrompt(entry.text, CONTEXT_MANIFEST_PREVIEW_CHARS),
1513
+ })),
1514
+ };
1515
+ }
1516
+ function contextCompactionModePrompt(mode) {
1517
+ if (mode === "critical_overflow") {
1518
+ 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, especially 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>`;
1519
+ }
1520
+ 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>`;
1521
+ }
1522
+ export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", mode = "standard") {
1523
+ 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>`;
572
1524
  }
573
- export function buildContextCompactionPrompt(transcript) {
574
- return `${CONTEXT_COMPACTION_FIXED_PROMPT}\n\n<transcript-json>\n${JSON.stringify(plannerTranscriptPayload(transcript), null, 2)}\n</transcript-json>`;
1525
+ function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
1526
+ return {
1527
+ role: "assistant",
1528
+ content,
1529
+ api: model.api,
1530
+ provider: model.provider,
1531
+ model: model.id,
1532
+ usage: {
1533
+ input: 0,
1534
+ output: 0,
1535
+ cacheRead: 0,
1536
+ cacheWrite: 0,
1537
+ totalTokens: 0,
1538
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1539
+ },
1540
+ stopReason,
1541
+ ...(errorMessage !== undefined ? { errorMessage } : {}),
1542
+ timestamp: Date.now(),
1543
+ };
1544
+ }
1545
+ function createContextCompactionStopStream(model, text) {
1546
+ const stream = createAssistantMessageEventStream();
1547
+ queueMicrotask(() => {
1548
+ const message = createContextCompactionAssistantMessage(model, [{ type: "text", text }], "stop");
1549
+ stream.push({ type: "done", reason: "stop", message });
1550
+ stream.end(message);
1551
+ });
1552
+ return stream;
1553
+ }
1554
+ function isContextCompactionOverflowError(model, errorMessage) {
1555
+ return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
1556
+ }
1557
+ export function getLowestContextCompactionThinkingLevel(model) {
1558
+ const supportedLevels = getSupportedThinkingLevels(model);
1559
+ for (const level of CONTEXT_COMPACTION_THINKING_LEVEL_ORDER) {
1560
+ if (supportedLevels.includes(level))
1561
+ return level;
1562
+ }
1563
+ return "off";
575
1564
  }
576
- export async function planContextDeletions(transcript, model, apiKey, headers, signal, thinkingLevel) {
1565
+ async function runContextDeletionAssistant(transcript, model, apiKey, headers, signal, mode = "standard") {
577
1566
  const maxTokens = Math.min(4096, model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
578
- const messages = [
579
- {
580
- role: "user",
581
- content: [{ type: "text", text: buildContextCompactionPrompt(transcript) }],
582
- timestamp: Date.now(),
1567
+ if (signal?.aborted) {
1568
+ throw new Error("Context compaction failed: Request was aborted");
1569
+ }
1570
+ const transcriptFile = writeContextCompactionTranscriptFile(transcript);
1571
+ const promptMessage = {
1572
+ role: "user",
1573
+ content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, mode) }],
1574
+ timestamp: Date.now(),
1575
+ };
1576
+ const deletionTool = createContextDeletionTool(transcript, { mode });
1577
+ const effectiveThinkingLevel = getLowestContextCompactionThinkingLevel(model);
1578
+ let compactionTurnCount = 0;
1579
+ const agent = new Agent({
1580
+ initialState: {
1581
+ systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
1582
+ model,
1583
+ thinkingLevel: effectiveThinkingLevel,
1584
+ tools: deletionTool.tools,
1585
+ },
1586
+ toolExecution: "parallel",
1587
+ streamFn: async (requestModel, context, streamOptions) => {
1588
+ compactionTurnCount += 1;
1589
+ if (compactionTurnCount > CONTEXT_COMPACTION_MAX_TURNS) {
1590
+ return createContextCompactionStopStream(requestModel, `Reached the context compaction turn cap (${CONTEXT_COMPACTION_MAX_TURNS}); using the deletions recorded so far.`);
1591
+ }
1592
+ return streamSimple(requestModel, context, {
1593
+ ...streamOptions,
1594
+ maxTokens,
1595
+ apiKey,
1596
+ headers: headers ?? streamOptions?.headers,
1597
+ });
583
1598
  },
584
- ];
585
- const options = model.reasoning && thinkingLevel && thinkingLevel !== "off"
586
- ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
587
- : { maxTokens, signal, apiKey, headers };
588
- const response = await completeSimple(model, { systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT, messages, tools: [CONTEXT_DELETION_PLAN_TOOL] }, options);
589
- if (response.stopReason === "error") {
590
- throw new Error(`Context compaction planning failed: ${response.errorMessage || "Unknown error"}`);
591
- }
592
- return parseContextDeletionPlanResponse(response);
593
- }
594
- export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel) {
595
- const plan = await planContextDeletions(preparation.transcript, model, apiKey, headers, signal, thinkingLevel);
596
- const validated = validateContextDeletionPlan(plan, preparation.transcript);
597
- if (validated.deletedTargets.length === 0) {
598
- throw new Error("No safe context deletions proposed");
599
- }
600
- return validated;
1599
+ });
1600
+ const abortOnSignal = () => agent.abort();
1601
+ signal?.addEventListener("abort", abortOnSignal, { once: true });
1602
+ try {
1603
+ await agent.prompt(promptMessage);
1604
+ }
1605
+ finally {
1606
+ signal?.removeEventListener("abort", abortOnSignal);
1607
+ transcriptFile.cleanup();
1608
+ }
1609
+ if (signal?.aborted) {
1610
+ throw new Error("Context compaction failed: Request was aborted");
1611
+ }
1612
+ if (agent.state.errorMessage) {
1613
+ if (isContextCompactionOverflowError(model, agent.state.errorMessage)) {
1614
+ return {
1615
+ validatedResult: deletionTool.getValidatedResult(),
1616
+ lastToolError: deletionTool.getLastError(),
1617
+ };
1618
+ }
1619
+ throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
1620
+ }
1621
+ if (deletionTool.getCallCount() === 0) {
1622
+ 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})`);
1623
+ }
1624
+ return {
1625
+ validatedResult: deletionTool.getValidatedResult(),
1626
+ lastToolError: deletionTool.getLastError(),
1627
+ };
1628
+ }
1629
+ export async function contextCompact(preparation, model, apiKey, headers, signal, _thinkingLevel, mode = preparation.mode ?? "standard") {
1630
+ const { validatedResult, lastToolError } = await runContextDeletionAssistant(preparation.transcript, model, apiKey, headers, signal, mode);
1631
+ if (!validatedResult || validatedResult.deletedTargets.length === 0) {
1632
+ throw new Error(lastToolError ? `No safe context deletions proposed; last deletion tool error: ${lastToolError}` : "No safe context deletions proposed");
1633
+ }
1634
+ return validatedResult;
601
1635
  }
602
1636
  //# sourceMappingURL=context-compaction.js.map