opencode-acp 1.8.2 → 1.9.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
@@ -1497,7 +1497,7 @@ var defaultConfig = {
1497
1497
  protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
1498
1498
  protectTags: false,
1499
1499
  protectUserMessages: false,
1500
- maxSummaryLengthHard: 4e3,
1500
+ maxSummaryLengthHard: 1e4,
1501
1501
  minCompressRange: 2e3
1502
1502
  },
1503
1503
  strategies: {
@@ -2245,20 +2245,39 @@ function resolveBoundaryIds(context, state, startId, endId) {
2245
2245
  }
2246
2246
  let startReference = lookup.get(parsedStartId.ref);
2247
2247
  let endReference = lookup.get(parsedEndId.ref);
2248
+ if (!startReference && parsedStartId.kind === "message") {
2249
+ const clamped = clampMessageRef(parsedStartId, context, state);
2250
+ if (clamped) {
2251
+ startReference = lookup.get(clamped.ref);
2252
+ if (startReference) {
2253
+ parsedStartId.ref = clamped.ref;
2254
+ }
2255
+ }
2256
+ }
2257
+ if (!endReference && parsedEndId.kind === "message") {
2258
+ const clamped = clampMessageRef(parsedEndId, context, state);
2259
+ if (clamped) {
2260
+ endReference = lookup.get(clamped.ref);
2261
+ if (endReference) {
2262
+ parsedEndId.ref = clamped.ref;
2263
+ }
2264
+ }
2265
+ }
2248
2266
  if (!startReference) {
2249
2267
  issues.push(
2250
- `startId ${parsedStartId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`
2268
+ `startId ${parsedStartId.ref} is not available \u2014 likely consumed by an existing block.`
2251
2269
  );
2252
2270
  }
2253
2271
  if (!endReference) {
2254
2272
  issues.push(
2255
- `endId ${parsedEndId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`
2273
+ `endId ${parsedEndId.ref} is not available \u2014 likely consumed by an existing block.`
2256
2274
  );
2257
2275
  }
2258
2276
  if (issues.length > 0) {
2259
- throw new Error(
2260
- issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n")
2261
- );
2277
+ const hint = buildBoundaryRecoveryHint(context, state);
2278
+ const body = issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n");
2279
+ throw new Error(hint ? `${body}
2280
+ ${hint}` : body);
2262
2281
  }
2263
2282
  if (!startReference || !endReference) {
2264
2283
  throw new Error("Failed to resolve boundary IDs");
@@ -2268,6 +2287,40 @@ function resolveBoundaryIds(context, state, startId, endId) {
2268
2287
  }
2269
2288
  return { startReference, endReference };
2270
2289
  }
2290
+ function buildBoundaryRecoveryHint(context, state) {
2291
+ const visibleRefs = [];
2292
+ for (const [messageRef, messageId] of state.messageIds.byRef) {
2293
+ if (context.rawMessagesById.has(messageId)) {
2294
+ visibleRefs.push(messageRef);
2295
+ }
2296
+ }
2297
+ const parts = [];
2298
+ if (visibleRefs.length > 0) {
2299
+ visibleRefs.sort();
2300
+ const first = visibleRefs[0];
2301
+ const last = visibleRefs[visibleRefs.length - 1];
2302
+ parts.push(`Current visible: ${first}\u2013${last} (${visibleRefs.length} msgs).`);
2303
+ }
2304
+ const blockCount = context.summaryByBlockId.size;
2305
+ if (blockCount > 0) {
2306
+ parts.push(`${blockCount} active compressed block${blockCount === 1 ? "" : "s"}.`);
2307
+ }
2308
+ if (parts.length === 0) {
2309
+ return "";
2310
+ }
2311
+ return `${parts.join(" ")} Call acp_status() to see which blocks consumed which IDs, then retry with valid IDs.`;
2312
+ }
2313
+ function clampMessageRef(requested, context, state) {
2314
+ if (state.messageIds.byRef.has(requested.ref)) return null;
2315
+ let maxIndex = -1;
2316
+ for (const [messageRef, messageId] of state.messageIds.byRef) {
2317
+ if (!context.rawMessagesById.has(messageId)) continue;
2318
+ const idx = parseMessageRef(messageRef);
2319
+ if (idx !== null && idx > maxIndex) maxIndex = idx;
2320
+ }
2321
+ if (maxIndex < 0 || requested.index <= maxIndex) return null;
2322
+ return { ref: formatMessageRef(maxIndex) };
2323
+ }
2271
2324
  function resolveSelection(context, startReference, endReference) {
2272
2325
  const startRawIndex = startReference.rawIndex;
2273
2326
  const endRawIndex = endReference.rawIndex;
@@ -3188,6 +3241,7 @@ function resetOnCompaction(state) {
3188
3241
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3189
3242
  lastPerMessageNudgeTurn: 0,
3190
3243
  lastPerMessageNudgeTokens: void 0,
3244
+ lastToolOutputNudgeTokens: void 0,
3191
3245
  shouldInjectThisTurn: void 0
3192
3246
  };
3193
3247
  state.messageIds = {
@@ -3198,38 +3252,45 @@ function resetOnCompaction(state) {
3198
3252
  }
3199
3253
 
3200
3254
  // lib/state/persistence.ts
3201
- var LEGACY_STORAGE_DIR = join2(
3202
- process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
3203
- "opencode",
3204
- "storage",
3205
- "plugin",
3206
- "dcp"
3207
- );
3208
- var STORAGE_DIR = join2(
3209
- process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
3210
- "opencode",
3211
- "storage",
3212
- "plugin",
3213
- "acp"
3214
- );
3255
+ function getLegacyStorageDir() {
3256
+ return join2(
3257
+ process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
3258
+ "opencode",
3259
+ "storage",
3260
+ "plugin",
3261
+ "dcp"
3262
+ );
3263
+ }
3264
+ function getStorageDir() {
3265
+ return join2(
3266
+ process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
3267
+ "opencode",
3268
+ "storage",
3269
+ "plugin",
3270
+ "acp"
3271
+ );
3272
+ }
3215
3273
  function migrateFromLegacyIfNeeded(logger) {
3216
- if (existsSyncSync(STORAGE_DIR)) return;
3217
- if (!existsSyncSync(LEGACY_STORAGE_DIR)) return;
3274
+ const storageDir = getStorageDir();
3275
+ const legacyDir = getLegacyStorageDir();
3276
+ if (existsSyncSync(storageDir)) return;
3277
+ if (!existsSyncSync(legacyDir)) return;
3218
3278
  try {
3219
- cpSync(LEGACY_STORAGE_DIR, STORAGE_DIR, { recursive: true });
3220
- logger.info(`[ACP] Migrated storage from ${LEGACY_STORAGE_DIR} \u2192 ${STORAGE_DIR}`);
3279
+ cpSync(legacyDir, storageDir, { recursive: true });
3280
+ logger.info(`[ACP] Migrated storage from ${legacyDir} \u2192 ${storageDir}`);
3221
3281
  } catch (e) {
3222
3282
  logger.warn(`[ACP] Storage migration failed: ${e.message}`);
3223
3283
  }
3224
3284
  }
3225
3285
  async function ensureStorageDir(logger) {
3226
- if (!existsSync2(STORAGE_DIR)) {
3286
+ const storageDir = getStorageDir();
3287
+ if (!existsSync2(storageDir)) {
3227
3288
  migrateFromLegacyIfNeeded(logger);
3228
- await fs.mkdir(STORAGE_DIR, { recursive: true });
3289
+ await fs.mkdir(storageDir, { recursive: true });
3229
3290
  }
3230
3291
  }
3231
3292
  function getSessionFilePath(sessionId) {
3232
- return join2(STORAGE_DIR, `${sessionId}.json`);
3293
+ return join2(getStorageDir(), `${sessionId}.json`);
3233
3294
  }
3234
3295
  async function writePersistedSessionState(sessionId, state, logger) {
3235
3296
  await ensureStorageDir(logger);
@@ -3256,7 +3317,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
3256
3317
  turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
3257
3318
  iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
3258
3319
  lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
3259
- lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens
3320
+ lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
3321
+ lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens
3260
3322
  },
3261
3323
  stats: sessionState.stats,
3262
3324
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3355,14 +3417,15 @@ async function loadAllSessionStats(logger) {
3355
3417
  sessionCount: 0
3356
3418
  };
3357
3419
  try {
3358
- if (!existsSync2(STORAGE_DIR)) {
3420
+ const storageDir = getStorageDir();
3421
+ if (!existsSync2(storageDir)) {
3359
3422
  return result;
3360
3423
  }
3361
- const files = await fs.readdir(STORAGE_DIR);
3424
+ const files = await fs.readdir(storageDir);
3362
3425
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
3363
3426
  for (const file of jsonFiles) {
3364
3427
  try {
3365
- const filePath = join2(STORAGE_DIR, file);
3428
+ const filePath = join2(storageDir, file);
3366
3429
  const content = await fs.readFile(filePath, "utf-8");
3367
3430
  const state = JSON.parse(content);
3368
3431
  if (state?.stats?.totalPruneTokens && state?.prune) {
@@ -3473,6 +3536,7 @@ function createSessionState() {
3473
3536
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3474
3537
  lastPerMessageNudgeTurn: 0,
3475
3538
  lastPerMessageNudgeTokens: void 0,
3539
+ lastToolOutputNudgeTokens: void 0,
3476
3540
  shouldInjectThisTurn: void 0
3477
3541
  },
3478
3542
  stats: {
@@ -3513,6 +3577,7 @@ function resetSessionState(state) {
3513
3577
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3514
3578
  lastPerMessageNudgeTurn: 0,
3515
3579
  lastPerMessageNudgeTokens: void 0,
3580
+ lastToolOutputNudgeTokens: void 0,
3516
3581
  shouldInjectThisTurn: void 0
3517
3582
  };
3518
3583
  state.stats = {
@@ -3560,6 +3625,7 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
3560
3625
  );
3561
3626
  state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
3562
3627
  state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
3628
+ state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
3563
3629
  state.stats = {
3564
3630
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
3565
3631
  totalPruneTokens: persisted.stats?.totalPruneTokens || 0
@@ -4589,7 +4655,7 @@ ${output}`);
4589
4655
  }
4590
4656
 
4591
4657
  // lib/compress/message.ts
4592
- function buildSchema() {
4658
+ function buildSchema(maxSummaryLengthHard) {
4593
4659
  return {
4594
4660
  topic: tool2.schema.string().describe(
4595
4661
  "Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
@@ -4603,7 +4669,7 @@ function buildSchema() {
4603
4669
  )
4604
4670
  })
4605
4671
  ).describe("Batch of individual message summaries to create in one tool call"),
4606
- summaryMaxChars: tool2.schema.number().optional().describe("Override max summary length (default max: 4000 chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.")
4672
+ summaryMaxChars: tool2.schema.number().optional().describe(`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`)
4607
4673
  };
4608
4674
  }
4609
4675
  function createCompressMessageTool(ctx) {
@@ -4611,7 +4677,7 @@ function createCompressMessageTool(ctx) {
4611
4677
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4612
4678
  return tool2({
4613
4679
  description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
4614
- args: buildSchema(),
4680
+ args: buildSchema(ctx.config.compress.maxSummaryLengthHard),
4615
4681
  async execute(args, toolCtx) {
4616
4682
  const input = args;
4617
4683
  validateArgs(input);
@@ -4867,7 +4933,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
4867
4933
  }
4868
4934
 
4869
4935
  // lib/compress/range.ts
4870
- function buildSchema2() {
4936
+ function buildSchema2(maxSummaryLengthHard) {
4871
4937
  return {
4872
4938
  topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
4873
4939
  content: tool3.schema.array(
@@ -4883,7 +4949,7 @@ function buildSchema2() {
4883
4949
  ).describe(
4884
4950
  "One or more ranges to compress, each with start/end boundaries and a summary"
4885
4951
  ),
4886
- summaryMaxChars: tool3.schema.number().optional().describe("Override max summary length (default max: 4000 chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.")
4952
+ summaryMaxChars: tool3.schema.number().optional().describe(`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`)
4887
4953
  };
4888
4954
  }
4889
4955
  function createCompressRangeTool(ctx) {
@@ -4891,7 +4957,7 @@ function createCompressRangeTool(ctx) {
4891
4957
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4892
4958
  return tool3({
4893
4959
  description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
4894
- args: buildSchema2(),
4960
+ args: buildSchema2(ctx.config.compress.maxSummaryLengthHard),
4895
4961
  async execute(args, toolCtx) {
4896
4962
  const input = args;
4897
4963
  validateArgs2(input);
@@ -5062,10 +5128,11 @@ import { tool as tool4 } from "@opencode-ai/plugin";
5062
5128
  // lib/messages/utils.ts
5063
5129
  import { createHash } from "crypto";
5064
5130
  var SUMMARY_ID_HASH_LENGTH = 16;
5065
- var MERGED_SUMMARY_HEADER = (blockId) => `[ACP compressed context summary (block ${blockId}) \u2014 prior conversation recap]
5131
+ var MERGED_SUMMARY_HEADER = (blockId) => `<acp-compression-summary>
5132
+ [ACP model-generated recap (block ${blockId}) \u2014 NOT a user message]
5066
5133
  `;
5067
5134
  var MERGED_SUMMARY_FOOTER = `
5068
- [End ACP compressed context summary]
5135
+ </acp-compression-summary>
5069
5136
 
5070
5137
  `;
5071
5138
  var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/(?:dcp|acp)-message-id>)/g;
@@ -5076,33 +5143,54 @@ var generateStableId = (prefix, seed) => {
5076
5143
  const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
5077
5144
  return `${prefix}_${hash}`;
5078
5145
  };
5079
- var createSyntheticUserMessage = (baseMessage, content, stableSeed) => {
5080
- const userInfo = baseMessage.info;
5146
+ var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
5147
+ const baseInfo = baseMessage.info;
5081
5148
  const now = Date.now();
5082
- const deterministicSeed = stableSeed?.trim() || userInfo.id;
5149
+ const deterministicSeed = stableSeed?.trim() || baseInfo.id;
5083
5150
  const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
5084
5151
  const partId = generateStableId("prt_dcp_summary", deterministicSeed);
5085
- return {
5086
- info: {
5152
+ const parts = [
5153
+ {
5154
+ id: partId,
5155
+ sessionID: baseInfo.sessionID,
5156
+ messageID: messageId,
5157
+ type: "text",
5158
+ text: content,
5159
+ synthetic: true
5160
+ }
5161
+ ];
5162
+ if (role === "assistant") {
5163
+ const isAssistant = baseInfo.role === "assistant";
5164
+ const assistantBase = isAssistant ? baseInfo : void 0;
5165
+ const userModel = !isAssistant ? baseInfo.model : void 0;
5166
+ const info2 = {
5087
5167
  id: messageId,
5088
- sessionID: userInfo.sessionID,
5089
- role: "user",
5090
- agent: userInfo.agent,
5091
- model: userInfo.model,
5092
- time: { created: now }
5093
- },
5094
- parts: [
5095
- {
5096
- id: partId,
5097
- sessionID: userInfo.sessionID,
5098
- messageID: messageId,
5099
- type: "text",
5100
- text: content,
5101
- synthetic: true
5102
- }
5103
- ]
5168
+ sessionID: baseInfo.sessionID,
5169
+ role: "assistant",
5170
+ time: { created: now },
5171
+ parentID: assistantBase?.parentID ?? "",
5172
+ modelID: assistantBase?.modelID ?? userModel?.modelID ?? "",
5173
+ providerID: assistantBase?.providerID ?? userModel?.providerID ?? "",
5174
+ mode: assistantBase?.mode ?? "code",
5175
+ agent: baseInfo.agent ?? "code",
5176
+ path: assistantBase?.path ?? { cwd: "", root: "" },
5177
+ cost: 0,
5178
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
5179
+ };
5180
+ return { info: info2, parts };
5181
+ }
5182
+ const userInfo = baseInfo;
5183
+ const info = {
5184
+ id: messageId,
5185
+ sessionID: userInfo.sessionID,
5186
+ role: "user",
5187
+ agent: userInfo.agent,
5188
+ model: userInfo.model,
5189
+ time: { created: now }
5104
5190
  };
5191
+ return { info, parts };
5105
5192
  };
5193
+ var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
5106
5194
  var prependCompressionSummary = (message, summary, blockId) => {
5107
5195
  const parts = Array.isArray(message.parts) ? message.parts : [];
5108
5196
  const header = MERGED_SUMMARY_HEADER(blockId);
@@ -5241,6 +5329,11 @@ var stripHallucinations = (messages) => {
5241
5329
  };
5242
5330
 
5243
5331
  // lib/messages/prune.ts
5332
+ var STANDALONE_SUMMARY_HEADER = (blockId) => `<acp-compression-summary>
5333
+ [ACP model-generated recap (block ${blockId}) \u2014 NOT a user message]
5334
+ `;
5335
+ var STANDALONE_SUMMARY_FOOTER = `
5336
+ </acp-compression-summary>`;
5244
5337
  var prune = (state, logger, config, messages) => {
5245
5338
  filterCompressedRanges(state, logger, config, messages);
5246
5339
  stripStepMarkers(messages);
@@ -5303,41 +5396,18 @@ var filterCompressedRanges = (state, logger, config, messages) => {
5303
5396
  summaryLength: summaryContent.length
5304
5397
  });
5305
5398
  } else {
5306
- const userMessage = getLastUserMessage(messages, i);
5399
+ const taggedContent = STANDALONE_SUMMARY_HEADER(summary.blockId) + summaryContent + STANDALONE_SUMMARY_FOOTER;
5307
5400
  const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
5308
- if (userMessage) {
5309
- result.push(
5310
- createSyntheticUserMessage(userMessage, summaryContent, summarySeed)
5311
- );
5312
- logger.info("Injected compress summary", {
5313
- anchorMessageId: msgId,
5314
- summaryLength: summaryContent.length
5315
- });
5316
- } else {
5317
- const anchorInfo = msg.info;
5318
- const fallbackBase = {
5319
- info: {
5320
- id: anchorInfo.id || msgId,
5321
- sessionID: anchorInfo.sessionID || "",
5322
- role: "user",
5323
- agent: anchorInfo.agent || "code",
5324
- model: anchorInfo.model || {
5325
- providerID: "",
5326
- modelID: "",
5327
- variant: void 0
5328
- },
5329
- time: { created: anchorInfo.time?.created || Date.now() }
5330
- },
5331
- parts: []
5332
- };
5333
- result.push(
5334
- createSyntheticUserMessage(fallbackBase, summaryContent, summarySeed)
5335
- );
5336
- logger.info("Injected compress summary (fallback, no preceding user message)", {
5337
- anchorMessageId: msgId,
5338
- summaryLength: summaryContent.length
5339
- });
5340
- }
5401
+ const userMessage = getLastUserMessage(messages, i);
5402
+ const baseForSummary = userMessage ?? msg;
5403
+ result.push(
5404
+ createSyntheticMessage(baseForSummary, taggedContent, summarySeed, "assistant")
5405
+ );
5406
+ logger.info("Injected compress summary as assistant role", {
5407
+ anchorMessageId: msgId,
5408
+ summaryLength: taggedContent.length,
5409
+ hadUserBase: userMessage !== null
5410
+ });
5341
5411
  }
5342
5412
  }
5343
5413
  }
@@ -5797,12 +5867,15 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
5797
5867
  };
5798
5868
  }
5799
5869
  function computeShouldNudge(params) {
5800
- const { currentTokens, modelContextLimit, overMinLimit, overMaxLimit } = params;
5801
- const contextPct = modelContextLimit && currentTokens ? currentTokens / modelContextLimit * 100 : 0;
5802
- const lastNudgeTokens = params.lastNudgeTokens;
5803
- const growthSinceLastNudge = (currentTokens ?? 0) - (lastNudgeTokens ?? 0);
5804
- const frequencyTriggered = lastNudgeTokens === void 0 || growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
5805
- const shouldNudge = contextPct >= params.minNudgeContextPercent && frequencyTriggered;
5870
+ const { currentTokens, overMinLimit, overMaxLimit } = params;
5871
+ if (currentTokens === void 0) {
5872
+ return { shouldNudge: false, tipsVariant: null };
5873
+ }
5874
+ if (params.lastNudgeTokens === void 0) {
5875
+ return { shouldNudge: false, tipsVariant: null };
5876
+ }
5877
+ const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
5878
+ const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
5806
5879
  if (!shouldNudge) {
5807
5880
  return { shouldNudge: false, tipsVariant: null };
5808
5881
  }
@@ -5932,7 +6005,7 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
5932
6005
  return `
5933
6006
 
5934
6007
  Context: ${formatK(currentTokens)} tokens.
5935
- All compression serves the primary task, but be frugal. Context capacity is precious \u2014 compress waste promptly. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
6008
+ All compression serves the primary task, but be frugal. Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
5936
6009
  }
5937
6010
  function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
5938
6011
  const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
@@ -6014,6 +6087,85 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
6014
6087
  ""
6015
6088
  );
6016
6089
  }
6090
+ function estimateCodeTokens(text) {
6091
+ let codeChars = 0;
6092
+ let inCode = false;
6093
+ for (const line of text.split("\n")) {
6094
+ if (line.trim().startsWith("```")) {
6095
+ inCode = !inCode;
6096
+ codeChars += line.length + 1;
6097
+ continue;
6098
+ }
6099
+ if (inCode) codeChars += line.length + 1;
6100
+ }
6101
+ return Math.round(codeChars / 4);
6102
+ }
6103
+ function estimateContextComposition(messages, state) {
6104
+ let toolTokens = 0;
6105
+ let codeTokens = 0;
6106
+ let summaryTokens = 0;
6107
+ let messageTokens = 0;
6108
+ const perMessage = [];
6109
+ const perTool = [];
6110
+ const perCode = [];
6111
+ const perText = [];
6112
+ for (const msg of messages) {
6113
+ const text = (msg.parts || []).filter((p) => p.type === "text").map((p) => p.text || "").join("");
6114
+ const msgId = msg.info?.id || "";
6115
+ const isSummary = msgId.startsWith("msg_dcp_summary") || text.includes("[Compressed conversation section]");
6116
+ let msgTotal = 0;
6117
+ let msgTool = 0;
6118
+ let msgCode = 0;
6119
+ let msgText = 0;
6120
+ for (const part of msg.parts || []) {
6121
+ if (part.type === "text" && typeof part.text === "string") {
6122
+ const partText = part.text;
6123
+ const tokens = Math.round(partText.length / 4);
6124
+ msgTotal += tokens;
6125
+ if (isSummary) {
6126
+ summaryTokens += tokens;
6127
+ } else {
6128
+ messageTokens += tokens;
6129
+ msgText += tokens;
6130
+ const cTokens = estimateCodeTokens(partText);
6131
+ if (cTokens > 0) {
6132
+ codeTokens += cTokens;
6133
+ msgCode += cTokens;
6134
+ }
6135
+ }
6136
+ } else if (part.type !== "text" && part.type !== "reasoning") {
6137
+ const raw = JSON.stringify(part);
6138
+ const tokens = Math.round(raw.length / 4);
6139
+ msgTotal += tokens;
6140
+ toolTokens += tokens;
6141
+ msgTool += tokens;
6142
+ }
6143
+ }
6144
+ if (!isSummary) {
6145
+ const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
6146
+ if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
6147
+ if (msgTool > 500) perTool.push({ ref, tokens: msgTool });
6148
+ if (msgCode > 300) perCode.push({ ref, tokens: msgCode });
6149
+ if (msgText > 500 && msgCode === 0) perText.push({ ref, tokens: msgText });
6150
+ }
6151
+ }
6152
+ perMessage.sort((a, b) => b.tokens - a.tokens);
6153
+ perTool.sort((a, b) => b.tokens - a.tokens);
6154
+ perCode.sort((a, b) => b.tokens - a.tokens);
6155
+ perText.sort((a, b) => b.tokens - a.tokens);
6156
+ return {
6157
+ toolTokens,
6158
+ codeTokens,
6159
+ summaryTokens,
6160
+ messageTokens,
6161
+ textTokens: Math.max(0, messageTokens - codeTokens),
6162
+ total: toolTokens + summaryTokens + messageTokens,
6163
+ largestRanges: perMessage.slice(0, 10),
6164
+ largestToolRanges: perTool.slice(0, 5),
6165
+ largestCodeRanges: perCode.slice(0, 5),
6166
+ largestMessageRanges: perText.slice(0, 5)
6167
+ };
6168
+ }
6017
6169
 
6018
6170
  // lib/messages/inject/inject.ts
6019
6171
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
@@ -6046,7 +6198,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6046
6198
  state.nudges.turnNudgeAnchors.clear();
6047
6199
  state.nudges.iterationNudgeAnchors.clear();
6048
6200
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
6049
- void saveSessionState(state, logger);
6201
+ saveSessionState(state, logger).catch(() => {
6202
+ });
6050
6203
  return;
6051
6204
  }
6052
6205
  let anchorsChanged = false;
@@ -6109,6 +6262,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6109
6262
  }
6110
6263
  const suffixMessage = createSuffixMessage(messages);
6111
6264
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
6265
+ const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
6266
+ if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
6267
+ state.nudges.lastPerMessageNudgeTokens = currentTokens;
6268
+ }
6112
6269
  const decision = computeShouldNudge({
6113
6270
  currentTokens,
6114
6271
  modelContextLimit,
@@ -6116,18 +6273,65 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6116
6273
  overMaxLimit,
6117
6274
  lastNudgeTokens: state.nudges.lastPerMessageNudgeTokens,
6118
6275
  minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
6119
- nudgeGrowthTokens: config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit)
6276
+ nudgeGrowthTokens
6120
6277
  });
6121
6278
  state.nudges.shouldInjectThisTurn = decision.shouldNudge;
6279
+ if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
6280
+ state.nudges.lastPerMessageNudgeTokens = currentTokens;
6281
+ }
6282
+ const composition = estimateContextComposition(messages, state);
6283
+ const toolOutputThreshold = config.compress?.toolOutputNudgeThreshold ?? 5e3;
6284
+ let toolOutputReminder = null;
6285
+ if (composition.toolTokens > 0) {
6286
+ if (state.nudges.lastToolOutputNudgeTokens === void 0) {
6287
+ state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
6288
+ } else {
6289
+ const toolGrowth = composition.toolTokens - state.nudges.lastToolOutputNudgeTokens;
6290
+ if (toolGrowth >= toolOutputThreshold) {
6291
+ const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6292
+ const topRanges = composition.largestRanges.slice(0, 5).map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ");
6293
+ toolOutputReminder = `
6294
+
6295
+ \u26A0\uFE0F ${fmt(toolGrowth)} new tool outputs accumulated (${fmt(composition.toolTokens)} total). Largest: ${topRanges}. Use compress tool to compress these ranges now.`;
6296
+ state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
6297
+ anchorsChanged = true;
6298
+ }
6299
+ }
6300
+ }
6122
6301
  let tipsText = null;
