opencode-acp 1.17.1 → 1.18.0-pr.392.147

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 (40) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +999 -236
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/compress/range-utils.d.ts +16 -1
  5. package/dist/lib/compress/range-utils.d.ts.map +1 -1
  6. package/dist/lib/compress/range.d.ts.map +1 -1
  7. package/dist/lib/compress/search.d.ts +3 -1
  8. package/dist/lib/compress/search.d.ts.map +1 -1
  9. package/dist/lib/compress/status.d.ts +1 -1
  10. package/dist/lib/compress/status.d.ts.map +1 -1
  11. package/dist/lib/config-validation.d.ts.map +1 -1
  12. package/dist/lib/config.d.ts +7 -1
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/hooks.d.ts.map +1 -1
  15. package/dist/lib/messages/inject/candidates.d.ts +30 -0
  16. package/dist/lib/messages/inject/candidates.d.ts.map +1 -0
  17. package/dist/lib/messages/inject/inject.d.ts +1 -1
  18. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  19. package/dist/lib/messages/sync.d.ts +1 -1
  20. package/dist/lib/messages/sync.d.ts.map +1 -1
  21. package/dist/lib/prompts/compress-range.d.ts +9 -1
  22. package/dist/lib/prompts/compress-range.d.ts.map +1 -1
  23. package/dist/lib/prompts/context-limit-nudge.d.ts +6 -0
  24. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  25. package/dist/lib/prompts/iteration-nudge.d.ts +5 -0
  26. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  27. package/dist/lib/prompts/store.d.ts +2 -1
  28. package/dist/lib/prompts/store.d.ts.map +1 -1
  29. package/dist/lib/prompts/system.d.ts +10 -1
  30. package/dist/lib/prompts/system.d.ts.map +1 -1
  31. package/dist/lib/prompts/turn-nudge.d.ts +5 -0
  32. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  33. package/dist/lib/state/rebuild.d.ts +8 -0
  34. package/dist/lib/state/rebuild.d.ts.map +1 -1
  35. package/dist/lib/state/state.d.ts.map +1 -1
  36. package/dist/lib/state/types.d.ts +2 -0
  37. package/dist/lib/state/types.d.ts.map +1 -1
  38. package/dist/lib/state/utils.d.ts +1 -0
  39. package/dist/lib/state/utils.d.ts.map +1 -1
  40. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -885,6 +885,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
885
885
  "compress.permission",
886
886
  "compress.showCompression",
887
887
  "compress.summaryBuffer",
888
+ "compress.candidates",
888
889
  "compress.maxContextLimit",
889
890
  "compress.minContextLimit",
890
891
  "compress.modelMaxLimits",
