opencode-acp 1.17.1 → 1.18.0-pr.392.149

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 +982 -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 +6 -0
  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
+ ` Output: ${candidatePlan.candidates.length} candidate(s) (${candidatePlan.truncatedCount} truncated)`
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" ? "\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 : "";
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,16 @@ 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) {
10163
+ const candidates = renderCompressionCandidates(rawMessages, ctx);
10164
+ if (candidates.length > 0) {
9449
10165
  lines.push("");
9450
- lines.push(
9451
- formatCompressibleRanges(contextRanges.compressible, contextRanges.protected)
9452
- );
10166
+ lines.push(...candidates);
9453
10167
  }
9454
10168
  }
9455
10169
  lines.push("");
9456
10170
  const hintTool = topToolName || "bash";
9457
10171
  lines.push(
9458
- `Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`
10172
+ `Tip: acp_status({scope:"uncompressed", view:"ranges"}) for raw ranges, or acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`
9459
10173
  );
9460
10174
  return lines;
9461
10175
  }
@@ -9492,6 +10206,44 @@ function renderUncompressedRanges(rawMessages, ctx) {
9492
10206
  lines.push(`Filter by tool: acp_status({scope:"uncompressed", view:"messages", tool:"bash"})`);
9493
10207
  return lines;
9494
10208
  }
10209
+ function renderCompressionCandidates(rawMessages, ctx) {
10210
+ const lines = ["COMPRESSION CANDIDATES"];
10211
+ if (!ctx.config?.compress || ctx.config.compress.candidates !== true) {
10212
+ return renderUncompressedRanges(rawMessages, ctx);
10213
+ }
10214
+ try {
10215
+ const plan = planCompressionCandidates(rawMessages, ctx.state, ctx.config);
10216
+ const visibleMessages = rawMessages.filter((message) => {
10217
+ const entry = ctx.state.prune.messages.byMessageId.get(message.info.id);
10218
+ return !entry || entry.activeBlockIds.length === 0;
10219
+ });
10220
+ const protectedRefs = computeProtectedRefs(visibleMessages, ctx.state, ctx.config.compress);
10221
+ const contextRanges = buildCompressibleRanges(
10222
+ visibleMessages,
10223
+ ctx.state,
10224
+ ctx.config.compress.protectedTools ?? [],
10225
+ ctx.config.protectedFilePatterns ?? [],
10226
+ protectedRefs
10227
+ );
10228
+ const protectedText = formatCompressibleRanges([], contextRanges.protected);
10229
+ if (plan.candidates.length === 0) {
10230
+ lines.push(" (no eligible candidates)");
10231
+ if (protectedText.includes("PROTECTED")) lines.push("", protectedText);
10232
+ return lines;
10233
+ }
10234
+ lines.length = 0;
10235
+ lines.push(formatCompressionCandidates(plan));
10236
+ if (protectedText.includes("PROTECTED")) lines.push("", protectedText);
10237
+ if (plan.omitted.length > 0) {
10238
+ lines.push(
10239
+ ` ${plan.omitted.length} structurally unsafe or undersized candidate${plan.omitted.length === 1 ? "" : "s"} omitted`
10240
+ );
10241
+ }
10242
+ return lines;
10243
+ } catch {
10244
+ return ["COMPRESSION CANDIDATES", " (candidate planning unavailable)"];
10245
+ }
10246
+ }
9495
10247
  function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
9496
10248
  const lines = [];
9497
10249
  let filtered = visibleMessages;
@@ -9594,14 +10346,12 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
9594
10346
  }