6123
6302
  if (decision.shouldNudge) {
6124
6303
  injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
6304
+ if (suffixMessage && composition.total > 0) {
6305
+ const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6306
+ const pct = (n) => Math.round(n / composition.total * 100);
6307
+ const growth = currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 ? currentTokens - state.nudges.lastPerMessageNudgeTokens : 0;
6308
+ const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
6309
+ const plainTextTokens = composition.textTokens;
6310
+ let breakdown = `
6311
+ Breakdown: ${fmt(composition.toolTokens)} tool (${pct(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct(plainTextTokens)}%)${growthStr}`;
6312
+ const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
6313
+ if (topBlocks.length > 0) {
6314
+ breakdown += `
6315
+ Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt(b.compressedTokens)}\u2192${fmt(b.summaryTokens)}`).join(", ")}`;
6316
+ }
6317
+ breakdown += `
6318
+ \u{1F4A1} Compress incrementally \u2014 compress the largest consumed ranges first.`;
6319
+ if (composition.largestToolRanges.length > 0) {
6320
+ breakdown += `
6321
+ Largest tool outputs: ${composition.largestToolRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
6322
+ }
6323
+ if (composition.largestCodeRanges.length > 0) {
6324
+ breakdown += `
6325
+ Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
6326
+ }
6327
+ if (composition.largestMessageRanges.length > 0) {
6328
+ breakdown += `
6329
+ Largest text messages: ${composition.largestMessageRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
6330
+ }
6331
+ appendToLastTextPart(suffixMessage, breakdown);
6332
+ }
6125
6333
  if (decision.tipsVariant === "maxLimit") {
6126
6334
  tipsText = '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
6127
- } else if (decision.tipsVariant === "minLimit") {
6128
- tipsText = "\n\n\u26A0\uFE0F Context is growing \u2014 consider compressing older work. Tools: compress, decompress, search_context.";
6129
- } else {
6130
- tipsText = "\n\n\u{1F4A1} Tools: compress, decompress, search_context.";
6131
6335
  }
6132
6336
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
6133
6337
  state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
@@ -6150,11 +6354,30 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6150
6354
  }