@@ -1759,6 +1760,7 @@ var defaultConfig = {
1759
1760
  permission: "allow",
1760
1761
  showCompression: true,
1761
1762
  summaryBuffer: true,
1763
+ candidates: false,
1762
1764
  maxContextLimit: "80%",
1763
1765
  minContextLimit: "80%",
1764
1766
  contextLimitFallback: 128e3,
@@ -1927,6 +1929,7 @@ function mergeCompress(base, override) {
1927
1929
  permission: override.permission ?? base.permission,
1928
1930
  showCompression: override.showCompression ?? base.showCompression,
1929
1931
  summaryBuffer: override.summaryBuffer ?? base.summaryBuffer,
1932
+ candidates: override.candidates ?? base.candidates,
1930
1933
  maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,
1931
1934
  minContextLimit: override.minContextLimit ?? base.minContextLimit,
1932
1935
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
@@ -2513,12 +2516,12 @@ function serializePruneMessagesState(messagesState) {
2513
2516
  markedForCleanup: Array.from(messagesState.markedForCleanup)
2514
2517
  };
2515
2518
  }
2516
- async function isSubAgentSession(client, sessionID) {
2519
+ async function getSessionParentId(client, sessionID) {
2517
2520
  try {
2518
2521
  const result = await client.session.get({ path: { id: sessionID } });
2519
- return !!result.data?.parentID;
2522
+ return typeof result.data?.parentID === "string" ? result.data.parentID : void 0;
2520
2523
  } catch (error) {
2521
- return false;
2524
+ return void 0;
2522
2525
  }
2523
2526
  }
2524
2527
  function findLastCompactionTimestamp(messages) {
@@ -2559,7 +2562,8 @@ function createPruneMessagesState() {
2559
2562
  activeByAnchorMessageId: /* @__PURE__ */ new Map(),
2560
2563
  nextBlockId: 1,
2561
2564
  nextRunId: 1,
2562
- markedForCleanup: /* @__PURE__ */ new Set()
2565
+ markedForCleanup: /* @__PURE__ */ new Set(),
2566
+ membershipsVerified: false
2563
2567
  };
2564
2568
  }
2565
2569
  function loadPruneMessagesState(persisted) {
@@ -3691,7 +3695,7 @@ function clampMessageRef(requested, context, state) {
3691
3695
  if (maxIndex < 0 || requested.index <= maxIndex) return null;
3692
3696
  return { ref: formatMessageRef(maxIndex) };
3693
3697
  }
3694
- function resolveSelection(context, startReference, endReference) {
3698
+ function resolveSelection(context, startReference, endReference, options) {
3695
3699
  const startRawIndex = startReference.rawIndex;
3696
3700
  const endRawIndex = endReference.rawIndex;
3697
3701
  const messageIds = [];
@@ -3714,7 +3718,7 @@ function resolveSelection(context, startReference, endReference) {
3714
3718
  messageSeen.add(messageId);
3715
3719
  messageIds.push(messageId);
3716
3720
  }
3717
- if (!messageTokenById.has(messageId)) {
3721
+ if (options?.includeTokenAccounting !== false && !messageTokenById.has(messageId)) {
3718
3722
  messageTokenById.set(messageId, estimateAllMessageTokensFast(rawMessage));
3719
3723
  }
3720
3724
  const parts = Array.isArray(rawMessage.parts) ? rawMessage.parts : [];
@@ -3825,10 +3829,10 @@ function buildBoundaryLookup(context, state) {
3825
3829
  }
3826
3830
  return lookup;
3827
3831
  }
3828
- var SEARCH_CONTEXT_TOOL_DESCRIPTION = `Search through all compressed block summaries AND visible messages to find relevant content. Use this BEFORE decompressing to find the right block. Returns a hit list with block/message IDs, relevance scores, and previews.
3832
+ var SEARCH_CONTEXT_TOOL_DESCRIPTION = `Search through active compressed block summaries to find relevant content. Use this BEFORE decompressing to find the right block. Returns a hit list with block IDs, relevance scores, and previews.
3829
3833
 
3830
3834
  Examples:
3831
- - search_context({ query: "decoder accuracy" }) \u2014 find blocks/messages about decoder accuracy
3835
+ - search_context({ query: "decoder accuracy" }) \u2014 find compressed blocks about decoder accuracy
3832
3836
  - search_context({ query: "training loss PPL" }) \u2014 find training results
3833
3837
  - search_context({ query: "architecture design", limit: 5 }) \u2014 top 5 results`;
3834
3838
  function countOccurrences(text, term) {
@@ -3859,7 +3863,7 @@ function createSearchContextTool(factoryCtx) {
3859
3863
  query: tool.schema.string().describe("Search query \u2014 keywords or phrase to find"),
3860
3864
  limit: tool.schema.number().optional().describe("Maximum results to return (default: 10)"),
3861
3865
  deep: tool.schema.boolean().optional().describe(
3862
- "If true, also search visible (uncompressed) messages. Slower but more thorough (default: false)"
3866
+ "Reserved for compatibility; the current search indexes active compressed block summaries."
3863
3867
  )
3864
3868
  },
3865
3869
  async execute(args, toolCtx) {
@@ -4311,7 +4315,7 @@ function validateArgs(args) {
4311
4315
  }
4312
4316
  }
4313
4317
  }
4314
- function resolveRanges(args, searchContext, state, logger) {
4318
+ function resolveRanges(args, searchContext, state, logger, options) {
4315
4319
  return args.content.map((entry, index) => {
4316
4320
  const normalizedEntry = {
4317
4321
  topic: typeof entry.topic === "string" && entry.topic.trim().length > 0 ? entry.topic.trim() : void 0,
@@ -4326,7 +4330,7 @@ function resolveRanges(args, searchContext, state, logger) {
4326
4330
  normalizedEntry.endId,
4327
4331
  logger
4328
4332
  );
4329
- const selection = resolveSelection(searchContext, startReference, endReference);
4333
+ const selection = resolveSelection(searchContext, startReference, endReference, options);
4330
4334
  return {
4331
4335
  index,
4332
4336
  entry: normalizedEntry,
@@ -4359,6 +4363,54 @@ function validateNonOverlapping(plans) {
4359
4363
  );
4360
4364
  }
4361
4365
  }
4366
+ function prepareExecutableRangePlans(args, searchContext, state, config, logger, options) {
4367
+ const normalizedArgs = "content" in args ? args : {
4368
+ content: [args]
4369
+ };
4370
+ const resolvedPlans = resolveRanges(normalizedArgs, searchContext, state, logger, options);
4371
+ validateNonOverlapping(resolvedPlans);
4372
+ const plans = resolvedPlans.map((plan) => ({
4373
+ ...plan,
4374
+ selection: filterProtectedToolMessages(
4375
+ plan.selection,
4376
+ searchContext,
4377
+ config.compress.protectedTools,
4378
+ config.protectedFilePatterns
4379
+ )
4380
+ })).map((plan) => ({
4381
+ ...plan,
4382
+ selection: filterLastUserMessage(plan.selection, searchContext, state, config.compress)
4383
+ })).map((plan) => ({
4384
+ ...plan,
4385
+ selection: filterProtectedRecentMessages(
4386
+ plan.selection,
4387
+ searchContext,
4388
+ state,
4389
+ config.compress
4390
+ )
4391
+ })).filter((plan) => plan.selection.messageIds.length > 0);
4392
+ if (plans.length === 0) {
4393
+ throw new Error(
4394
+ "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
4395
+ );
4396
+ }
4397
+ const counted = /* @__PURE__ */ new Set();
4398
+ let totalChars = 0;
4399
+ for (const plan of plans) {
4400
+ for (const messageId of plan.selection.messageIds) {
4401
+ if (counted.has(messageId)) continue;
4402
+ counted.add(messageId);
4403
+ const rawMessage = searchContext.rawMessagesById.get(messageId);
4404
+ if (rawMessage) totalChars += countMessageCharacters(rawMessage);
4405
+ }
4406
+ }
4407
+ if (config.compress.minCompressRange > 0 && totalChars < config.compress.minCompressRange) {
4408
+ throw new Error(
4409
+ `Range too small (${totalChars} chars, min ${config.compress.minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
4410
+ );
4411
+ }
4412
+ return { plans, totalChars };
4413
+ }
4362
4414
  function parseBlockPlaceholders(summary) {
4363
4415
  const placeholders = [];
4364
4416
  const regex = new RegExp(BLOCK_PLACEHOLDER_REGEX);
@@ -4430,6 +4482,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
4430
4482
  }
4431
4483
 
4432
4484
  // lib/state/rebuild.ts
4485
+ import { createHash } from "crypto";
4433
4486
  function collectCompressInvocations(messages) {
4434
4487
  const invocations = [];
4435
4488
  for (const message of messages) {
@@ -4481,6 +4534,128 @@ function dedupeBlockIds(ids) {
4481
4534
  }
4482
4535
  return result;
4483
4536
  }
4537
+ function fingerprintMessage(message) {
4538
+ const parts = (message.parts ?? []).map((part) => {
4539
+ if (part.type === "text") {
4540
+ return { type: "text", text: part.text };
4541
+ }
4542
+ if (part.type === "tool") {
4543
+ if (part.tool === "compress") {
4544
+ return { type: "tool", tool: part.tool, status: part.state?.status };
4545
+ }
4546
+ return {
4547
+ type: "tool",
4548
+ tool: part.tool,
4549
+ status: part.state?.status,
4550
+ input: part.state?.input,
4551
+ output: part.state?.status === "completed" ? part.state.output : void 0
4552
+ };
4553
+ }
4554
+ return { type: part.type };
4555
+ });
4556
+ return createHash("sha256").update(JSON.stringify({ role: message.info.role, parts })).digest("hex");
4557
+ }
4558
+ function mapForkIds(state, parent, parentMessages, forkMessages) {
4559
+ const parentRefs = parent.messageIds?.byRef;
4560
+ if (!parentRefs) return null;
4561
+ const parentById = new Map(parentMessages.map((message) => [message.info.id, message]));
4562
+ const forkById = new Map(forkMessages.map((message) => [message.info.id, message]));
4563
+ const messages = /* @__PURE__ */ new Map();
4564
+ const tools = /* @__PURE__ */ new Map();
4565
+ for (const [ref, parentId] of Object.entries(parentRefs)) {
4566
+ const forkId = state.messageIds.byRef.get(ref);
4567
+ if (!forkId) continue;
4568
+ const parentMessage = parentById.get(parentId);
4569
+ const forkMessage = forkById.get(forkId);
4570
+ if (!parentMessage || !forkMessage) continue;
4571
+ if (fingerprintMessage(parentMessage) !== fingerprintMessage(forkMessage)) continue;
4572
+ messages.set(parentId, forkId);
4573
+ for (let index = 0; index < parentMessage.parts.length; index++) {
4574
+ const parentPart = parentMessage.parts[index];
4575
+ const forkPart = forkMessage.parts[index];
4576
+ if (parentPart?.type === "tool" && typeof parentPart.callID === "string" && forkPart?.type === "tool" && forkPart.tool === parentPart.tool && typeof forkPart.callID === "string") {
4577
+ tools.set(parentPart.callID, forkPart.callID);
4578
+ }
4579
+ }
4580
+ }
4581
+ return { messages, tools };
4582
+ }
4583
+ function translateIds(ids, mapped) {
4584
+ const translated = ids.map((id) => mapped.get(id));
4585
+ return translated.some((id) => !id) ? null : translated;
4586
+ }
4587
+ function restoreForkCompressionState(state, forkMessages, parent, parentMessages, logger) {
4588
+ if (!parent.prune.messages || !parent.messageIds) return 0;
4589
+ assignMessageRefs(state, forkMessages);
4590
+ const mapped = mapForkIds(state, parent, parentMessages, forkMessages);
4591
+ if (!mapped) return 0;
4592
+ const parentBlocks = Object.values(parent.prune.messages.blocksById);
4593
+ const translatedBlocks = /* @__PURE__ */ new Map();
4594
+ for (const block of parentBlocks) {
4595
+ const anchorMessageId = mapped.messages.get(block.anchorMessageId);
4596
+ const compressMessageId = mapped.messages.get(block.compressMessageId);
4597
+ const directMessageIds = translateIds(block.directMessageIds, mapped.messages);
4598
+ const effectiveMessageIds = translateIds(block.effectiveMessageIds, mapped.messages);
4599
+ const directToolIds = translateIds(block.directToolIds, mapped.tools);
4600
+ const effectiveToolIds = translateIds(block.effectiveToolIds, mapped.tools);
4601
+ const compressCallId = block.compressCallId ? mapped.tools.get(block.compressCallId) : void 0;
4602
+ if (!anchorMessageId || !compressMessageId || !directMessageIds || !effectiveMessageIds || !directToolIds || !effectiveToolIds || block.compressCallId && !compressCallId) {
4603
+ continue;
4604
+ }
4605
+ translatedBlocks.set(block.blockId, {
4606
+ ...block,
4607
+ anchorMessageId,
4608
+ compressMessageId,
4609
+ compressCallId,
4610
+ directMessageIds,
4611
+ directToolIds,
4612
+ effectiveMessageIds,
4613
+ effectiveToolIds
4614
+ });
4615
+ }
4616
+ const activeParentIds = new Set(parent.prune.messages.activeBlockIds);
4617
+ if (activeParentIds.size === 0 || Array.from(activeParentIds).some((blockId) => !translatedBlocks.has(blockId))) {
4618
+ return 0;
4619
+ }
4620
+ const copiedBlockIds = new Set(translatedBlocks.keys());
4621
+ for (const block of translatedBlocks.values()) {
4622
+ if (block.active && [...block.consumedBlockIds, ...block.includedBlockIds].some(
4623
+ (blockId) => !copiedBlockIds.has(blockId)
4624
+ )) {
4625
+ return 0;
4626
+ }
4627
+ }
4628
+ const messagesState = createPruneMessagesState();
4629
+ for (const block of translatedBlocks.values()) {
4630
+ messagesState.blocksById.set(block.blockId, block);
4631
+ if (block.active) {
4632
+ messagesState.activeBlockIds.add(block.blockId);
4633
+ messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
4634
+ }
4635
+ }
4636
+ for (const [parentMessageId, entry] of Object.entries(parent.prune.messages.byMessageId)) {
4637
+ const forkMessageId = mapped.messages.get(parentMessageId);
4638
+ if (!forkMessageId) continue;
4639
+ const allBlockIds = entry.allBlockIds.filter((blockId) => copiedBlockIds.has(blockId));
4640
+ if (allBlockIds.length === 0) continue;
4641
+ messagesState.byMessageId.set(forkMessageId, {
4642
+ tokenCount: entry.tokenCount,
4643
+ allBlockIds,
4644
+ activeBlockIds: allBlockIds.filter(
4645
+ (blockId) => messagesState.activeBlockIds.has(blockId)
4646
+ )
4647
+ });
4648
+ }
4649
+ messagesState.nextBlockId = Math.max(parent.prune.messages.nextBlockId, ...copiedBlockIds) + 1;
4650
+ messagesState.nextRunId = parent.prune.messages.nextRunId;
4651
+ messagesState.membershipsVerified = true;
4652
+ state.prune.messages = messagesState;
4653
+ logger.info("fork: restored compression state from parent", {
4654
+ blocks: copiedBlockIds.size,
4655
+ activeBlocks: messagesState.activeBlockIds.size
4656
+ });
4657
+ return copiedBlockIds.size;
4658
+ }
4484
4659
  function rebuildRangeInvocation(state, input, searchContext, invocation, protectedTools, protectedFilePatterns, gcConfig, logger) {
4485
4660
  const plans = resolveRanges(input, searchContext, state);
4486
4661
  const runId = allocateRunId(state);
@@ -4547,12 +4722,7 @@ function resolveMessageEntry(entry, searchContext, state) {
4547
4722
  return null;
4548
4723
  }
4549
4724
  try {
4550
- const { startReference, endReference } = resolveBoundaryIds(
4551
- searchContext,
4552
- state,
4553
- ref,
4554
- ref
4555
- );
4725
+ const { startReference, endReference } = resolveBoundaryIds(searchContext, state, ref, ref);
4556
4726
  const selection = resolveSelection(searchContext, startReference, endReference);
4557
4727
  return {
4558
4728
  selection,
@@ -4856,8 +5026,9 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
4856
5026
  resetSessionState(state);
4857
5027
  state.sessionId = sessionId;
4858
5028
  state.storageDir = config?.storagePath ? resolveStorageDir(config.storagePath, projectDir ?? cwd()) : void 0;
4859
- const isSubAgent = await isSubAgentSession(client, sessionId);
4860
- state.isSubAgent = isSubAgent;
5029
+ const parentSessionId = await getSessionParentId(client, sessionId);
5030
+ const isChildSession = parentSessionId !== void 0;
5031
+ state.isSubAgent = isChildSession;
4861
5032
  state.lastCompaction = findLastCompactionTimestamp(messages);
4862
5033
  state.currentTurn = countTurns(state, messages);
4863
5034
  state.nudges.turnNudgeAnchors = collectTurnNudgeAnchors(messages);
@@ -4871,13 +5042,42 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
4871
5042
  );
4872
5043
  }
4873
5044
  if (config) {
4874
- const rebuilt = rebuildCompressionState(state, messages, config, logger);
4875
- if (rebuilt > 0) {
5045
+ let restored = 0;
5046
+ if (parentSessionId) {
5047
+ try {
5048
+ const parent = await loadSessionState(parentSessionId, logger);
5049
+ const response = parent ? await client.session.messages({ path: { id: parentSessionId } }) : void 0;
5050
+ const parentMessages = Array.isArray(response?.data) ? response.data : Array.isArray(response) ? response : [];
5051
+ if (parent && parentMessages.length > 0) {
5052
+ state.isSubAgent = false;
5053
+ try {
5054
+ restored = restoreForkCompressionState(
5055
+ state,
5056
+ messages,
5057
+ parent,
5058
+ parentMessages,
5059
+ logger
5060
+ );
5061
+ } finally {
5062
+ state.isSubAgent = isChildSession;
5063
+ }
5064
+ }
5065
+ } catch (error) {
5066
+ logger.warn("fork: parent state transfer unavailable, replaying history", {
5067
+ parentSessionId,
5068
+ error: error?.message
5069
+ });
5070
+ }
5071
+ }
5072
+ const rebuilt = restored > 0 ? 0 : rebuildCompressionState(state, messages, config, logger);
5073
+ if (restored > 0 || rebuilt > 0) {
4876
5074
  await saveSessionState(state, logger);
4877
5075
  }
4878
5076
  }
5077
+ state.isSubAgent = isChildSession;
4879
5078
  return;
4880
5079
  }
5080
+ state.isSubAgent = isChildSession;
4881
5081
  state.prune.messages = loadPruneMessagesState(persisted.prune.messages);
4882
5082
  state.nudges.contextLimitAnchors = new Set(persisted.nudges.contextLimitAnchors || []);
4883
5083
  state.nudges.turnNudgeAnchors = /* @__PURE__ */ new Set([
@@ -6588,12 +6788,12 @@ ${afterClose}`;
6588
6788
  }
6589
6789
 
6590
6790
  // lib/messages/utils.ts
6591
- import { createHash } from "crypto";
6791
+ import { createHash as createHash2 } from "crypto";
6592
6792
  var SUMMARY_ID_HASH_LENGTH = 16;
6593
6793
  var DCP_PAIRED_TAG_REGEX = /<(?:dcp|acp)[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
6594
6794
  var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
6595
6795
  var generateStableId = (prefix, seed) => {
6596
- const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
6796
+ const hash = createHash2("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
6597
6797
  return `${prefix}_${hash}`;
6598
6798
  };
6599
6799
  var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
@@ -7590,58 +7790,13 @@ function createCompressRangeTool(factoryCtx) {
7590
7790
  ...ctx0,
7591
7791
  config: applyCompressOverrides(ctx0.config, providerId, modelId)
7592
7792
  };
7593
- const resolvedPlans = resolveRanges(input, searchContext, ctx.state, ctx.logger);
7594
- validateNonOverlapping(resolvedPlans);
7595
- const filteredPlans = resolvedPlans.map((plan) => ({
7596
- ...plan,
7597
- selection: filterProtectedToolMessages(
7598
- plan.selection,
7599
- searchContext,
7600
- ctx.config.compress.protectedTools,
7601
- ctx.config.protectedFilePatterns
7602
- )
7603
- })).map((plan) => ({
7604
- ...plan,
7605
- selection: filterLastUserMessage(
7606
- plan.selection,
7607
- searchContext,
7608
- ctx.state,
7609
- ctx.config.compress
7610
- )
7611
- })).map((plan) => ({
7612
- ...plan,
7613
- selection: filterProtectedRecentMessages(
7614
- plan.selection,
7615
- searchContext,
7616
- ctx.state,
7617
- ctx.config.compress
7618
- )
7619
- })).filter((plan) => plan.selection.messageIds.length > 0);
7620
- if (filteredPlans.length === 0) {
7621
- throw new Error(
7622
- "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
7623
- );
7624
- }
7625
- const minCompressRange = ctx.config.compress.minCompressRange;
7626
- if (minCompressRange > 0) {
7627
- let totalChars = 0;
7628
- const counted = /* @__PURE__ */ new Set();
7629
- for (const plan of filteredPlans) {
7630
- for (const messageId of plan.selection.messageIds) {
7631
- if (counted.has(messageId)) continue;
7632
- counted.add(messageId);
7633
- const rawMessage = searchContext.rawMessagesById.get(messageId);
7634
- if (rawMessage) {
7635
- totalChars += countMessageCharacters(rawMessage);
7636
- }
7637
- }
7638
- }
7639
- if (totalChars < minCompressRange) {
7640
- throw new Error(
7641
- `Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
7642
- );
7643
- }
7644
- }
7793
+ const { plans: filteredPlans } = prepareExecutableRangePlans(
7794
+ input,
7795
+ searchContext,
7796
+ ctx.state,
7797
+ ctx.config,
7798
+ ctx.logger
7799
+ );
7645
7800
  const notifications = [];
7646
7801
  let preparedPlans = [];
7647
7802
  let totalCompressedMessages = 0;
@@ -7746,7 +7901,9 @@ function createCompressRangeTool(factoryCtx) {
7746
7901
  const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
7747
7902
  const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
7748
7903
  if (ignoredAcknowledgeRisk) {
7749
- ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
7904
+ ctx.logger.warn(
7905
+ "compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending"
7906
+ );
7750
7907
  }
7751
7908
  ctx.state.qualityGateRetryPending = false;
7752
7909
  if (!bypassQuality) {
@@ -7916,10 +8073,24 @@ function sortBlocksByCreation(a, b) {
7916
8073
  }
7917
8074
  return a.blockId - b.blockId;
7918
8075
  }
8076
+ function sameBlockIds(left, right) {
8077
+ if (left.size !== right.size) return false;
8078
+ return Array.from(left).every((id) => right.has(id));
8079
+ }
7919
8080
  var syncCompressionBlocks = (state, logger, messages) => {
7920
8081
  const messagesState = state.prune.messages;
7921
8082
  if (!messagesState?.blocksById?.size) {
7922
- return;
8083
+ if (!messagesState.membershipsVerified) {
8084
+ messagesState.activeBlockIds.clear();
8085
+ messagesState.activeByAnchorMessageId.clear();
8086
+ for (const entry of messagesState.byMessageId.values()) {
8087
+ entry.allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
8088
+ entry.activeBlockIds = [];
8089
+ }
8090
+ messagesState.membershipsVerified = true;
8091
+ return true;
8092
+ }
8093
+ return false;
7923
8094
  }
7924
8095
  const messageIds = new Set(messages.map((msg) => msg.info.id));
7925
8096
  const structureVersion = messagesState.structureVersion ?? 0;
@@ -7937,11 +8108,12 @@ var syncCompressionBlocks = (state, logger, messages) => {
7937
8108
  previous.set(anchor, id);
7938
8109
  }
7939
8110
  }
7940
- return;
8111
+ return false;
7941
8112
  }
7942
8113
  const previousActiveBlockIds = new Set(
7943
8114
  Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
7944
8115
  );
8116
+ const indexedActiveBlockIds = new Set(messagesState.activeBlockIds);
7945
8117
  messagesState.activeBlockIds.clear();
7946
8118
  messagesState.activeByAnchorMessageId.clear();
7947
8119
  const now = Date.now();
@@ -7981,10 +8153,14 @@ var syncCompressionBlocks = (state, logger, messages) => {
7981
8153
  messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
7982
8154
  }
7983
8155
  }
7984
- for (const entry of messagesState.byMessageId.values()) {
7985
- const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
7986
- entry.allBlockIds = allBlockIds;
7987
- entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
8156
+ const membershipsRebuilt = !messagesState.membershipsVerified || !sameBlockIds(indexedActiveBlockIds, messagesState.activeBlockIds);
8157
+ if (membershipsRebuilt) {
8158
+ for (const entry of messagesState.byMessageId.values()) {
8159
+ const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
8160
+ entry.allBlockIds = allBlockIds;
8161
+ entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
8162
+ }
8163
+ messagesState.membershipsVerified = true;
7988
8164
  }
7989
8165
  const nextActiveBlockIds = messagesState.activeBlockIds;
7990
8166
  let deactivatedCount = 0;
@@ -8006,6 +8182,7 @@ var syncCompressionBlocks = (state, logger, messages) => {
8006
8182
  });
8007
8183
  }
8008
8184
  messagesState.lastSyncedStructureVersion = structureVersion;
8185
+ return membershipsRebuilt || deactivatedCount > 0 || reactivatedCount > 0;
8009
8186
  };
8010
8187
 
8011
8188
  // lib/host-permissions.ts
@@ -8080,6 +8257,484 @@ var syncCompressPermissionState = (state, config, hostPermissions, messages) =>
8080
8257
  );
8081
8258
  };
