opencode-acp 1.5.1 → 1.6.0

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.
package/dist/index.js CHANGED
@@ -900,6 +900,9 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
900
900
  "compress.protectedTools",
901
901
  "compress.protectTags",
902
902
  "compress.protectUserMessages",
903
+ "compress.maxSummaryLength",
904
+ "compress.maxSummaryLengthHard",
905
+ "compress.minCompressRange",
903
906
  "gc",
904
907
  "gc.algorithm",
905
908
  "gc.promotionThreshold",
@@ -1160,6 +1163,55 @@ function validateConfigTypes(config) {
1160
1163
  actual: typeof compress.protectUserMessages
1161
1164
  });
1162
1165
  }
1166
+ if (compress.maxSummaryLength !== void 0 && typeof compress.maxSummaryLength !== "number") {
1167
+ errors.push({
1168
+ key: "compress.maxSummaryLength",
1169
+ expected: "number",
1170
+ actual: typeof compress.maxSummaryLength
1171
+ });
1172
+ }
1173
+ if (typeof compress.maxSummaryLength === "number" && compress.maxSummaryLength < 1) {
1174
+ errors.push({
1175
+ key: "compress.maxSummaryLength",
1176
+ expected: "positive number (>= 1)",
1177
+ actual: `${compress.maxSummaryLength}`
1178
+ });
1179
+ }
1180
+ if (compress.maxSummaryLengthHard !== void 0 && typeof compress.maxSummaryLengthHard !== "number") {
1181
+ errors.push({
1182
+ key: "compress.maxSummaryLengthHard",
1183
+ expected: "number",
1184
+ actual: typeof compress.maxSummaryLengthHard
1185
+ });
1186
+ }
1187
+ if (typeof compress.maxSummaryLengthHard === "number" && compress.maxSummaryLengthHard < 1) {
1188
+ errors.push({
1189
+ key: "compress.maxSummaryLengthHard",
1190
+ expected: "positive number (>= 1)",
1191
+ actual: `${compress.maxSummaryLengthHard}`
1192
+ });
1193
+ }
1194
+ if (typeof compress.maxSummaryLength === "number" && typeof compress.maxSummaryLengthHard === "number" && compress.maxSummaryLengthHard < compress.maxSummaryLength) {
1195
+ errors.push({
1196
+ key: "compress.maxSummaryLengthHard",
1197
+ expected: `>= maxSummaryLength (${compress.maxSummaryLength})`,
1198
+ actual: `${compress.maxSummaryLengthHard}`
1199
+ });
1200
+ }
1201
+ if (compress.minCompressRange !== void 0 && typeof compress.minCompressRange !== "number") {
1202
+ errors.push({
1203
+ key: "compress.minCompressRange",
1204
+ expected: "number",
1205
+ actual: typeof compress.minCompressRange
1206
+ });
1207
+ }
1208
+ if (typeof compress.minCompressRange === "number" && compress.minCompressRange < 0) {
1209
+ errors.push({
1210
+ key: "compress.minCompressRange",
1211
+ expected: "non-negative number (>= 0)",
1212
+ actual: `${compress.minCompressRange}`
1213
+ });
1214
+ }
1163
1215
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1164
1216
  errors.push({
1165
1217
  key: "compress.iterationNudgeThreshold",
@@ -1393,8 +1445,6 @@ var DEFAULT_PROTECTED_TOOLS = [
1393
1445
  "todoread",
1394
1446
  "compress",
1395
1447
  "decompress",
1396
- "mark_block",
1397
- "unmark_block",
1398
1448
  "batch",
1399
1449
  "plan_enter",
1400
1450
  "plan_exit",
@@ -1474,7 +1524,10 @@ var defaultConfig = {
1474
1524
  nudgeForce: "soft",
1475
1525
  protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
1476
1526
  protectTags: false,
1477
- protectUserMessages: false
1527
+ protectUserMessages: false,
1528
+ maxSummaryLength: 200,
1529
+ maxSummaryLengthHard: 800,
1530
+ minCompressRange: 2e3
1478
1531
  },
1479
1532
  strategies: {
1480
1533
  deduplication: {
@@ -1626,7 +1679,10 @@ function mergeCompress(base, override) {
1626
1679
  nudgeForce: override.nudgeForce ?? base.nudgeForce,
1627
1680
  protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
1628
1681
  protectTags: override.protectTags ?? base.protectTags,
1629
- protectUserMessages: override.protectUserMessages ?? base.protectUserMessages
1682
+ protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
1683
+ maxSummaryLength: override.maxSummaryLength ?? base.maxSummaryLength,
1684
+ maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
1685
+ minCompressRange: override.minCompressRange ?? base.minCompressRange
1630
1686
  };
1631
1687
  }
1632
1688
  function mergeCommands(base, override) {
@@ -1990,6 +2046,20 @@ function countAllMessageTokens(msg) {
1990
2046
  if (texts.length === 0) return 0;
1991
2047
  return estimateTokensBatch(texts);
1992
2048
  }
2049
+ function countMessageCharacters(msg) {
2050
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
2051
+ let total = 0;
2052
+ for (const part of parts) {
2053
+ if (part.type === "text" && typeof part.text === "string") {
2054
+ total += part.text.length;
2055
+ } else {
2056
+ for (const content of extractToolContent(part)) {
2057
+ total += content.length;
2058
+ }
2059
+ }
2060
+ }
2061
+ return total;
2062
+ }
1993
2063
 
1994
2064
  // lib/prompts/extensions/tool.ts
1995
2065
  var RANGE_FORMAT_EXTENSION = `
@@ -3550,20 +3620,20 @@ function matchesGlob(inputPath, pattern) {
3550
3620
  regex += "$";
3551
3621
  return new RegExp(regex).test(input);
3552
3622
  }
3553
- function getFilePathsFromParameters(tool5, parameters) {
3623
+ function getFilePathsFromParameters(tool4, parameters) {
3554
3624
  if (typeof parameters !== "object" || parameters === null) {
3555
3625
  return [];
3556
3626
  }
3557
3627
  const paths = [];
3558
3628
  const params = parameters;
3559
- if (tool5 === "apply_patch" && typeof params.patchText === "string") {
3629
+ if (tool4 === "apply_patch" && typeof params.patchText === "string") {
3560
3630
  const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
3561
3631
  let match;
3562
3632
  while ((match = pathRegex.exec(params.patchText)) !== null) {
3563
3633
  paths.push(match[1].trim());
3564
3634
  }
3565
3635
  }
3566
- if (tool5 === "multiedit") {
3636
+ if (tool4 === "multiedit") {
3567
3637
  if (typeof params.filePath === "string") {
3568
3638
  paths.push(params.filePath);
3569
3639
  }
@@ -3658,13 +3728,13 @@ var deduplicate = (state, logger, config, messages) => {
3658
3728
  logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
3659
3729
  }
3660
3730
  };
3661
- function createToolSignature(tool5, parameters) {
3731
+ function createToolSignature(tool4, parameters) {
3662
3732
  if (!parameters) {
3663
- return tool5;
3733
+ return tool4;
3664
3734
  }
3665
3735
  const normalized = normalizeParameters(parameters);
3666
3736
  const sorted = sortObjectKeys(normalized);
3667
- return `${tool5}::${JSON.stringify(sorted)}`;
3737
+ return `${tool4}::${JSON.stringify(sorted)}`;
3668
3738
  }
3669
3739
  function normalizeParameters(params) {
3670
3740
  if (typeof params !== "object" || params === null) return params;
@@ -3739,9 +3809,9 @@ var purgeErrors = (state, logger, config, messages) => {
3739
3809
  };
3740
3810
 
3741
3811
  // lib/ui/utils.ts
3742
- function extractParameterKey(tool5, parameters) {
3812
+ function extractParameterKey(tool4, parameters) {
3743
3813
  if (!parameters) return "";
3744
- if (tool5 === "read" && parameters.filePath) {
3814
+ if (tool4 === "read" && parameters.filePath) {
3745
3815
  const offset = parameters.offset;
3746
3816
  const limit = parameters.limit;
3747
3817
  if (offset !== void 0 && limit !== void 0) {
@@ -3755,10 +3825,10 @@ function extractParameterKey(tool5, parameters) {
3755
3825
  }
3756
3826
  return parameters.filePath;
3757
3827
  }
3758
- if ((tool5 === "write" || tool5 === "edit" || tool5 === "multiedit") && parameters.filePath) {
3828
+ if ((tool4 === "write" || tool4 === "edit" || tool4 === "multiedit") && parameters.filePath) {
3759
3829
  return parameters.filePath;
3760
3830
  }
3761
- if (tool5 === "apply_patch" && typeof parameters.patchText === "string") {
3831
+ if (tool4 === "apply_patch" && typeof parameters.patchText === "string") {
3762
3832
  const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
3763
3833
  const paths = [];
3764
3834
  let match;
@@ -3775,51 +3845,51 @@ function extractParameterKey(tool5, parameters) {
3775
3845
  }
3776
3846
  return "patch";
3777
3847
  }
3778
- if (tool5 === "list") {
3848
+ if (tool4 === "list") {
3779
3849
  return parameters.path || "(current directory)";
3780
3850
  }
3781
- if (tool5 === "glob") {
3851
+ if (tool4 === "glob") {
3782
3852
  if (parameters.pattern) {
3783
3853
  const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
3784
3854
  return `"${parameters.pattern}"${pathInfo}`;
3785
3855
  }
3786
3856
  return "(unknown pattern)";
3787
3857
  }
3788
- if (tool5 === "grep") {
3858
+ if (tool4 === "grep") {
3789
3859
  if (parameters.pattern) {
3790
3860
  const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
3791
3861
  return `"${parameters.pattern}"${pathInfo}`;
3792
3862
  }
3793
3863
  return "(unknown pattern)";
3794
3864
  }
3795
- if (tool5 === "bash") {
3865
+ if (tool4 === "bash") {
3796
3866
  if (parameters.description) return parameters.description;
3797
3867
  if (parameters.command) {
3798
3868
  return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
3799
3869
  }
3800
3870
  }
3801
- if (tool5 === "webfetch" && parameters.url) {
3871
+ if (tool4 === "webfetch" && parameters.url) {
3802
3872
  return parameters.url;
3803
3873
  }
3804
- if (tool5 === "websearch" && parameters.query) {
3874
+ if (tool4 === "websearch" && parameters.query) {
3805
3875
  return `"${parameters.query}"`;
3806
3876
  }
3807
- if (tool5 === "codesearch" && parameters.query) {
3877
+ if (tool4 === "codesearch" && parameters.query) {
3808
3878
  return `"${parameters.query}"`;
3809
3879
  }
3810
- if (tool5 === "todowrite") {
3880
+ if (tool4 === "todowrite") {
3811
3881
  return `${parameters.todos?.length || 0} todos`;
3812
3882
  }
3813
- if (tool5 === "todoread") {
3883
+ if (tool4 === "todoread") {
3814
3884
  return "read todo list";
3815
3885
  }
3816
- if (tool5 === "task" && parameters.description) {
3886
+ if (tool4 === "task" && parameters.description) {
3817
3887
  return parameters.description;
3818
3888
  }
3819
- if (tool5 === "skill" && parameters.name) {
3889
+ if (tool4 === "skill" && parameters.name) {
3820
3890
  return parameters.name;
3821
3891
  }
3822
- if (tool5 === "lsp") {
3892
+ if (tool4 === "lsp") {
3823
3893
  const op = parameters.operation || "lsp";
3824
3894
  const path = parameters.filePath || "";
3825
3895
  const line = parameters.line;
@@ -3832,7 +3902,7 @@ function extractParameterKey(tool5, parameters) {
3832
3902
  }
3833
3903
  return op;
3834
3904
  }
3835
- if (tool5 === "question") {
3905
+ if (tool4 === "question") {
3836
3906
  const questions = parameters.questions;
3837
3907
  if (Array.isArray(questions) && questions.length > 0) {
3838
3908
  const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
@@ -4411,7 +4481,7 @@ ${output}`);
4411
4481
  }
4412
4482
 
4413
4483
  // lib/compress/message.ts
4414
- function buildSchema() {
4484
+ function buildSchema(maxSummaryLength) {
4415
4485
  return {
4416
4486
  topic: tool.schema.string().describe(
4417
4487
  "Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
@@ -4420,7 +4490,9 @@ function buildSchema() {
4420
4490
  tool.schema.object({
4421
4491
  messageId: tool.schema.string().describe("Raw message ID to compress (e.g. m00001)"),
4422
4492
  topic: tool.schema.string().describe("Short label (3-5 words) for this one message summary"),
4423
- summary: tool.schema.string().describe("Complete technical summary replacing that one message")
4493
+ summary: tool.schema.string().describe(
4494
+ `Complete technical summary replacing that one message. Aim for <=${maxSummaryLength} chars; exceed only when strictly necessary to preserve critical detail (file paths, decisions, signatures, exact values). Never pad.`
4495
+ )
4424
4496
  })
4425
4497
  ).describe("Batch of individual message summaries to create in one tool call")
4426
4498
  };
@@ -4430,10 +4502,18 @@ function createCompressMessageTool(ctx) {
4430
4502
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4431
4503
  return tool({
4432
4504
  description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
4433
- args: buildSchema(),
4505
+ args: buildSchema(ctx.config.compress.maxSummaryLength),
4434
4506
  async execute(args, toolCtx) {
4435
4507
  const input = args;
4436
4508
  validateArgs(input);
4509
+ const maxSummaryLengthHard = ctx.config.compress.maxSummaryLengthHard;
4510
+ for (const entry of input.content) {
4511
+ if (entry.summary.length > maxSummaryLengthHard) {
4512
+ throw new Error(
4513
+ `Summary too long (${entry.summary.length} chars, hard ceiling ${maxSummaryLengthHard}). Aim for <=${ctx.config.compress.maxSummaryLength}; exceed only when strictly necessary. Rewrite more concisely.`
4514
+ );
4515
+ }
4516
+ }
4437
4517
  const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
4438
4518
  const { rawMessages, searchContext } = await prepareSession(
4439
4519
  ctx,
@@ -4449,6 +4529,26 @@ function createCompressMessageTool(ctx) {
4449
4529
  if (plans.length === 0 && skippedCount > 0) {
4450
4530
  throw new Error(formatIssues(skippedIssues, skippedCount));
4451
4531
  }
4532
+ const minCompressRange = ctx.config.compress.minCompressRange;
4533
+ if (minCompressRange > 0) {
4534
+ let totalChars = 0;
4535
+ const counted = /* @__PURE__ */ new Set();
4536
+ for (const plan of plans) {
4537
+ for (const messageId of plan.selection.messageIds) {
4538
+ if (counted.has(messageId)) continue;
4539
+ counted.add(messageId);
4540
+ const rawMessage = searchContext.rawMessagesById.get(messageId);
4541
+ if (rawMessage) {
4542
+ totalChars += countMessageCharacters(rawMessage);
4543
+ }
4544
+ }
4545
+ }
4546
+ if (totalChars < minCompressRange) {
4547
+ throw new Error(
4548
+ `Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
4549
+ );
4550
+ }
4551
+ }
4452
4552
  const notifications = [];
4453
4553
  const preparedPlans = [];
4454
4554
  for (const plan of plans) {
@@ -4633,7 +4733,13 @@ function validateSummaryPlaceholders(placeholders, requiredBlockIds, startRefere
4633
4733
  }
4634
4734
  placeholders.length = 0;
4635
4735
  placeholders.push(...validPlaceholders);
4636
- return strictRequiredIds.filter((id) => !keptPlaceholderIds.has(id));
4736
+ const missingIds = strictRequiredIds.filter((id) => !keptPlaceholderIds.has(id));
4737
+ if (missingIds.length > 0) {
4738
+ console.warn(
4739
+ `[ACP] compress summary omitted placeholders for required blocks: ${missingIds.map((id) => `b${id}`).join(", ")}. They will be auto-attached as consumed blocks.`
4740
+ );
4741
+ }
4742
+ return missingIds;
4637
4743
  }
4638
4744
  function injectBlockPlaceholders(summary, _placeholders, _summaryByBlockId, _startReference, _endReference) {
4639
4745
  return {
@@ -4649,7 +4755,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
4649
4755
  }
4650
4756
 
4651
4757
  // lib/compress/range.ts
4652
- function buildSchema2() {
4758
+ function buildSchema2(maxSummaryLength) {
4653
4759
  return {
4654
4760
  topic: tool2.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
4655
4761
  content: tool2.schema.array(
@@ -4658,7 +4764,9 @@ function buildSchema2() {
4658
4764
  "Message or block ID marking the beginning of range (e.g. m00001, b2)"
4659
4765
  ),
4660
4766
  endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
4661
- summary: tool2.schema.string().describe("Complete technical summary replacing all content in range")
4767
+ summary: tool2.schema.string().describe(
4768
+ `Complete technical summary replacing all content in range. Aim for <=${maxSummaryLength} chars; exceed only when strictly necessary to preserve critical detail (file paths, decisions, signatures, exact values). Never pad.`
4769
+ )
4662
4770
  })
4663
4771
  ).describe(
4664
4772
  "One or more ranges to compress, each with start/end boundaries and a summary"
@@ -4670,10 +4778,18 @@ function createCompressRangeTool(ctx) {
4670
4778
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4671
4779
  return tool2({
4672
4780
  description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
4673
- args: buildSchema2(),
4781
+ args: buildSchema2(ctx.config.compress.maxSummaryLength),
4674
4782
  async execute(args, toolCtx) {
4675
4783
  const input = args;
4676
4784
  validateArgs2(input);
4785
+ const maxSummaryLengthHard = ctx.config.compress.maxSummaryLengthHard;
4786
+ for (const entry of input.content) {
4787
+ if (entry.summary.length > maxSummaryLengthHard) {
4788
+ throw new Error(
4789
+ `Summary too long (${entry.summary.length} chars, hard ceiling ${maxSummaryLengthHard}). Aim for <=${ctx.config.compress.maxSummaryLength}; exceed only when strictly necessary. Rewrite more concisely.`
4790
+ );
4791
+ }
4792
+ }
4677
4793
  const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
4678
4794
  const { rawMessages, searchContext } = await prepareSession(
4679
4795
  ctx,
@@ -4682,6 +4798,26 @@ function createCompressRangeTool(ctx) {
4682
4798
  );
4683
4799
  const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
4684
4800
  validateNonOverlapping(resolvedPlans);
4801
+ const minCompressRange = ctx.config.compress.minCompressRange;
4802
+ if (minCompressRange > 0) {
4803
+ let totalChars = 0;
4804
+ const counted = /* @__PURE__ */ new Set();
4805
+ for (const plan of resolvedPlans) {
4806
+ for (const messageId of plan.selection.messageIds) {
4807
+ if (counted.has(messageId)) continue;
4808
+ counted.add(messageId);
4809
+ const rawMessage = searchContext.rawMessagesById.get(messageId);
4810
+ if (rawMessage) {
4811
+ totalChars += countMessageCharacters(rawMessage);
4812
+ }
4813
+ }
4814
+ }
4815
+ if (totalChars < minCompressRange) {
4816
+ throw new Error(
4817
+ `Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
4818
+ );
4819
+ }
4820
+ }
4685
4821
  const notifications = [];
4686
4822
  const preparedPlans = [];
4687
4823
  let totalCompressedMessages = 0;
@@ -4731,10 +4867,19 @@ function createCompressRangeTool(ctx) {
4731
4867
  searchContext.summaryByBlockId,
4732
4868
  injected.consumedBlockIds
4733
4869
  );
4734
- const mergeConsumedBlockIds = extractBoundaryConsumedBlocks(
4870
+ const boundaryConsumed = extractBoundaryConsumedBlocks(
4735
4871
  plan.selection.startReference,
4736
4872
  plan.selection.endReference
4737
4873
  );
4874
+ const seenConsumed = /* @__PURE__ */ new Set();
4875
+ const mergeConsumedBlockIds = [
4876
+ ...plan.selection.requiredBlockIds,
4877
+ ...boundaryConsumed
4878
+ ].filter((id) => {
4879
+ if (seenConsumed.has(id)) return false;
4880
+ seenConsumed.add(id);
4881
+ return true;
4882
+ });
4738
4883
  preparedPlans.push({
4739
4884
  entry: plan.entry,
4740
4885
  selection: plan.selection,
@@ -4981,6 +5126,36 @@ var stripHallucinations = (messages) => {
4981
5126
  // lib/messages/prune.ts
4982
5127
  var prune = (state, logger, config, messages) => {
4983
5128
  filterCompressedRanges(state, logger, config, messages);
5129
+ stripStepMarkers(messages);
5130
+ };
5131
+ var MAX_STEP_FINISH_REASON = 50;
5132
+ var stripStepMarkers = (messages) => {
5133
+ for (const msg of messages) {
5134
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
5135
+ let changed = false;
5136
+ const filtered = [];
5137
+ for (const part of parts) {
5138
+ if (part.type === "step-start") {
5139
+ changed = true;
5140
+ continue;
5141
+ }
5142
+ if (part.type === "step-finish") {
5143
+ const reason = part.reason;
5144
+ if (typeof reason === "string" && reason.length > MAX_STEP_FINISH_REASON) {
5145
+ const truncated = reason.slice(0, MAX_STEP_FINISH_REASON) + "...";
5146
+ if (truncated !== reason) {
5147
+ filtered.push({ ...part, reason: truncated });
5148
+ changed = true;
5149
+ continue;
5150
+ }
5151
+ }
5152
+ }
5153
+ filtered.push(part);
5154
+ }
5155
+ if (changed) {
5156
+ msg.parts = filtered;
5157
+ }
5158
+ }
4984
5159
  };
4985
5160
  var filterCompressedRanges = (state, logger, config, messages) => {
4986
5161
  if (state.prune.messages.byMessageId.size === 0 && state.prune.messages.activeByAnchorMessageId.size === 0) {
@@ -5217,8 +5392,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
5217
5392
  agentName ? hostPermissions.agents[agentName] : void 0
5218
5393
  ) ? "deny" : basePermission;
5219
5394
  };
5220
- var hasExplicitToolPermission = (permissionConfig, tool5) => {
5221
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool5) : false;
5395
+ var hasExplicitToolPermission = (permissionConfig, tool4) => {
5396
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool4) : false;
5222
5397
  };
5223
5398
 
5224
5399
  // lib/compress-permission.ts
@@ -5250,13 +5425,42 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
5250
5425
  const lines = [
5251
5426
  "Compressed block context:",
5252
5427
  `- Active compressed blocks: ${blockCount} (${blockList})`,
5253
- "- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using `(bN)`."
5428
+ "- System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary."
5254
5429
  ];
5255
5430
  if (includeHint) {
5256
5431
  lines.push("- \u{1F4A1} When you've finished using tool outputs, compress them \u2014 you can decompress later if needed. Lean context improves accuracy.");
5257
5432
  }
5258
5433
  if (blockCount > 50) {
5259
- lines.push(`- \u{1F500} You have ${blockCount} blocks \u2014 consider merging adjacent same-topic blocks instead of finding new content to compress. This permanently reduces per-turn overhead.`);
5434
+ const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
5435
+ const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
5436
+ const visibleMessageIds = context?.visibleMessageIds;
5437
+ const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
5438
+ if (visibleOldBlocks.length > 5) {
5439
+ const blocksWithRef = visibleOldBlocks.map((block) => {
5440
+ const ref = state.messageIds.byRawId.get(block.anchorMessageId);
5441
+ return ref ? { block, ref } : null;
5442
+ }).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
5443
+ const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
5444
+ const totalK = Math.max(1, Math.round(totalTokens / 1e3));
5445
+ const targets = [];
5446
+ const chunkSize = Math.ceil(blocksWithRef.length / 3);
5447
+ for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
5448
+ const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
5449
+ if (chunk.length < 2) continue;
5450
+ const startRef = chunk[0].ref;
5451
+ const endRef = chunk[chunk.length - 1].ref;
5452
+ const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
5453
+ const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
5454
+ targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
5455
+ }
5456
+ if (targets.length > 0) {
5457
+ lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
5458
+ lines.push(...targets);
5459
+ lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary.`);
5460
+ }
5461
+ } else {
5462
+ lines.push(`- \u{1F500} You have ${blockCount} blocks \u2014 use compress to consolidate adjacent same-topic blocks.`);
5463
+ }
5260
5464
  }
5261
5465
  const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
5262
5466
  if (gcConfig && usageRatio > 0.5) {
@@ -5617,11 +5821,11 @@ ${base}`;
5617
5821
  }
5618
5822
  let guidance;
5619
5823
  if (pct < minPct) {
5620
- guidance = " \u{1F4A1} Be frugal with context \u2014 if you see large completed outputs (>2000 tokens), compress them into summaries. If everything is already compressed, skip this nudge. You can decompress later if needed. Extract and keep what matters: user intent, key decisions, file paths, and important findings. Compress everything else.";
5824
+ guidance = " \u{1F4A1} Be frugal with context. If any visible tool output exceeds 5000 characters and you've finished reading it, compress it into a summary now \u2014 don't keep large outputs 'just in case'. You can decompress later if needed.";
5621
5825
  } else if (pct < maxPct) {
5622
- guidance = " \u26A0\uFE0F Context is growing \u2014 compress completed sections and high-token waste now. Preserve key details.";
5826
+ guidance = " \u26A0\uFE0F Context is growing \u2014 compress completed sections and high-token waste now.";
5623
5827
  } else {
5624
- guidance = " \u{1F525} Context is high \u2014 compress aggressively but selectively. Preserve only what is essential.";
5828
+ guidance = " \u{1F525} Context is high \u2014 compress aggressively, preserve only what is essential.";
5625
5829
  }
5626
5830
  return `
5627
5831
 
@@ -5818,7 +6022,15 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
5818
6022
  const shouldNudge = shouldInjectPerMessageNudge(state, config, currentTokens, modelContextLimit);
5819
6023
  injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit, !shouldNudge);
5820
6024
  if (config.compress.mode !== "message") {
5821
- const blockGuidance = buildCompressedBlockGuidance(state, config.gc, { currentTokens, modelContextLimit, includeHint: shouldNudge });
6025
+ const visibleMessageIds = new Set(
6026
+ messages.map((message) => message.info.id)
6027
+ );
6028
+ const blockGuidance = buildCompressedBlockGuidance(state, config.gc, {
6029
+ currentTokens,
6030
+ modelContextLimit,
6031
+ includeHint: shouldNudge,
6032
+ visibleMessageIds
6033
+ });
5822
6034
  if (blockGuidance.trim() && suffixMessage) {
5823
6035
  appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
5824
6036
  }
@@ -6365,109 +6577,6 @@ function createDecompressTool(ctx) {
6365
6577
  });
6366
6578
  }
6367
6579
 
6368
- // lib/compress/mark-block.ts
6369
- import { tool as tool4 } from "@opencode-ai/plugin";
6370
- async function prepareMarkSession(ctx, toolCtx) {
6371
- await toolCtx.ask({
6372
- permission: "compress",
6373
- patterns: ["*"],
6374
- always: ["*"],
6375
- metadata: {}
6376
- });
6377
- toolCtx.metadata({ title: "Mark block" });
6378
- const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
6379
- await ensureSessionInitialized(
6380
- ctx.client,
6381
- ctx.state,
6382
- toolCtx.sessionID,
6383
- ctx.logger,
6384
- rawMessages,
6385
- ctx.config.manualMode.enabled
6386
- );
6387
- assignMessageRefs(ctx.state, rawMessages);
6388
- }
6389
- var MARK_DESCRIPTION = `Marks a compressed block for batch merge-cleanup.
6390
-
6391
- Use this for blocks whose detailed content you no longer need, but whose summaries
6392
- you want to keep in context for now (to preserve prompt cache). Marked blocks stay
6393
- fully active with zero immediate effect on context or cache. When context pressure
6394
- rises, all marked blocks are merge-compressed together into a single summary in one
6395
- cache break, instead of being handled one at a time.
6396
-
6397
- Argument: blockId \u2014 the block reference to mark (e.g., "b1", "b3")
6398
-
6399
- Use mark_block instead of compress when you want deferred cleanup: the block keeps
6400
- serving cache hits now and gets consolidated later only if context gets tight.`;
6401
- var UNMARK_DESCRIPTION = `Removes the batch cleanup mark from a compressed block.
6402
-
6403
- Reverses mark_block. The block returns to normal handling and will not be
6404
- auto-merged during batch cleanup.
6405
-
6406
- Argument: blockId \u2014 the block reference to unmark (e.g., "b1", "b3")`;
6407
- function buildSchema4() {
6408
- return {
6409
- blockId: tool4.schema.string().describe('Block reference to mark (e.g., "b1", "b3")')
6410
- };
6411
- }
6412
- function buildUnmarkSchema() {
6413
- return {
6414
- blockId: tool4.schema.string().describe('Block reference to unmark (e.g., "b1", "b3")')
6415
- };
6416
- }
6417
- function createMarkBlockTool(ctx) {
6418
- return tool4({
6419
- description: MARK_DESCRIPTION,
6420
- args: buildSchema4(),
6421
- async execute(args, toolCtx) {
6422
- await prepareMarkSession(ctx, toolCtx);
6423
- const targetBlockId = parseBlockRef(String(args.blockId));
6424
- if (targetBlockId === null) {
6425
- return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
6426
- }
6427
- const messagesState = ctx.state.prune.messages;
6428
- const block = messagesState.blocksById.get(targetBlockId);
6429
- if (!block) {
6430
- return `Error: Block ${formatBlockRef(targetBlockId)} does not exist.`;
6431
- }
6432
- if (!block.active) {
6433
- return `Error: Block ${formatBlockRef(targetBlockId)} is not active.`;
6434
- }
6435
- messagesState.markedForCleanup.add(targetBlockId);
6436
- await saveSessionState(ctx.state, ctx.logger);
6437
- const ref = formatBlockRef(targetBlockId);
6438
- const markedCount = messagesState.markedForCleanup.size;
6439
- ctx.logger.info("mark_block: block marked for cleanup", {
6440
- blockId: targetBlockId,
6441
- markedCount
6442
- });
6443
- return `Block ${ref} marked for cleanup. It will be merge-compressed together with other marked blocks when context pressure rises. No immediate effect on context or cache. (${markedCount} block(s) currently marked.)`;
6444
- }
6445
- });
6446
- }
6447
- function createUnmarkBlockTool(ctx) {
6448
- return tool4({
6449
- description: UNMARK_DESCRIPTION,
6450
- args: buildUnmarkSchema(),
6451
- async execute(args, toolCtx) {
6452
- await prepareMarkSession(ctx, toolCtx);
6453
- const targetBlockId = parseBlockRef(String(args.blockId));
6454
- if (targetBlockId === null) {
6455
- return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
6456
- }
6457
- const messagesState = ctx.state.prune.messages;
6458
- if (!messagesState.markedForCleanup.has(targetBlockId)) {
6459
- return `Block ${formatBlockRef(targetBlockId)} was not marked for cleanup.`;
6460
- }
6461
- messagesState.markedForCleanup.delete(targetBlockId);
6462
- await saveSessionState(ctx.state, ctx.logger);
6463
- ctx.logger.info("unmark_block: block unmarked", {
6464
- blockId: targetBlockId
6465
- });
6466
- return `Block ${formatBlockRef(targetBlockId)} unmarked. It will no longer be auto-merged during batch cleanup.`;
6467
- }
6468
- });
6469
- }
6470
-
6471
6580
  // lib/logger.ts
6472
6581
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
6473
6582
  import { join as join3 } from "path";
@@ -6676,7 +6785,7 @@ var SYSTEM = `
6676
6785
 
6677
6786
  You operate in a context-constrained environment. 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.
6678
6787
 
6679
- The tools you have for context management are \`compress\`, \`decompress\`, \`mark_block\`, and \`unmark_block\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`mark_block\` flags a compressed block for deferred batch merge-cleanup \u2014 it has zero immediate effect on context or cache, but marked blocks are merge-compressed together in a single cache break when context pressure rises. Use it for blocks you no longer need in detail but want to keep cached for now. \`unmark_block\` removes that flag.
6788
+ The tools you have for context management are \`compress\` and \`decompress\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details.
6680
6789
 
6681
6790
  \`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
6682
6791
 
@@ -6690,9 +6799,9 @@ Target the largest UNCOMPRESSED content first. Savings scale with original size
6690
6799
 
6691
6800
  CONTEXT PRESSURE LEVELS
6692
6801
 
6693
- - Normal: Be frugal \u2014 compress tool outputs you've finished using into summaries. You can decompress later. Extract and keep what matters from any message; compress verbose parts \u2014 including large logs in user messages or generated code.
6694
- - Elevated: Context is growing. Compress completed sections and high-token waste more urgently.
6695
- - Critical: Compress aggressively now. Every compression should free meaningful tokens. Preserve only what is essential for the current task.
6802
+ - Normal: Be frugal \u2014 compress large completed outputs into summaries. You can decompress later if needed.
6803
+ - Elevated: Context is growing \u2014 compress completed sections and high-token waste now.
6804
+ - Critical: Compress aggressively now \u2014 preserve only what is essential for the current task.
6696
6805
 
6697
6806
  WHAT TO COMPRESS FIRST (high value, low risk)
6698
6807
 
@@ -6756,33 +6865,18 @@ Directly quote user messages when they are short enough to include safely. Direc
6756
6865
  Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
6757
6866
 
6758
6867
  COMPRESSED BLOCK PLACEHOLDERS
6759
- When the selected range includes previously compressed blocks, use this exact placeholder format when referencing one:
6760
-
6761
- - \`(bN)\`
6868
+ 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.
6762
6869
 
6763
6870
  Compressed block sections in context are clearly marked with a header:
6764
6871
 
6765
6872
  - \`[Compressed conversation section]\`
6766
6873
 
6767
- Compressed block IDs always use the \`bN\` form (never \`mNNNNN\`) and are represented in the same XML metadata tag format.
6768
-
6769
6874
  Rules:
6770
6875
 
6771
- - Include every required block placeholder exactly once.
6876
+ - Write a short prose summary. The system handles block consumption automatically.
6772
6877
  - Do not invent placeholders for blocks outside the selected range.
6773
- - Treat \`(bN)\` placeholders as RESERVED TOKENS. Do not emit \`(bN)\` text anywhere except intentional placeholders.
6774
- - If you need to mention a block in prose, use plain text like \`compressed bN\` (not as a placeholder).
6775
- - Preflight check before finalizing: the set of \`(bN)\` placeholders in your summary must exactly match the required set, with no duplicates.
6776
-
6777
- These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
6778
-
6779
- FLOW PRESERVATION WITH PLACEHOLDERS
6780
- When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
6781
-
6782
- - Treat each placeholder as a stand-in for a full conversation segment, not as a short label.
6783
- - Ensure transitions before and after each placeholder preserve chronology and causality.
6784
- - Do not write text that depends on the placeholder staying literal (for example, "as noted in \`(b2)\`").
6785
- - Your final meaning must be coherent once each placeholder is replaced with its full compressed block content.
6878
+ - Treat \`(bN)\` as a RESERVED TOKEN. Do not emit \`(bN)\` text anywhere in the summary.
6879
+ - If you need to mention a block in prose, use plain text like \`compressed bN\` (never as a placeholder).
6786
6880
 
6787
6881
  BOUNDARY IDS
6788
6882
  You specify boundaries by ID using the injected IDs visible in the conversation:
@@ -7707,7 +7801,7 @@ var COMPRESS_TRIGGER_PROMPT = [
7707
7801
  "Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
7708
7802
  "Return after compress with a brief explanation of what content was compressed."
7709
7803
  ].join("\n\n");
7710
- function getTriggerPrompt(tool5, state, config, userFocus) {
7804
+ function getTriggerPrompt(tool4, state, config, userFocus) {
7711
7805
  const base = COMPRESS_TRIGGER_PROMPT;
7712
7806
  const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
7713
7807
  const sections = [base, compressedBlockGuidance];
@@ -7736,8 +7830,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
7736
7830
  );
7737
7831
  logger.info("Manual mode toggled", { manualMode: state.manualMode });
7738
7832
  }
7739
- async function handleManualTriggerCommand(ctx, tool5, userFocus) {
7740
- return getTriggerPrompt(tool5, ctx.state, ctx.config, userFocus);
7833
+ async function handleManualTriggerCommand(ctx, tool4, userFocus) {
7834
+ return getTriggerPrompt(tool4, ctx.state, ctx.config, userFocus);
7741
7835
  }
7742
7836
  function applyPendingManualTrigger(state, messages, logger) {
7743
7837
  const pending = state.pendingManualTrigger;
@@ -8267,23 +8361,6 @@ function parseGcThreshold(limit, modelContextLimit) {
8267
8361
  }
8268
8362
 
8269
8363
  // lib/gc/merge.ts
8270
- var DEFAULT_BATCH_CLEANUP = {
8271
- lowThreshold: "55%",
8272
- highThreshold: "75%",
8273
- forceThreshold: "90%"
8274
- };
8275
- var ESCALATE_MIN_MARKED = 3;
8276
- var ESCALATE_MIN_RATIO = 0.4;
8277
- function resolveBatchCleanup(gc) {
8278
- return gc.batchCleanup ?? DEFAULT_BATCH_CLEANUP;
8279
- }
8280
- function percentToTokens(value, modelContextLimit) {
8281
- if (typeof value === "number") return value;
8282
- const percent = parseFloat(value.slice(0, -1));
8283
- if (isNaN(percent)) return modelContextLimit;
8284
- const clamped = Math.max(0, Math.min(100, Math.round(percent)));
8285
- return Math.round(clamped / 100 * modelContextLimit);
8286
- }
8287
8364
  function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
8288
8365
  const blocks = [];
8289
8366
  const ids = Array.from(state.prune.messages.activeBlockIds).sort((a, b) => a - b);
@@ -8296,27 +8373,13 @@ function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
8296
8373
  }
8297
8374
  return blocks;
8298
8375
  }
8299
- function collectActiveMarkedBlocks(state) {
8300
- const messagesState = state.prune.messages;
8301
- const ids = Array.from(messagesState.markedForCleanup).sort((a, b) => a - b);
8302
- const blocks = [];
8303
- for (const id of ids) {
8304
- const block = messagesState.blocksById.get(id);
8305
- if (!block || !block.active) {
8306
- messagesState.markedForCleanup.delete(id);
8307
- continue;
8308
- }
8309
- blocks.push(block);
8310
- }
8311
- return blocks;
8312
- }
8313
8376
  function extractSummaryBody(summary) {
8314
8377
  let body = summary;
8315
8378
  const headerPrefix = COMPRESSED_BLOCK_HEADER + "\n";
8316
8379
  if (body.startsWith(headerPrefix)) {
8317
8380
  body = body.slice(headerPrefix.length);
8318
8381
  }
8319
- body = body.replace(/\n<dcp-message-id[^>]*>b\d+<\/dcp-message-id>$/, "");
8382
+ body = body.replace(/\n]*>b\d+<\/dcp-message-id>$/, "");
8320
8383
  return body.trim();
8321
8384
  }
8322
8385
  function truncateMergedSummary(merged, maxLength) {
@@ -8430,55 +8493,6 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
8430
8493
  const savedTokens = Math.max(0, sourceTokens - newSummaryTokens);
8431
8494
  return { mergedCount: sourceBlocks.length, savedTokens };
8432
8495
  }
8433
- function estimateTokens(blocks) {
8434
- return blocks.reduce(
8435
- (sum, block) => sum + (block.summaryTokens || Math.round(block.summary.length / 4)),
8436
- 0
8437
- );
8438
- }
8439
- function buildNudgeText(state, maxMergedLength) {
8440
- const marked = collectActiveMarkedBlocks(state);
8441
- const oldGen = collectActiveOldGenBlocks(state, maxMergedLength);
8442
- if (oldGen.length === 0) return void 0;
8443
- const oldGenIds = new Set(oldGen.map((b) => b.blockId));
8444
- const markedOldGen = marked.filter((b) => oldGenIds.has(b.blockId));
8445
- const markedOldGenCount = markedOldGen.length;
8446
- const oldGenCount = oldGen.length;
8447
- const ratio = markedOldGenCount / oldGenCount;
8448
- const ratioPct = Math.round(ratio * 100);
8449
- const escalateMinPct = Math.round(ESCALATE_MIN_RATIO * 100);
8450
- if (markedOldGenCount >= ESCALATE_MIN_MARKED && ratio >= ESCALATE_MIN_RATIO) {
8451
- const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
8452
- const firstRef = formatBlockRef(marked[0].blockId);
8453
- const lastRef = formatBlockRef(marked[marked.length - 1].blockId);
8454
- const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
8455
- return [
8456
- `\u{1F525} ${markedOldGenCount}/${oldGenCount} old-gen blocks marked (${ratioPct}%) \u2014 ready for batch cleanup.`,
8457
- `Compressing ${refs} (range ${firstRef}\u2013${lastRef}) would free ~${estimatedSavings} tokens in one cache break.`,
8458
- `Call compress with this range now to consolidate them.`
8459
- ].join(" ");
8460
- }
8461
- if (marked.length >= 1) {
8462
- const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
8463
- const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
8464
- return [
8465
- `\u26A0\uFE0F ${marked.length} block(s) marked for batch cleanup (${refs}).`,
8466
- `Merge-compressing them would free ~${estimatedSavings} tokens.`,
8467
- marked.length >= 2 ? "They will auto-merge when context pressure reaches the high threshold." : "A single marked block won't auto-merge on its own \u2014 use compress to consolidate it, or unmark_block if no longer needed.",
8468
- `Mark more old-gen blocks (need \u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}%) to trigger batch cleanup sooner.`,
8469
- "To act now, use compress with a range covering these blocks."
8470
- ].join(" ");
8471
- }
8472
- const shown = oldGen.slice(0, 5);
8473
- const oldGenRefs = shown.map((b) => formatBlockRef(b.blockId)).join(", ");
8474
- const more = oldGenCount > 5 ? ` (+${oldGenCount - 5} more)` : "";
8475
- return [
8476
- `\u{1F4CB} Context pressure rising \u2014 ${oldGenCount} old-gen compressed block(s) occupy ~${estimateTokens(oldGen)} tokens (${oldGenRefs}${more}).`,
8477
- `Review which blocks contain information you no longer need, and use mark_block to flag them.`,
8478
- `Once enough are marked (\u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}% of old-gen), they'll be batch-merged in one cache break to preserve cache hit rate.`,
8479
- `Do NOT mark blocks you may still need.`
8480
- ].join(" ");
8481
- }
8482
8496
  function runBatchCleanup(state, config, logger, messages) {
8483
8497
  const noop = {
8484
8498
  tier: 0,
@@ -8490,74 +8504,31 @@ function runBatchCleanup(state, config, logger, messages) {
8490
8504
  return noop;
8491
8505
  }
8492
8506
  const currentTokens = getCurrentTokenUsage(state, messages);
8493
- const limit = state.modelContextLimit;
8494
- const batchCleanup = resolveBatchCleanup(config.gc);
8495
- const maxMergedLength = config.gc.maxOldGenSummaryLength;
8496
- const forceTokens = percentToTokens(batchCleanup.forceThreshold, limit);
8497
- const highTokens = percentToTokens(batchCleanup.highThreshold, limit);
8498
- const lowTokens = percentToTokens(batchCleanup.lowThreshold, limit);
8499
- if (currentTokens >= forceTokens) {
8500
- const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
8501
- if (oldGenBlocks.length < 2) {
8502
- return noop;
8503
- }
8504
- const ids = oldGenBlocks.map((b) => b.blockId);
8505
- const result = mergeMarkedBlocks(state, ids, maxMergedLength);
8506
- if (result.mergedCount === 0) {
8507
- return noop;
8508
- }
8509
- logger.info("Batch cleanup tier 3 (force): merged old-gen blocks", {
8510
- mergedCount: result.mergedCount,
8511
- savedTokens: result.savedTokens,
8512
- currentTokens,
8513
- forceThreshold: batchCleanup.forceThreshold
8514
- });
8515
- return {
8516
- tier: 3,
8517
- action: "merge",
8518
- mergedCount: result.mergedCount,
8519
- savedTokens: result.savedTokens
8520
- };
8507
+ if (currentTokens < state.modelContextLimit) {
8508
+ return noop;
8521
8509
  }
8522
- if (currentTokens >= highTokens) {
8523
- const marked = collectActiveMarkedBlocks(state);
8524
- if (marked.length >= 2) {
8525
- const ids = marked.map((b) => b.blockId);
8526
- const result = mergeMarkedBlocks(state, ids, maxMergedLength);
8527
- if (result.mergedCount > 0) {
8528
- logger.info("Batch cleanup tier 2 (high): merged marked blocks", {
8529
- mergedCount: result.mergedCount,
8530
- savedTokens: result.savedTokens,
8531
- currentTokens,
8532
- highThreshold: batchCleanup.highThreshold
8533
- });
8534
- return {
8535
- tier: 2,
8536
- action: "merge",
8537
- mergedCount: result.mergedCount,
8538
- savedTokens: result.savedTokens
8539
- };
8540
- }
8541
- }
8510
+ const maxMergedLength = config.gc.maxOldGenSummaryLength;
8511
+ const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
8512
+ if (oldGenBlocks.length < 2) {
8513
+ return noop;
8542
8514
  }
8543
- if (currentTokens >= lowTokens) {
8544
- const nudgeText = buildNudgeText(state, maxMergedLength);
8545
- if (!nudgeText) {
8546
- return noop;
8547
- }
8548
- logger.info("Batch cleanup tier 1 (low): nudge injected", {
8549
- currentTokens,
8550
- lowThreshold: batchCleanup.lowThreshold
8551
- });
8552
- return {
8553
- tier: 1,
8554
- action: "nudge",
8555
- mergedCount: 0,
8556
- savedTokens: 0,
8557
- nudgeText
8558
- };
8515
+ const ids = oldGenBlocks.map((b) => b.blockId);
8516
+ const result = mergeMarkedBlocks(state, ids, maxMergedLength);
8517
+ if (result.mergedCount === 0) {
8518
+ return noop;
8559
8519
  }
8560
- return noop;
8520
+ logger.info("Batch cleanup force fallback (100%): merged old-gen blocks", {
8521
+ mergedCount: result.mergedCount,
8522
+ savedTokens: result.savedTokens,
8523
+ currentTokens,
8524
+ contextLimit: state.modelContextLimit
8525
+ });
8526
+ return {
8527
+ tier: 3,
8528
+ action: "merge",
8529
+ mergedCount: result.mergedCount,
8530
+ savedTokens: result.savedTokens
8531
+ };
8561
8532
  }
8562
8533
 
8563
8534
  // lib/hooks.ts
@@ -8668,11 +8639,6 @@ function runMajorGC(state, config, logger, messages) {
8668
8639
  void saveSessionState(state, logger);
8669
8640
  }
8670
8641
  }
8671
- function appendBatchCleanupNudge(messages, nudgeText) {
8672
- const lastUser = getLastUserMessage(messages);
8673
- if (!lastUser) return;
8674
- appendToLastTextPart(lastUser, nudgeText);
8675
- }
8676
8642
  function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
8677
8643
  return async (input, output) => {
8678
8644
  const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
@@ -8704,9 +8670,6 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
8704
8670
  buildToolIdList(state, output.messages);
8705
8671
  runMajorGC(state, config, logger, output.messages);
8706
8672
  const batchResult = runBatchCleanup(state, config, logger, output.messages);
8707
- if (batchResult.tier === 1 && batchResult.nudgeText) {
8708
- appendBatchCleanupNudge(output.messages, batchResult.nudgeText);
8709
- }
8710
8673
  if (batchResult.mergedCount > 0) {
8711
8674
  void saveSessionState(state, logger);
8712
8675
  }
@@ -9128,9 +9091,7 @@ var server = (async (ctx) => {
9128
9091
  tool: {
9129
9092
  ...config.compress.permission !== "deny" && {
9130
9093
  compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
9131
- decompress: createDecompressTool(compressToolContext),
9132
- mark_block: createMarkBlockTool(compressToolContext),
9133
- unmark_block: createUnmarkBlockTool(compressToolContext)
9094
+ decompress: createDecompressTool(compressToolContext)
9134
9095
  }
9135
9096
  },
9136
9097
  config: async (opencodeConfig) => {
@@ -9146,7 +9107,7 @@ var server = (async (ctx) => {
9146
9107
  }
9147
9108
  const toolsToAdd = [];
9148
9109
  if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
9149
- toolsToAdd.push("compress", "decompress", "mark_block", "unmark_block");
9110
+ toolsToAdd.push("compress", "decompress");
9150
9111
  }
9151
9112
  if (toolsToAdd.length > 0) {
9152
9113
  const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];