6151
6355
  injectVisibleIdRange(state, messages, suffixMessage);
6152
6356
  }
6357
+ if (toolOutputReminder && suffixMessage) {
6358
+ if (!decision.shouldNudge) {
6359
+ injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
6360
+ if (composition.total > 0) {
6361
+ const fmt2 = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6362
+ const pct2 = (n) => Math.round(n / composition.total * 100);
6363
+ const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
6364
+ let mini = `
6365
+ Breakdown: ${fmt2(composition.toolTokens)} tool outputs (${pct2(composition.toolTokens)}%) | ${fmt2(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt2(composition.messageTokens)} messages (${pct2(composition.messageTokens)}%)`;
6366
+ if (topBlocks.length > 0) {
6367
+ mini += `
6368
+ Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt2(b.compressedTokens)}\u2192${fmt2(b.summaryTokens)}`).join(", ")}`;
6369
+ }
6370
+ appendToLastTextPart(suffixMessage, mini);
6371
+ }
6372
+ }
6373
+ appendToLastTextPart(suffixMessage, toolOutputReminder);
6374
+ }
6153
6375
  if (suffixMessage) {
6154
6376
  appendToLastTextPart(suffixMessage, "\n");
6155
6377
  }
6156
6378
  if (anchorsChanged) {
6157
- void saveSessionState(state, logger);
6379
+ saveSessionState(state, logger).catch(() => {
6380
+ });
6158
6381
  }
6159
6382
  };
6160
6383
  function injectContextUsage(target, config, currentTokens, modelContextLimit) {
@@ -6728,45 +6951,107 @@ ${content}`;
6728
6951
 
6729
6952
  // lib/compress/status.ts
6730
6953
  import { tool as tool5 } from "@opencode-ai/plugin";
6731
- var ACP_STATUS_TOOL_DESCRIPTION = `Show detailed status of all active compressed context blocks. Returns a table of block IDs, summary sizes, ages, and topics \u2014 use this to decide which blocks to decompress or search. No arguments needed.
6954
+ var ACP_STATUS_TOOL_DESCRIPTION = `Show detailed status of all active compressed context blocks. Returns block IDs, sizes, ages, topics, and the message-ID ranges each block consumed \u2014 use this to see what has been compressed away and to choose safe compress boundaries.
6732
6955
 
6733
6956
  Use this tool when:
6734
- - You need to know what content has been compressed away
6957
+ - You are unsure which mNNNNN refs are still compressible
6958
+ - Before choosing compress boundaries, if any prior compressions exist
6735
6959
  - You want to see block sizes before deciding to decompress
6736
- - You need a quick overview of context compression state`;
6960
+ - A compress call failed with "not available" (the ID was likely consumed)
6961
+
6962
+ Args:
6963
+ - mode: "summary" (default) \u2014 one line per block with size/range/topic. "detailed" \u2014 adds age, generation, effective message count, consumed block lineage.
6964
+ - sort: "recent" (default) | "size" (largest compressed first) | "age" (oldest surviving first, nearing GC).
6965
+ - limit: max blocks to show (default 30).`;
6737
6966
  function formatTokens(n) {
6967
+ if (!Number.isFinite(n) || n <= 0) return "0";
6738
6968
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6739
6969
  }
6970
+ function formatSizePair(compressed, summary) {
6971
+ return `${formatTokens(compressed)}\u2192${formatTokens(summary)}`;
6972
+ }
6973
+ function formatIdRange(block) {
6974
+ const start = (block.startId || "").trim();
6975
+ const end = (block.endId || "").trim();
6976
+ if (!start || !end) return "\u2014";
6977
+ if (start === end) return start;
6978
+ return `${start}\u2013${end}`;
6979
+ }
6980
+ function sortBlocks(blocks, sort) {
6981
+ const copy = [...blocks];
6982
+ if (sort === "size") {
6983
+ copy.sort((a, b) => (b.compressedTokens || 0) - (a.compressedTokens || 0));
6984
+ } else if (sort === "age") {
6985
+ copy.sort((a, b) => (b.survivedCount || 0) - (a.survivedCount || 0));
6986
+ } else {
6987
+ copy.sort((a, b) => b.createdAt - a.createdAt);
6988
+ }
6989
+ return copy;
6990
+ }
6991
+ function renderSummaryRow(block, idWidth) {
6992
+ const idStr = `b${block.blockId}`.padEnd(idWidth + 1);
6993
+ const sizeStr = formatSizePair(block.compressedTokens, block.summaryTokens).padStart(13);
6994
+ const ageStr = formatAge(block.createdAt).padStart(10);
6995
+ const rangeStr = formatIdRange(block).padStart(19);
6996
+ const topic = block.topic || "(no topic)";
6997
+ return ` ${idStr} ${sizeStr} ${ageStr} ${rangeStr} "${topic}"`;
6998
+ }
6999
+ function renderDetailedRow(block, idWidth) {
7000
+ const idStr = `b${block.blockId}`.padEnd(idWidth + 1);
7001
+ const sizeStr = formatSizePair(block.compressedTokens, block.summaryTokens).padStart(13);
7002
+ const ageStr = formatAge(block.createdAt).padStart(10);
7003
+ const rangeStr = formatIdRange(block).padStart(19);
7004
+ const survived = block.survivedCount ?? 0;
7005
+ const gen = block.generation ?? "young";
7006
+ const effCount = block.effectiveMessageIds?.length ?? 0;
7007
+ const consumedLineage = block.consumedBlockIds && block.consumedBlockIds.length > 0 ? ` nested=[${block.consumedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
7008
+ const topic = block.topic || "(no topic)";
7009
+ return ` ${idStr} ${sizeStr} ${ageStr} ${rangeStr} age=${survived} ${gen} eff=${effCount}${consumedLineage} "${topic}"`;
7010
+ }
6740
7011
  function createAcpStatusTool(ctx) {
6741
7012
  ctx.prompts.reload();
6742
7013
  return tool5({
6743
7014
  description: ACP_STATUS_TOOL_DESCRIPTION,
6744
- args: {},
6745
- async execute() {
7015
+ args: {
7016
+ mode: tool5.schema.string().optional().describe('Output detail level: "summary" (default) or "detailed"'),
7017
+ sort: tool5.schema.string().optional().describe('Sort order: "recent" (default), "size", or "age"'),
7018
+ limit: tool5.schema.number().optional().describe("Maximum blocks to show (default 30)")
7019
+ },
7020
+ async execute(args) {
7021
+ const mode = args.mode === "detailed" ? "detailed" : "summary";
7022
+ const sort = args.sort === "size" || args.sort === "age" ? args.sort : "recent";
7023
+ const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
6746
7024
  const messages = ctx.state.prune.messages;
6747
7025
  const activeIds = Array.from(messages.activeBlockIds).sort((a, b) => a - b);
6748
7026
  if (activeIds.length === 0) {
6749
7027
  return "No compressed blocks. Context is fully visible.";
6750
7028
  }
6751
- const blocks = activeIds.map((id) => messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6752
- const totalSummary = blocks.reduce((sum, b) => sum + b.summaryTokens, 0);
6753
- const totalCompressed = blocks.reduce((sum, b) => sum + b.compressedTokens, 0);
7029
+ const allBlocks = activeIds.map((id) => messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
7030
+ if (allBlocks.length === 0) {
7031
+ return "No compressed blocks. Context is fully visible.";
7032
+ }
7033
+ const totalSummary = allBlocks.reduce((s, b) => s + (b.summaryTokens || 0), 0);
7034
+ const totalCompressed = allBlocks.reduce((s, b) => s + (b.compressedTokens || 0), 0);
7035
+ const sorted = sortBlocks(allBlocks, sort);
7036
+ const shown = sorted.slice(0, limit);
7037
+ const truncated = sorted.length - shown.length;
7038
+ const idWidth = Math.max(...shown.map((b) => String(b.blockId).length));
6754
7039
  const lines = [
6755
- `ACP Status \u2014 ${blocks.length} active compressed block${blocks.length === 1 ? "" : "s"} (${formatTokens(totalSummary)} summary tokens, ${formatTokens(totalCompressed)} original content compressed)`,
7040
+ `ACP Status \u2014 ${allBlocks.length} active compressed block${allBlocks.length === 1 ? "" : "s"} (${formatTokens(totalSummary)} summary, ${formatTokens(totalCompressed)} original compressed)`,
6756
7041
  ""
6757
7042
  ];
6758
- const idWidth = String(Math.max(...blocks.map((b) => b.blockId))).length;
6759
- for (const b of blocks) {
6760
- const idStr = `b${b.blockId}`.padEnd(idWidth + 1);
6761
- const tokStr = `${formatTokens(b.summaryTokens)}t`.padStart(7);
6762
- const ageStr = formatAge(b.createdAt).padStart(10);
6763
- const topic = b.topic || "(no topic)";
6764
- lines.push(` ${idStr} ${tokStr} ${ageStr} "${topic}"`);
7043
+ for (const b of shown) {
7044
+ lines.push(
7045
+ mode === "detailed" ? renderDetailedRow(b, idWidth) : renderSummaryRow(b, idWidth)
7046
+ );
7047
+ }
7048
+ if (truncated > 0) {
7049
+ lines.push("");
7050
+ lines.push(`${shown.length} of ${sorted.length} blocks shown (${truncated} hidden). Raise limit or change sort to see more.`);
6765
7051
  }
6766
7052
  lines.push("");
6767
- lines.push(
6768
- "Use decompress to restore a block's full content, or search_context to search within compressed blocks."
6769
- );
7053
+ const sortHint = sort === "recent" ? 'sorted by recent. Use acp_status({sort:"size"}) for largest, {sort:"age"} for near-GC.' : `sorted by ${sort}.`;
7054
+ lines.push(`${sortHint} Use decompress to restore a block's full content, or search_context to search within compressed blocks.`);
6770
7055
  return lines.join("\n");
6771
7056
  }
6772
7057
  });