8082
8259
 
8260
+ // lib/prompts/context-limit-nudge.ts
8261
+ var CONTEXT_LIMIT_NUDGE = `
8262
+ <system-reminder>
8263
+ \u26A0\uFE0F Context limit reached \u2014 time to compress the largest ranges you no longer need. Prioritize completed tool outputs and resolved work. You can decompress specific blocks later if you need details. Keeping context lean helps you stay accurate.
8264
+
8265
+ If mid-atomic-operation, finish that step first, then compress.
8266
+
8267
+ HOW TO CALL COMPRESS:
8268
+ {
8269
+ "topic": "Short Label",
8270
+ "content": [
8271
+ {
8272
+ "startId": "<ID from early in this conversation>",
8273
+ "endId": "<ID from later in this conversation>",
8274
+ "summary": "Complete technical summary of everything in the range"
8275
+ }
8276
+ ]
8277
+ }
8278
+
8279
+ \u26A0\uFE0F ID RULES \u2014 MOST COMMON CAUSE OF ERRORS:
8280
+ - ONLY use IDs you can see in tags in the messages ABOVE.
8281
+ - Do NOT copy IDs from this example. Do NOT invent IDs.
8282
+ - Do NOT use IDs from compressed block summaries \u2014 they are stale.
8283
+ - startId must appear BEFORE endId in the conversation.
8284
+
8285
+ RANGE STRATEGY:
8286
+ - Prefer one large range over multiple small ones.
8287
+ - Compress OLDER resolved history first. Keep recent active work.
8288
+ </system-reminder>
8289
+ `;
8290
+ var CANDIDATE_GUIDANCE = `
8291
+ <system-reminder>
8292
+ COMPRESSION CANDIDATES:
8293
+ - MICRO identifies one large message or complete tool transaction.
8294
+ - EPISODE identifies a contiguous historical segment made from smaller units.
8295
+ - Candidate entries are independent and non-overlapping, so they can be batched in one \`content[]\` array.
8296
+ - Candidates are suggestions, not mandatory targets. Choose only content no longer needed for the current task and do not invent a target when none is listed.
8297
+ - Use \`acp_status\` for a fresh candidate view if the list is stale or missing.
8298
+ - When the context limit is reached and candidates are listed, select at least one clearly stale candidate and call the \`compress\` tool in your next reply. Batch only additional candidates that are also clearly stale. Do not merely recommend compression.
8299
+ </system-reminder>
8300
+ `;
8301
+
8302
+ // lib/prompts/turn-nudge.ts
8303
+ var TURN_NUDGE = `
8304
+ <system-reminder>
8305
+ Context is getting full. If you've finished reading tool outputs or exploration results, compress them \u2014 you can decompress later if needed. This keeps your focus on the current task and improves accuracy.
8306
+
8307
+ {
8308
+ "topic": "Short Label",
8309
+ "content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
8310
+ }
8311
+
8312
+ \u26A0\uFE0F ONLY use IDs from tags visible above. Do NOT invent or copy example IDs.
8313
+ </system-reminder>
8314
+ `;
8315
+ var TURN_CANDIDATE_GUIDANCE = `
8316
+ MICRO candidates are complete large messages or tool transactions. EPISODE candidates are contiguous historical segments. They are independent suggestions and may be batched, but compress only content no longer needed for the current task. Do not compress active work or every candidate.
8317
+ `;
8318
+
8319
+ // lib/prompts/iteration-nudge.ts
8320
+ var ITERATION_NUDGE = `
8321
+ <system-reminder>
8322
+ You've been iterating for a while. If any earlier work is closed and unlikely to be referenced, compress it now.
8323
+
8324
+ {
8325
+ "topic": "Short Label",
8326
+ "content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
8327
+ }
8328
+
8329
+ \u26A0\uFE0F ONLY use IDs from tags visible above. Do NOT invent or copy example IDs.
8330
+ </system-reminder>
8331
+ `;
8332
+ var ITERATION_CANDIDATE_GUIDANCE = `
8333
+ MICRO candidates are complete large messages or tool transactions. EPISODE candidates are contiguous historical segments. They are independent suggestions and may be batched, but preserve anything still needed for the current task. Do not compress active work or every candidate.
8334
+ `;
8335
+
8336
+ // lib/messages/inject/candidates.ts
8337
+ function partToolNames(message) {
8338
+ const names = /* @__PURE__ */ new Set();
8339
+ for (const part of message.parts ?? []) {
8340
+ if (part.type === "tool" && part.tool !== "compress") {
8341
+ names.add(part.tool);
8342
+ }
8343
+ }
8344
+ return [...names];
8345
+ }
8346
+ function hasNonCompressTool(message) {
8347
+ return partToolNames(message).length > 0;
8348
+ }
8349
+ function collectToolSpans(messages) {
8350
+ const spansByCallId = /* @__PURE__ */ new Map();
8351
+ for (let index = 0; index < messages.length; index++) {
8352
+ const message = messages[index];
8353
+ if (!message) continue;
8354
+ const callIds = /* @__PURE__ */ new Set();
8355
+ for (const part of message.parts ?? []) {
8356
+ if (part.type !== "tool" || part.tool === "compress" || !part.callID) continue;
8357
+ callIds.add(part.callID);
8358
+ }
8359
+ for (const callID of callIds) {
8360
+ const existing = spansByCallId.get(callID);
8361
+ if (existing) {
8362
+ existing.endIndex = index;
8363
+ } else {
8364
+ spansByCallId.set(callID, { startIndex: index, endIndex: index });
8365
+ }
8366
+ }
8367
+ }
8368
+ return [...spansByCallId.values()].sort(
8369
+ (left, right) => left.startIndex - right.startIndex || left.endIndex - right.endIndex
8370
+ );
8371
+ }
8372
+ function mergeOverlappingSpans(messages, spans) {
8373
+ const merged = [];
8374
+ for (const span of spans) {
8375
+ const last = merged[merged.length - 1];
8376
+ if (last && span.startIndex <= last.endIndex) {
8377
+ last.endIndex = Math.max(last.endIndex, span.endIndex);
8378
+ } else {
8379
+ merged.push({ ...span });
8380
+ }
8381
+ }
8382
+ const result = [];
8383
+ let spanIndex = 0;
8384
+ let index = 0;
8385
+ while (index < messages.length) {
8386
+ while (spanIndex < merged.length && merged[spanIndex].endIndex < index) {
8387
+ spanIndex++;
8388
+ }
8389
+ const span = merged[spanIndex];
8390
+ if (span?.startIndex === index) {
8391
+ result.push(span);
8392
+ index = span.endIndex + 1;
8393
+ continue;
8394
+ }
8395
+ result.push({ startIndex: index, endIndex: index });
8396
+ index++;
8397
+ }
8398
+ return result;
8399
+ }
8400
+ function buildAtomicUnits(messages, state) {
8401
+ const omissions = [];
8402
+ const spans = mergeOverlappingSpans(messages, collectToolSpans(messages));
8403
+ const units = [];
8404
+ for (const span of spans) {
8405
+ const source = messages.slice(span.startIndex, span.endIndex + 1);
8406
+ const refs = source.map((message) => state.messageIds.byRawId.get(message.info.id));
8407
+ const startRef = refs[0];
8408
+ const endRef = refs[refs.length - 1];
8409
+ if (!startRef || !endRef) {
8410
+ omissions.push({ kind: "micro", startRef, endRef, reason: "missing-reference" });
8411
+ continue;
8412
+ }
8413
+ const invalidShape = source.some(
8414
+ (message) => isSyntheticMessage(message) || isIgnoredUserMessage(message)
8415
+ );
8416
+ if (invalidShape) {
8417
+ omissions.push({ kind: "micro", startRef, endRef, reason: "synthetic-or-ignored" });
8418
+ continue;
8419
+ }
8420
+ const active = source.some((message) => {
8421
+ const entry = state.prune.messages.byMessageId.get(message.info.id);
8422
+ return entry !== void 0 && entry.activeBlockIds.length > 0;
8423
+ });
8424
+ if (active) {
8425
+ omissions.push({ kind: "micro", startRef, endRef, reason: "active-compression" });
8426
+ continue;
8427
+ }
8428
+ let isTool = false;
8429
+ const toolNames = /* @__PURE__ */ new Set();
8430
+ let retainedChars = 0;
8431
+ let toolMessageCount = 0;
8432
+ let textMessageCount = 0;
8433
+ for (const message of source) {
8434
+ retainedChars += countMessageCharacters(message);
8435
+ if (hasNonCompressTool(message)) {
8436
+ isTool = true;
8437
+ toolMessageCount++;
8438
+ } else {
8439
+ textMessageCount++;
8440
+ }
8441
+ for (const toolName of partToolNames(message)) toolNames.add(toolName);
8442
+ }
8443
+ units.push({
8444
+ startIndex: span.startIndex,
8445
+ endIndex: span.endIndex,
8446
+ messages: source,
8447
+ sourceMessageIds: source.map((message) => message.info.id),
8448
+ startRef,
8449
+ endRef,
8450
+ retainedChars,
8451
+ estimatedTokens: Math.ceil(retainedChars / 4),
8452
+ isTool,
8453
+ toolNames: [...toolNames],
8454
+ toolMessageCount,
8455
+ textMessageCount
8456
+ });
8457
+ }
8458
+ return { units, omissions };
8459
+ }
8460
+ function draftCandidateLabel(kind, units) {
8461
+ if (kind === "episode") return "historical conversation segment";
8462
+ const toolNames = [...new Set(units.flatMap((unit) => unit.toolNames))];
8463
+ if (toolNames.length > 0) {
8464
+ return `${toolNames.slice(0, 3).join("/")} transaction`;
8465
+ }
8466
+ return "large conversation message";
8467
+ }
8468
+ function mergeUnits(units) {
8469
+ const first = units[0];
8470
+ const last = units[units.length - 1];
8471
+ const toolNames = /* @__PURE__ */ new Set();
8472
+ for (const unit of units) {
8473
+ for (const toolName of unit.toolNames) toolNames.add(toolName);
8474
+ }
8475
+ return {
8476
+ startIndex: first.startIndex,
8477
+ endIndex: last.endIndex,
8478
+ messages: units.flatMap((unit) => unit.messages),
8479
+ sourceMessageIds: units.flatMap((unit) => unit.sourceMessageIds),
8480
+ startRef: first.startRef,
8481
+ endRef: last.endRef,
8482
+ retainedChars: units.reduce((total, unit) => total + unit.retainedChars, 0),
8483
+ estimatedTokens: units.reduce((total, unit) => total + unit.estimatedTokens, 0),
8484
+ isTool: units.some((unit) => unit.isTool),
8485
+ toolNames: [...toolNames],
8486
+ toolMessageCount: units.reduce((total, unit) => total + unit.toolMessageCount, 0),
8487
+ textMessageCount: units.reduce((total, unit) => total + unit.textMessageCount, 0)
8488
+ };
8489
+ }
8490
+ function buildDrafts(units, minChars, state, config, protectedRefs) {
8491
+ const drafts = [];
8492
+ const omissions = [];
8493
+ const residual = [];
8494
+ for (const unit of units) {
8495
+ const recentProtected = unit.sourceMessageIds.some(
8496
+ (id) => protectedRefs.has(state.messageIds.byRawId.get(id) ?? "")
8497
+ );
8498
+ const protectedToolOrFile = unit.messages.some(
8499
+ (message) => messageContainsProtectedTool(
8500
+ message,
8501
+ config.compress.protectedTools,
8502
+ config.protectedFilePatterns
8503
+ )
8504
+ );
8505
+ if (recentProtected || protectedToolOrFile) {
8506
+ if (residual.length > 0) {
8507
+ const merged = mergeUnits(residual);
8508
+ if (merged.retainedChars >= minChars) {
8509
+ drafts.push({ kind: "episode", units: [...residual] });
8510
+ }
8511
+ residual.length = 0;
8512
+ }
8513
+ omissions.push({
8514
+ kind: unit.retainedChars >= minChars ? "micro" : "episode",
8515
+ startRef: unit.startRef,
8516
+ endRef: unit.endRef,
8517
+ reason: recentProtected ? "recent-protection" : "protected-tool-or-file"
8518
+ });
8519
+ continue;
8520
+ }
8521
+ if (unit.retainedChars > 0 && (minChars <= 0 || unit.retainedChars >= minChars)) {
8522
+ drafts.push({ kind: "micro", units: [unit] });
8523
+ } else if (minChars > 0 && unit.retainedChars > 0) {
8524
+ residual.push(unit);
8525
+ }
8526
+ }
8527
+ let episode = [];
8528
+ const flushEpisode = () => {
8529
+ if (episode.length === 0) return;
8530
+ const merged = mergeUnits(episode);
8531
+ if (merged.retainedChars >= minChars) {
8532
+ drafts.push({ kind: "episode", units: [...episode] });
8533
+ }
8534
+ episode = [];
8535
+ };
8536
+ for (let index = 0; index < residual.length; index++) {
8537
+ const unit = residual[index];
8538
+ const previous = residual[index - 1];
8539
+ if (previous && unit.startIndex !== previous.endIndex + 1) flushEpisode();
8540
+ episode.push(unit);
8541
+ }
8542
+ flushEpisode();
8543
+ return { drafts, omissions };
8544
+ }
8545
+ function compareIds(left, right) {
8546
+ const leftNumber = Number.parseInt(left.slice(1), 10);
8547
+ const rightNumber = Number.parseInt(right.slice(1), 10);
8548
+ return leftNumber - rightNumber || left.localeCompare(right);
8549
+ }
8550
+ function candidateDraftRange(draft) {
8551
+ return mergeUnits(draft.units);
8552
+ }
8553
+ function selectionMatchesSource(result, sourceMessageIds, unit) {
8554
+ if (result.plans.length !== 1) return false;
8555
+ const plan = result.plans[0];
8556
+ if (!plan) return false;
8557
+ if (plan.selection.startReference.rawIndex !== unit.startIndex || plan.selection.endReference.rawIndex !== unit.endIndex) {
8558
+ return false;
8559
+ }
8560
+ if (plan.selection.messageIds.length !== sourceMessageIds.length) return false;
8561
+ return plan.selection.messageIds.every((id, index) => id === sourceMessageIds[index]);
8562
+ }
8563
+ function validateDraft(draft, unit, messages, state, config, protectedRefs, prepared) {
8564
+ const source = messages.slice(unit.startIndex, unit.endIndex + 1);
8565
+ if (source.some(
8566
+ (message) => protectedRefs.has(state.messageIds.byRawId.get(message.info.id) ?? "")
8567
+ )) {
8568
+ return {
8569
+ omission: {
8570
+ kind: draft.kind,
8571
+ startRef: unit.startRef,
8572
+ endRef: unit.endRef,
8573
+ reason: "recent-protection"
8574
+ }
8575
+ };
8576
+ }
8577
+ if (source.some(
8578
+ (message) => messageContainsProtectedTool(
8579
+ message,
8580
+ config.compress.protectedTools,
8581
+ config.protectedFilePatterns
8582
+ )
8583
+ )) {
8584
+ return {
8585
+ omission: {
8586
+ kind: draft.kind,
8587
+ startRef: unit.startRef,
8588
+ endRef: unit.endRef,
8589
+ reason: "protected-tool-or-file"
8590
+ }
8591
+ };
8592
+ }
8593
+ if (config.compress.minCompressRange > 0 && unit.retainedChars < config.compress.minCompressRange) {
8594
+ return {
8595
+ omission: {
8596
+ kind: draft.kind,
8597
+ startRef: unit.startRef,
8598
+ endRef: unit.endRef,
8599
+ reason: "below-minimum"
8600
+ }
8601
+ };
8602
+ }
8603
+ try {
8604
+ let result = prepared;
8605
+ if (!result) {
8606
+ const searchContext = buildSearchContext(state, messages);
8607
+ result = prepareExecutableRangePlans(
8608
+ {
8609
+ content: [
8610
+ {
8611
+ topic: draftCandidateLabel(draft.kind, draft.units),
8612
+ startId: unit.startRef,
8613
+ endId: unit.endRef,
8614
+ summary: "candidate"
8615
+ }
8616
+ ]
8617
+ },
8618
+ searchContext,
8619
+ state,
8620
+ config
8621
+ );
8622
+ }
8623
+ if (!selectionMatchesSource(result, unit.sourceMessageIds, unit)) {
8624
+ return {
8625
+ omission: {
8626
+ kind: draft.kind,
8627
+ startRef: unit.startRef,
8628
+ endRef: unit.endRef,
8629
+ reason: "executor-selection-drift"
8630
+ }
8631
+ };
8632
+ }
8633
+ return {
8634
+ candidate: {
8635
+ kind: draft.kind,
8636
+ startRef: unit.startRef,
8637
+ endRef: unit.endRef,
8638
+ messageCount: unit.sourceMessageIds.length,
8639
+ retainedChars: unit.retainedChars,
8640
+ estimatedTokens: unit.estimatedTokens,
8641
+ toolPct: Math.round(unit.toolMessageCount / unit.sourceMessageIds.length * 100),
8642
+ textPct: Math.round(unit.textMessageCount / unit.sourceMessageIds.length * 100),
8643
+ label: draftCandidateLabel(draft.kind, draft.units),
8644
+ sourceMessageIds: [...unit.sourceMessageIds]
8645
+ }
8646
+ };
8647
+ } catch {
8648
+ return {
8649
+ omission: {
8650
+ kind: draft.kind,
8651
+ startRef: unit.startRef,
8652
+ endRef: unit.endRef,
8653
+ reason: "executor-rejected"
8654
+ }
8655
+ };
8656
+ }
8657
+ }
8658
+ function planCompressionCandidates(messages, state, config) {
8659
+ const protectedRefs = computeProtectedRefs(messages, state, config.compress);
8660
+ const { units, omissions } = buildAtomicUnits(messages, state);
8661
+ const minChars = config.compress.minCompressRange ?? 5e3;
8662
+ const draftResult = buildDrafts(units, minChars, state, config, protectedRefs);
8663
+ const drafts = draftResult.drafts;
8664
+ const candidates = [];
8665
+ const allOmissions = [...omissions, ...draftResult.omissions];
8666
+ let batchResult;
8667
+ try {
8668
+ const searchContext = buildSearchContext(state, messages);
8669
+ const entries = drafts.map((draft) => {
8670
+ const unit = candidateDraftRange(draft);
8671
+ return {
8672
+ topic: draftCandidateLabel(draft.kind, draft.units),
8673
+ startId: unit.startRef,
8674
+ endId: unit.endRef,
8675
+ summary: "candidate"
8676
+ };
8677
+ });
8678
+ if (entries.length > 0) {
8679
+ batchResult = prepareExecutableRangePlans(
8680
+ { content: entries },
8681
+ searchContext,
8682
+ state,
8683
+ config,
8684
+ void 0,
8685
+ { includeTokenAccounting: false }
8686
+ );
8687
+ }
8688
+ } catch {
8689
+ }
8690
+ const preparedPlansByIndex = new Map(batchResult?.plans.map((plan) => [plan.index, plan]));
8691
+ for (let draftIndex = 0; draftIndex < drafts.length; draftIndex++) {
8692
+ const draft = drafts[draftIndex];
8693
+ const unit = candidateDraftRange(draft);
8694
+ const preparedPlan = preparedPlansByIndex.get(draftIndex);
8695
+ const prepared = batchResult ? {
8696
+ plans: preparedPlan ? [preparedPlan] : [],
8697
+ totalChars: preparedPlan ? unit.retainedChars : 0
8698
+ } : void 0;
8699
+ const validated = validateDraft(
8700
+ draft,
8701
+ unit,
8702
+ messages,
8703
+ state,
8704
+ config,
8705
+ protectedRefs,
8706
+ prepared
8707
+ );
8708
+ if ("candidate" in validated) {
8709
+ candidates.push(validated.candidate);
8710
+ } else {
8711
+ allOmissions.push(validated.omission);
8712
+ }
8713
+ }
8714
+ candidates.sort((left, right) => {
8715
+ return right.estimatedTokens - left.estimatedTokens || (left.kind === "micro" ? -1 : 1) - (right.kind === "micro" ? -1 : 1) || compareIds(left.startRef, right.startRef) || compareIds(left.endRef, right.endRef);
8716
+ });
8717
+ const maxCandidates = 12;
8718
+ const bounded = candidates.slice(0, maxCandidates);
8719
+ return {
8720
+ candidates: bounded,
8721
+ omitted: allOmissions,
8722
+ truncatedCount: Math.max(0, candidates.length - bounded.length)
8723
+ };
8724
+ }
8725
+ function formatCompressionCandidates(plan) {
8726
+ if (plan.candidates.length === 0) return "";
8727
+ const fmt = (value) => value >= 1e3 ? `${(value / 1e3).toFixed(1)}K` : String(value);
8728
+ const lines = plan.candidates.map((candidate) => {
8729
+ const range = `${candidate.startRef}\u2013${candidate.endRef}`;
8730
+ return ` ${candidate.kind.toUpperCase().padEnd(7)} ${range} ${candidate.messageCount} msg${candidate.messageCount === 1 ? "" : "s"} ${fmt(candidate.estimatedTokens)} tokens ${candidate.label}`;
8731
+ });
8732
+ const suffix = plan.truncatedCount > 0 ? `
8733
+ ...${plan.truncatedCount} additional candidate${plan.truncatedCount === 1 ? "" : "s"} omitted` : "";
8734
+ return `COMPRESSION CANDIDATES (independent; batchable)
8735
+ ${lines.join("\n")}${suffix}`;
8736
+ }
8737
+
8083
8738
  // lib/messages/inject/inject.ts