9595
10347
  function buildStatusReport(renderCtx, rawMessages, options) {
9596
10348
  const scope = options?.scope;
9597
- const view = options?.view ?? "ranges";
10349
+ const view = options?.view ?? "candidates";
9598
10350
  const toolFilter = options?.tool;
9599
10351
  const sort = options?.sort ?? "size";
9600
10352
  const limit = options?.limit ?? 30;
9601
10353
  const msgState = renderCtx.state.prune.messages;
9602
- const allBlocks = Array.from(msgState.blocksById.values()).sort(
9603
- (a, b) => a.blockId - b.blockId
9604
- );
10354
+ const allBlocks = Array.from(msgState.blocksById.values()).sort((a, b) => a.blockId - b.blockId);
9605
10355
  const activeBlocks = allBlocks.filter((b) => b.active);
9606
10356
  const lines = [];
9607
10357
  if (scope === "compressed") {
@@ -9615,8 +10365,10 @@ function buildStatusReport(renderCtx, rawMessages, options) {
9615
10365
  if (scope === "uncompressed") {
9616
10366
  if (view === "messages") {
9617
10367
  lines.push(...renderUncompressedDrilldown(visibleMsgs, toolFilter, sort, limit));
9618
- } else {
10368
+ } else if (view === "ranges") {
9619
10369
  lines.push(...renderUncompressedRanges(rawMessages, renderCtx));
10370
+ } else {
10371
+ lines.push(...renderCompressionCandidates(rawMessages, renderCtx));
9620
10372
  }
9621
10373
  } else {
9622
10374
  lines.push(
@@ -9640,7 +10392,7 @@ function createAcpStatusTool(factoryCtx) {
9640
10392
  args: {
9641
10393
  scope: tool4.schema.string().optional().describe('Drill down: "compressed" or "uncompressed". No arg = overview of both.'),
9642
10394
  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)'
10395
+ '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
10396
  ),
9645
10397
  tool: tool4.schema.string().optional().describe(
9646
10398
  'Filter by tool type (only with scope:"uncompressed", view:"messages"). e.g., "bash", "todowrite", "write"'
@@ -9651,16 +10403,16 @@ function createAcpStatusTool(factoryCtx) {
9651
10403
  async execute(args, toolCtx) {
9652
10404
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
9653
10405
  const scope = args.scope === "compressed" || args.scope === "uncompressed" ? args.scope : void 0;
9654
- const view = args.view === "messages" ? "messages" : "ranges";
10406
+ const view = args.view === "messages" || args.view === "ranges" || args.view === "candidates" ? args.view : "candidates";
9655
10407
  const toolFilter = typeof args.tool === "string" ? args.tool : void 0;
9656
10408
  const sort = args.sort === "time" || args.sort === "tool" || args.sort === "age" ? args.sort : "size";
9657
10409
  const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
9658
10410
  if (scope === "compressed") {
9659
- return buildStatusReport(
9660
- { state: ctx.state, config: ctx.config },
9661
- [],
9662
- { scope: "compressed", sort, limit }
9663
- );
10411
+ return buildStatusReport({ state: ctx.state, config: ctx.config }, [], {
10412
+ scope: "compressed",
10413
+ sort,
10414
+ limit
10415
+ });
9664
10416
  }
9665
10417
  let rawMessages = [];
9666
10418
  try {
@@ -9670,11 +10422,13 @@ function createAcpStatusTool(factoryCtx) {
9670
10422
  rawMessages = [];
9671
10423
  }
9672
10424
  hideConsumedCompressCalls(ctx.state, rawMessages);
9673
- return buildStatusReport(
9674
- { state: ctx.state, config: ctx.config },
9675
- rawMessages,
9676
- { scope, view, tool: toolFilter, sort, limit }
9677
- );
10425
+ return buildStatusReport({ state: ctx.state, config: ctx.config }, rawMessages, {
10426
+ scope,
10427
+ view,
10428
+ tool: toolFilter,
10429
+ sort,
10430
+ limit
10431
+ });
9678
10432
  }
9679
10433
  });
9680
10434
  }
@@ -9782,7 +10536,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9782
10536
  import { join as join4 } from "path";
9783
10537
  import { existsSync as existsSync4 } from "fs";
9784
10538
  import { homedir as homedir3 } from "os";
9785
- var LOG_VERSION = true ? "1.17.1" : "dev";
10539
+ var LOG_VERSION = true ? "1.18.0-pr.392.149" : "dev";
9786
10540
  var LEVEL_RANK = {
9787
10541
  debug: 10,
9788
10542
  info: 20,
@@ -10005,7 +10759,32 @@ import { join as join5, dirname as dirname2 } from "path";
10005
10759
  import { homedir as homedir4 } from "os";
10006
10760
 
10007
10761
  // lib/prompts/system.ts
10008
- var SYSTEM = `
10762
+ 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.`;
10763
+ 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.`;
10764
+ 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.`;
10765
+ 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.`;
10766
+ var COMPRESSION_CANDIDATES_SECTION = `COMPRESSION CANDIDATES
10767
+
10768
+ ACP may append a bounded list headed \`COMPRESSION CANDIDATES\` below a nudge or status report:
10769
+
10770
+ - \`MICRO\` is one large plain message or one complete tool transaction, including every message needed to keep its tool call/result structure valid.
10771
+ - \`EPISODE\` is a contiguous historical segment made from smaller adjacent units.
10772
+ - Candidate ranges are independent, non-overlapping, and can be submitted together as separate \`content[]\` entries.
10773
+ - 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.
10774
+ - Keep summaries self-contained and use the existing \`compress\` tool. Candidate labels do not replace the semantic judgment to preserve useful context.
10775
+ - 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.`;
10776
+ 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.`;
10777
+ 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.`;
10778
+ function buildSystemPrompt(candidatesEnabled) {
10779
+ const acpStatusToolLine = candidatesEnabled ? ACP_STATUS_TOOL_LINE_CANDIDATES : ACP_STATUS_TOOL_LINE_RANGES;
10780
+ const philosophyTail = candidatesEnabled ? PHILOSOPHY_TAIL_CANDIDATES : PHILOSOPHY_TAIL_RANGES;
10781
+ const compressionCandidatesBlock = candidatesEnabled ? `
10782
+
10783
+ ${COMPRESSION_CANDIDATES_SECTION}
10784
+
10785
+ ` : "\n\n";
10786
+ const breakdownTail = candidatesEnabled ? BREAKDOWN_TAIL_CANDIDATES : BREAKDOWN_TAIL_RANGES;
10787
+ return `
10009
10788
 
10010
10789
  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
10790
 
@@ -10031,7 +10810,7 @@ You have five context-management tools:
10031
10810
  - \`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
10811
  - \`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
10812
  - \`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.
10813
+ ${acpStatusToolLine}
10035
10814
 
10036
10815
  COMPRESSION PHILOSOPHY
10037
10816
 
@@ -10039,7 +10818,7 @@ Two failure modes to avoid:
10039
10818
  - Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
10040
10819
  - Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
10041
10820
 
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.
10821
+ 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
10822
 
10044
10823
  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
10824
 
@@ -10060,9 +10839,7 @@ WHEN NOT TO COMPRESS
10060
10839
  - Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
10061
10840
  - Protected tool outputs (default: \`skill\` only) \u2014 hard-excluded from compression ranges, survive intact in visible context.
10062
10841
 
10063
- ${HOW_TO_COMPRESS_RULES}
10064
-
10065
- MULTI-TIER COMPRESSION
10842
+ ${HOW_TO_COMPRESS_RULES}${compressionCandidatesBlock}MULTI-TIER COMPRESSION
10066
10843
 
10067
10844
  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
10845
 
@@ -10087,13 +10864,21 @@ Breakdown: 4.2K system (21%) | 8.0K tool (40%) | 2.0K summaries (10%) | 2.6K cod
10087
10864
  - "text" = plain text messages
10088
10865
  - "reasoning" = model thinking blocks (counted to match real API usage; freed when their message is compressed)
10089
10866
 
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.
10867
+ ${breakdownTail}
10091
10868
 
10092
10869
  Each compression creates a reusable summary block you can decompress later if needed.
10093
10870
  `;
10871
+ }
10872
+ var SYSTEM = buildSystemPrompt(true);
10094
10873
 
10095
10874
  // lib/prompts/compress-range.ts
10096
- var COMPRESS_RANGE = `Collapse a range in the conversation into a detailed summary.
10875
+ var CANDIDATE_GUIDANCE_SECTION = `CANDIDATE GUIDANCE
10876
+ 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.`;
10877
+ function buildCompressRangePrompt(candidatesEnabled) {
10878
+ const candidateGuidanceBlock = candidatesEnabled ? `${CANDIDATE_GUIDANCE_SECTION}
10879
+
10880
+ ` : "";
10881
+ return `Collapse a range in the conversation into a detailed summary.
10097
10882
 
10098
10883
  COMPRESSED BLOCK PLACEHOLDERS
10099
10884
  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 +10908,7 @@ Rules:
10123
10908
 
10124
10909
  - Pick \`startId\` and \`endId\` directly from injected IDs in context.
10125
10910
  - 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\`.
10911
+ - Prefer \`startId\` before \`endId\` in conversation order. ACP can normalize reversed boundaries, but do not rely on that behavior.
10127
10912
  - Do not invent IDs. Use only IDs that are present in context.
10128
10913
  - 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
10914
 
@@ -10140,7 +10925,7 @@ compress({ content: [
10140
10925
  ]})
10141
10926
  \`\`\`
10142
10927
 
10143
- KEEP AND REF MARKERS
10928
+ ${candidateGuidanceBlock}KEEP AND REF MARKERS
10144
10929
  When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
10145
10930
 
10146
10931
  - \`[[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 +10942,8 @@ The rest of the bash calls were repetitive export commands. See [[REF:m00078|tes
10157
10942
 
10158
10943
  Use KEEP sparingly \u2014 each expansion adds to the summary length. Prefer REF for content that is important but not immediately critical.
10159
10944
  `;
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
- }
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
10945
  }
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
- `;
10946
+ var COMPRESS_RANGE = buildCompressRangePrompt(true);
10219
10947
 
10220
10948
  // lib/prompts/extensions/system.ts
10221
10949
  var SUBAGENT_SYSTEM_EXTENSION = `<dcp-system-reminder>
@@ -10307,24 +11035,27 @@ var HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
10307
11035
  var LEGACY_INLINE_COMMENT_LINE_REGEX = /^[ \t]*\/\/.*?\/\/[ \t]*$/gm;
10308
11036
  var DCP_SYSTEM_REMINDER_TAG_REGEX = /^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/(?:dcp|acp)-system-reminder>\s*$/i;
10309
11037
  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
- };
11038
+ function getBundledEditablePrompts(candidatesEnabled) {
11039
+ return {
11040
+ system: buildSystemPrompt(candidatesEnabled),
11041
+ compressRange: buildCompressRangePrompt(candidatesEnabled),
11042
+ contextLimitNudge: CONTEXT_LIMIT_NUDGE,
11043
+ turnNudge: TURN_NUDGE,
11044
+ iterationNudge: ITERATION_NUDGE
11045
+ };
11046
+ }
10317
11047
  var INTERNAL_PROMPT_EXTENSIONS = {
10318
11048
  subagentExtension: SUBAGENT_SYSTEM_EXTENSION,
10319
11049
  decompressExtension: DECOMPRESS_SYSTEM_EXTENSION
10320
11050
  };
10321
- function createBundledRuntimePrompts() {
11051
+ function createBundledRuntimePrompts(candidatesEnabled) {
11052
+ const bundled = getBundledEditablePrompts(candidatesEnabled);
10322
11053
  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,
11054
+ system: bundled.system,
11055
+ compressRange: bundled.compressRange,
11056
+ contextLimitNudge: bundled.contextLimitNudge,
11057
+ turnNudge: bundled.turnNudge,
11058
+ iterationNudge: bundled.iterationNudge,
10328
11059
  subagentExtension: INTERNAL_PROMPT_EXTENSIONS.subagentExtension,
10329
11060
  decompressExtension: INTERNAL_PROMPT_EXTENSIONS.decompressExtension
10330
11061
  };
@@ -10469,12 +11200,14 @@ var PromptStore = class {
10469
11200
  logger;
10470
11201
  paths;
10471
11202
  customPromptsEnabled;
11203
+ candidatesEnabled;
10472
11204
  runtimePrompts;
10473
- constructor(logger, workingDirectory, customPromptsEnabled = false) {
11205
+ constructor(logger, workingDirectory, customPromptsEnabled = false, candidatesEnabled = false) {
10474
11206
  this.logger = logger;
10475
11207
  this.paths = resolvePromptPaths(workingDirectory);
10476
11208
  this.customPromptsEnabled = customPromptsEnabled;
10477
- this.runtimePrompts = createBundledRuntimePrompts();
11209
+ this.candidatesEnabled = candidatesEnabled;
11210
+ this.runtimePrompts = createBundledRuntimePrompts(this.candidatesEnabled);
10478
11211
  if (this.customPromptsEnabled) {
10479
11212
  this.ensureDefaultFiles();
10480
11213
  }
@@ -10484,13 +11217,13 @@ var PromptStore = class {
10484
11217
  return { ...this.runtimePrompts };
10485
11218
  }
10486
11219
  reload() {
10487
- const nextPrompts = createBundledRuntimePrompts();
11220
+ const nextPrompts = createBundledRuntimePrompts(this.candidatesEnabled);
10488
11221
  if (!this.customPromptsEnabled) {
10489
11222
  this.runtimePrompts = nextPrompts;
10490
11223
  return;
10491
11224
  }
10492
11225
  for (const definition of PROMPT_DEFINITIONS) {
10493
- const bundledSource = BUNDLED_EDITABLE_PROMPTS[definition.runtimeField];
11226
+ const bundledSource = getBundledEditablePrompts(this.candidatesEnabled)[definition.runtimeField];
10494
11227
  const bundledEditable = toEditablePromptText(definition, bundledSource);
10495
11228
  const bundledRuntime = wrapRuntimePromptContent(definition, bundledEditable);
10496
11229
  const fallbackValue = bundledRuntime || bundledSource.trim();
@@ -10551,13 +11284,14 @@ var PromptStore = class {
10551
11284
  });
10552
11285
  return;
10553
11286
  }
11287
+ const bundledPrompts = getBundledEditablePrompts(this.candidatesEnabled);
10554
11288
  for (const definition of PROMPT_DEFINITIONS) {
10555
11289
  const bundledEditable = toEditablePromptText(
10556
11290
  definition,
10557
- BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
11291
+ bundledPrompts[definition.runtimeField]
10558
11292
  );
10559
11293
  const managedContent = buildDefaultPromptFileContent(
10560
- bundledEditable || BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
11294
+ bundledEditable || bundledPrompts[definition.runtimeField]
10561
11295
  );
10562
11296
  const filePath = join5(this.paths.defaultsDir, definition.fileName);
10563
11297
  try {
@@ -12002,8 +12736,8 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
12002
12736
  cacheSystemPromptTokens(state, output.messages);
12003
12737
  assignMessageRefs(state, output.messages);
12004
12738
  const activeBlockCountBefore = state.prune.messages.activeBlockIds.size;
12005
- syncCompressionBlocks(state, logger, output.messages);
12006
- if (state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
12739
+ const compressionStateChanged = syncCompressionBlocks(state, logger, output.messages);
12740
+ if (compressionStateChanged || state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
12007
12741
  saveSessionState(state, logger).catch(() => {
12008
12742
  });
12009
12743
  }
@@ -12015,9 +12749,8 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
12015
12749
  });
12016
12750
  }
12017
12751
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
12752
+ const candidateMessages = output.messages.slice();
12018
12753
  prune(state, logger, config, output.messages);
12019
- truncateLargeToolOutputs(state, config, logger, output.messages);
12020
- enforceContextBudget(state, config, logger, output.messages);
12021
12754
  hideConsumedCompressCalls(state, output.messages);
12022
12755
  assignMessageRefs(state, output.messages);
12023
12756
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -12042,8 +12775,16 @@ ${text}`);
12042
12775
  }).catch(() => {
12043
12776
  });
12044
12777
  } : void 0,
12045
- prePruneTokens
12778
+ prePruneTokens,
12779
+ candidateMessages
12780
+ );
12781
+ truncateLargeToolOutputs(
12782
+ state,
12783
+ config,
12784
+ logger,
12785
+ output.messages.filter((message) => !isSyntheticMessage(message))
12046
12786
  );
12787
+ enforceContextBudget(state, config, logger, output.messages);
12047
12788
  injectMessageIds(state, config, output.messages, compressionPriorities);
12048
12789
  hideFailedCompressCalls(output.messages);
12049
12790
  stripStaleMetadata(output.messages);
@@ -12478,7 +13219,7 @@ var server = (async (ctx) => {
12478
13219
  }
12479
13220
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12480
13221
  logger.info("ACP plugin initialized", {
12481
- version: true ? "1.17.1" : "dev",
13222
+ version: true ? "1.18.0-pr.392.149" : "dev",
12482
13223
  workspace: ctx.directory,
12483
13224
  logLevel: logger.level,
12484
13225
  debug: config.debug,
@@ -12486,7 +13227,12 @@ var server = (async (ctx) => {
12486
13227
  secureMode: isSecureMode()
12487
13228
  });
12488
13229
  const registry4 = new SessionStateRegistry(logger, ctx.directory);
12489
- const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
13230
+ const prompts = new PromptStore(
13231
+ logger,
13232
+ ctx.directory,
13233
+ config.experimental.customPrompts,
13234
+ config.compress.candidates === true
13235
+ );
12490
13236
  const hostPermissions = {
12491
13237
  global: void 0,
12492
13238
  agents: {}