@@ -6978,23 +7263,76 @@ import { homedir as homedir4 } from "os";
6978
7263
  // lib/prompts/system.ts
6979
7264
  var SYSTEM = `
6980
7265
 
6981
- 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.
7266
+ 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.
7267
+
7268
+ ACP TAGS
6982
7269
 
6983
- The tools you have for context management are \`compress\`, \`decompress\`, and \`search_context\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`search_context\` searches compressed block summaries (and visible messages) to locate relevant content before you decompress.
7270
+ \`<acp-context>\` tags wrap ACP (Agent Context Pruning) system metadata \u2014 context management information injected each turn. This is system data, not user input. You may also see \`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags \u2014 these are equivalent (DCP was the previous name for ACP). Treat them as boundary metadata only, not as tool-result content.
6984
7271
 
6985
- \`<acp-context>\` tags wrap ACP (Agent Context Pruning) system metadata \u2014 context management information injected each turn. This is system data, not user input. You may also see \`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags \u2014 these are equivalent (DCP was the previous name for ACP).
7272
+ TOOLS
7273
+
7274
+ You have four context-management tools:
7275
+
7276
+ - \`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). Example: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`.
7277
+ - \`decompress\` \u2014 Restore a previously compressed block's full original content, optionally to a file for large blocks. Use when a summary lacks the exact detail you need. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\`.
7278
+ - \`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" })\`.
7279
+ - \`acp_status\` \u2014 List all active compressed blocks with their sizes, ages, and the message ranges they consumed. Use when you are unsure which IDs are still compressible, or before choosing compress boundaries. Example: \`acp_status({ mode: "summary", sort: "recent" })\`.
6986
7280
 
6987
7281
  COMPRESSION PHILOSOPHY
6988
7282
 
6989
- All compression serves the primary task, but be frugal. Two failure modes to avoid:
7283
+ Two failure modes to avoid:
6990
7284
  - Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
6991
7285
  - Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
6992
7286
 
6993
- Balance is key. Compress selectively to keep context lean. But never compress content you're actively using for an ongoing task. Use \`search_context\` to find compressed content when needed, and \`decompress\` to restore details.
7287
+ 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, it is a candidate. When uncertain, lean toward keeping content.
6994
7288
 
6995
7289
  BE FRUGAL
6996
7290
 
6997
- Be frugal with context \u2014 compress obvious waste promptly. Examples include verbose command output (build/test logs, git diff/status, npm install), sub-agent results once consumed, experiment/training logs (keep final metrics only), duplicate file reads, and failed explorations. Any content that is finished serving the task and would not be needed in upcoming turns should be compressed \u2014 not just these examples.
7291
+ Be frugal with context. Compress obvious waste proactively when you encounter it \u2014 verbose outputs you have already used, duplicate reads, abandoned explorations. Do not wait until context is critically full before compressing; that harms retrieval quality and risks overflow. When compressing, cover the largest range you can in a single call \u2014 aim for 20+ messages. Compressing 3-5 messages at a time creates many small summaries that collectively waste more tokens than they save. But never let the urge to compress distract from the actual task.
7292
+
7293
+ WHEN TO COMPRESS
7294
+
7295
+ - A sub-agent or delegated task has returned a large result that you have already extracted the key facts from.
7296
+ - Verbose command output (build/test logs, \`git diff\`, \`npm install\`, directory listings) where you have already used the information you need.
7297
+ - Exploration that led nowhere \u2014 compress the dead-ends but preserve the lessons learned: what was tried, what failed, and why.
7298
+ - Repeated reads of the same file or repeated status checks once the decision is recorded.
7299
+ - Resolved discussion threads where a decision has been captured in the summary or in code \u2014 compress the back-and-forth but preserve the decision rationale if it will be referenced later.
7300
+ - Intermediate steps of a completed multi-step task, once the final result is recorded.
7301
+ - When a task phase ends \u2014 such as finishing a bug hunt, locating a root cause, wrapping up a codebase exploration, or completing a research sprint \u2014 proactively compress the phase's redundant churn (exploratory reads, failed attempts, verbose outputs) while preserving what endures: key findings, relevant code and file paths, decision rationale, and lessons learned (what worked, what didn't, what's worth remembering next time).
7302
+ - Any other content where compression serves the primary task \u2014 be frugal.
7303
+
7304
+ WHEN NOT TO COMPRESS
7305
+
7306
+ - Content the current task step is actively reading or reasoning about.
7307
+ - Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
7308
+ - Outputs from protected tools (e.g. \`task\`, \`skill\`, \`todowrite\`, \`write\`, \`edit\`) \u2014 these are appended to summaries automatically, not compressed away.
7309
+
7310
+ PERIODIC CONTEXT STATUS
7311
+
7312
+ Periodically, as context grows, the system appends a short status line in a synthetic suffix message. It looks like:
7313
+
7314
+ [ACP] Context: 47.3K tokens. Visible: m00001\u2013m00929, m00944\u2013m00950 (810 msgs). 3 active blocks. \`acp_status\` for details.
7315
+
7316
+ This line is INFORMATION, not an instruction. Seeing it does not mean you should compress. Compress only when one of the WHEN TO COMPRESS conditions actually holds. Between these lines, context is not under additional pressure \u2014 you do not need to seek things to compress.
7317
+
7318
+ If you are unsure which \`mNNNNN\` refs are still compressible, or which blocks have already consumed which ranges, call \`acp_status\` first. It returns the block IDs, their sizes, and the message-ID ranges each covers.
7319
+
7320
+ CONTEXT BREAKDOWN
7321
+
7322
+ When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
7323
+
7324
+ Breakdown: 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text (22%)
7325
+
7326
+ - "tool" = tool call outputs (largest category \u2014 compress first when consumed)
7327
+ - "summaries" = existing compression block summaries (already compressed; do not re-compress standalone)
7328
+ - "code" = messages containing code blocks
7329
+ - "text" = plain text messages
7330
+
7331
+ Below the breakdown, the system lists the largest ranges in each category (e.g. \`Largest tool outputs: m00175 (20.7K), m00200 (8.1K)\`). These are high-value compression candidates \u2014 compress those whose content you have already consumed (extracted the facts you need). Keep any you still need to reference.
7332
+
7333
+ Compress incrementally: target one large consumed range per compress call (e.g. m00150\u2192m00200), not the entire context at once. Each compression creates a reusable summary block you can decompress later if needed.
7334
+
7335
+ <acp-compression-summary>\`<acp-compression-summary>\` tags wrap ACP model-generated recaps of previously compressed conversation ranges. These are system-generated metadata, not user messages. Treat them as reference material for the compressed history.
6998
7336
  `;