8084
8739
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
8085
8740
  function createSuffixMessage(messages) {
@@ -8089,7 +8744,7 @@ function createSuffixMessage(messages) {
8089
8744
  messages.push(synthetic);
8090
8745
  return synthetic;
8091
8746
  }
8092
- var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify, preCompressTokens) => {
8747
+ var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify, preCompressTokens, candidateMessages) => {
8093
8748
  if (compressPermission(state, config) === "deny") {
8094
8749
  return;
8095
8750
  }
@@ -8201,7 +8856,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8201
8856
  (message) => message.info.id === lastUserMessage.info.id
8202
8857
  );
8203
8858
  if (lastUserMessageIndex >= 0) {
8204
- const messagesSinceUser = countMessagesAfterIndex(messages, lastUserMessageIndex);
8859
+ const messagesSinceUser = countMessagesAfterIndex(
8860
+ messages,
8861
+ lastUserMessageIndex
8862
+ );
8205
8863
  const iterationThreshold = getIterationNudgeThreshold(config);
8206
8864
  if (lastMessage.index > lastUserMessageIndex && messagesSinceUser >= iterationThreshold) {
8207
8865
  const interval = getNudgeFrequency(config);
@@ -8276,24 +8934,69 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8276
8934
  config.protectedFilePatterns,
8277
8935
  protectedRefs
8278
8936
  ) : { compressible: [], protected: [] };
8279
- const unprotectedCompressible = excludeProtectedRanges(contextRanges.compressible, protectedRefs);
8937
+ const unprotectedCompressible = excludeProtectedRanges(
8938
+ contextRanges.compressible,
8939
+ protectedRefs
8940
+ );
8280
8941
  const recommendedRanges = filterRecommendedRanges(
8281
8942
  unprotectedCompressible,
8282
8943
  contextRanges.protected,
8283
8944
  { logger, minEffectiveTokens: resolveEffectiveFloor(config) }
8284
8945
  );
8285
- const hasRecommendations = recommendedRanges.length > 0;
8946
+ let candidatePlan = {
8947
+ candidates: [],
8948
+ omitted: [],
8949
+ truncatedCount: 0
8950
+ };
8951
+ const candidatesEnabled = config.compress.candidates === true;
8952
+ if (nudgeAllowed && candidatesEnabled) {
8953
+ try {
8954
+ candidatePlan = planCompressionCandidates(candidateMessages ?? messages, state, config);
8955
+ } catch (error) {
8956
+ logger.warn("Compression candidate planning failed closed", {
8957
+ session: state.sessionId,
8958
+ error: error instanceof Error ? error.message : String(error)
8959
+ });
8960
+ }
8961
+ }
8962
+ const candidateText = candidatesEnabled ? formatCompressionCandidates(candidatePlan) || (candidateMessages === void 0 && recommendedRanges.length > 0 ? formatCompressibleRanges(recommendedRanges, contextRanges.protected) : "") : recommendedRanges.length > 0 ? formatCompressibleRanges(recommendedRanges, contextRanges.protected) : "";
8286
8963
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
8287
8964
  const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
8288
8965
  const allBelowMin = contextRanges.compressible.length > 0 && recommendedRanges.length === 0;
8289
- const nothingToCompress = allProtected || allInProtectedZone || allBelowMin;
8966
+ const noExecutableCandidates = candidatesEnabled && nudgeAllowed && candidateMessages !== void 0 && candidatePlan.candidates.length === 0;
8967
+ const nothingToCompress = candidatesEnabled && candidateMessages !== void 0 ? noExecutableCandidates : allProtected || allInProtectedZone || allBelowMin;
8290
8968
  const emergencyNoTargets = emergencyOverride && nothingToCompress;
8291
8969
  const noticeCadenceMet = state.nudges.lastNudgeShownTokens === void 0 || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
8292
8970
  const shouldInjectNudge = nudgeAllowed && !nothingToCompress;
8293
8971
  const shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
8294
8972
  let shouldInject = shouldInjectNudge || shouldInjectNotice;
8295
8973
  if (shouldInjectNudge) {
8296
- applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
8974
+ const effectivePrompts = candidatesEnabled ? {
8975
+ ...prompts,
8976
+ contextLimitNudge: prompts.contextLimitNudge ? `${prompts.contextLimitNudge}
8977
+
8978
+ ${CANDIDATE_GUIDANCE}` : CANDIDATE_GUIDANCE,
8979
+ turnNudge: prompts.turnNudge ? `${prompts.turnNudge}
8980
+
8981
+ ${TURN_CANDIDATE_GUIDANCE}` : TURN_CANDIDATE_GUIDANCE,
8982
+ iterationNudge: prompts.iterationNudge ? `${prompts.iterationNudge}
8983
+
8984
+ ${ITERATION_CANDIDATE_GUIDANCE}` : ITERATION_CANDIDATE_GUIDANCE
8985
+ } : prompts;
8986
+ applyAnchoredNudges(
8987
+ state,
8988
+ config,
8989
+ messages,
8990
+ effectivePrompts,
8991
+ compressionPriorities,
8992
+ currentTokens,
8993
+ modelContextLimit,
8994
+ suffixMessage
8995
+ );
8996
+ if (candidatesEnabled && suffixMessage && candidateText && effectiveTipsVariant !== "maxLimit") {
8997
+ const actionHint = hasPendingNudge ? "\n\n\u26A0\uFE0F This compression nudge has already been shown without a compression. If candidates are clearly stale, call the `compress` tool for one clearly stale candidate before continuing. Do not compress active work or things needed to proceed well." : "\n\n\u{1F4A1} If one listed candidate is clearly complete and no longer needed, call the `compress` tool in this reply before continuing. Do not compress active work or every candidate.";
8998
+ appendToLastTextPart(suffixMessage, actionHint);
8999
+ }
8297
9000
  }
8298
9001
  if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
8299
9002
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
@@ -8302,14 +9005,26 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8302
9005
  if (suffixMessage && !shouldInject) {
8303
9006
  const tierUsage = tierUsageEarly;
8304
9007
  const tierChecks = [
8305
- { triggerTier: 2, targetTier: 1, tokens: tierUsage.tier1Tokens, lastNudge: state.nudges.lastTier2NudgeTokens },
8306
- { triggerTier: 3, targetTier: 2, tokens: tierUsage.tier2Tokens, lastNudge: state.nudges.lastTier3NudgeTokens }
9008
+ {
9009
+ triggerTier: 2,
9010
+ targetTier: 1,
9011
+ tokens: tierUsage.tier1Tokens,
9012
+ lastNudge: state.nudges.lastTier2NudgeTokens
9013
+ },
9014
+ {
9015
+ triggerTier: 3,
9016
+ targetTier: 2,
9017
+ tokens: tierUsage.tier2Tokens,
9018
+ lastNudge: state.nudges.lastTier3NudgeTokens
9019
+ }
8307
9020
  ];
8308
9021
  for (const tc of tierChecks) {
8309
9022
  if (tc.tokens < nudgeGrowthTokens) continue;
8310
9023
  const cadenceMet = tc.lastNudge === void 0 || currentTokens !== void 0 && currentTokens - tc.lastNudge >= growthFloor;
8311
9024
  if (!cadenceMet) continue;
8312
- let candidates = [...state.prune.messages.activeBlockIds].map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active && (b.tier ?? 1) === tc.targetTier).sort((a, b) => a.blockId - b.blockId);
9025
+ let candidates = [...state.prune.messages.activeBlockIds].map((id) => state.prune.messages.blocksById.get(id)).filter(
9026
+ (b) => b !== void 0 && b.active && (b.tier ?? 1) === tc.targetTier
9027
+ ).sort((a, b) => a.blockId - b.blockId);
8313
9028
  if (candidates.length >= 2) {
8314
9029
  const firstId = candidates[0].blockId;
8315
9030
  const lastId = candidates[candidates.length - 1].blockId;
@@ -8357,7 +9072,9 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8357
9072
  const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
8358
9073
  const sourceTier = tc.triggerTier === 2 ? "Tier 1" : "Tier 2";
8359
9074
  const action = tc.triggerTier === 2 ? "Distill" : "Condense";
8360
- const blockList = candidates.slice(0, 10).map((b) => `b${b.blockId} (age=${b.survivedCount}, ${fmt(b.summaryTokens)}tok): "${b.topic}"`).join("\n");
9075
+ const blockList = candidates.slice(0, 10).map(
9076
+ (b) => `b${b.blockId} (age=${b.survivedCount}, ${fmt(b.summaryTokens)}tok): "${b.topic}"`
9077
+ ).join("\n");
8361
9078
  const extraCount = candidates.length > 10 ? `
8362
9079
  ...and ${candidates.length - 10} more` : "";
8363
9080
  const tierText = `
@@ -8398,7 +9115,19 @@ ${rules}`;
8398
9115
  usagePct,
8399
9116
  growthSinceBaseline,
8400
9117
  growthFloor,
8401
- recommendedRanges: recommendedRanges.length
9118
+ candidateCount: candidatePlan.candidates.length,
9119
+ microCandidates: candidatePlan.candidates.filter(
9120
+ (candidate) => candidate.kind === "micro"
9121
+ ).length,
9122
+ episodeCandidates: candidatePlan.candidates.filter(
9123
+ (candidate) => candidate.kind === "episode"
9124
+ ).length,
9125
+ estimatedCandidateTokens: candidatePlan.candidates.reduce(
9126
+ (total, candidate) => total + candidate.estimatedTokens,
9127
+ 0
9128
+ ),
9129
+ omittedCandidates: candidatePlan.omitted.length,
9130
+ truncatedCandidates: candidatePlan.truncatedCount
8402
9131
  });
8403
9132
  } else if (shouldInjectNotice) {
8404
9133
  logger.info("Emergency /compact notice injected (no compressible targets)", {
@@ -8409,7 +9138,7 @@ ${rules}`;
8409
9138
  } else if (nudgeAllowed && nothingToCompress) {
8410
9139
  logger.info("Nudge suppressed: nothing to compress", {
8411
9140
  session: state.sessionId,
8412
- reason: allProtected ? "all_protected" : allInProtectedZone ? "in_protected_zone" : "below_effective_floor",
9141
+ reason: allProtected ? "all_protected" : allInProtectedZone ? "in_protected_zone" : allBelowMin ? "below_effective_floor" : "no_executable_candidates",
8413
9142
  currentTokens,
8414
9143
  usagePct,
8415
9144
  compressibleRanges: contextRanges.compressible.length
@@ -8421,7 +9150,7 @@ ${rules}`;
8421
9150
  const lines = [
8422
9151
  `[ACP Debug] Recommendation filter:`,
8423
9152
  ` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
8424
- ` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
9153
+ candidatesEnabled ? ` Output: ${candidatePlan.candidates.length} candidate(s) (${candidatePlan.truncatedCount} truncated)` : ` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
8425
9154
  ];
8426
9155
  logger.debug(lines.join("\n"));
8427
9156
  }
@@ -8433,10 +9162,7 @@ ${rules}`;
8433
9162
  const growth = currentTokens !== void 0 && (state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens) !== void 0 ? currentTokens - (state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens) : 0;
8434
9163
  const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
8435
9164
  const plainTextTokens = composition.textTokens;
8436
- const efficiencyNote = effectiveTipsVariant !== "maxLimit" ? `
8437
- This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
8438
-
8439
- ${COMPRESS_PHILOSOPHY}` : "";
9165
+ const efficiencyNote = effectiveTipsVariant !== "maxLimit" ? candidatesEnabled ? "\nThis is an efficiency nudge to compress early when content is no longer needed and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" + COMPRESS_PHILOSOPHY : "\nThis is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" + COMPRESS_PHILOSOPHY : "";
8440
9166
  const sysPart = composition.systemTokens > 0 ? `${fmt(composition.systemTokens)} system (${pct2(composition.systemTokens)}%) | ` : "";
8441
9167
  let breakdown = `${efficiencyNote}
8442
9168
  Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%) | ${fmt(composition.reasoningTokens)} reasoning (${pct2(composition.reasoningTokens)}%)${growthStr}`;
@@ -8445,21 +9171,20 @@ Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.too
8445
9171
  breakdown += `
8446
9172
  \u26A0\uFE0F ${fmt(composition.protectedTokens)} tokens are protected (environment-managed tools) \u2014 not compressible. Effective compressible: ~${fmt(compressibleTokens)}.`;
8447
9173
  }
8448
- if (recommendedRanges.length > 0) {
9174
+ if (candidateText) {
8449
9175
  breakdown += `
8450
9176
 
8451
9177
  ${HOW_TO_COMPRESS_RULES}
8452
9178
 
8453
- ${formatCompressibleRanges(recommendedRanges, contextRanges.protected)}`;
8454
- breakdown += `
8455
- \u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
9179
+ ${candidateText}`;
9180
+ breakdown += candidatesEnabled ? "\n\u{1F4A1} You may batch selected independent candidates in one call (pass multiple content entries: `content: [{...}, {...}]`); omit any candidate whose content is still needed." : "\n\u{1F4A1} Compress all ranges in one call (pass multiple content entries: `content: [{...}, {...}]`).";
8456
9181
  }
8457
9182
  breakdown += `
8458
9183
  Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
8459
9184
  appendToLastTextPart(suffixMessage, breakdown);
8460
9185
  }
8461
9186
  if (effectiveTipsVariant === "maxLimit" && !emergencyNoTargets) {
8462
- tipsText = "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
9187
+ tipsText = candidatesEnabled ? "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Select at least one listed candidate whose content is no longer needed and call the `compress` tool in your next reply. Do not merely recommend compression. Candidates are advisory about selection: preserve current intent and active work, and never compress content still needed for the current task.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. If several selected candidates are stale, older work may be preferable; never override semantic judgment or compress content still needed for the current task.' : "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
8463
9188
  } else if (shouldInjectNotice) {
8464
9189
  const emergencyPct = currentTokens !== void 0 && modelContextLimit !== void 0 && modelContextLimit > 0 ? Math.round(currentTokens / modelContextLimit * 100) : void 0;
8465
9190
  tipsText = `
@@ -8474,9 +9199,7 @@ Then stop retrying and await the user's response.`;
8474
9199
  }
8475
9200
  state.nudges.lastNudgeShownTokens = currentTokens;
8476
9201
  {
8477
- const visibleMessageIds = new Set(
8478
- messages.map((message) => message.info.id)
8479
- );
9202
+ const visibleMessageIds = new Set(messages.map((message) => message.info.id));
8480
9203
  const blockGuidance = buildCompressedBlockGuidance(state, {
8481
9204
  currentTokens,
8482
9205
  modelContextLimit,
@@ -9260,14 +9983,14 @@ function hideConsumedCompressCalls(state, messages) {
9260
9983
  }
9261
9984
 
9262
9985
  // lib/compress/status.ts
9263
- var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview includes compressible ranges by default.
9986
+ var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview includes compressible ranges (compression candidates when compress.candidates is enabled).
9264
9987
 
9265
- No args: Overview with totals, compressed blocks, and compressible ranges.
9266
- scope:"uncompressed": Compressible ranges only (default view:"ranges"). Add view:"messages" for per-message listing with tool/sort filters.
9988
+ No args: Overview with totals, compressed blocks, and compressible ranges (or candidates when enabled).
9989
+ scope:"uncompressed": Compressible ranges by default (view:"candidates" when compress.candidates is enabled). Use view:"ranges" for raw grouped ranges or view:"messages" for per-message listing.
9267
9990
  scope:"compressed": Drill into compressed blocks \u2014 list each with full details (age, generation, consumed lineage).
9268
9991
 
9269
9992
  Use this tool to:
9270
- - See what's consuming context + compressible ranges in one call (no args)
9993
+ - See what's consuming context + compressible targets in one call (no args)
9271
9994
  - Focus on ranges only (scope:"uncompressed")
9272
9995
  - Find all messages of a specific tool type (scope:"uncompressed", view:"messages", tool:"bash")
9273
9996
  - Check block details before decompressing (scope:"compressed")`;
@@ -9437,25 +10160,33 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
9437
10160
  const entry = pruneMap.get(msgId);
9438
10161
  return !entry || entry.activeBlockIds.length === 0;
9439
10162
  });
9440
- const protectedRefs = ctx.config?.compress ? computeProtectedRefs(visibleRaw, ctx.state, ctx.config.compress) : /* @__PURE__ */ new Set();
9441
- const contextRanges = buildCompressibleRanges(
9442
- visibleRaw,
9443
- ctx.state,
9444
- ctx.config?.compress?.protectedTools ?? [],
9445
- ctx.config?.protectedFilePatterns ?? [],
9446
- protectedRefs
9447
- );
9448
- if (contextRanges.compressible.length > 0 || contextRanges.protected.length > 0) {
9449
- lines.push("");
9450
- lines.push(
9451
- formatCompressibleRanges(contextRanges.compressible, contextRanges.protected)
10163
+ if (ctx.config?.compress?.candidates === true) {
10164
+ const candidates = renderCompressionCandidates(rawMessages, ctx);
10165
+ if (candidates.length > 0) {
10166
+ lines.push("");
10167
+ lines.push(...candidates);
10168
+ }
10169
+ } else {
10170
+ const protectedRefs = ctx.config?.compress ? computeProtectedRefs(visibleRaw, ctx.state, ctx.config.compress) : /* @__PURE__ */ new Set();
10171
+ const contextRanges = buildCompressibleRanges(
10172
+ visibleRaw,
10173
+ ctx.state,
10174
+ ctx.config?.compress?.protectedTools ?? [],
10175
+ ctx.config?.protectedFilePatterns ?? [],
10176
+ protectedRefs
9452
10177
  );
10178
+ if (contextRanges.compressible.length > 0 || contextRanges.protected.length > 0) {
10179
+ lines.push("");
10180
+ lines.push(
10181
+ formatCompressibleRanges(contextRanges.compressible, contextRanges.protected)
10182
+ );
10183
+ }
9453
10184
  }
9454
10185
  }
9455
10186
  lines.push("");
9456
10187
  const hintTool = topToolName || "bash";
9457
10188
  lines.push(
9458
- `Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`
10189
+ ctx.config?.compress?.candidates === true ? `Tip: acp_status({scope:"uncompressed", view:"ranges"}) for raw ranges, or acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing` : `Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`
9459
10190
  );
9460
10191
  return lines;
9461
10192
  }
@@ -9492,6 +10223,44 @@ function renderUncompressedRanges(rawMessages, ctx) {
9492
10223
  lines.push(`Filter by tool: acp_status({scope:"uncompressed", view:"messages", tool:"bash"})`);
9493
10224
  return lines;
9494
10225
  }
10226
+ function renderCompressionCandidates(rawMessages, ctx) {
10227
+ const lines = ["COMPRESSION CANDIDATES"];
10228
+ if (!ctx.config?.compress || ctx.config.compress.candidates !== true) {
10229
+ return renderUncompressedRanges(rawMessages, ctx);
10230
+ }
10231
+ try {
10232
+ const plan = planCompressionCandidates(rawMessages, ctx.state, ctx.config);
10233
+ const visibleMessages = rawMessages.filter((message) => {
10234
+ const entry = ctx.state.prune.messages.byMessageId.get(message.info.id);
10235
+ return !entry || entry.activeBlockIds.length === 0;
10236
+ });
10237
+ const protectedRefs = computeProtectedRefs(visibleMessages, ctx.state, ctx.config.compress);
10238
+ const contextRanges = buildCompressibleRanges(
10239
+ visibleMessages,
10240
+ ctx.state,
10241
+ ctx.config.compress.protectedTools ?? [],
10242
+ ctx.config.protectedFilePatterns ?? [],
10243
+ protectedRefs
10244
+ );
10245
+ const protectedText = formatCompressibleRanges([], contextRanges.protected);
10246
+ if (plan.candidates.length === 0) {
10247
+ lines.push(" (no eligible candidates)");
10248
+ if (protectedText.includes("PROTECTED")) lines.push("", protectedText);
10249
+ return lines;
10250
+ }
10251
+ lines.length = 0;
10252
+ lines.push(formatCompressionCandidates(plan));
10253
+ if (protectedText.includes("PROTECTED")) lines.push("", protectedText);
10254
+ if (plan.omitted.length > 0) {
10255
+ lines.push(
10256
+ ` ${plan.omitted.length} structurally unsafe or undersized candidate${plan.omitted.length === 1 ? "" : "s"} omitted`
10257
+ );
10258
+ }
10259
+ return lines;
10260
+ } catch {
10261
+ return ["COMPRESSION CANDIDATES", " (candidate planning unavailable)"];
10262
+ }
10263
+ }
9495
10264
  function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
9496
10265
  const lines = [];
9497
10266
  let filtered = visibleMessages;
@@ -9594,14 +10363,12 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
9594
10363
  }
9595
10364
  function buildStatusReport(renderCtx, rawMessages, options) {
9596
10365
  const scope = options?.scope;
9597
- const view = options?.view ?? "ranges";
10366
+ const view = options?.view ?? "candidates";
9598
10367
  const toolFilter = options?.tool;
9599
10368
  const sort = options?.sort ?? "size";
9600
10369
  const limit = options?.limit ?? 30;
9601
10370
  const msgState = renderCtx.state.prune.messages;
9602
- const allBlocks = Array.from(msgState.blocksById.values()).sort(
9603
- (a, b) => a.blockId - b.blockId
9604
- );
10371
+ const allBlocks = Array.from(msgState.blocksById.values()).sort((a, b) => a.blockId - b.blockId);
9605
10372
  const activeBlocks = allBlocks.filter((b) => b.active);
9606
10373
  const lines = [];
9607
10374
  if (scope === "compressed") {
@@ -9615,8 +10382,10 @@ function buildStatusReport(renderCtx, rawMessages, options) {
9615
10382
  if (scope === "uncompressed") {
9616
10383
  if (view === "messages") {
9617
10384
  lines.push(...renderUncompressedDrilldown(visibleMsgs, toolFilter, sort, limit));
9618
- } else {
10385
+ } else if (view === "ranges") {
9619
10386
  lines.push(...renderUncompressedRanges(rawMessages, renderCtx));
10387
+ } else {
10388
+ lines.push(...renderCompressionCandidates(rawMessages, renderCtx));
9620
10389
  }
9621
10390
  } else {
9622
10391
  lines.push(
@@ -9640,7 +10409,7 @@ function createAcpStatusTool(factoryCtx) {
9640
10409
  args: {
9641
10410
  scope: tool4.schema.string().optional().describe('Drill down: "compressed" or "uncompressed". No arg = overview of both.'),
9642
10411
  view: tool4.schema.string().optional().describe(
9643
- 'Display format for scope:"uncompressed": "ranges" (default, grouped by turn \u2014 matches nudge format) or "messages" (per-message listing with sort/filter)'
10412
+ 'Display format for scope:"uncompressed": "candidates" (default when compress.candidates is enabled \u2014 otherwise "ranges"), "ranges" (raw grouped ranges), or "messages" (per-message listing with sort/filter)'
9644
10413
  ),
9645
10414
  tool: tool4.schema.string().optional().describe(
9646
10415
  'Filter by tool type (only with scope:"uncompressed", view:"messages"). e.g., "bash", "todowrite", "write"'
@@ -9651,16 +10420,16 @@ function createAcpStatusTool(factoryCtx) {
9651
10420
  async execute(args, toolCtx) {
9652
10421
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
9653
10422
  const scope = args.scope === "compressed" || args.scope === "uncompressed" ? args.scope : void 0;
9654
- const view = args.view === "messages" ? "messages" : "ranges";
10423
+ const view = args.view === "messages" || args.view === "ranges" || args.view === "candidates" ? args.view : "candidates";
9655
10424
  const toolFilter = typeof args.tool === "string" ? args.tool : void 0;
9656
10425
  const sort = args.sort === "time" || args.sort === "tool" || args.sort === "age" ? args.sort : "size";
9657
10426
  const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
9658
10427
  if (scope === "compressed") {
9659
- return buildStatusReport(
9660
- { state: ctx.state, config: ctx.config },
9661
- [],
9662
- { scope: "compressed", sort, limit }
9663
- );
10428
+ return buildStatusReport({ state: ctx.state, config: ctx.config }, [], {
10429
+ scope: "compressed",
10430
+ sort,
10431
+ limit
10432
+ });
9664
10433
  }
9665
10434
  let rawMessages = [];
9666
10435
  try {
@@ -9670,11 +10439,13 @@ function createAcpStatusTool(factoryCtx) {
9670
10439
  rawMessages = [];
9671
10440
  }
9672
10441
  hideConsumedCompressCalls(ctx.state, rawMessages);
9673
- return buildStatusReport(
9674
- { state: ctx.state, config: ctx.config },
9675
- rawMessages,
9676
- { scope, view, tool: toolFilter, sort, limit }
9677
- );
10442
+ return buildStatusReport({ state: ctx.state, config: ctx.config }, rawMessages, {
10443
+ scope,
10444
+ view,
10445
+ tool: toolFilter,
10446
+ sort,
10447
+ limit
10448
+ });
9678
10449
  }
9679
10450
  });
9680
10451
  }
@@ -9782,7 +10553,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9782
10553
  import { join as join4 } from "path";
9783
10554
  import { existsSync as existsSync4 } from "fs";
9784
10555
  import { homedir as homedir3 } from "os";
9785
- var LOG_VERSION = true ? "1.17.1" : "dev";
10556
+ var LOG_VERSION = true ? "1.18.0-pr.392.147" : "dev";
9786
10557
  var LEVEL_RANK = {
9787
10558
  debug: 10,
9788
10559
  info: 20,
@@ -10005,7 +10776,32 @@ import { join as join5, dirname as dirname2 } from "path";
10005
10776
  import { homedir as homedir4 } from "os";
10006
10777
 
10007
10778
  // lib/prompts/system.ts
10008
- var SYSTEM = `
10779
+ var ACP_STATUS_TOOL_LINE_RANGES = `- \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.`;
10780
+ var ACP_STATUS_TOOL_LINE_CANDIDATES = `- \`acp_status\` \u2014 Context status with compression candidates. No args = overview + candidates. \`scope:"uncompressed"\` defaults to independent candidates; use \`view:"ranges"\` for raw grouped ranges or \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` shows block details.`;
10781
+ var PHILOSOPHY_TAIL_RANGES = `All ranges listed in the context breakdown should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
10782
+ var PHILOSOPHY_TAIL_CANDIDATES = `Candidate guidance is advisory: a candidate is structurally safe to submit, not a command to compress. Preserve current intent and active work even when a candidate is displayed.`;
10783
+ var COMPRESSION_CANDIDATES_SECTION = `COMPRESSION CANDIDATES
10784
+
10785
+ ACP may append a bounded list headed \`COMPRESSION CANDIDATES\` below a nudge or status report:
10786
+
10787
+ - \`MICRO\` is one large plain message or one complete tool transaction, including every message needed to keep its tool call/result structure valid.
10788
+ - \`EPISODE\` is a contiguous historical segment made from smaller adjacent units.
10789
+ - Candidate ranges are independent, non-overlapping, and can be submitted together as separate \`content[]\` entries.
10790
+ - Choose candidates only when their content is no longer needed for the current task. Do not invent a target when no candidate is listed; call \`acp_status\` for a fresh view. If an arbitrary range is unavoidable, verify current visible IDs, protection boundaries, and complete tool-pair coverage first.
10791
+ - Keep summaries self-contained and use the existing \`compress\` tool. Candidate labels do not replace the semantic judgment to preserve useful context.
10792
+ - When a nudge lists a clearly stale candidate, call \`compress\` in that reply before continuing. A repeated nudge means compression was not completed; act on one clearly stale candidate rather than merely recommending compression.`;
10793
+ var BREAKDOWN_TAIL_RANGES = `Below the breakdown, the system lists compressible ranges grouped by conversation turn. All listed ranges should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct. Compress the largest ranges first when the current step no longer needs them.`;
10794
+ var BREAKDOWN_TAIL_CANDIDATES = `Below the breakdown, the system may list independent compression candidates. Prefer a large stale micro-range when it removes an isolated artifact; use an episode range when a completed historical phase is more coherent as one summary. Compress only content the current step no longer needs.`;
10795
+ function buildSystemPrompt(candidatesEnabled) {
10796
+ const acpStatusToolLine = candidatesEnabled ? ACP_STATUS_TOOL_LINE_CANDIDATES : ACP_STATUS_TOOL_LINE_RANGES;
10797
+ const philosophyTail = candidatesEnabled ? PHILOSOPHY_TAIL_CANDIDATES : PHILOSOPHY_TAIL_RANGES;
10798
+ const compressionCandidatesBlock = candidatesEnabled ? `
10799
+
10800
+ ${COMPRESSION_CANDIDATES_SECTION}
10801
+
10802
+ ` : "\n\n";
10803
+ const breakdownTail = candidatesEnabled ? BREAKDOWN_TAIL_CANDIDATES : BREAKDOWN_TAIL_RANGES;
10804
+ return `
10009
10805
 
10010
10806
  You operate in a context-constrained environment. All compression serves the primary task, but be frugal. Context management helps preserve retrieval quality, but your primary goal is completing the task at hand. Do not let context management distract from the actual work.
10011
10807
 
@@ -10031,7 +10827,7 @@ You have five context-management tools:
10031
10827
  - \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
10032
10828
  - \`decompress\` \u2014 Restore a previously compressed block's content. By default restores one tier up (T2\u2192T1 summaries, not raw messages). Use \`full: true\` to restore all the way to original messages. Use \`toFile\` to write to file instead of inflating context. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\` or \`decompress({ blockId: "b5", full: true })\`.
10033
10829
  - \`search_context\` \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: \`search_context({ query: "auth token refresh" })\`.
10034
- - \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.
10830
+ ${acpStatusToolLine}
10035
10831
 
10036
10832
  COMPRESSION PHILOSOPHY
10037
10833
 
@@ -10039,7 +10835,7 @@ Two failure modes to avoid:
10039
10835
  - Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
10040
10836
  - Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
10041
10837
 
10042
- Balance is key. The single test for whether to compress is: "Is this content still needed by the current task step?" If yes, keep it. If no, compress it. All ranges listed in the context breakdown should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.
10838
+ Balance is key. The single test for whether to compress is: "Is this content still needed by the current task step?" If yes, keep it. If no, compress it. ${philosophyTail}
10043
10839
 
10044
10840
  Be frugal with context. Compress obvious waste proactively \u2014 verbose outputs you have already used, duplicate reads, abandoned explorations. Do not wait until context is critically full; that harms retrieval quality and risks overflow. But never let the urge to compress distract from the actual task.
10045
10841
 
@@ -10060,9 +10856,7 @@ WHEN NOT TO COMPRESS
10060
10856
  - Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
10061
10857
  - Protected tool outputs (default: \`skill\` only) \u2014 hard-excluded from compression ranges, survive intact in visible context.
10062
10858
 
10063
- ${HOW_TO_COMPRESS_RULES}
10064
-
10065
- MULTI-TIER COMPRESSION
10859
+ ${HOW_TO_COMPRESS_RULES}${compressionCandidatesBlock}MULTI-TIER COMPRESSION
10066
10860
 
10067
10861
  Summaries accumulate as the session grows. When tier-1 summaries pile up, the system injects a [Tier 2 Trigger] prompting you to DISTILL old blocks into a single tier-2 summary. If tier-2 summaries also accumulate, a [Tier 3 Trigger] asks you to CONDENSE them further.
10068
10862
 
@@ -10087,13 +10881,21 @@ Breakdown: 4.2K system (21%) | 8.0K tool (40%) | 2.0K summaries (10%) | 2.6K cod
10087
10881
  - "text" = plain text messages
10088
10882
  - "reasoning" = model thinking blocks (counted to match real API usage; freed when their message is compressed)
10089
10883
 
10090
- Below the breakdown, the system lists compressible ranges grouped by conversation turn. All listed ranges should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct. Compress the largest ranges first when the current step no longer needs them.
10884
+ ${breakdownTail}
10091
10885
 
10092
10886
  Each compression creates a reusable summary block you can decompress later if needed.
10093
10887
  `;
10888
+ }
10889
+ var SYSTEM = buildSystemPrompt(true);
10094
10890
 
10095
10891
  // lib/prompts/compress-range.ts
10096
- var COMPRESS_RANGE = `Collapse a range in the conversation into a detailed summary.
10892
+ var CANDIDATE_GUIDANCE_SECTION = `CANDIDATE GUIDANCE
10893
+ ACP may display a \`COMPRESSION CANDIDATES\` list in a nudge or \`acp_status\` report. A \`MICRO\` entry targets one large message or complete tool transaction; an \`EPISODE\` entry targets a contiguous historical segment. Displayed candidates are independent and non-overlapping, so they may be batched, but they are advisory: compress only entries whose content is no longer needed. When a nudge lists a clearly stale candidate, use its exact IDs and call \`compress\` before continuing. Keep current intent and active work visible, and use \`acp_status\` when the candidate list is stale.`;
10894
+ function buildCompressRangePrompt(candidatesEnabled) {
10895
+ const candidateGuidanceBlock = candidatesEnabled ? `${CANDIDATE_GUIDANCE_SECTION}
10896
+
10897
+ ` : "";
10898
+ return `Collapse a range in the conversation into a detailed summary.
10097
10899
 
10098
10900
  COMPRESSED BLOCK PLACEHOLDERS
10099
10901
  The system auto-detects any previously compressed blocks whose anchor messages fall inside your selected range. You do NOT need to manually list \`(bN)\` placeholders in your summary \u2014 every consumed block is tracked automatically.
@@ -10123,7 +10925,7 @@ Rules:
10123
10925
 
10124
10926
  - Pick \`startId\` and \`endId\` directly from injected IDs in context.
10125
10927
  - IDs must exist in the current visible context. If you cannot see an ID in the messages above, it is stale and will fail.
10126
- - \`startId\` must appear before \`endId\`.
10928
+ - Prefer \`startId\` before \`endId\` in conversation order. ACP can normalize reversed boundaries, but do not rely on that behavior.
10127
10929
  - Do not invent IDs. Use only IDs that are present in context.
10128
10930
  - NEVER use IDs from compressed block summaries, previous nudges, or your own memory \u2014 only IDs currently visible as XML metadata tags in the conversation.
10129
10931
 
@@ -10140,7 +10942,7 @@ compress({ content: [
10140
10942
  ]})
10141
10943
  \`\`\`
10142
10944
 
10143
- KEEP AND REF MARKERS
10945
+ ${candidateGuidanceBlock}KEEP AND REF MARKERS
10144
10946
  When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
10145
10947
 
10146
10948
  - \`[[KEEP:mNNNNN]]\` \u2014 Expands to the original message content inline (truncated to a max length). Use for critical content you want preserved verbatim in the summary without re-typing it: key function definitions, important error messages, essential file contents.
@@ -10157,65 +10959,8 @@ The rest of the bash calls were repetitive export commands. See [[REF:m00078|tes
10157
10959
 
10158
10960
  Use KEEP sparingly \u2014 each expansion adds to the summary length. Prefer REF for content that is important but not immediately critical.
10159
10961
  `;
10160
-
10161
- // lib/prompts/context-limit-nudge.ts
10162
- var CONTEXT_LIMIT_NUDGE = `
10163
- <system-reminder>
10164
- \u26A0\uFE0F Context limit reached \u2014 time to compress the largest ranges you no longer need. Prioritize completed tool outputs and resolved work. You can decompress specific blocks later if you need details. Keeping context lean helps you stay accurate.
10165
-
10166
- If mid-atomic-operation, finish that step first, then compress.
10167
-
10168
- HOW TO CALL COMPRESS:
10169
- {
10170
- "topic": "Short Label",
10171
- "content": [
10172
- {
10173
- "startId": "<ID from early in this conversation>",
10174
- "endId": "<ID from later in this conversation>",
10175
- "summary": "Complete technical summary of everything in the range"
10176
- }
10177
- ]
10178
10962
  }
10179
-
10180
- \u26A0\uFE0F ID RULES \u2014 MOST COMMON CAUSE OF ERRORS:
10181
- - ONLY use IDs you can see in tags in the messages ABOVE.
10182
- - Do NOT copy IDs from this example. Do NOT invent IDs.
10183
- - Do NOT use IDs from compressed block summaries \u2014 they are stale.
10184
- - startId must appear BEFORE endId in the conversation.
10185
-
10186
- RANGE STRATEGY:
10187
- - Prefer one large range over multiple small ones.
10188
- - Compress OLDER resolved history first. Keep recent active work.
10189
- </system-reminder>
10190
- `;
10191
-
10192
- // lib/prompts/turn-nudge.ts
10193
- var TURN_NUDGE = `
10194
- <system-reminder>
10195
- Context is getting full. If you've finished reading tool outputs or exploration results, compress them \u2014 you can decompress later if needed. This keeps your focus on the current task and improves accuracy.
10196
-
10197
- {
10198
- "topic": "Short Label",
10199
- "content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
10200
- }
10201
-
10202
- \u26A0\uFE0F ONLY use IDs from tags visible above. Do NOT invent or copy example IDs.
10203
- </system-reminder>
10204
- `;
10205
-
10206
- // lib/prompts/iteration-nudge.ts
10207
- var ITERATION_NUDGE = `
10208
- <system-reminder>
10209
- You've been iterating for a while. If any earlier work is closed and unlikely to be referenced, compress it now.
10210
-
10211
- {
10212
- "topic": "Short Label",
10213
- "content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
10214
- }
10215
-
10216
- \u26A0\uFE0F ONLY use IDs from tags visible above. Do NOT invent or copy example IDs.
10217
- </system-reminder>
10218
- `;
10963
+ var COMPRESS_RANGE = buildCompressRangePrompt(true);
10219
10964
 
10220
10965
  // lib/prompts/extensions/system.ts
10221
10966
  var SUBAGENT_SYSTEM_EXTENSION = `<dcp-system-reminder>
@@ -10307,24 +11052,27 @@ var HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
10307
11052
  var LEGACY_INLINE_COMMENT_LINE_REGEX = /^[ \t]*\/\/.*?\/\/[ \t]*$/gm;
10308
11053
  var DCP_SYSTEM_REMINDER_TAG_REGEX = /^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/(?:dcp|acp)-system-reminder>\s*$/i;
10309
11054
  var DEFAULTS_README_FILE = "README.md";
10310
- var BUNDLED_EDITABLE_PROMPTS = {
10311
- system: SYSTEM,
10312
- compressRange: COMPRESS_RANGE,
10313
- contextLimitNudge: CONTEXT_LIMIT_NUDGE,
10314
- turnNudge: TURN_NUDGE,
10315
- iterationNudge: ITERATION_NUDGE
10316
- };
11055
+ function getBundledEditablePrompts(candidatesEnabled) {
11056
+ return {
11057
+ system: buildSystemPrompt(candidatesEnabled),
11058
+ compressRange: buildCompressRangePrompt(candidatesEnabled),
11059
+ contextLimitNudge: CONTEXT_LIMIT_NUDGE,
11060
+ turnNudge: TURN_NUDGE,
11061
+ iterationNudge: ITERATION_NUDGE
11062
+ };
11063
+ }
10317
11064
  var INTERNAL_PROMPT_EXTENSIONS = {
10318
11065
  subagentExtension: SUBAGENT_SYSTEM_EXTENSION,
10319
11066
  decompressExtension: DECOMPRESS_SYSTEM_EXTENSION
10320
11067
  };
10321
- function createBundledRuntimePrompts() {
11068
+ function createBundledRuntimePrompts(candidatesEnabled) {
11069
+ const bundled = getBundledEditablePrompts(candidatesEnabled);
10322
11070
  return {
10323
- system: BUNDLED_EDITABLE_PROMPTS.system,
10324
- compressRange: BUNDLED_EDITABLE_PROMPTS.compressRange,
10325
- contextLimitNudge: BUNDLED_EDITABLE_PROMPTS.contextLimitNudge,
10326
- turnNudge: BUNDLED_EDITABLE_PROMPTS.turnNudge,
10327
- iterationNudge: BUNDLED_EDITABLE_PROMPTS.iterationNudge,
11071
+ system: bundled.system,
11072
+ compressRange: bundled.compressRange,
11073
+ contextLimitNudge: bundled.contextLimitNudge,
11074
+ turnNudge: bundled.turnNudge,
11075
+ iterationNudge: bundled.iterationNudge,
10328
11076
  subagentExtension: INTERNAL_PROMPT_EXTENSIONS.subagentExtension,
10329
11077
  decompressExtension: INTERNAL_PROMPT_EXTENSIONS.decompressExtension
10330
11078
  };
@@ -10469,12 +11217,14 @@ var PromptStore = class {
10469
11217
  logger;
10470
11218
  paths;
10471
11219
  customPromptsEnabled;
11220
+ candidatesEnabled;
10472
11221
  runtimePrompts;
10473
- constructor(logger, workingDirectory, customPromptsEnabled = false) {
11222
+ constructor(logger, workingDirectory, customPromptsEnabled = false, candidatesEnabled = false) {
10474
11223
  this.logger = logger;
10475
11224
  this.paths = resolvePromptPaths(workingDirectory);
10476
11225
  this.customPromptsEnabled = customPromptsEnabled;
10477
- this.runtimePrompts = createBundledRuntimePrompts();
11226
+ this.candidatesEnabled = candidatesEnabled;
11227
+ this.runtimePrompts = createBundledRuntimePrompts(this.candidatesEnabled);
10478
11228
  if (this.customPromptsEnabled) {
10479
11229
  this.ensureDefaultFiles();
10480
11230
  }
@@ -10484,13 +11234,13 @@ var PromptStore = class {
10484
11234
  return { ...this.runtimePrompts };
10485
11235
  }
10486
11236
  reload() {
10487
- const nextPrompts = createBundledRuntimePrompts();
11237
+ const nextPrompts = createBundledRuntimePrompts(this.candidatesEnabled);
10488
11238
  if (!this.customPromptsEnabled) {
10489
11239
  this.runtimePrompts = nextPrompts;
10490
11240
  return;
10491
11241
  }
10492
11242
  for (const definition of PROMPT_DEFINITIONS) {
10493
- const bundledSource = BUNDLED_EDITABLE_PROMPTS[definition.runtimeField];
11243
+ const bundledSource = getBundledEditablePrompts(this.candidatesEnabled)[definition.runtimeField];
10494
11244
  const bundledEditable = toEditablePromptText(definition, bundledSource);
10495
11245
  const bundledRuntime = wrapRuntimePromptContent(definition, bundledEditable);
10496
11246
  const fallbackValue = bundledRuntime || bundledSource.trim();
@@ -10551,13 +11301,14 @@ var PromptStore = class {
10551
11301
  });
10552
11302
  return;
10553
11303
  }
11304
+ const bundledPrompts = getBundledEditablePrompts(this.candidatesEnabled);
10554
11305
  for (const definition of PROMPT_DEFINITIONS) {
10555
11306
  const bundledEditable = toEditablePromptText(
10556
11307
  definition,
10557
- BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
11308
+ bundledPrompts[definition.runtimeField]
10558
11309
  );
10559
11310
  const managedContent = buildDefaultPromptFileContent(
10560
- bundledEditable || BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
11311
+ bundledEditable || bundledPrompts[definition.runtimeField]
10561
11312
  );
10562
11313
  const filePath = join5(this.paths.defaultsDir, definition.fileName);
10563
11314
  try {
@@ -12002,8 +12753,8 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
12002
12753
  cacheSystemPromptTokens(state, output.messages);
12003
12754
  assignMessageRefs(state, output.messages);
12004
12755
  const activeBlockCountBefore = state.prune.messages.activeBlockIds.size;
12005
- syncCompressionBlocks(state, logger, output.messages);
12006
- if (state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
12756
+ const compressionStateChanged = syncCompressionBlocks(state, logger, output.messages);
12757
+ if (compressionStateChanged || state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
12007
12758
  saveSessionState(state, logger).catch(() => {
12008
12759
  });
12009
12760
  }
@@ -12015,9 +12766,8 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
12015
12766
  });
12016
12767
  }
12017
12768
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
12769
+ const candidateMessages = config.compress.candidates === true ? output.messages.slice() : void 0;
12018
12770
  prune(state, logger, config, output.messages);
12019
- truncateLargeToolOutputs(state, config, logger, output.messages);
12020
- enforceContextBudget(state, config, logger, output.messages);
12021
12771
  hideConsumedCompressCalls(state, output.messages);
12022
12772
  assignMessageRefs(state, output.messages);
12023
12773
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -12042,8 +12792,16 @@ ${text}`);
12042
12792
  }).catch(() => {
12043
12793
  });
12044
12794
  } : void 0,
12045
- prePruneTokens
12795
+ prePruneTokens,
12796
+ candidateMessages
12797
+ );
12798
+ truncateLargeToolOutputs(
12799
+ state,
12800
+ config,
12801
+ logger,
12802
+ output.messages.filter((message) => !isSyntheticMessage(message))
12046
12803
  );
12804
+ enforceContextBudget(state, config, logger, output.messages);
12047
12805
  injectMessageIds(state, config, output.messages, compressionPriorities);
12048
12806
  hideFailedCompressCalls(output.messages);
12049
12807
  stripStaleMetadata(output.messages);
@@ -12478,7 +13236,7 @@ var server = (async (ctx) => {
12478
13236
  }
12479
13237
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12480
13238
  logger.info("ACP plugin initialized", {
12481
- version: true ? "1.17.1" : "dev",
13239
+ version: true ? "1.18.0-pr.392.147" : "dev",
12482
13240
  workspace: ctx.directory,
12483
13241
  logLevel: logger.level,
12484
13242
  debug: config.debug,
@@ -12486,7 +13244,12 @@ var server = (async (ctx) => {
12486
13244
  secureMode: isSecureMode()
12487
13245
  });
12488
13246
  const registry4 = new SessionStateRegistry(logger, ctx.directory);
12489
- const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
13247
+ const prompts = new PromptStore(
13248
+ logger,
13249
+ ctx.directory,
13250
+ config.experimental.customPrompts,
13251
+ config.compress.candidates === true
13252
+ );
12490
13253
  const hostPermissions = {
12491
13254
  global: void 0,
12492
13255
  agents: {}