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