6999
7337
 
7000
7338
  // lib/prompts/compress-range.ts
@@ -7049,7 +7387,7 @@ When multiple independent ranges are ready and their boundaries do not overlap,
7049
7387
  var COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
7050
7388
 
7051
7389
  THE SUMMARY
7052
- Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after the raw message is removed.
7390
+ Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after it is summarized. The original content can be restored via decompress if needed later.
7053
7391
 
7054
7392
  USER INTENT FIDELITY
7055
7393
  When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
@@ -8751,7 +9089,8 @@ function runMajorGC(state, config, logger, messages) {
8751
9089
  agedOutTokens,
8752
9090
  maxBlockAge
8753
9091
  });
8754
- void saveSessionState(state, logger);
9092
+ saveSessionState(state, logger).catch(() => {
9093
+ });
8755
9094
  }
8756
9095
  if (!state.modelContextLimit) return;
8757
9096
  const currentTokens = getCurrentTokenUsage(state, messages);
@@ -8781,7 +9120,8 @@ function runMajorGC(state, config, logger, messages) {
8781
9120
  currentTokens,
8782
9121
  threshold: config.gc.majorGcThresholdPercent
8783
9122
  });
8784
- void saveSessionState(state, logger);
9123
+ saveSessionState(state, logger).catch(() => {
9124
+ });
8785
9125
  }
8786
9126
  }
8787
9127
  function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
@@ -8809,14 +9149,16 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
8809
9149
  const activeBlockCountBefore = state.prune.messages.activeBlockIds.size;
8810
9150
  syncCompressionBlocks(state, logger, output.messages);
8811
9151
  if (state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
8812
- void saveSessionState(state, logger);
9152
+ saveSessionState(state, logger).catch(() => {
9153
+ });
8813
9154
  }
8814
9155
  syncToolCache(state, config, logger, output.messages);
8815
9156
  buildToolIdList(state, output.messages);
8816
9157
  runMajorGC(state, config, logger, output.messages);
8817
9158
  const batchResult = runBatchCleanup(state, config, logger, output.messages);
8818
9159
  if (batchResult.mergedCount > 0) {
8819
- void saveSessionState(state, logger);
9160
+ saveSessionState(state, logger).catch(() => {
9161
+ });
8820
9162
  }
8821
9163
  prune(state, logger, config, output.messages);
8822
9164
  assignMessageRefs(state, output.messages);