opencode-acp 1.14.25-pr.351.82 → 1.14.25-pr.351.88

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
@@ -1324,7 +1324,72 @@ function validateConfigTypes(config) {
1324
1324
  });
1325
1325
  }
1326
1326
  };
1327
- const validateModelOverrides = (prefix, overrides) => {
1327
+ const OVERRIDE_FIELD_TYPES = {
1328
+ showCompression: "boolean",
1329
+ summaryBuffer: "boolean",
1330
+ protectTags: "boolean",
1331
+ protectUserMessages: "boolean",
1332
+ lastSegmentSoftBlock: "boolean",
1333
+ preserveLastUserMessage: "boolean",
1334
+ maxContextLimit: "limit",
1335
+ emergencyThresholdPercent: "limit",
1336
+ minNudgeContextPercent: "percent",
1337
+ nudgeFrequency: "positiveNumber",
1338
+ iterationNudgeThreshold: "positiveNumber",
1339
+ toolOutputNudgeThreshold: "nonNegativeNumber",
1340
+ nudgeGrowthTokens: "nonNegativeNumber",
1341
+ minNudgeGrowthRatio: "nonNegativeNumber",
1342
+ minNudgeGrowthFloor: "nonNegativeNumber",
1343
+ nudgeForce: "nudgeForce",
1344
+ protectedTools: "stringArray",
1345
+ maxSummaryLengthHard: "positiveNumber",
1346
+ minCompressRange: "nonNegativeNumber",
1347
+ maxVisibleSegments: "positiveNumber",
1348
+ keepEmbedMaxChars: "nonNegativeNumber",
1349
+ preserveRecentMessages: "nonNegativeNumber",
1350
+ preserveRecentTokens: "nonNegativeNumber"
1351
+ };
1352
+ const validateOverrideField = (key, type, value) => {
1353
+ if (value === void 0) {
1354
+ return;
1355
+ }
1356
+ switch (type) {
1357
+ case "boolean":
1358
+ if (typeof value !== "boolean") {
1359
+ errors.push({ key, expected: "boolean", actual: typeof value });
1360
+ }
1361
+ break;
1362
+ case "nonNegativeNumber":
1363
+ case "positiveNumber": {
1364
+ const min = type === "positiveNumber" ? 1 : 0;
1365
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min) {
1366
+ errors.push({
1367
+ key,
1368
+ expected: type === "positiveNumber" ? "number (>= 1)" : "number (>= 0)",
1369
+ actual: JSON.stringify(value)
1370
+ });
1371
+ }
1372
+ break;
1373
+ }
1374
+ case "percent":
1375
+ validatePercentField(key, value);
1376
+ break;
1377
+ case "limit":
1378
+ validateLimitValue(key, value);
1379
+ break;
1380
+ case "nudgeForce":
1381
+ if (value !== "strong" && value !== "soft") {
1382
+ errors.push({ key, expected: "'strong' | 'soft'", actual: JSON.stringify(value) });
1383
+ }
1384
+ break;
1385
+ case "stringArray":
1386
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
1387
+ errors.push({ key, expected: "string[]", actual: JSON.stringify(value) });
1388
+ }
1389
+ break;
1390
+ }
1391
+ };
1392
+ const validateOverrideObject = (prefix, overrides) => {
1328
1393
  if (overrides === void 0) {
1329
1394
  return;
1330
1395
  }
@@ -1334,16 +1399,19 @@ function validateConfigTypes(config) {
1334
1399
  }
1335
1400
  const model = overrides;
1336
1401
  for (const field of Object.keys(model)) {
1337
- if (field !== "minNudgeContextPercent") {
1402
+ const type = OVERRIDE_FIELD_TYPES[field];
1403
+ if (type === void 0) {
1338
1404
  errors.push({
1339
1405
  key: `${prefix}.${field}`,
1340
- expected: "known override field (minNudgeContextPercent)",
1406
+ expected: "known override field (see CompressModelOverrides)",
1341
1407
  actual: "unknown field"
1342
1408
  });
1409
+ continue;
1343
1410
  }
1411
+ validateOverrideField(`${prefix}.${field}`, type, model[field]);
1344
1412
  }
1345
- validatePercentField(`${prefix}.minNudgeContextPercent`, model.minNudgeContextPercent);
1346
1413
  };
1414
+ const validateModelOverrides = validateOverrideObject;
1347
1415
  const validateProviderOverrides = (providers) => {
1348
1416
  if (providers === void 0) {
1349
1417
  return;
@@ -1364,15 +1432,20 @@ function validateConfigTypes(config) {
1364
1432
  }
1365
1433
  const provider = providerValue;
1366
1434
  for (const field of Object.keys(provider)) {
1367
- if (field !== "minNudgeContextPercent" && field !== "models") {
1435
+ if (field === "models") {
1436
+ continue;
1437
+ }
1438
+ const type = OVERRIDE_FIELD_TYPES[field];
1439
+ if (type === void 0) {
1368
1440
  errors.push({
1369
1441
  key: `${prefix}.${field}`,
1370
- expected: "known override field (minNudgeContextPercent, models)",
1442
+ expected: "known override field (see CompressModelOverrides)",
1371
1443
  actual: "unknown field"
1372
1444
  });
1445
+ continue;
1373
1446
  }
1447
+ validateOverrideField(`${prefix}.${field}`, type, provider[field]);
1374
1448
  }
1375
- validatePercentField(`${prefix}.minNudgeContextPercent`, provider.minNudgeContextPercent);
1376
1449
  const models = provider.models;
1377
1450
  if (models === void 0) {
1378
1451
  continue;
@@ -6143,1493 +6216,1530 @@ function truncate(text, maxChars) {
6143
6216
  ... [truncated, ${text.length} chars total]`;
6144
6217
  }
6145
6218
 
6146
- // lib/compress/range.ts
6147
- function buildSchema(maxSummaryLengthHard) {
6219
+ // lib/prompts/extensions/nudge.ts
6220
+ function buildCompressedBlockGuidance(state, context) {
6221
+ const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
6222
+ const blockCount = activeBlockIds.length;
6223
+ const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6224
+ const totalSummaryTokens = blocksForStats.reduce((s, b) => s + (b.summaryTokens ?? 0), 0);
6225
+ const totalSummaryDisplay = totalSummaryTokens >= 1e3 ? `${(totalSummaryTokens / 1e3).toFixed(1)}K` : String(totalSummaryTokens);
6226
+ const lastBlock = blocksForStats.length > 0 ? blocksForStats.reduce((latest, b) => b.createdAt > latest.createdAt ? b : latest) : null;
6227
+ const ageStr = lastBlock ? formatAge(lastBlock.createdAt) : "never";
6228
+ const lines = [
6229
+ `- Compressed blocks: ${blockCount} (${totalSummaryDisplay} summary, last ${ageStr}). Use acp_status for details.`
6230
+ ];
6231
+ if (blockCount > 50) {
6232
+ const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
6233
+ const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
6234
+ const visibleMessageIds = context?.visibleMessageIds;
6235
+ const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
6236
+ if (visibleOldBlocks.length > 5) {
6237
+ const blocksWithRef = visibleOldBlocks.map((block) => {
6238
+ const ref = state.messageIds.byRawId.get(block.anchorMessageId);
6239
+ return ref ? { block, ref } : null;
6240
+ }).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
6241
+ const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6242
+ const totalK = Math.max(1, Math.round(totalTokens / 1e3));
6243
+ const targets = [];
6244
+ const chunkSize = Math.ceil(blocksWithRef.length / 3);
6245
+ for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
6246
+ const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
6247
+ if (chunk.length < 2) continue;
6248
+ const startRef = chunk[0].ref;
6249
+ const endRef = chunk[chunk.length - 1].ref;
6250
+ const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6251
+ const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
6252
+ targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
6253
+ }
6254
+ if (targets.length > 0) {
6255
+ lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
6256
+ lines.push(...targets);
6257
+ lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write your summary normally.`);
6258
+ }
6259
+ }
6260
+ }
6261
+ return lines.join("\n");
6262
+ }
6263
+ function appendGuidanceToDcpTag(nudgeText, guidance) {
6264
+ if (!guidance.trim()) {
6265
+ return nudgeText;
6266
+ }
6267
+ const closeTag = "</dcp-system-reminder>";
6268
+ const closeTagIndex = nudgeText.lastIndexOf(closeTag);
6269
+ if (closeTagIndex === -1) {
6270
+ return nudgeText;
6271
+ }
6272
+ const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd();
6273
+ const afterClose = nudgeText.slice(closeTagIndex);
6274
+ return `${beforeClose}
6275
+
6276
+ ${guidance}
6277
+ ${afterClose}`;
6278
+ }
6279
+
6280
+ // lib/messages/utils.ts
6281
+ import { createHash } from "crypto";
6282
+ var SUMMARY_ID_HASH_LENGTH = 16;
6283
+ var DCP_PAIRED_TAG_REGEX = /<(?:dcp|acp)[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
6284
+ var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
6285
+ var generateStableId = (prefix, seed) => {
6286
+ const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
6287
+ return `${prefix}_${hash}`;
6288
+ };
6289
+ var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
6290
+ const baseInfo = baseMessage.info;
6291
+ const now = Date.now();
6292
+ const deterministicSeed = stableSeed?.trim() || baseInfo.id;
6293
+ const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
6294
+ const partId = generateStableId("prt_dcp_summary", deterministicSeed);
6295
+ const parts = [
6296
+ {
6297
+ id: partId,
6298
+ sessionID: baseInfo.sessionID,
6299
+ messageID: messageId,
6300
+ type: "text",
6301
+ text: content,
6302
+ synthetic: true
6303
+ }
6304
+ ];
6305
+ if (role === "assistant") {
6306
+ const isAssistant = baseInfo.role === "assistant";
6307
+ const assistantBase = isAssistant ? baseInfo : void 0;
6308
+ const userModel = !isAssistant ? baseInfo.model : void 0;
6309
+ const info2 = {
6310
+ id: messageId,
6311
+ sessionID: baseInfo.sessionID,
6312
+ role: "assistant",
6313
+ time: { created: now },
6314
+ parentID: assistantBase?.parentID ?? "",
6315
+ modelID: assistantBase?.modelID ?? userModel?.modelID ?? "",
6316
+ providerID: assistantBase?.providerID ?? userModel?.providerID ?? "",
6317
+ mode: assistantBase?.mode ?? "code",
6318
+ agent: baseInfo.agent ?? "code",
6319
+ path: assistantBase?.path ?? { cwd: "", root: "" },
6320
+ cost: 0,
6321
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
6322
+ };
6323
+ return { info: info2, parts };
6324
+ }
6325
+ const userInfo = baseInfo;
6326
+ const info = {
6327
+ id: messageId,
6328
+ sessionID: userInfo.sessionID,
6329
+ role: "user",
6330
+ agent: userInfo.agent,
6331
+ model: userInfo.model,
6332
+ time: { created: now }
6333
+ };
6334
+ return { info, parts };
6335
+ };
6336
+ var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
6337
+ var createSyntheticTextPart = (baseMessage, content, stableSeed) => {
6338
+ const userInfo = baseMessage.info;
6339
+ const deterministicSeed = stableSeed?.trim() || userInfo.id;
6340
+ const partId = generateStableId("prt_dcp_text", deterministicSeed);
6148
6341
  return {
6149
- topic: tool2.schema.string().optional().describe(
6150
- "Fallback topic for entries without their own. Omit when each content entry specifies its own topic."
6151
- ),
6152
- content: tool2.schema.array(
6153
- tool2.schema.object({
6154
- topic: tool2.schema.string().optional().describe(
6155
- "Short label (3-5 words) for THIS range, e.g. 'Auth System Exploration'. Omit to use top-level topic. When compressing multiple unrelated ranges, give each its own topic for better quality."
6156
- ),
6157
- startId: tool2.schema.string().describe(
6158
- "Message or block ID marking the beginning of range (e.g. m00001, b2)"
6159
- ),
6160
- endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
6161
- summary: tool2.schema.string().describe(
6162
- "Complete technical summary replacing all content in range. Keep only essential details (conclusions, file paths, decisions, exact values, etc.)."
6163
- )
6164
- })
6165
- ).describe(
6166
- "One or more ranges to compress, each with start/end boundaries and a summary. When compressing multiple unrelated ranges in one call, give each its own topic."
6167
- ),
6168
- summaryMaxChars: tool2.schema.number().optional().describe(
6169
- `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.`
6170
- ),
6171
- dangerous: tool2.schema.boolean().optional().describe(
6172
- "Set to true ONLY when you are certain the most recent message(s) must be compressed. Required when a range includes the tail of the conversation."
6173
- ),
6174
- acknowledgeRisk: tool2.schema.boolean().optional()
6342
+ id: partId,
6343
+ sessionID: userInfo.sessionID,
6344
+ messageID: userInfo.id,
6345
+ type: "text",
6346
+ text: content
6175
6347
  };
6348
+ };
6349
+ var appendToLastTextPart = (message, injection) => {
6350
+ const textPart = findLastTextPart(message);
6351
+ if (!textPart) {
6352
+ return false;
6353
+ }
6354
+ return appendToTextPart(textPart, injection);
6355
+ };
6356
+ var findLastTextPart = (message) => {
6357
+ for (let i = message.parts.length - 1; i >= 0; i--) {
6358
+ const part = message.parts[i];
6359
+ if (part.type === "text") {
6360
+ return part;
6361
+ }
6362
+ }
6363
+ return null;
6364
+ };
6365
+ var appendToTextPart = (part, injection) => {
6366
+ if (typeof part.text !== "string") {
6367
+ return false;
6368
+ }
6369
+ const normalizedInjection = injection.replace(/^\n+/, "");
6370
+ if (!normalizedInjection.trim()) {
6371
+ return false;
6372
+ }
6373
+ if (part.text.includes(normalizedInjection)) {
6374
+ return true;
6375
+ }
6376
+ const baseText = part.text.replace(/\n*$/, "");
6377
+ part.text = baseText.length > 0 ? `${baseText}
6378
+
6379
+ ${normalizedInjection}` : normalizedInjection;
6380
+ return true;
6381
+ };
6382
+ var appendToAllToolParts = (message, tag) => {
6383
+ let injected = false;
6384
+ for (const part of message.parts) {
6385
+ if (part.type === "tool") {
6386
+ injected = appendToToolPart(part, tag) || injected;
6387
+ }
6388
+ }
6389
+ return injected;
6390
+ };
6391
+ var appendToToolPart = (part, tag) => {
6392
+ if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
6393
+ return false;
6394
+ }
6395
+ if (part.state.output.includes(tag)) {
6396
+ return true;
6397
+ }
6398
+ part.state.output = `${part.state.output}${tag}`;
6399
+ return true;
6400
+ };
6401
+ var hasContent = (message) => {
6402
+ return message.parts.some(
6403
+ (part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0 || part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string"
6404
+ );
6405
+ };
6406
+ function buildToolIdList(state, messages) {
6407
+ const toolIds = [];
6408
+ for (const msg of messages) {
6409
+ if (isMessageCompacted(state, msg)) {
6410
+ continue;
6411
+ }
6412
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
6413
+ if (parts.length > 0) {
6414
+ for (const part of parts) {
6415
+ if (part.type === "tool" && part.callID && part.tool) {
6416
+ toolIds.push(part.callID);
6417
+ }
6418
+ }
6419
+ }
6420
+ }
6421
+ state.toolIdList = toolIds;
6422
+ return toolIds;
6176
6423
  }
6177
- function createCompressRangeTool(factoryCtx) {
6178
- factoryCtx.prompts.reload();
6179
- const runtimePrompts = factoryCtx.prompts.getRuntimePrompts();
6180
- return tool2({
6181
- description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
6182
- args: buildSchema(factoryCtx.config.compress.maxSummaryLengthHard),
6183
- async execute(args, toolCtx) {
6184
- const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
6185
- const input = args;
6186
- validateArgs(input);
6187
- const maxLen = args.summaryMaxChars ?? ctx.config.compress.maxSummaryLengthHard;
6188
- for (const entry of input.content) {
6189
- if (entry.summary.length > maxLen) {
6190
- throw new Error(
6191
- `Summary too long (${entry.summary.length} chars, max ${maxLen}).
6192
- 1. If this summary is nearly the same size as the original content, it may not be worth compressing \u2014 skip it.
6193
- 2. Strip noise (failed attempts, verbose outputs) but keep project-critical details (file paths, decisions, exact values).
6194
- 3. For important content needing detail, pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit. Example: add "summaryMaxChars": 6000 to the tool call args.`
6195
- );
6196
- }
6424
+ var stripHallucinationsFromString = (text) => {
6425
+ return text.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
6426
+ };
6427
+ var stripHallucinations = (messages) => {
6428
+ for (const message of messages) {
6429
+ for (const part of message.parts) {
6430
+ if (part.type === "text" && typeof part.text === "string") {
6431
+ part.text = stripHallucinationsFromString(part.text);
6197
6432
  }
6198
- const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
6199
- const { rawMessages, searchContext } = await prepareSession(
6200
- ctx,
6201
- toolCtx,
6202
- `Compress Range: ${input.topic ?? "(batch)"}`
6203
- );
6204
- const resolvedPlans = resolveRanges(input, searchContext, ctx.state, ctx.logger);
6205
- validateNonOverlapping(resolvedPlans);
6206
- const filteredPlans = resolvedPlans.map((plan) => ({
6207
- ...plan,
6208
- selection: filterProtectedToolMessages(
6209
- plan.selection,
6210
- searchContext,
6211
- ctx.config.compress.protectedTools,
6212
- ctx.config.protectedFilePatterns
6213
- )
6214
- })).map((plan) => ({
6215
- ...plan,
6216
- selection: filterLastUserMessage(
6217
- plan.selection,
6218
- searchContext,
6219
- ctx.state,
6220
- ctx.config.compress
6221
- )
6222
- })).map((plan) => ({
6223
- ...plan,
6224
- selection: filterProtectedRecentMessages(
6225
- plan.selection,
6226
- searchContext,
6227
- ctx.state,
6228
- ctx.config.compress
6229
- )
6230
- })).filter((plan) => plan.selection.messageIds.length > 0);
6231
- if (filteredPlans.length === 0) {
6232
- throw new Error(
6233
- "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
6234
- );
6433
+ if (part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string") {
6434
+ part.state.output = stripHallucinationsFromString(part.state.output);
6235
6435
  }
6236
- const minCompressRange = ctx.config.compress.minCompressRange;
6237
- if (minCompressRange > 0) {
6238
- let totalChars = 0;
6239
- const counted = /* @__PURE__ */ new Set();
6240
- for (const plan of filteredPlans) {
6241
- for (const messageId of plan.selection.messageIds) {
6242
- if (counted.has(messageId)) continue;
6243
- counted.add(messageId);
6244
- const rawMessage = searchContext.rawMessagesById.get(messageId);
6245
- if (rawMessage) {
6246
- totalChars += countMessageCharacters(rawMessage);
6247
- }
6436
+ }
6437
+ }
6438
+ };
6439
+ var dropEmptyMessages = (messages) => {
6440
+ let removed = 0;
6441
+ for (let i = messages.length - 1; i >= 0; i--) {
6442
+ const parts = Array.isArray(messages[i].parts) ? messages[i].parts : [];
6443
+ const isEmpty = parts.every(
6444
+ (part) => part.type === "text" && (typeof part.text !== "string" || part.text.trim().length === 0 || part.ignored === true)
6445
+ );
6446
+ if (isEmpty) {
6447
+ messages.splice(i, 1);
6448
+ removed++;
6449
+ }
6450
+ }
6451
+ return removed;
6452
+ };
6453
+
6454
+ // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
6455
+ var NUDGE_GROWTH_FLOOR = 6e3;
6456
+ var NUDGE_GROWTH_CAP = 5e4;
6457
+ var NUDGE_GROWTH_RATIO = 0.05;
6458
+ function computeShouldNudge(params) {
6459
+ const { currentTokens, overMinLimit, overMaxLimit } = params;
6460
+ if (currentTokens === void 0) {
6461
+ return { shouldNudge: false, tipsVariant: null };
6462
+ }
6463
+ if (params.lastNudgeTokens === void 0) {
6464
+ return { shouldNudge: false, tipsVariant: null };
6465
+ }
6466
+ const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
6467
+ const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
6468
+ if (!shouldNudge) {
6469
+ return { shouldNudge: false, tipsVariant: null };
6470
+ }
6471
+ const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
6472
+ return { shouldNudge: true, tipsVariant };
6473
+ }
6474
+ function resolveAdaptiveNudgeGrowth(modelContextLimit) {
6475
+ if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
6476
+ return Math.min(
6477
+ NUDGE_GROWTH_CAP,
6478
+ Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
6479
+ );
6480
+ }
6481
+ var defaultTriggerPolicy = {
6482
+ name: "context-compress-algorithms-trigger",
6483
+ version: "1.0.0",
6484
+ description: "Growth-only cadence: nudge when context growth since last nudge exceeds adaptive threshold, or when over max limit.",
6485
+ computeShouldNudge,
6486
+ resolveAdaptiveNudgeGrowth
6487
+ };
6488
+
6489
+ // lib/messages/inject/policy/registry.ts
6490
+ var registry2 = /* @__PURE__ */ new Map();
6491
+ var defaultPolicy = null;
6492
+ function registerTriggerPolicy2(policy) {
6493
+ if (!policy.name) {
6494
+ throw new Error("TriggerPolicy must have a name");
6495
+ }
6496
+ registry2.set(policy.name, policy);
6497
+ if (!defaultPolicy) {
6498
+ defaultPolicy = policy;
6499
+ }
6500
+ }
6501
+ function getDefaultTriggerPolicy() {
6502
+ return defaultPolicy;
6503
+ }
6504
+
6505
+ // lib/messages/inject/policy/index.ts
6506
+ function ensureBuiltinTriggerPolicyRegistered() {
6507
+ registerTriggerPolicy2(defaultTriggerPolicy);
6508
+ }
6509
+
6510
+ // lib/messages/inject/utils.ts
6511
+ function getNudgeFrequency(config) {
6512
+ return Math.max(1, Math.floor(config.compress.nudgeFrequency || 1));
6513
+ }
6514
+ function getIterationNudgeThreshold(config) {
6515
+ return Math.max(1, Math.floor(config.compress.iterationNudgeThreshold || 1));
6516
+ }
6517
+ function findLastNonIgnoredMessage(messages) {
6518
+ for (let i = messages.length - 1; i >= 0; i--) {
6519
+ const message = messages[i];
6520
+ if (isIgnoredUserMessage(message)) {
6521
+ continue;
6522
+ }
6523
+ if (isSyntheticMessage(message)) {
6524
+ continue;
6525
+ }
6526
+ return { message, index: i };
6527
+ }
6528
+ return null;
6529
+ }
6530
+ function countMessagesAfterIndex(messages, index) {
6531
+ let count = 0;
6532
+ for (let i = index + 1; i < messages.length; i++) {
6533
+ const message = messages[i];
6534
+ if (isIgnoredUserMessage(message)) {
6535
+ continue;
6536
+ }
6537
+ count++;
6538
+ }
6539
+ return count;
6540
+ }
6541
+ function getModelInfo(messages) {
6542
+ const lastUserMessage = getLastUserMessage(messages);
6543
+ if (!lastUserMessage) {
6544
+ return {
6545
+ providerId: void 0,
6546
+ modelId: void 0
6547
+ };
6548
+ }
6549
+ const userInfo = lastUserMessage.info;
6550
+ return {
6551
+ providerId: userInfo.model?.providerID,
6552
+ modelId: userInfo.model?.modelID
6553
+ };
6554
+ }
6555
+ function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6556
+ const parseLimitValue = (limit) => {
6557
+ if (limit === void 0) {
6558
+ return void 0;
6559
+ }
6560
+ if (typeof limit === "number") {
6561
+ return limit;
6562
+ }
6563
+ if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6564
+ return void 0;
6565
+ }
6566
+ const parsedPercent = parseFloat(limit.slice(0, -1));
6567
+ if (isNaN(parsedPercent)) {
6568
+ return void 0;
6569
+ }
6570
+ const roundedPercent = Math.round(parsedPercent);
6571
+ const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6572
+ return Math.round(clampedPercent / 100 * state.modelContextLimit);
6573
+ };
6574
+ if (threshold === "max") {
6575
+ const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
6576
+ if (nestedLimit !== void 0) {
6577
+ return parseLimitValue(nestedLimit);
6578
+ }
6579
+ }
6580
+ const modelLimits = threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits;
6581
+ if (modelLimits && providerId !== void 0 && modelId !== void 0) {
6582
+ const providerModelId = `${providerId}/${modelId}`;
6583
+ const modelLimit = modelLimits[providerModelId];
6584
+ if (modelLimit !== void 0) {
6585
+ return parseLimitValue(modelLimit);
6586
+ }
6587
+ }
6588
+ const globalLimit = threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit;
6589
+ return parseLimitValue(globalLimit);
6590
+ }
6591
+ function isContextOverLimits(config, state, providerId, modelId, messages) {
6592
+ const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
6593
+ state,
6594
+ new Set(messages.map((m) => m.info.id))
6595
+ ) : 0;
6596
+ const resolvedMaxContextLimit = resolveContextTokenLimit(
6597
+ config,
6598
+ state,
6599
+ providerId,
6600
+ modelId,
6601
+ "max"
6602
+ );
6603
+ const maxContextLimit = resolvedMaxContextLimit === void 0 ? void 0 : resolvedMaxContextLimit + summaryTokenExtension;
6604
+ const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min");
6605
+ const currentTokens = getCurrentTokenUsage(state, messages);
6606
+ let overMaxLimit = maxContextLimit === void 0 ? false : currentTokens > maxContextLimit;
6607
+ const overMinLimit = minContextLimit === void 0 ? false : currentTokens >= minContextLimit;
6608
+ if (overMaxLimit) {
6609
+ const recentCompressCount = 3;
6610
+ const recentMessages = messages.slice(-recentCompressCount);
6611
+ for (const msg of recentMessages) {
6612
+ if (msg.info.role === "assistant" && msg.parts) {
6613
+ for (const part of msg.parts) {
6614
+ if (part.type === "tool" && part.tool === "compress") {
6615
+ overMaxLimit = false;
6616
+ break;
6248
6617
  }
6249
6618
  }
6250
- if (totalChars < minCompressRange) {
6251
- throw new Error(
6252
- `Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
6253
- );
6254
- }
6255
6619
  }
6256
- const notifications = [];
6257
- let preparedPlans = [];
6258
- let totalCompressedMessages = 0;
6259
- for (const plan of filteredPlans) {
6260
- const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
6261
- validateSummaryPlaceholders(
6262
- parsedPlaceholders,
6263
- plan.selection.requiredBlockIds,
6264
- plan.selection.startReference,
6265
- plan.selection.endReference,
6266
- searchContext.summaryByBlockId,
6267
- ctx.logger
6268
- );
6269
- const injected = injectBlockPlaceholders(
6270
- plan.entry.summary,
6271
- parsedPlaceholders,
6272
- searchContext.summaryByBlockId,
6273
- plan.selection.startReference,
6274
- plan.selection.endReference
6275
- );
6276
- const summaryWithUsers = appendProtectedUserMessages(
6277
- injected.expandedSummary,
6278
- plan.selection,
6279
- searchContext,
6280
- ctx.state,
6281
- ctx.config.compress.protectUserMessages
6282
- );
6283
- const summaryWithPromptInfo = appendProtectedPromptInfo(
6284
- summaryWithUsers,
6285
- plan.selection,
6286
- searchContext,
6287
- ctx.state,
6288
- ctx.config.compress.protectTags
6289
- );
6290
- const summaryWithTools = await appendProtectedTools(
6291
- ctx.client,
6292
- ctx.state,
6293
- summaryWithPromptInfo,
6294
- plan.selection,
6295
- searchContext,
6296
- ctx.config.compress.protectedTools,
6297
- ctx.config.protectedFilePatterns
6298
- );
6299
- const completedSummary = appendMissingBlockSummaries(
6300
- summaryWithTools,
6301
- [],
6302
- searchContext.summaryByBlockId,
6303
- injected.consumedBlockIds
6304
- );
6305
- const boundaryConsumed = extractBoundaryConsumedBlocks2(
6306
- plan.selection.startReference,
6307
- plan.selection.endReference
6308
- );
6309
- const seenConsumed = /* @__PURE__ */ new Set();
6310
- const mergeConsumedBlockIds = [
6311
- ...plan.selection.requiredBlockIds,
6312
- ...boundaryConsumed
6313
- ].filter((id) => {
6314
- if (seenConsumed.has(id)) return false;
6315
- seenConsumed.add(id);
6316
- return true;
6317
- });
6318
- preparedPlans.push({
6319
- entry: plan.entry,
6320
- selection: plan.selection,
6321
- anchorMessageId: plan.anchorMessageId,
6322
- finalSummary: completedSummary.expandedSummary,
6323
- consumedBlockIds: mergeConsumedBlockIds
6324
- });
6325
- }
6326
- const partition = partitionPhantomPlans(
6327
- identifyPhantomPlans(
6328
- ctx.state,
6329
- preparedPlans.map((p) => ({
6330
- messageIds: p.selection.messageIds,
6331
- consumedBlockIds: p.consumedBlockIds
6332
- }))
6333
- ),
6334
- preparedPlans.length
6335
- );
6336
- let phantomSkipNotice = null;
6337
- if (partition.kind === "all-phantom") {
6338
- ctx.logger.warn("Batch compress: ALL entries phantom", {
6339
- totalCount: preparedPlans.length,
6340
- details: partition.details
6341
- });
6342
- throw new Error(buildPhantomErrorMessage(partition.details));
6343
- }
6344
- if (partition.kind === "partial") {
6345
- ctx.logger.warn("Batch compress: phantom entries detected", {
6346
- phantomCount: partition.dropIndices.length,
6347
- totalCount: preparedPlans.length,
6348
- phantomIndices: partition.dropIndices,
6349
- details: partition.details
6350
- });
6351
- const dropSet = new Set(partition.dropIndices);
6352
- preparedPlans = preparedPlans.filter((_, i) => !dropSet.has(i));
6353
- phantomSkipNotice = partition.notice;
6354
- }
6355
- const acknowledgeRisk = args.acknowledgeRisk === true;
6356
- const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
6357
- const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
6358
- const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
6359
- if (ignoredAcknowledgeRisk) {
6360
- ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
6361
- }
6362
- ctx.state.qualityGateRetryPending = false;
6363
- if (!bypassQuality) {
6364
- for (const plan of preparedPlans) {
6365
- const result = evaluatePreCommitQuality(
6366
- rawMessages,
6367
- plan.selection.messageIds,
6368
- plan.selection.messageTokenById,
6369
- plan.finalSummary,
6370
- ctx.config,
6371
- ctx.logger
6372
- );
6373
- if (result && !result.passed) {
6374
- ctx.state.qualityGateRetryPending = true;
6375
- throw buildQualityRejectionError(
6376
- {
6377
- startId: plan.entry.startId,
6378
- endId: plan.entry.endId,
6379
- summary: plan.finalSummary,
6380
- messageIds: plan.selection.messageIds,
6381
- messageTokenById: plan.selection.messageTokenById
6382
- },
6383
- result
6384
- );
6385
- }
6386
- }
6387
- }
6388
- const snapshot = snapshotCompressionState(ctx.state);
6389
- const runId = allocateRunId(ctx.state);
6390
- try {
6391
- for (const preparedPlan of preparedPlans) {
6392
- const blockId = allocateBlockId(ctx.state);
6393
- const keepResult = resolveKeepMarkers(
6394
- preparedPlan.finalSummary,
6395
- rawMessages,
6396
- ctx.state,
6397
- ctx.config
6398
- );
6399
- preparedPlan.finalSummary = keepResult.summary;
6400
- const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
6401
- const summaryTokens = countTokens2(storedSummary);
6402
- const applied = applyCompressionState(
6403
- ctx.state,
6404
- {
6405
- topic: preparedPlan.entry.topic ?? input.topic ?? "",
6406
- batchTopic: input.topic,
6407
- startId: preparedPlan.entry.startId,
6408
- endId: preparedPlan.entry.endId,
6409
- mode: "range",
6410
- runId,
6411
- compressMessageId: toolCtx.messageID,
6412
- compressCallId: callId,
6413
- summaryTokens
6414
- },
6415
- preparedPlan.selection,
6416
- preparedPlan.anchorMessageId,
6417
- blockId,
6418
- storedSummary,
6419
- preparedPlan.consumedBlockIds,
6420
- ctx.config.gc
6421
- );
6422
- totalCompressedMessages += applied.messageIds.length;
6423
- notifications.push({
6424
- blockId,
6425
- runId,
6426
- summary: preparedPlan.finalSummary,
6427
- summaryTokens
6428
- });
6429
- }
6430
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
6431
- } catch (error) {
6432
- restoreCompressionState(ctx.state, snapshot);
6433
- ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
6434
- throw error;
6435
- }
6436
- const skippedNote = phantomSkipNotice !== null ? `
6437
- \u26A0\uFE0F ${phantomSkipNotice}
6438
- ` : "";
6439
- const ackNote = ignoredAcknowledgeRisk ? `
6440
- \u26A0\uFE0F acknowledgeRisk was ignored: no quality gate rejection was pending, so quality checks ran normally. Only pass it when retrying immediately after a quality gate rejection.
6441
- ` : "";
6442
- return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}${ackNote}
6443
- IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
6444
- \u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
6620
+ if (!overMaxLimit) break;
6445
6621
  }
6446
- });
6622
+ }
6623
+ return {
6624
+ overMaxLimit,
6625
+ overMinLimit,
6626
+ currentTokens,
6627
+ modelContextLimit: state.modelContextLimit
6628
+ };
6447
6629
  }
6448
- function extractBoundaryConsumedBlocks2(startReference, endReference) {
6449
- const consumed = [];
6450
- const seen = /* @__PURE__ */ new Set();
6451
- for (const ref of [startReference, endReference]) {
6452
- if (ref.kind === "compressed-block" && ref.blockId !== void 0 && !seen.has(ref.blockId)) {
6453
- seen.add(ref.blockId);
6454
- consumed.push(ref.blockId);
6455
- }
6630
+ ensureBuiltinTriggerPolicyRegistered();
6631
+ function computeShouldNudge2(params) {
6632
+ const policy = getDefaultTriggerPolicy();
6633
+ if (!policy) {
6634
+ return { shouldNudge: false, tipsVariant: null };
6456
6635
  }
6457
- return consumed;
6636
+ return policy.computeShouldNudge(params);
6458
6637
  }
6459
-
6460
- // lib/compress/decompress.ts
6461
- import { tool as tool3 } from "@opencode-ai/plugin";
6462
-
6463
- // lib/messages/prune.ts
6464
- var prune = (state, logger, config, messages) => {
6465
- filterCompressedRanges(state, messages);
6466
- stripStepMarkers(messages);
6467
- };
6468
- var MAX_STEP_FINISH_REASON = 50;
6469
- var stripStepMarkers = (messages) => {
6470
- for (const msg of messages) {
6471
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
6472
- let changed = false;
6473
- const filtered = [];
6474
- for (const part of parts) {
6475
- if (part.type === "step-start") {
6476
- changed = true;
6477
- continue;
6478
- }
6479
- if (part.type === "step-finish") {
6480
- const reason = part.reason;
6481
- if (typeof reason === "string" && reason.length > MAX_STEP_FINISH_REASON) {
6482
- const truncated = reason.slice(0, MAX_STEP_FINISH_REASON) + "...";
6483
- if (truncated !== reason) {
6484
- filtered.push({ ...part, reason: truncated });
6485
- changed = true;
6486
- continue;
6487
- }
6488
- }
6489
- }
6490
- filtered.push(part);
6491
- }
6492
- if (changed) {
6493
- msg.parts = filtered;
6494
- }
6638
+ var DEFAULT_NUDGE_GROWTH_TOKENS = 5e4;
6639
+ var DEFAULT_MIN_NUDGE_CONTEXT_PERCENT = 5;
6640
+ function resolveCompressOverrides(config, providerId, modelId) {
6641
+ if (providerId === void 0) {
6642
+ return {};
6495
6643
  }
6496
- };
6497
- var filterCompressedRanges = (state, messages) => {
6498
- if (state.prune.messages.byMessageId.size === 0) {
6499
- return;
6644
+ const providerEntry = config.compress?.providers?.[providerId];
6645
+ if (!providerEntry) {
6646
+ return {};
6500
6647
  }
6501
- const survive = messages.map((msg) => {
6502
- const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id);
6503
- if (!pruneEntry || pruneEntry.activeBlockIds.length === 0) {
6504
- return true;
6505
- }
6648
+ const { models: _models, ...providerFields } = providerEntry;
6649
+ const modelEntry = modelId !== void 0 ? providerEntry.models?.[modelId] : void 0;
6650
+ if (modelEntry) {
6651
+ return { ...providerFields, ...modelEntry };
6652
+ }
6653
+ return providerFields;
6654
+ }
6655
+ function resolveMinNudgeContextPercent(config, providerId, modelId) {
6656
+ const overrides = resolveCompressOverrides(config, providerId, modelId);
6657
+ if (overrides.minNudgeContextPercent !== void 0) {
6658
+ return overrides.minNudgeContextPercent;
6659
+ }
6660
+ return config.compress?.minNudgeContextPercent;
6661
+ }
6662
+ function resolveMinNudgeFloorTokens(config, modelContextLimit, providerId, modelId) {
6663
+ if (modelContextLimit === void 0) {
6664
+ return void 0;
6665
+ }
6666
+ const percent = resolveMinNudgeContextPercent(config, providerId, modelId) ?? DEFAULT_MIN_NUDGE_CONTEXT_PERCENT;
6667
+ const clamped = Math.min(100, Math.max(0, percent));
6668
+ return Math.round(clamped / 100 * modelContextLimit);
6669
+ }
6670
+ var OVERRIDE_BLANKET_APPLY_EXCLUDE = /* @__PURE__ */ new Set(["maxContextLimit"]);
6671
+ function applyCompressOverrides(config, providerId, modelId) {
6672
+ const overrides = resolveCompressOverrides(config, providerId, modelId);
6673
+ const applied = Object.keys(overrides).filter(
6674
+ (key) => !OVERRIDE_BLANKET_APPLY_EXCLUDE.has(key)
6675
+ );
6676
+ if (applied.length === 0 || !config.compress) {
6677
+ return config;
6678
+ }
6679
+ const effectiveCompress = { ...config.compress };
6680
+ for (const key of applied) {
6681
+ effectiveCompress[key] = overrides[key];
6682
+ }
6683
+ return { ...config, compress: effectiveCompress };
6684
+ }
6685
+ function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
6686
+ if (anchorMessageIndex < 0) {
6506
6687
  return false;
6507
- });
6508
- const firstUserIdx = messages.findIndex((msg) => msg.info.role === "user");
6509
- if (firstUserIdx >= 0) {
6510
- survive[firstUserIdx] = true;
6511
6688
  }
6512
- const result = [];
6513
- for (let i = 0; i < messages.length; i++) {
6514
- if (survive[i]) {
6515
- result.push(messages[i]);
6689
+ let latestAnchorMessageIndex = -1;
6690
+ for (let i = messages.length - 1; i >= 0; i--) {
6691
+ if (anchorMessageIds.has(messages[i].info.id)) {
6692
+ latestAnchorMessageIndex = i;
6693
+ break;
6516
6694
  }
6517
6695
  }
6518
- messages.length = 0;
6519
- messages.push(...result);
6520
- };
6521
-
6522
- // lib/messages/sync.ts
6523
- function sortBlocksByCreation(a, b) {
6524
- const createdAtDiff = a.createdAt - b.createdAt;
6525
- if (createdAtDiff !== 0) {
6526
- return createdAtDiff;
6696
+ const shouldAdd = latestAnchorMessageIndex < 0 || anchorMessageIndex - latestAnchorMessageIndex >= interval;
6697
+ if (!shouldAdd) {
6698
+ return false;
6527
6699
  }
6528
- return a.blockId - b.blockId;
6700
+ const previousSize = anchorMessageIds.size;
6701
+ anchorMessageIds.add(anchorMessageId);
6702
+ return anchorMessageIds.size !== previousSize;
6529
6703
  }
6530
- var syncCompressionBlocks = (state, logger, messages) => {
6531
- const messagesState = state.prune.messages;
6532
- if (!messagesState?.blocksById?.size) {
6704
+ function injectAnchoredNudge(message, nudgeText) {
6705
+ if (!nudgeText.trim()) {
6533
6706
  return;
6534
6707
  }
6535
- const messageIds = new Set(messages.map((msg) => msg.info.id));
6536
- const previousActiveBlockIds = new Set(
6537
- Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
6538
- );
6539
- messagesState.activeBlockIds.clear();
6540
- messagesState.activeByAnchorMessageId.clear();
6541
- const now = Date.now();
6542
- const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
6543
- for (const block of orderedBlocks) {
6544
- if (block.deactivatedByUser || block.deactivatedByUserDeep) {
6545
- block.active = false;
6546
- if (block.deactivatedAt === void 0) {
6547
- block.deactivatedAt = now;
6548
- }
6549
- block.deactivatedByBlockId = void 0;
6550
- continue;
6551
- }
6552
- for (const consumedBlockId of block.consumedBlockIds) {
6553
- if (!messagesState.activeBlockIds.has(consumedBlockId)) {
6554
- continue;
6555
- }
6556
- const consumedBlock = messagesState.blocksById.get(consumedBlockId);
6557
- if (consumedBlock) {
6558
- consumedBlock.active = false;
6559
- consumedBlock.deactivatedAt = now;
6560
- consumedBlock.deactivatedByBlockId = block.blockId;
6561
- const mappedBlockId = messagesState.activeByAnchorMessageId.get(
6562
- consumedBlock.anchorMessageId
6563
- );
6564
- if (mappedBlockId === consumedBlock.blockId) {
6565
- messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
6566
- }
6567
- }
6568
- messagesState.activeBlockIds.delete(consumedBlockId);
6569
- }
6570
- block.active = true;
6571
- block.deactivatedAt = void 0;
6572
- block.deactivatedByBlockId = void 0;
6573
- messagesState.activeBlockIds.add(block.blockId);
6574
- if (messageIds.has(block.anchorMessageId)) {
6575
- messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
6708
+ if (message.info.role === "user") {
6709
+ if (appendToLastTextPart(message, nudgeText)) {
6710
+ return;
6576
6711
  }
6712
+ message.parts.push(createSyntheticTextPart(message, nudgeText));
6713
+ return;
6577
6714
  }
6578
- for (const entry of messagesState.byMessageId.values()) {
6579
- const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
6580
- entry.allBlockIds = allBlockIds;
6581
- entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
6715
+ if (message.info.role !== "assistant") {
6716
+ return;
6582
6717
  }
6583
- const nextActiveBlockIds = messagesState.activeBlockIds;
6584
- let deactivatedCount = 0;
6585
- let reactivatedCount = 0;
6586
- for (const blockId of previousActiveBlockIds) {
6587
- if (!nextActiveBlockIds.has(blockId)) {
6588
- deactivatedCount++;
6589
- }
6718
+ if (!hasContent(message)) {
6719
+ return;
6590
6720
  }
6591
- for (const blockId of nextActiveBlockIds) {
6592
- if (!previousActiveBlockIds.has(blockId)) {
6593
- reactivatedCount++;
6721
+ for (const part of message.parts) {
6722
+ if (part.type === "text") {
6723
+ if (appendToTextPart(part, nudgeText)) {
6724
+ return;
6725
+ }
6594
6726
  }
6595
6727
  }
6596
- if (deactivatedCount > 0 || reactivatedCount > 0) {
6597
- logger.info("Synced compress block state", {
6598
- deactivatedCount,
6599
- reactivatedCount
6728
+ const syntheticPart = createSyntheticTextPart(message, nudgeText);
6729
+ const firstToolIndex = message.parts.findIndex((p) => p.type === "tool");
6730
+ if (firstToolIndex === -1) {
6731
+ message.parts.push(syntheticPart);
6732
+ } else {
6733
+ message.parts.splice(firstToolIndex, 0, syntheticPart);
6734
+ }
6735
+ }
6736
+ function collectAnchoredMessages(anchorMessageIds, messages) {
6737
+ const anchoredMessages = [];
6738
+ for (const anchorMessageId of anchorMessageIds) {
6739
+ const index = messages.findIndex((message) => message.info.id === anchorMessageId);
6740
+ if (index === -1) {
6741
+ continue;
6742
+ }
6743
+ anchoredMessages.push({
6744
+ message: messages[index],
6745
+ index
6600
6746
  });
6601
6747
  }
6602
- };
6603
-
6604
- // lib/host-permissions.ts
6605
- var findLastMatchingRule = (rules, predicate) => {
6606
- for (let index = rules.length - 1; index >= 0; index -= 1) {
6607
- const rule = rules[index];
6608
- if (rule && predicate(rule)) {
6609
- return rule;
6748
+ return anchoredMessages;
6749
+ }
6750
+ function collectTurnNudgeAnchors2(state, config, messages) {
6751
+ const turnNudgeAnchors = /* @__PURE__ */ new Set();
6752
+ const targetRole = config.compress.nudgeForce === "strong" ? "user" : "assistant";
6753
+ for (const message of messages) {
6754
+ if (!state.nudges.turnNudgeAnchors.has(message.info.id)) continue;
6755
+ if (message.info.role === targetRole) {
6756
+ turnNudgeAnchors.add(message.info.id);
6610
6757
  }
6611
6758
  }
6612
- return void 0;
6613
- };
6614
- var wildcardMatch = (value, pattern) => {
6615
- const normalizedValue = value.replaceAll("\\", "/");
6616
- let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
6617
- if (escaped.endsWith(" .*")) {
6618
- escaped = escaped.slice(0, -3) + "( .*)?";
6759
+ return turnNudgeAnchors;
6760
+ }
6761
+ function applyRangeModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText, compressedBlockGuidance) {
6762
+ const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance);
6763
+ if (!nudgeText.trim()) {
6764
+ return;
6619
6765
  }
6620
- const flags = process.platform === "win32" ? "si" : "s";
6621
- return new RegExp(`^${escaped}$`, flags).test(normalizedValue);
6622
- };
6623
- var getPermissionRules = (permissionConfigs) => {
6624
- const rules = [];
6625
- for (const permissionConfig of permissionConfigs) {
6626
- if (!permissionConfig) {
6627
- continue;
6766
+ for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
6767
+ injectAnchoredNudge(message, nudgeText);
6768
+ }
6769
+ }
6770
+ function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
6771
+ const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
6772
+ if (suffixMessage) {
6773
+ const nudgeParts = [];
6774
+ if (state.nudges.contextLimitAnchors.size > 0) {
6775
+ nudgeParts.push(prompts.contextLimitNudge);
6628
6776
  }
6629
- for (const [permission, value] of Object.entries(permissionConfig)) {
6630
- if (value === "ask" || value === "allow" || value === "deny") {
6631
- rules.push({ permission, pattern: "*", action: value });
6632
- continue;
6633
- }
6634
- for (const [pattern, action] of Object.entries(value)) {
6635
- if (action === "ask" || action === "allow" || action === "deny") {
6636
- rules.push({ permission, pattern, action });
6637
- }
6638
- }
6777
+ if (turnNudgeAnchors.size > 0) {
6778
+ nudgeParts.push(prompts.turnNudge);
6779
+ }
6780
+ if (state.nudges.iterationNudgeAnchors.size > 0) {
6781
+ nudgeParts.push(prompts.iterationNudge);
6782
+ }
6783
+ const combined = nudgeParts.join("\n\n");
6784
+ if (combined.trim()) {
6785
+ injectAnchoredNudge(suffixMessage, combined);
6639
6786
  }
6787
+ return;
6640
6788
  }
6641
- return rules;
6642
- };
6643
- var compressDisabledByOpencode = (...permissionConfigs) => {
6644
- const match = findLastMatchingRule(
6645
- getPermissionRules(permissionConfigs),
6646
- (rule) => wildcardMatch("compress", rule.permission)
6789
+ applyRangeModeAnchoredNudge(
6790
+ state.nudges.contextLimitAnchors,
6791
+ messages,
6792
+ prompts.contextLimitNudge,
6793
+ ""
6647
6794
  );
6648
- return match?.pattern === "*" && match.action === "deny";
6649
- };
6650
- var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agentName) => {
6651
- if (basePermission === "deny") {
6652
- return "deny";
6795
+ applyRangeModeAnchoredNudge(turnNudgeAnchors, messages, prompts.turnNudge, "");
6796
+ applyRangeModeAnchoredNudge(
6797
+ state.nudges.iterationNudgeAnchors,
6798
+ messages,
6799
+ prompts.iterationNudge,
6800
+ ""
6801
+ );
6802
+ }
6803
+ function estimateCodeTokens(text) {
6804
+ let codeChars = 0;
6805
+ let inCode = false;
6806
+ for (const line of text.split("\n")) {
6807
+ if (line.trim().startsWith("```")) {
6808
+ inCode = !inCode;
6809
+ codeChars += line.length + 1;
6810
+ continue;
6811
+ }
6812
+ if (inCode) codeChars += line.length + 1;
6653
6813
  }
6654
- return compressDisabledByOpencode(
6655
- hostPermissions.global,
6656
- agentName ? hostPermissions.agents[agentName] : void 0
6657
- ) ? "deny" : basePermission;
6658
- };
6659
- var hasExplicitToolPermission = (permissionConfig, tool6) => {
6660
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool6) : false;
6661
- };
6662
-
6663
- // lib/compress-permission.ts
6664
- var compressPermission = (state, config) => {
6665
- return state.compressPermission ?? config.compress.permission;
6666
- };
6667
- var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
6668
- const activeAgent = getLastUserMessage(messages)?.info.agent;
6669
- state.compressPermission = resolveEffectiveCompressPermission(
6670
- config.compress.permission,
6671
- hostPermissions,
6672
- activeAgent
6673
- );
6674
- };
6675
-
6676
- // lib/messages/utils.ts
6677
- import { createHash } from "crypto";
6678
- var SUMMARY_ID_HASH_LENGTH = 16;
6679
- var DCP_PAIRED_TAG_REGEX = /<(?:dcp|acp)[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
6680
- var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
6681
- var generateStableId = (prefix, seed) => {
6682
- const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
6683
- return `${prefix}_${hash}`;
6684
- };
6685
- var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
6686
- const baseInfo = baseMessage.info;
6687
- const now = Date.now();
6688
- const deterministicSeed = stableSeed?.trim() || baseInfo.id;
6689
- const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
6690
- const partId = generateStableId("prt_dcp_summary", deterministicSeed);
6691
- const parts = [
6692
- {
6693
- id: partId,
6694
- sessionID: baseInfo.sessionID,
6695
- messageID: messageId,
6696
- type: "text",
6697
- text: content,
6698
- synthetic: true
6699
- }
6700
- ];
6701
- if (role === "assistant") {
6702
- const isAssistant = baseInfo.role === "assistant";
6703
- const assistantBase = isAssistant ? baseInfo : void 0;
6704
- const userModel = !isAssistant ? baseInfo.model : void 0;
6705
- const info2 = {
6706
- id: messageId,
6707
- sessionID: baseInfo.sessionID,
6708
- role: "assistant",
6709
- time: { created: now },
6710
- parentID: assistantBase?.parentID ?? "",
6711
- modelID: assistantBase?.modelID ?? userModel?.modelID ?? "",
6712
- providerID: assistantBase?.providerID ?? userModel?.providerID ?? "",
6713
- mode: assistantBase?.mode ?? "code",
6714
- agent: baseInfo.agent ?? "code",
6715
- path: assistantBase?.path ?? { cwd: "", root: "" },
6716
- cost: 0,
6717
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
6718
- };
6719
- return { info: info2, parts };
6720
- }
6721
- const userInfo = baseInfo;
6722
- const info = {
6723
- id: messageId,
6724
- sessionID: userInfo.sessionID,
6725
- role: "user",
6726
- agent: userInfo.agent,
6727
- model: userInfo.model,
6728
- time: { created: now }
6729
- };
6730
- return { info, parts };
6731
- };
6732
- var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
6733
- var createSyntheticTextPart = (baseMessage, content, stableSeed) => {
6734
- const userInfo = baseMessage.info;
6735
- const deterministicSeed = stableSeed?.trim() || userInfo.id;
6736
- const partId = generateStableId("prt_dcp_text", deterministicSeed);
6737
- return {
6738
- id: partId,
6739
- sessionID: userInfo.sessionID,
6740
- messageID: userInfo.id,
6741
- type: "text",
6742
- text: content
6743
- };
6744
- };
6745
- var appendToLastTextPart = (message, injection) => {
6746
- const textPart = findLastTextPart(message);
6747
- if (!textPart) {
6748
- return false;
6749
- }
6750
- return appendToTextPart(textPart, injection);
6751
- };
6752
- var findLastTextPart = (message) => {
6753
- for (let i = message.parts.length - 1; i >= 0; i--) {
6754
- const part = message.parts[i];
6755
- if (part.type === "text") {
6756
- return part;
6757
- }
6758
- }
6759
- return null;
6760
- };
6761
- var appendToTextPart = (part, injection) => {
6762
- if (typeof part.text !== "string") {
6763
- return false;
6764
- }
6765
- const normalizedInjection = injection.replace(/^\n+/, "");
6766
- if (!normalizedInjection.trim()) {
6767
- return false;
6768
- }
6769
- if (part.text.includes(normalizedInjection)) {
6770
- return true;
6771
- }
6772
- const baseText = part.text.replace(/\n*$/, "");
6773
- part.text = baseText.length > 0 ? `${baseText}
6774
-
6775
- ${normalizedInjection}` : normalizedInjection;
6776
- return true;
6777
- };
6778
- var appendToAllToolParts = (message, tag) => {
6779
- let injected = false;
6780
- for (const part of message.parts) {
6781
- if (part.type === "tool") {
6782
- injected = appendToToolPart(part, tag) || injected;
6783
- }
6784
- }
6785
- return injected;
6786
- };
6787
- var appendToToolPart = (part, tag) => {
6788
- if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
6789
- return false;
6790
- }
6791
- if (part.state.output.includes(tag)) {
6792
- return true;
6793
- }
6794
- part.state.output = `${part.state.output}${tag}`;
6795
- return true;
6796
- };
6797
- var hasContent = (message) => {
6798
- return message.parts.some(
6799
- (part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0 || part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string"
6800
- );
6801
- };
6802
- function buildToolIdList(state, messages) {
6803
- const toolIds = [];
6814
+ return Math.round(codeChars / 4);
6815
+ }
6816
+ function estimateContextComposition(messages, state, protectedTools = [], protectedFilePatterns = []) {
6817
+ let toolTokens = 0;
6818
+ let codeTokens = 0;
6819
+ let summaryTokens = 0;
6820
+ let messageTokens = 0;
6821
+ let protectedTokens = 0;
6822
+ const perMessage = [];
6823
+ const perTool = [];
6824
+ const perCode = [];
6825
+ const perText = [];
6826
+ const toolTypeMap = /* @__PURE__ */ new Map();
6804
6827
  for (const msg of messages) {
6805
- if (isMessageCompacted(state, msg)) {
6806
- continue;
6807
- }
6808
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
6809
- if (parts.length > 0) {
6810
- for (const part of parts) {
6811
- if (part.type === "tool" && part.callID && part.tool) {
6812
- toolIds.push(part.callID);
6828
+ const text = (msg.parts || []).filter((p) => p.type === "text").map((p) => p.text || "").join("");
6829
+ const msgId = msg.info?.id || "";
6830
+ const isSummary = msgId.startsWith("msg_dcp_summary") || text.includes("[Compressed conversation section]");
6831
+ const isProtected = (protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns);
6832
+ let msgTotal = 0;
6833
+ let msgTool = 0;
6834
+ let msgCode = 0;
6835
+ let msgText = 0;
6836
+ let msgToolName = "";
6837
+ for (const part of msg.parts || []) {
6838
+ if (part.type === "text" && typeof part.text === "string") {
6839
+ const partText = part.text;
6840
+ const tokens = Math.round(partText.length / 4);
6841
+ msgTotal += tokens;
6842
+ if (isSummary) {
6843
+ summaryTokens += tokens;
6844
+ } else {
6845
+ messageTokens += tokens;
6846
+ msgText += tokens;
6847
+ const cTokens = estimateCodeTokens(partText);
6848
+ if (cTokens > 0) {
6849
+ codeTokens += cTokens;
6850
+ msgCode += cTokens;
6851
+ }
6852
+ }
6853
+ } else if (part.type === "tool") {
6854
+ const raw = JSON.stringify(part);
6855
+ const tokens = Math.round(raw.length / 4);
6856
+ msgTotal += tokens;
6857
+ const toolName = part?.tool || "unknown";
6858
+ let summaryPartTokens = 0;
6859
+ if (toolName === "compress") {
6860
+ const input = part?.state?.input;
6861
+ if (input?.content && Array.isArray(input.content)) {
6862
+ for (const entry of input.content) {
6863
+ if (typeof entry?.summary === "string") {
6864
+ summaryPartTokens += Math.round(entry.summary.length / 4);
6865
+ }
6866
+ }
6867
+ }
6813
6868
  }
6869
+ const toolPartTokens = Math.max(0, tokens - summaryPartTokens);
6870
+ toolTokens += toolPartTokens;
6871
+ msgTool += toolPartTokens;
6872
+ summaryTokens += summaryPartTokens;
6873
+ toolTypeMap.set(toolName, (toolTypeMap.get(toolName) || 0) + toolPartTokens);
6874
+ if (!msgToolName) msgToolName = toolName;
6814
6875
  }
6815
6876
  }
6816
- }
6817
- state.toolIdList = toolIds;
6818
- return toolIds;
6819
- }
6820
- var stripHallucinationsFromString = (text) => {
6821
- return text.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
6822
- };
6823
- var stripHallucinations = (messages) => {
6824
- for (const message of messages) {
6825
- for (const part of message.parts) {
6826
- if (part.type === "text" && typeof part.text === "string") {
6827
- part.text = stripHallucinationsFromString(part.text);
6828
- }
6829
- if (part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string") {
6830
- part.state.output = stripHallucinationsFromString(part.state.output);
6831
- }
6877
+ if (isProtected && !isSummary) {
6878
+ protectedTokens += msgTotal;
6832
6879
  }
6833
- }
6834
- };
6835
- var dropEmptyMessages = (messages) => {
6836
- let removed = 0;
6837
- for (let i = messages.length - 1; i >= 0; i--) {
6838
- const parts = Array.isArray(messages[i].parts) ? messages[i].parts : [];
6839
- const isEmpty = parts.every(
6840
- (part) => part.type === "text" && (typeof part.text !== "string" || part.text.trim().length === 0 || part.ignored === true)
6841
- );
6842
- if (isEmpty) {
6843
- messages.splice(i, 1);
6844
- removed++;
6880
+ if (!isSummary) {
6881
+ const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
6882
+ if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
6883
+ if (msgTool > 500) perTool.push({ ref, tokens: msgTool, tool: msgToolName });
6884
+ if (msgCode > 300) perCode.push({ ref, tokens: msgCode });
6885
+ if (msgText > 500 && msgCode === 0) perText.push({ ref, tokens: msgText });
6845
6886
  }
6846
6887
  }
6847
- return removed;
6848
- };
6849
-
6850
- // lib/prompts/extensions/nudge.ts
6851
- function buildCompressedBlockGuidance(state, context) {
6852
- const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
6853
- const blockCount = activeBlockIds.length;
6854
- const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6855
- const totalSummaryTokens = blocksForStats.reduce((s, b) => s + (b.summaryTokens ?? 0), 0);
6856
- const totalSummaryDisplay = totalSummaryTokens >= 1e3 ? `${(totalSummaryTokens / 1e3).toFixed(1)}K` : String(totalSummaryTokens);
6857
- const lastBlock = blocksForStats.length > 0 ? blocksForStats.reduce((latest, b) => b.createdAt > latest.createdAt ? b : latest) : null;
6858
- const ageStr = lastBlock ? formatAge(lastBlock.createdAt) : "never";
6859
- const lines = [
6860
- `- Compressed blocks: ${blockCount} (${totalSummaryDisplay} summary, last ${ageStr}). Use acp_status for details.`
6861
- ];
6862
- if (blockCount > 50) {
6863
- const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
6864
- const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
6865
- const visibleMessageIds = context?.visibleMessageIds;
6866
- const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
6867
- if (visibleOldBlocks.length > 5) {
6868
- const blocksWithRef = visibleOldBlocks.map((block) => {
6869
- const ref = state.messageIds.byRawId.get(block.anchorMessageId);
6870
- return ref ? { block, ref } : null;
6871
- }).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
6872
- const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6873
- const totalK = Math.max(1, Math.round(totalTokens / 1e3));
6874
- const targets = [];
6875
- const chunkSize = Math.ceil(blocksWithRef.length / 3);
6876
- for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
6877
- const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
6878
- if (chunk.length < 2) continue;
6879
- const startRef = chunk[0].ref;
6880
- const endRef = chunk[chunk.length - 1].ref;
6881
- const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6882
- const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
6883
- targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
6884
- }
6885
- if (targets.length > 0) {
6886
- lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
6887
- lines.push(...targets);
6888
- lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write your summary normally.`);
6889
- }
6890
- }
6891
- }
6892
- return lines.join("\n");
6893
- }
6894
- function appendGuidanceToDcpTag(nudgeText, guidance) {
6895
- if (!guidance.trim()) {
6896
- return nudgeText;
6897
- }
6898
- const closeTag = "</dcp-system-reminder>";
6899
- const closeTagIndex = nudgeText.lastIndexOf(closeTag);
6900
- if (closeTagIndex === -1) {
6901
- return nudgeText;
6902
- }
6903
- const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd();
6904
- const afterClose = nudgeText.slice(closeTagIndex);
6905
- return `${beforeClose}
6906
-
6907
- ${guidance}
6908
- ${afterClose}`;
6909
- }
6910
-
6911
- // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
6912
- var NUDGE_GROWTH_FLOOR = 6e3;
6913
- var NUDGE_GROWTH_CAP = 5e4;
6914
- var NUDGE_GROWTH_RATIO = 0.05;
6915
- function computeShouldNudge(params) {
6916
- const { currentTokens, overMinLimit, overMaxLimit } = params;
6917
- if (currentTokens === void 0) {
6918
- return { shouldNudge: false, tipsVariant: null };
6919
- }
6920
- if (params.lastNudgeTokens === void 0) {
6921
- return { shouldNudge: false, tipsVariant: null };
6922
- }
6923
- const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
6924
- const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
6925
- if (!shouldNudge) {
6926
- return { shouldNudge: false, tipsVariant: null };
6927
- }
6928
- const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
6929
- return { shouldNudge: true, tipsVariant };
6930
- }
6931
- function resolveAdaptiveNudgeGrowth(modelContextLimit) {
6932
- if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
6933
- return Math.min(
6934
- NUDGE_GROWTH_CAP,
6935
- Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
6936
- );
6937
- }
6938
- var defaultTriggerPolicy = {
6939
- name: "context-compress-algorithms-trigger",
6940
- version: "1.0.0",
6941
- description: "Growth-only cadence: nudge when context growth since last nudge exceeds adaptive threshold, or when over max limit.",
6942
- computeShouldNudge,
6943
- resolveAdaptiveNudgeGrowth
6944
- };
6945
-
6946
- // lib/messages/inject/policy/registry.ts
6947
- var registry2 = /* @__PURE__ */ new Map();
6948
- var defaultPolicy = null;
6949
- function registerTriggerPolicy2(policy) {
6950
- if (!policy.name) {
6951
- throw new Error("TriggerPolicy must have a name");
6952
- }
6953
- registry2.set(policy.name, policy);
6954
- if (!defaultPolicy) {
6955
- defaultPolicy = policy;
6956
- }
6957
- }
6958
- function getDefaultTriggerPolicy() {
6959
- return defaultPolicy;
6960
- }
6961
-
6962
- // lib/messages/inject/policy/index.ts
6963
- function ensureBuiltinTriggerPolicyRegistered() {
6964
- registerTriggerPolicy2(defaultTriggerPolicy);
6965
- }
6966
-
6967
- // lib/messages/inject/utils.ts
6968
- function getNudgeFrequency(config) {
6969
- return Math.max(1, Math.floor(config.compress.nudgeFrequency || 1));
6888
+ perMessage.sort((a, b) => b.tokens - a.tokens);
6889
+ perTool.sort((a, b) => b.tokens - a.tokens);
6890
+ perCode.sort((a, b) => b.tokens - a.tokens);
6891
+ perText.sort((a, b) => b.tokens - a.tokens);
6892
+ const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens);
6893
+ const systemTokens = state?.systemPromptTokens !== void 0 && state.systemPromptTokens > 0 ? state.systemPromptTokens : estimateSystemPromptTokens(messages);
6894
+ return {
6895
+ toolTokens,
6896
+ codeTokens,
6897
+ summaryTokens,
6898
+ messageTokens,
6899
+ textTokens: Math.max(0, messageTokens - codeTokens),
6900
+ systemTokens,
6901
+ protectedTokens,
6902
+ total: systemTokens + toolTokens + summaryTokens + messageTokens,
6903
+ largestRanges: perMessage.slice(0, 15),
6904
+ largestToolRanges: perTool.slice(0, 15),
6905
+ largestCodeRanges: perCode.slice(0, 5),
6906
+ largestMessageRanges: perText.slice(0, 5),
6907
+ toolTypeBreakdown
6908
+ };
6970
6909
  }
6971
- function getIterationNudgeThreshold(config) {
6972
- return Math.max(1, Math.floor(config.compress.iterationNudgeThreshold || 1));
6910
+ function refNum(ref) {
6911
+ const n = parseInt(ref.slice(1), 10);
6912
+ return Number.isNaN(n) ? -1 : n;
6973
6913
  }
6974
- function findLastNonIgnoredMessage(messages) {
6975
- for (let i = messages.length - 1; i >= 0; i--) {
6976
- const message = messages[i];
6977
- if (isIgnoredUserMessage(message)) {
6914
+ function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = [], protectedZoneRefs) {
6915
+ const msgInfo = [];
6916
+ const protectedMsgInfo = [];
6917
+ const lastUserRefIdx = [];
6918
+ for (let mi = 0; mi < messages.length; mi++) {
6919
+ const msg = messages[mi];
6920
+ if (isSyntheticMessage(msg)) continue;
6921
+ const ref = state.messageIds.byRawId.get(msg.info.id);
6922
+ if (!ref) continue;
6923
+ const rn = parseInt(ref.slice(1), 10);
6924
+ if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
6925
+ let tokens2 = 0;
6926
+ const tools = /* @__PURE__ */ new Set();
6927
+ for (const part of msg.parts || []) {
6928
+ if (part.type === "text" && typeof part.text === "string") {
6929
+ tokens2 += Math.round(part.text.length / 4);
6930
+ } else if (part.type !== "text" && part.type !== "reasoning") {
6931
+ tokens2 += Math.round(JSON.stringify(part).length / 4);
6932
+ const toolName = part?.tool;
6933
+ const callID = part?.callID;
6934
+ if (toolName && callID) {
6935
+ if (isToolNameProtected(toolName, protectedTools)) {
6936
+ tools.add(toolName);
6937
+ } else if (protectedFilePatterns.length > 0) {
6938
+ const filePaths = getFilePathsFromParameters(
6939
+ toolName,
6940
+ part?.state?.input
6941
+ );
6942
+ if (isFilePathProtected(filePaths, protectedFilePatterns)) {
6943
+ tools.add(toolName);
6944
+ }
6945
+ }
6946
+ }
6947
+ }
6948
+ }
6949
+ protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
6978
6950
  continue;
6979
6951
  }
6980
- if (isSyntheticMessage(message)) {
6981
- continue;
6952
+ let tokens = 0;
6953
+ let isTool = false;
6954
+ let hasMeaningfulPart = false;
6955
+ for (const part of msg.parts || []) {
6956
+ if (part.type === "text" && typeof part.text === "string") {
6957
+ tokens += Math.round(part.text.length / 4);
6958
+ if (part.text.trim().length > 0) hasMeaningfulPart = true;
6959
+ } else if (part.type !== "text" && part.type !== "reasoning") {
6960
+ tokens += Math.round(JSON.stringify(part).length / 4);
6961
+ isTool = true;
6962
+ hasMeaningfulPart = true;
6963
+ }
6982
6964
  }
6983
- return { message, index: i };
6984
- }
6985
- return null;
6986
- }
6987
- function countMessagesAfterIndex(messages, index) {
6988
- let count = 0;
6989
- for (let i = index + 1; i < messages.length; i++) {
6990
- const message = messages[i];
6991
- if (isIgnoredUserMessage(message)) {
6992
- continue;
6965
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
6966
+ lastUserRefIdx.length = 0;
6967
+ lastUserRefIdx.push(msgInfo.length);
6993
6968
  }
6994
- count++;
6969
+ msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
6995
6970
  }
6996
- return count;
6997
- }
6998
- function getModelInfo(messages) {
6999
- const lastUserMessage = getLastUserMessage(messages);
7000
- if (!lastUserMessage) {
7001
- return {
7002
- providerId: void 0,
7003
- modelId: void 0
7004
- };
6971
+ const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
6972
+ for (let i = 0; i < msgInfo.length; i++) {
6973
+ const info = msgInfo[i];
6974
+ info.effectiveTokens = i !== lastUserIdx && info.meaningful ? info.tokens : 0;
7005
6975
  }
7006
- const userInfo = lastUserMessage.info;
7007
- return {
7008
- providerId: userInfo.model?.providerID,
7009
- modelId: userInfo.model?.modelID
7010
- };
7011
- }
7012
- function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
7013
- const parseLimitValue = (limit) => {
7014
- if (limit === void 0) {
7015
- return void 0;
7016
- }
7017
- if (typeof limit === "number") {
7018
- return limit;
6976
+ const groups = [];
6977
+ let cur = null;
6978
+ let prevRefNum = -2;
6979
+ for (const info of msgInfo) {
6980
+ if (protectedZoneRefs?.has(info.ref)) {
6981
+ if (cur) {
6982
+ groups.push(cur);
6983
+ cur = null;
6984
+ }
6985
+ prevRefNum = info.refNum;
6986
+ continue;
7019
6987
  }
7020
- if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
7021
- return void 0;
6988
+ const hasGap = info.refNum > prevRefNum + 1;
6989
+ if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
6990
+ groups.push(cur);
6991
+ cur = null;
7022
6992
  }
7023
- const parsedPercent = parseFloat(limit.slice(0, -1));
7024
- if (isNaN(parsedPercent)) {
7025
- return void 0;
7026
- }
7027
- const roundedPercent = Math.round(parsedPercent);
7028
- const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
7029
- return Math.round(clampedPercent / 100 * state.modelContextLimit);
7030
- };
7031
- const modelLimits = threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits;
7032
- if (modelLimits && providerId !== void 0 && modelId !== void 0) {
7033
- const providerModelId = `${providerId}/${modelId}`;
7034
- const modelLimit = modelLimits[providerModelId];
7035
- if (modelLimit !== void 0) {
7036
- return parseLimitValue(modelLimit);
6993
+ prevRefNum = info.refNum;
6994
+ if (!cur) {
6995
+ cur = {
6996
+ startRef: info.ref,
6997
+ endRef: info.ref,
6998
+ count: 1,
6999
+ tokens: info.tokens,
7000
+ effectiveTokens: info.effectiveTokens,
7001
+ toolPct: info.isTool ? 100 : 0,
7002
+ textPct: info.isTool ? 0 : 100
7003
+ };
7004
+ } else {
7005
+ cur.endRef = info.ref;
7006
+ cur.count++;
7007
+ cur.tokens += info.tokens;
7008
+ cur.effectiveTokens += info.effectiveTokens;
7009
+ if (info.isTool) {
7010
+ cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
7011
+ } else {
7012
+ cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
7013
+ }
7014
+ cur.textPct = 100 - cur.toolPct;
7037
7015
  }
7038
7016
  }
7039
- const globalLimit = threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit;
7040
- return parseLimitValue(globalLimit);
7041
- }
7042
- function isContextOverLimits(config, state, providerId, modelId, messages) {
7043
- const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
7044
- state,
7045
- new Set(messages.map((m) => m.info.id))
7046
- ) : 0;
7047
- const resolvedMaxContextLimit = resolveContextTokenLimit(
7048
- config,
7049
- state,
7050
- providerId,
7051
- modelId,
7052
- "max"
7053
- );
7054
- const maxContextLimit = resolvedMaxContextLimit === void 0 ? void 0 : resolvedMaxContextLimit + summaryTokenExtension;
7055
- const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min");
7056
- const currentTokens = getCurrentTokenUsage(state, messages);
7057
- let overMaxLimit = maxContextLimit === void 0 ? false : currentTokens > maxContextLimit;
7058
- const overMinLimit = minContextLimit === void 0 ? false : currentTokens >= minContextLimit;
7059
- if (overMaxLimit) {
7060
- const recentCompressCount = 3;
7061
- const recentMessages = messages.slice(-recentCompressCount);
7062
- for (const msg of recentMessages) {
7063
- if (msg.info.role === "assistant" && msg.parts) {
7064
- for (const part of msg.parts) {
7065
- if (part.type === "tool" && part.tool === "compress") {
7066
- overMaxLimit = false;
7067
- break;
7068
- }
7069
- }
7017
+ if (cur) groups.push(cur);
7018
+ const protectedGroups = [];
7019
+ let pcur = null;
7020
+ let pPrevRefNum = -2;
7021
+ for (const info of protectedMsgInfo) {
7022
+ const hasGap = info.refNum > pPrevRefNum + 1;
7023
+ if (pcur && hasGap) {
7024
+ protectedGroups.push(pcur);
7025
+ pcur = null;
7026
+ }
7027
+ pPrevRefNum = info.refNum;
7028
+ if (!pcur) {
7029
+ pcur = {
7030
+ startRef: info.ref,
7031
+ endRef: info.ref,
7032
+ count: 1,
7033
+ tokens: info.tokens,
7034
+ tools: [...info.tools]
7035
+ };
7036
+ } else {
7037
+ pcur.endRef = info.ref;
7038
+ pcur.count++;
7039
+ pcur.tokens += info.tokens;
7040
+ for (const t of info.tools) {
7041
+ if (!pcur.tools.includes(t)) pcur.tools.push(t);
7070
7042
  }
7071
- if (!overMaxLimit) break;
7072
7043
  }
7073
7044
  }
7045
+ if (pcur) protectedGroups.push(pcur);
7074
7046
  return {
7075
- overMaxLimit,
7076
- overMinLimit,
7077
- currentTokens,
7078
- modelContextLimit: state.modelContextLimit
7047
+ compressible: groups.filter((g) => g.tokens > 0),
7048
+ protected: protectedGroups
7079
7049
  };
7080
7050
  }
7081
- ensureBuiltinTriggerPolicyRegistered();
7082
- function computeShouldNudge2(params) {
7083
- const policy = getDefaultTriggerPolicy();
7084
- if (!policy) {
7085
- return { shouldNudge: false, tipsVariant: null };
7086
- }
7087
- return policy.computeShouldNudge(params);
7088
- }
7089
- var DEFAULT_NUDGE_GROWTH_TOKENS = 5e4;
7090
- var DEFAULT_MIN_NUDGE_CONTEXT_PERCENT = 5;
7091
- function resolveMinNudgeContextPercent(config, providerId, modelId) {
7092
- const modelOverrides = providerId !== void 0 && modelId !== void 0 ? config.compress?.providers?.[providerId]?.models?.[modelId] : void 0;
7093
- if (modelOverrides?.minNudgeContextPercent !== void 0) {
7094
- return modelOverrides.minNudgeContextPercent;
7095
- }
7096
- const providerOverrides = providerId !== void 0 ? config.compress?.providers?.[providerId] : void 0;
7097
- if (providerOverrides?.minNudgeContextPercent !== void 0) {
7098
- return providerOverrides.minNudgeContextPercent;
7099
- }
7100
- return config.compress?.minNudgeContextPercent;
7101
- }
7102
- function resolveMinNudgeFloorTokens(config, modelContextLimit, providerId, modelId) {
7103
- if (modelContextLimit === void 0) {
7104
- return void 0;
7105
- }
7106
- const percent = resolveMinNudgeContextPercent(config, providerId, modelId) ?? DEFAULT_MIN_NUDGE_CONTEXT_PERCENT;
7107
- const clamped = Math.min(100, Math.max(0, percent));
7108
- return Math.round(clamped / 100 * modelContextLimit);
7051
+ var EFFECTIVE_MIN_COMPRESSIBLE_TOKENS = 1250;
7052
+ function resolveEffectiveFloor(config) {
7053
+ const minChars = config.compress?.minCompressRange ?? 5e3;
7054
+ return minChars > 0 ? Math.floor(minChars / 4) : 0;
7109
7055
  }
7110
- function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
7111
- if (anchorMessageIndex < 0) {
7112
- return false;
7113
- }
7114
- let latestAnchorMessageIndex = -1;
7115
- for (let i = messages.length - 1; i >= 0; i--) {
7116
- if (anchorMessageIds.has(messages[i].info.id)) {
7117
- latestAnchorMessageIndex = i;
7118
- break;
7119
- }
7120
- }
7121
- const shouldAdd = latestAnchorMessageIndex < 0 || anchorMessageIndex - latestAnchorMessageIndex >= interval;
7122
- if (!shouldAdd) {
7123
- return false;
7056
+ function filterRecommendedRanges(compressible, _protectedRanges, options) {
7057
+ const { logger } = options;
7058
+ const log = logger?.debug.bind(logger);
7059
+ if (compressible.length === 0) {
7060
+ log?.("filterRecommendedRanges: no compressible ranges, returning empty");
7061
+ return [];
7124
7062
  }
7125
- const previousSize = anchorMessageIds.size;
7126
- anchorMessageIds.add(anchorMessageId);
7127
- return anchorMessageIds.size !== previousSize;
7063
+ const floor = options.minEffectiveTokens ?? EFFECTIVE_MIN_COMPRESSIBLE_TOKENS;
7064
+ const kept = compressible.filter((r) => {
7065
+ const effective = r.effectiveTokens ?? r.tokens;
7066
+ return effective > 0 && effective >= floor;
7067
+ });
7068
+ const result = kept.map(
7069
+ (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7070
+ );
7071
+ log?.("filterRecommendedRanges: effective-token floor applied", {
7072
+ inputRanges: compressible.length,
7073
+ outputRanges: result.length,
7074
+ floor,
7075
+ dropped: compressible.filter((r) => {
7076
+ const effective = r.effectiveTokens ?? r.tokens;
7077
+ return effective <= 0 || effective < floor;
7078
+ }).map((r) => `${r.startRef}\u2013${r.endRef} (${r.effectiveTokens ?? r.tokens} eff tokens)`)
7079
+ });
7080
+ return result;
7128
7081
  }
7129
- function injectAnchoredNudge(message, nudgeText) {
7130
- if (!nudgeText.trim()) {
7131
- return;
7132
- }
7133
- if (message.info.role === "user") {
7134
- if (appendToLastTextPart(message, nudgeText)) {
7135
- return;
7136
- }
7137
- message.parts.push(createSyntheticTextPart(message, nudgeText));
7138
- return;
7082
+ function formatCompressibleRanges(ranges, protectedRanges) {
7083
+ const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
7084
+ if (!protectedRanges || protectedRanges.length === 0) {
7085
+ if (ranges.length === 0) return "";
7086
+ const lines2 = ranges.map((r) => {
7087
+ const eff = r.effectiveTokens ?? r.tokens;
7088
+ const size = eff < r.tokens ? `${fmt(eff)} effective of ${fmt(r.tokens)}` : fmt(r.tokens);
7089
+ const suffix = r.dangerous ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7090
+ return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${size} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
7091
+ });
7092
+ return `Compressible ranges (oldest first):
7093
+ ${lines2.join("\n")}`;
7139
7094
  }
7140
- if (message.info.role !== "assistant") {
7141
- return;
7095
+ const entries = [];
7096
+ for (const r of ranges) {
7097
+ entries.push({
7098
+ startRef: r.startRef,
7099
+ endRef: r.endRef,
7100
+ startNum: refNum(r.startRef),
7101
+ endNum: refNum(r.endRef),
7102
+ count: r.count,
7103
+ tokens: r.tokens,
7104
+ toolPct: r.toolPct,
7105
+ textPct: r.textPct,
7106
+ compressibleTokens: r.effectiveTokens ?? r.tokens,
7107
+ compressibleCount: r.count,
7108
+ protectedTokens: 0,
7109
+ protectedCount: 0,
7110
+ protectedTools: [],
7111
+ dangerous: r.dangerous ?? false
7112
+ });
7142
7113
  }
7143
- if (!hasContent(message)) {
7144
- return;
7114
+ for (const r of protectedRanges) {
7115
+ entries.push({
7116
+ startRef: r.startRef,
7117
+ endRef: r.endRef,
7118
+ startNum: refNum(r.startRef),
7119
+ endNum: refNum(r.endRef),
7120
+ count: r.count,
7121
+ tokens: r.tokens,
7122
+ toolPct: 0,
7123
+ textPct: 0,
7124
+ compressibleTokens: 0,
7125
+ compressibleCount: 0,
7126
+ protectedTokens: r.tokens,
7127
+ protectedCount: r.count,
7128
+ protectedTools: [...r.tools],
7129
+ dangerous: false
7130
+ });
7145
7131
  }
7146
- for (const part of message.parts) {
7147
- if (part.type === "text") {
7148
- if (appendToTextPart(part, nudgeText)) {
7149
- return;
7132
+ entries.sort((a, b) => a.startNum - b.startNum);
7133
+ const merged = [];
7134
+ for (const entry of entries) {
7135
+ const last = merged[merged.length - 1];
7136
+ if (last && entry.startNum <= last.endNum + 1) {
7137
+ last.endRef = entry.endRef;
7138
+ last.endNum = Math.max(last.endNum, entry.endNum);
7139
+ last.count += entry.count;
7140
+ last.tokens += entry.tokens;
7141
+ last.compressibleTokens += entry.compressibleTokens;
7142
+ last.compressibleCount += entry.compressibleCount;
7143
+ last.protectedTokens += entry.protectedTokens;
7144
+ last.protectedCount += entry.protectedCount;
7145
+ if (entry.dangerous) last.dangerous = true;
7146
+ for (const t of entry.protectedTools) {
7147
+ if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
7150
7148
  }
7149
+ } else {
7150
+ merged.push({ ...entry });
7151
7151
  }
7152
7152
  }
7153
- const syntheticPart = createSyntheticTextPart(message, nudgeText);
7154
- const firstToolIndex = message.parts.findIndex((p) => p.type === "tool");
7155
- if (firstToolIndex === -1) {
7156
- message.parts.push(syntheticPart);
7157
- } else {
7158
- message.parts.splice(firstToolIndex, 0, syntheticPart);
7159
- }
7160
- }
7161
- function collectAnchoredMessages(anchorMessageIds, messages) {
7162
- const anchoredMessages = [];
7163
- for (const anchorMessageId of anchorMessageIds) {
7164
- const index = messages.findIndex((message) => message.info.id === anchorMessageId);
7165
- if (index === -1) {
7166
- continue;
7153
+ const lines = merged.map((e) => {
7154
+ const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7155
+ if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
7156
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
7167
7157
  }
7168
- anchoredMessages.push({
7169
- message: messages[index],
7170
- index
7171
- });
7172
- }
7173
- return anchoredMessages;
7174
- }
7175
- function collectTurnNudgeAnchors2(state, config, messages) {
7176
- const turnNudgeAnchors = /* @__PURE__ */ new Set();
7177
- const targetRole = config.compress.nudgeForce === "strong" ? "user" : "assistant";
7178
- for (const message of messages) {
7179
- if (!state.nudges.turnNudgeAnchors.has(message.info.id)) continue;
7180
- if (message.info.role === targetRole) {
7181
- turnNudgeAnchors.add(message.info.id);
7158
+ if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
7159
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [${fmt(e.compressibleTokens)} compressible | ${fmt(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
7182
7160
  }
7183
- }
7184
- return turnNudgeAnchors;
7185
- }
7186
- function applyRangeModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText, compressedBlockGuidance) {
7187
- const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance);
7188
- if (!nudgeText.trim()) {
7189
- return;
7190
- }
7191
- for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
7192
- injectAnchoredNudge(message, nudgeText);
7193
- }
7161
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
7162
+ });
7163
+ return `Compressible ranges (oldest first):
7164
+ ${lines.join("\n")}`;
7194
7165
  }
7195
- function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
7196
- const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
7197
- if (suffixMessage) {
7198
- const nudgeParts = [];
7199
- if (state.nudges.contextLimitAnchors.size > 0) {
7200
- nudgeParts.push(prompts.contextLimitNudge);
7201
- }
7202
- if (turnNudgeAnchors.size > 0) {
7203
- nudgeParts.push(prompts.turnNudge);
7166
+ function computeProtectedRefs(messages, state, compress) {
7167
+ if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
7168
+ const preserveN = compress.preserveRecentMessages ?? 5;
7169
+ const preserveTokens = compress.preserveRecentTokens ?? 5e3;
7170
+ const result = /* @__PURE__ */ new Set();
7171
+ const visible = [];
7172
+ for (const msg of messages) {
7173
+ if (isSyntheticMessage(msg)) continue;
7174
+ if (isIgnoredUserMessage(msg)) continue;
7175
+ const ref = state.messageIds.byRawId.get(msg.info.id);
7176
+ if (!ref) continue;
7177
+ if (state.prune.messages.byMessageId.has(msg.info.id)) continue;
7178
+ let tokens = 0;
7179
+ for (const part of msg.parts || []) {
7180
+ if (part.type === "text" && typeof part.text === "string") {
7181
+ tokens += Math.round(part.text.length / 4);
7182
+ } else if (part.type !== "text" && part.type !== "reasoning") {
7183
+ tokens += Math.round(JSON.stringify(part).length / 4);
7184
+ }
7204
7185
  }
7205
- if (state.nudges.iterationNudgeAnchors.size > 0) {
7206
- nudgeParts.push(prompts.iterationNudge);
7186
+ visible.push({ ref, tokens, isUser: msg.info.role === "user" });
7187
+ }
7188
+ if (preserveN > 0) {
7189
+ for (const m of visible.slice(-preserveN)) {
7190
+ result.add(m.ref);
7207
7191
  }
7208
- const combined = nudgeParts.join("\n\n");
7209
- if (combined.trim()) {
7210
- injectAnchoredNudge(suffixMessage, combined);
7192
+ }
7193
+ if (preserveTokens > 0) {
7194
+ let tokenAccum = 0;
7195
+ for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
7196
+ result.add(visible[i].ref);
7197
+ tokenAccum += visible[i].tokens;
7211
7198
  }
7212
- return;
7213
7199
  }
7214
- applyRangeModeAnchoredNudge(
7215
- state.nudges.contextLimitAnchors,
7216
- messages,
7217
- prompts.contextLimitNudge,
7218
- ""
7219
- );
7220
- applyRangeModeAnchoredNudge(turnNudgeAnchors, messages, prompts.turnNudge, "");
7221
- applyRangeModeAnchoredNudge(
7222
- state.nudges.iterationNudgeAnchors,
7223
- messages,
7224
- prompts.iterationNudge,
7225
- ""
7200
+ return result;
7201
+ }
7202
+ function excludeProtectedRanges(ranges, protectedRefs) {
7203
+ if (protectedRefs.size === 0) return ranges;
7204
+ return ranges.filter(
7205
+ (r) => !protectedRefs.has(r.startRef) && !protectedRefs.has(r.endRef)
7226
7206
  );
7227
7207
  }
7228
- function estimateCodeTokens(text) {
7229
- let codeChars = 0;
7230
- let inCode = false;
7231
- for (const line of text.split("\n")) {
7232
- if (line.trim().startsWith("```")) {
7233
- inCode = !inCode;
7234
- codeChars += line.length + 1;
7235
- continue;
7236
- }
7237
- if (inCode) codeChars += line.length + 1;
7238
- }
7239
- return Math.round(codeChars / 4);
7208
+
7209
+ // lib/compress/range.ts
7210
+ function buildSchema(maxSummaryLengthHard) {
7211
+ return {
7212
+ topic: tool2.schema.string().optional().describe(
7213
+ "Fallback topic for entries without their own. Omit when each content entry specifies its own topic."
7214
+ ),
7215
+ content: tool2.schema.array(
7216
+ tool2.schema.object({
7217
+ topic: tool2.schema.string().optional().describe(
7218
+ "Short label (3-5 words) for THIS range, e.g. 'Auth System Exploration'. Omit to use top-level topic. When compressing multiple unrelated ranges, give each its own topic for better quality."
7219
+ ),
7220
+ startId: tool2.schema.string().describe(
7221
+ "Message or block ID marking the beginning of range (e.g. m00001, b2)"
7222
+ ),
7223
+ endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
7224
+ summary: tool2.schema.string().describe(
7225
+ "Complete technical summary replacing all content in range. Keep only essential details (conclusions, file paths, decisions, exact values, etc.)."
7226
+ )
7227
+ })
7228
+ ).describe(
7229
+ "One or more ranges to compress, each with start/end boundaries and a summary. When compressing multiple unrelated ranges in one call, give each its own topic."
7230
+ ),
7231
+ summaryMaxChars: tool2.schema.number().optional().describe(
7232
+ `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.`
7233
+ ),
7234
+ dangerous: tool2.schema.boolean().optional().describe(
7235
+ "Set to true ONLY when you are certain the most recent message(s) must be compressed. Required when a range includes the tail of the conversation."
7236
+ ),
7237
+ acknowledgeRisk: tool2.schema.boolean().optional()
7238
+ };
7240
7239
  }
7241
- function estimateContextComposition(messages, state, protectedTools = [], protectedFilePatterns = []) {
7242
- let toolTokens = 0;
7243
- let codeTokens = 0;
7244
- let summaryTokens = 0;
7245
- let messageTokens = 0;
7246
- let protectedTokens = 0;
7247
- const perMessage = [];
7248
- const perTool = [];
7249
- const perCode = [];
7250
- const perText = [];
7251
- const toolTypeMap = /* @__PURE__ */ new Map();
7252
- for (const msg of messages) {
7253
- const text = (msg.parts || []).filter((p) => p.type === "text").map((p) => p.text || "").join("");
7254
- const msgId = msg.info?.id || "";
7255
- const isSummary = msgId.startsWith("msg_dcp_summary") || text.includes("[Compressed conversation section]");
7256
- const isProtected = (protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns);
7257
- let msgTotal = 0;
7258
- let msgTool = 0;
7259
- let msgCode = 0;
7260
- let msgText = 0;
7261
- let msgToolName = "";
7262
- for (const part of msg.parts || []) {
7263
- if (part.type === "text" && typeof part.text === "string") {
7264
- const partText = part.text;
7265
- const tokens = Math.round(partText.length / 4);
7266
- msgTotal += tokens;
7267
- if (isSummary) {
7268
- summaryTokens += tokens;
7269
- } else {
7270
- messageTokens += tokens;
7271
- msgText += tokens;
7272
- const cTokens = estimateCodeTokens(partText);
7273
- if (cTokens > 0) {
7274
- codeTokens += cTokens;
7275
- msgCode += cTokens;
7276
- }
7240
+ function createCompressRangeTool(factoryCtx) {
7241
+ factoryCtx.prompts.reload();
7242
+ const runtimePrompts = factoryCtx.prompts.getRuntimePrompts();
7243
+ return tool2({
7244
+ description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
7245
+ args: buildSchema(factoryCtx.config.compress.maxSummaryLengthHard),
7246
+ async execute(args, toolCtx) {
7247
+ const ctx0 = resolveToolContext(factoryCtx, toolCtx.sessionID);
7248
+ const input = args;
7249
+ validateArgs(input);
7250
+ const maxLen = args.summaryMaxChars ?? ctx0.config.compress.maxSummaryLengthHard;
7251
+ for (const entry of input.content) {
7252
+ if (entry.summary.length > maxLen) {
7253
+ throw new Error(
7254
+ `Summary too long (${entry.summary.length} chars, max ${maxLen}).
7255
+ 1. If this summary is nearly the same size as the original content, it may not be worth compressing \u2014 skip it.
7256
+ 2. Strip noise (failed attempts, verbose outputs) but keep project-critical details (file paths, decisions, exact values).
7257
+ 3. For important content needing detail, pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit. Example: add "summaryMaxChars": 6000 to the tool call args.`
7258
+ );
7277
7259
  }
7278
- } else if (part.type === "tool") {
7279
- const raw = JSON.stringify(part);
7280
- const tokens = Math.round(raw.length / 4);
7281
- msgTotal += tokens;
7282
- const toolName = part?.tool || "unknown";
7283
- let summaryPartTokens = 0;
7284
- if (toolName === "compress") {
7285
- const input = part?.state?.input;
7286
- if (input?.content && Array.isArray(input.content)) {
7287
- for (const entry of input.content) {
7288
- if (typeof entry?.summary === "string") {
7289
- summaryPartTokens += Math.round(entry.summary.length / 4);
7290
- }
7260
+ }
7261
+ const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
7262
+ const { rawMessages, searchContext } = await prepareSession(
7263
+ ctx0,
7264
+ toolCtx,
7265
+ `Compress Range: ${input.topic ?? "(batch)"}`
7266
+ );
7267
+ const { providerId, modelId } = getModelInfo(rawMessages);
7268
+ const ctx = {
7269
+ ...ctx0,
7270
+ config: applyCompressOverrides(ctx0.config, providerId, modelId)
7271
+ };
7272
+ const resolvedPlans = resolveRanges(input, searchContext, ctx.state, ctx.logger);
7273
+ validateNonOverlapping(resolvedPlans);
7274
+ const filteredPlans = resolvedPlans.map((plan) => ({
7275
+ ...plan,
7276
+ selection: filterProtectedToolMessages(
7277
+ plan.selection,
7278
+ searchContext,
7279
+ ctx.config.compress.protectedTools,
7280
+ ctx.config.protectedFilePatterns
7281
+ )
7282
+ })).map((plan) => ({
7283
+ ...plan,
7284
+ selection: filterLastUserMessage(
7285
+ plan.selection,
7286
+ searchContext,
7287
+ ctx.state,
7288
+ ctx.config.compress
7289
+ )
7290
+ })).map((plan) => ({
7291
+ ...plan,
7292
+ selection: filterProtectedRecentMessages(
7293
+ plan.selection,
7294
+ searchContext,
7295
+ ctx.state,
7296
+ ctx.config.compress
7297
+ )
7298
+ })).filter((plan) => plan.selection.messageIds.length > 0);
7299
+ if (filteredPlans.length === 0) {
7300
+ throw new Error(
7301
+ "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
7302
+ );
7303
+ }
7304
+ const minCompressRange = ctx.config.compress.minCompressRange;
7305
+ if (minCompressRange > 0) {
7306
+ let totalChars = 0;
7307
+ const counted = /* @__PURE__ */ new Set();
7308
+ for (const plan of filteredPlans) {
7309
+ for (const messageId of plan.selection.messageIds) {
7310
+ if (counted.has(messageId)) continue;
7311
+ counted.add(messageId);
7312
+ const rawMessage = searchContext.rawMessagesById.get(messageId);
7313
+ if (rawMessage) {
7314
+ totalChars += countMessageCharacters(rawMessage);
7291
7315
  }
7292
7316
  }
7293
7317
  }
7294
- const toolPartTokens = Math.max(0, tokens - summaryPartTokens);
7295
- toolTokens += toolPartTokens;
7296
- msgTool += toolPartTokens;
7297
- summaryTokens += summaryPartTokens;
7298
- toolTypeMap.set(toolName, (toolTypeMap.get(toolName) || 0) + toolPartTokens);
7299
- if (!msgToolName) msgToolName = toolName;
7318
+ if (totalChars < minCompressRange) {
7319
+ throw new Error(
7320
+ `Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
7321
+ );
7322
+ }
7323
+ }
7324
+ const notifications = [];
7325
+ let preparedPlans = [];
7326
+ let totalCompressedMessages = 0;
7327
+ for (const plan of filteredPlans) {
7328
+ const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
7329
+ validateSummaryPlaceholders(
7330
+ parsedPlaceholders,
7331
+ plan.selection.requiredBlockIds,
7332
+ plan.selection.startReference,
7333
+ plan.selection.endReference,
7334
+ searchContext.summaryByBlockId,
7335
+ ctx.logger
7336
+ );
7337
+ const injected = injectBlockPlaceholders(
7338
+ plan.entry.summary,
7339
+ parsedPlaceholders,
7340
+ searchContext.summaryByBlockId,
7341
+ plan.selection.startReference,
7342
+ plan.selection.endReference
7343
+ );
7344
+ const summaryWithUsers = appendProtectedUserMessages(
7345
+ injected.expandedSummary,
7346
+ plan.selection,
7347
+ searchContext,
7348
+ ctx.state,
7349
+ ctx.config.compress.protectUserMessages
7350
+ );
7351
+ const summaryWithPromptInfo = appendProtectedPromptInfo(
7352
+ summaryWithUsers,
7353
+ plan.selection,
7354
+ searchContext,
7355
+ ctx.state,
7356
+ ctx.config.compress.protectTags
7357
+ );
7358
+ const summaryWithTools = await appendProtectedTools(
7359
+ ctx.client,
7360
+ ctx.state,
7361
+ summaryWithPromptInfo,
7362
+ plan.selection,
7363
+ searchContext,
7364
+ ctx.config.compress.protectedTools,
7365
+ ctx.config.protectedFilePatterns
7366
+ );
7367
+ const completedSummary = appendMissingBlockSummaries(
7368
+ summaryWithTools,
7369
+ [],
7370
+ searchContext.summaryByBlockId,
7371
+ injected.consumedBlockIds
7372
+ );
7373
+ const boundaryConsumed = extractBoundaryConsumedBlocks2(
7374
+ plan.selection.startReference,
7375
+ plan.selection.endReference
7376
+ );
7377
+ const seenConsumed = /* @__PURE__ */ new Set();
7378
+ const mergeConsumedBlockIds = [
7379
+ ...plan.selection.requiredBlockIds,
7380
+ ...boundaryConsumed
7381
+ ].filter((id) => {
7382
+ if (seenConsumed.has(id)) return false;
7383
+ seenConsumed.add(id);
7384
+ return true;
7385
+ });
7386
+ preparedPlans.push({
7387
+ entry: plan.entry,
7388
+ selection: plan.selection,
7389
+ anchorMessageId: plan.anchorMessageId,
7390
+ finalSummary: completedSummary.expandedSummary,
7391
+ consumedBlockIds: mergeConsumedBlockIds
7392
+ });
7393
+ }
7394
+ const partition = partitionPhantomPlans(
7395
+ identifyPhantomPlans(
7396
+ ctx.state,
7397
+ preparedPlans.map((p) => ({
7398
+ messageIds: p.selection.messageIds,
7399
+ consumedBlockIds: p.consumedBlockIds
7400
+ }))
7401
+ ),
7402
+ preparedPlans.length
7403
+ );
7404
+ let phantomSkipNotice = null;
7405
+ if (partition.kind === "all-phantom") {
7406
+ ctx.logger.warn("Batch compress: ALL entries phantom", {
7407
+ totalCount: preparedPlans.length,
7408
+ details: partition.details
7409
+ });
7410
+ throw new Error(buildPhantomErrorMessage(partition.details));
7411
+ }
7412
+ if (partition.kind === "partial") {
7413
+ ctx.logger.warn("Batch compress: phantom entries detected", {
7414
+ phantomCount: partition.dropIndices.length,
7415
+ totalCount: preparedPlans.length,
7416
+ phantomIndices: partition.dropIndices,
7417
+ details: partition.details
7418
+ });
7419
+ const dropSet = new Set(partition.dropIndices);
7420
+ preparedPlans = preparedPlans.filter((_, i) => !dropSet.has(i));
7421
+ phantomSkipNotice = partition.notice;
7422
+ }
7423
+ const acknowledgeRisk = args.acknowledgeRisk === true;
7424
+ const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
7425
+ const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
7426
+ const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
7427
+ if (ignoredAcknowledgeRisk) {
7428
+ ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
7429
+ }
7430
+ ctx.state.qualityGateRetryPending = false;
7431
+ if (!bypassQuality) {
7432
+ for (const plan of preparedPlans) {
7433
+ const result = evaluatePreCommitQuality(
7434
+ rawMessages,
7435
+ plan.selection.messageIds,
7436
+ plan.selection.messageTokenById,
7437
+ plan.finalSummary,
7438
+ ctx.config,
7439
+ ctx.logger
7440
+ );
7441
+ if (result && !result.passed) {
7442
+ ctx.state.qualityGateRetryPending = true;
7443
+ throw buildQualityRejectionError(
7444
+ {
7445
+ startId: plan.entry.startId,
7446
+ endId: plan.entry.endId,
7447
+ summary: plan.finalSummary,
7448
+ messageIds: plan.selection.messageIds,
7449
+ messageTokenById: plan.selection.messageTokenById
7450
+ },
7451
+ result
7452
+ );
7453
+ }
7454
+ }
7455
+ }
7456
+ const snapshot = snapshotCompressionState(ctx.state);
7457
+ const runId = allocateRunId(ctx.state);
7458
+ try {
7459
+ for (const preparedPlan of preparedPlans) {
7460
+ const blockId = allocateBlockId(ctx.state);
7461
+ const keepResult = resolveKeepMarkers(
7462
+ preparedPlan.finalSummary,
7463
+ rawMessages,
7464
+ ctx.state,
7465
+ ctx.config
7466
+ );
7467
+ preparedPlan.finalSummary = keepResult.summary;
7468
+ const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
7469
+ const summaryTokens = countTokens2(storedSummary);
7470
+ const applied = applyCompressionState(
7471
+ ctx.state,
7472
+ {
7473
+ topic: preparedPlan.entry.topic ?? input.topic ?? "",
7474
+ batchTopic: input.topic,
7475
+ startId: preparedPlan.entry.startId,
7476
+ endId: preparedPlan.entry.endId,
7477
+ mode: "range",
7478
+ runId,
7479
+ compressMessageId: toolCtx.messageID,
7480
+ compressCallId: callId,
7481
+ summaryTokens
7482
+ },
7483
+ preparedPlan.selection,
7484
+ preparedPlan.anchorMessageId,
7485
+ blockId,
7486
+ storedSummary,
7487
+ preparedPlan.consumedBlockIds,
7488
+ ctx.config.gc
7489
+ );
7490
+ totalCompressedMessages += applied.messageIds.length;
7491
+ notifications.push({
7492
+ blockId,
7493
+ runId,
7494
+ summary: preparedPlan.finalSummary,
7495
+ summaryTokens
7496
+ });
7497
+ }
7498
+ await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
7499
+ } catch (error) {
7500
+ restoreCompressionState(ctx.state, snapshot);
7501
+ ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
7502
+ throw error;
7300
7503
  }
7504
+ const skippedNote = phantomSkipNotice !== null ? `
7505
+ \u26A0\uFE0F ${phantomSkipNotice}
7506
+ ` : "";
7507
+ const ackNote = ignoredAcknowledgeRisk ? `
7508
+ \u26A0\uFE0F acknowledgeRisk was ignored: no quality gate rejection was pending, so quality checks ran normally. Only pass it when retrying immediately after a quality gate rejection.
7509
+ ` : "";
7510
+ return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}${ackNote}
7511
+ IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
7512
+ \u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
7301
7513
  }
7302
- if (isProtected && !isSummary) {
7303
- protectedTokens += msgTotal;
7304
- }
7305
- if (!isSummary) {
7306
- const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
7307
- if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
7308
- if (msgTool > 500) perTool.push({ ref, tokens: msgTool, tool: msgToolName });
7309
- if (msgCode > 300) perCode.push({ ref, tokens: msgCode });
7310
- if (msgText > 500 && msgCode === 0) perText.push({ ref, tokens: msgText });
7514
+ });
7515
+ }
7516
+ function extractBoundaryConsumedBlocks2(startReference, endReference) {
7517
+ const consumed = [];
7518
+ const seen = /* @__PURE__ */ new Set();
7519
+ for (const ref of [startReference, endReference]) {
7520
+ if (ref.kind === "compressed-block" && ref.blockId !== void 0 && !seen.has(ref.blockId)) {
7521
+ seen.add(ref.blockId);
7522
+ consumed.push(ref.blockId);
7311
7523
  }
7312
7524
  }
7313
- perMessage.sort((a, b) => b.tokens - a.tokens);
7314
- perTool.sort((a, b) => b.tokens - a.tokens);
7315
- perCode.sort((a, b) => b.tokens - a.tokens);
7316
- perText.sort((a, b) => b.tokens - a.tokens);
7317
- const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens);
7318
- const systemTokens = state?.systemPromptTokens !== void 0 && state.systemPromptTokens > 0 ? state.systemPromptTokens : estimateSystemPromptTokens(messages);
7319
- return {
7320
- toolTokens,
7321
- codeTokens,
7322
- summaryTokens,
7323
- messageTokens,
7324
- textTokens: Math.max(0, messageTokens - codeTokens),
7325
- systemTokens,
7326
- protectedTokens,
7327
- total: systemTokens + toolTokens + summaryTokens + messageTokens,
7328
- largestRanges: perMessage.slice(0, 15),
7329
- largestToolRanges: perTool.slice(0, 15),
7330
- largestCodeRanges: perCode.slice(0, 5),
7331
- largestMessageRanges: perText.slice(0, 5),
7332
- toolTypeBreakdown
7333
- };
7334
- }
7335
- function refNum(ref) {
7336
- const n = parseInt(ref.slice(1), 10);
7337
- return Number.isNaN(n) ? -1 : n;
7525
+ return consumed;
7338
7526
  }
7339
- function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = [], protectedZoneRefs) {
7340
- const msgInfo = [];
7341
- const protectedMsgInfo = [];
7342
- const lastUserRefIdx = [];
7343
- for (let mi = 0; mi < messages.length; mi++) {
7344
- const msg = messages[mi];
7345
- if (isSyntheticMessage(msg)) continue;
7346
- const ref = state.messageIds.byRawId.get(msg.info.id);
7347
- if (!ref) continue;
7348
- const rn = parseInt(ref.slice(1), 10);
7349
- if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
7350
- let tokens2 = 0;
7351
- const tools = /* @__PURE__ */ new Set();
7352
- for (const part of msg.parts || []) {
7353
- if (part.type === "text" && typeof part.text === "string") {
7354
- tokens2 += Math.round(part.text.length / 4);
7355
- } else if (part.type !== "text" && part.type !== "reasoning") {
7356
- tokens2 += Math.round(JSON.stringify(part).length / 4);
7357
- const toolName = part?.tool;
7358
- const callID = part?.callID;
7359
- if (toolName && callID) {
7360
- if (isToolNameProtected(toolName, protectedTools)) {
7361
- tools.add(toolName);
7362
- } else if (protectedFilePatterns.length > 0) {
7363
- const filePaths = getFilePathsFromParameters(
7364
- toolName,
7365
- part?.state?.input
7366
- );
7367
- if (isFilePathProtected(filePaths, protectedFilePatterns)) {
7368
- tools.add(toolName);
7369
- }
7370
- }
7527
+
7528
+ // lib/compress/decompress.ts
7529
+ import { tool as tool3 } from "@opencode-ai/plugin";
7530
+
7531
+ // lib/messages/prune.ts
7532
+ var prune = (state, logger, config, messages) => {
7533
+ filterCompressedRanges(state, messages);
7534
+ stripStepMarkers(messages);
7535
+ };
7536
+ var MAX_STEP_FINISH_REASON = 50;
7537
+ var stripStepMarkers = (messages) => {
7538
+ for (const msg of messages) {
7539
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
7540
+ let changed = false;
7541
+ const filtered = [];
7542
+ for (const part of parts) {
7543
+ if (part.type === "step-start") {
7544
+ changed = true;
7545
+ continue;
7546
+ }
7547
+ if (part.type === "step-finish") {
7548
+ const reason = part.reason;
7549
+ if (typeof reason === "string" && reason.length > MAX_STEP_FINISH_REASON) {
7550
+ const truncated = reason.slice(0, MAX_STEP_FINISH_REASON) + "...";
7551
+ if (truncated !== reason) {
7552
+ filtered.push({ ...part, reason: truncated });
7553
+ changed = true;
7554
+ continue;
7371
7555
  }
7372
7556
  }
7373
7557
  }
7374
- protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
7375
- continue;
7376
- }
7377
- let tokens = 0;
7378
- let isTool = false;
7379
- let hasMeaningfulPart = false;
7380
- for (const part of msg.parts || []) {
7381
- if (part.type === "text" && typeof part.text === "string") {
7382
- tokens += Math.round(part.text.length / 4);
7383
- if (part.text.trim().length > 0) hasMeaningfulPart = true;
7384
- } else if (part.type !== "text" && part.type !== "reasoning") {
7385
- tokens += Math.round(JSON.stringify(part).length / 4);
7386
- isTool = true;
7387
- hasMeaningfulPart = true;
7388
- }
7558
+ filtered.push(part);
7389
7559
  }
7390
- if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
7391
- lastUserRefIdx.length = 0;
7392
- lastUserRefIdx.push(msgInfo.length);
7560
+ if (changed) {
7561
+ msg.parts = filtered;
7393
7562
  }
7394
- msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
7395
7563
  }
7396
- const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
7397
- for (let i = 0; i < msgInfo.length; i++) {
7398
- const info = msgInfo[i];
7399
- info.effectiveTokens = i !== lastUserIdx && info.meaningful ? info.tokens : 0;
7564
+ };
7565
+ var filterCompressedRanges = (state, messages) => {
7566
+ if (state.prune.messages.byMessageId.size === 0) {
7567
+ return;
7400
7568
  }
7401
- const groups = [];
7402
- let cur = null;
7403
- let prevRefNum = -2;
7404
- for (const info of msgInfo) {
7405
- if (protectedZoneRefs?.has(info.ref)) {
7406
- if (cur) {
7407
- groups.push(cur);
7408
- cur = null;
7409
- }
7410
- prevRefNum = info.refNum;
7411
- continue;
7412
- }
7413
- const hasGap = info.refNum > prevRefNum + 1;
7414
- if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
7415
- groups.push(cur);
7416
- cur = null;
7569
+ const survive = messages.map((msg) => {
7570
+ const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id);
7571
+ if (!pruneEntry || pruneEntry.activeBlockIds.length === 0) {
7572
+ return true;
7417
7573
  }
7418
- prevRefNum = info.refNum;
7419
- if (!cur) {
7420
- cur = {
7421
- startRef: info.ref,
7422
- endRef: info.ref,
7423
- count: 1,
7424
- tokens: info.tokens,
7425
- effectiveTokens: info.effectiveTokens,
7426
- toolPct: info.isTool ? 100 : 0,
7427
- textPct: info.isTool ? 0 : 100
7428
- };
7429
- } else {
7430
- cur.endRef = info.ref;
7431
- cur.count++;
7432
- cur.tokens += info.tokens;
7433
- cur.effectiveTokens += info.effectiveTokens;
7434
- if (info.isTool) {
7435
- cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
7436
- } else {
7437
- cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
7438
- }
7439
- cur.textPct = 100 - cur.toolPct;
7574
+ return false;
7575
+ });
7576
+ const firstUserIdx = messages.findIndex((msg) => msg.info.role === "user");
7577
+ if (firstUserIdx >= 0) {
7578
+ survive[firstUserIdx] = true;
7579
+ }
7580
+ const result = [];
7581
+ for (let i = 0; i < messages.length; i++) {
7582
+ if (survive[i]) {
7583
+ result.push(messages[i]);
7440
7584
  }
7441
7585
  }
7442
- if (cur) groups.push(cur);
7443
- const protectedGroups = [];
7444
- let pcur = null;
7445
- let pPrevRefNum = -2;
7446
- for (const info of protectedMsgInfo) {
7447
- const hasGap = info.refNum > pPrevRefNum + 1;
7448
- if (pcur && hasGap) {
7449
- protectedGroups.push(pcur);
7450
- pcur = null;
7586
+ messages.length = 0;
7587
+ messages.push(...result);
7588
+ };
7589
+
7590
+ // lib/messages/sync.ts
7591
+ function sortBlocksByCreation(a, b) {
7592
+ const createdAtDiff = a.createdAt - b.createdAt;
7593
+ if (createdAtDiff !== 0) {
7594
+ return createdAtDiff;
7595
+ }
7596
+ return a.blockId - b.blockId;
7597
+ }
7598
+ var syncCompressionBlocks = (state, logger, messages) => {
7599
+ const messagesState = state.prune.messages;
7600
+ if (!messagesState?.blocksById?.size) {
7601
+ return;
7602
+ }
7603
+ const messageIds = new Set(messages.map((msg) => msg.info.id));
7604
+ const previousActiveBlockIds = new Set(
7605
+ Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
7606
+ );
7607
+ messagesState.activeBlockIds.clear();
7608
+ messagesState.activeByAnchorMessageId.clear();
7609
+ const now = Date.now();
7610
+ const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
7611
+ for (const block of orderedBlocks) {
7612
+ if (block.deactivatedByUser || block.deactivatedByUserDeep) {
7613
+ block.active = false;
7614
+ if (block.deactivatedAt === void 0) {
7615
+ block.deactivatedAt = now;
7616
+ }
7617
+ block.deactivatedByBlockId = void 0;
7618
+ continue;
7451
7619
  }
7452
- pPrevRefNum = info.refNum;
7453
- if (!pcur) {
7454
- pcur = {
7455
- startRef: info.ref,
7456
- endRef: info.ref,
7457
- count: 1,
7458
- tokens: info.tokens,
7459
- tools: [...info.tools]
7460
- };
7461
- } else {
7462
- pcur.endRef = info.ref;
7463
- pcur.count++;
7464
- pcur.tokens += info.tokens;
7465
- for (const t of info.tools) {
7466
- if (!pcur.tools.includes(t)) pcur.tools.push(t);
7620
+ for (const consumedBlockId of block.consumedBlockIds) {
7621
+ if (!messagesState.activeBlockIds.has(consumedBlockId)) {
7622
+ continue;
7623
+ }
7624
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId);
7625
+ if (consumedBlock) {
7626
+ consumedBlock.active = false;
7627
+ consumedBlock.deactivatedAt = now;
7628
+ consumedBlock.deactivatedByBlockId = block.blockId;
7629
+ const mappedBlockId = messagesState.activeByAnchorMessageId.get(
7630
+ consumedBlock.anchorMessageId
7631
+ );
7632
+ if (mappedBlockId === consumedBlock.blockId) {
7633
+ messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
7634
+ }
7467
7635
  }
7636
+ messagesState.activeBlockIds.delete(consumedBlockId);
7637
+ }
7638
+ block.active = true;
7639
+ block.deactivatedAt = void 0;
7640
+ block.deactivatedByBlockId = void 0;
7641
+ messagesState.activeBlockIds.add(block.blockId);
7642
+ if (messageIds.has(block.anchorMessageId)) {
7643
+ messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
7468
7644
  }
7469
7645
  }
7470
- if (pcur) protectedGroups.push(pcur);
7471
- return {
7472
- compressible: groups.filter((g) => g.tokens > 0),
7473
- protected: protectedGroups
7474
- };
7475
- }
7476
- var EFFECTIVE_MIN_COMPRESSIBLE_TOKENS = 1250;
7477
- function resolveEffectiveFloor(config) {
7478
- const minChars = config.compress?.minCompressRange ?? 5e3;
7479
- return minChars > 0 ? Math.floor(minChars / 4) : 0;
7480
- }
7481
- function filterRecommendedRanges(compressible, _protectedRanges, options) {
7482
- const { logger } = options;
7483
- const log = logger?.debug.bind(logger);
7484
- if (compressible.length === 0) {
7485
- log?.("filterRecommendedRanges: no compressible ranges, returning empty");
7486
- return [];
7646
+ for (const entry of messagesState.byMessageId.values()) {
7647
+ const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
7648
+ entry.allBlockIds = allBlockIds;
7649
+ entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
7487
7650
  }
7488
- const floor = options.minEffectiveTokens ?? EFFECTIVE_MIN_COMPRESSIBLE_TOKENS;
7489
- const kept = compressible.filter((r) => {
7490
- const effective = r.effectiveTokens ?? r.tokens;
7491
- return effective > 0 && effective >= floor;
7492
- });
7493
- const result = kept.map(
7494
- (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7495
- );
7496
- log?.("filterRecommendedRanges: effective-token floor applied", {
7497
- inputRanges: compressible.length,
7498
- outputRanges: result.length,
7499
- floor,
7500
- dropped: compressible.filter((r) => {
7501
- const effective = r.effectiveTokens ?? r.tokens;
7502
- return effective <= 0 || effective < floor;
7503
- }).map((r) => `${r.startRef}\u2013${r.endRef} (${r.effectiveTokens ?? r.tokens} eff tokens)`)
7504
- });
7505
- return result;
7506
- }
7507
- function formatCompressibleRanges(ranges, protectedRanges) {
7508
- const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
7509
- if (!protectedRanges || protectedRanges.length === 0) {
7510
- if (ranges.length === 0) return "";
7511
- const lines2 = ranges.map((r) => {
7512
- const eff = r.effectiveTokens ?? r.tokens;
7513
- const size = eff < r.tokens ? `${fmt(eff)} effective of ${fmt(r.tokens)}` : fmt(r.tokens);
7514
- const suffix = r.dangerous ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7515
- return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${size} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
7516
- });
7517
- return `Compressible ranges (oldest first):
7518
- ${lines2.join("\n")}`;
7651
+ const nextActiveBlockIds = messagesState.activeBlockIds;
7652
+ let deactivatedCount = 0;
7653
+ let reactivatedCount = 0;
7654
+ for (const blockId of previousActiveBlockIds) {
7655
+ if (!nextActiveBlockIds.has(blockId)) {
7656
+ deactivatedCount++;
7657
+ }
7519
7658
  }
7520
- const entries = [];
7521
- for (const r of ranges) {
7522
- entries.push({
7523
- startRef: r.startRef,
7524
- endRef: r.endRef,
7525
- startNum: refNum(r.startRef),
7526
- endNum: refNum(r.endRef),
7527
- count: r.count,
7528
- tokens: r.tokens,
7529
- toolPct: r.toolPct,
7530
- textPct: r.textPct,
7531
- compressibleTokens: r.effectiveTokens ?? r.tokens,
7532
- compressibleCount: r.count,
7533
- protectedTokens: 0,
7534
- protectedCount: 0,
7535
- protectedTools: [],
7536
- dangerous: r.dangerous ?? false
7537
- });
7659
+ for (const blockId of nextActiveBlockIds) {
7660
+ if (!previousActiveBlockIds.has(blockId)) {
7661
+ reactivatedCount++;
7662
+ }
7538
7663
  }
7539
- for (const r of protectedRanges) {
7540
- entries.push({
7541
- startRef: r.startRef,
7542
- endRef: r.endRef,
7543
- startNum: refNum(r.startRef),
7544
- endNum: refNum(r.endRef),
7545
- count: r.count,
7546
- tokens: r.tokens,
7547
- toolPct: 0,
7548
- textPct: 0,
7549
- compressibleTokens: 0,
7550
- compressibleCount: 0,
7551
- protectedTokens: r.tokens,
7552
- protectedCount: r.count,
7553
- protectedTools: [...r.tools],
7554
- dangerous: false
7664
+ if (deactivatedCount > 0 || reactivatedCount > 0) {
7665
+ logger.info("Synced compress block state", {
7666
+ deactivatedCount,
7667
+ reactivatedCount
7555
7668
  });
7556
7669
  }
7557
- entries.sort((a, b) => a.startNum - b.startNum);
7558
- const merged = [];
7559
- for (const entry of entries) {
7560
- const last = merged[merged.length - 1];
7561
- if (last && entry.startNum <= last.endNum + 1) {
7562
- last.endRef = entry.endRef;
7563
- last.endNum = Math.max(last.endNum, entry.endNum);
7564
- last.count += entry.count;
7565
- last.tokens += entry.tokens;
7566
- last.compressibleTokens += entry.compressibleTokens;
7567
- last.compressibleCount += entry.compressibleCount;
7568
- last.protectedTokens += entry.protectedTokens;
7569
- last.protectedCount += entry.protectedCount;
7570
- if (entry.dangerous) last.dangerous = true;
7571
- for (const t of entry.protectedTools) {
7572
- if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
7573
- }
7574
- } else {
7575
- merged.push({ ...entry });
7670
+ };
7671
+
7672
+ // lib/host-permissions.ts
7673
+ var findLastMatchingRule = (rules, predicate) => {
7674
+ for (let index = rules.length - 1; index >= 0; index -= 1) {
7675
+ const rule = rules[index];
7676
+ if (rule && predicate(rule)) {
7677
+ return rule;
7576
7678
  }
7577
7679
  }
7578
- const lines = merged.map((e) => {
7579
- const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7580
- if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
7581
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
7582
- }
7583
- if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
7584
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [${fmt(e.compressibleTokens)} compressible | ${fmt(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
7680
+ return void 0;
7681
+ };
7682
+ var wildcardMatch = (value, pattern) => {
7683
+ const normalizedValue = value.replaceAll("\\", "/");
7684
+ let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
7685
+ if (escaped.endsWith(" .*")) {
7686
+ escaped = escaped.slice(0, -3) + "( .*)?";
7687
+ }
7688
+ const flags = process.platform === "win32" ? "si" : "s";
7689
+ return new RegExp(`^${escaped}$`, flags).test(normalizedValue);
7690
+ };
7691
+ var getPermissionRules = (permissionConfigs) => {
7692
+ const rules = [];
7693
+ for (const permissionConfig of permissionConfigs) {
7694
+ if (!permissionConfig) {
7695
+ continue;
7585
7696
  }
7586
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
7587
- });
7588
- return `Compressible ranges (oldest first):
7589
- ${lines.join("\n")}`;
7590
- }
7591
- function computeProtectedRefs(messages, state, compress) {
7592
- if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
7593
- const preserveN = compress.preserveRecentMessages ?? 5;
7594
- const preserveTokens = compress.preserveRecentTokens ?? 5e3;
7595
- const result = /* @__PURE__ */ new Set();
7596
- const visible = [];
7597
- for (const msg of messages) {
7598
- if (isSyntheticMessage(msg)) continue;
7599
- if (isIgnoredUserMessage(msg)) continue;
7600
- const ref = state.messageIds.byRawId.get(msg.info.id);
7601
- if (!ref) continue;
7602
- if (state.prune.messages.byMessageId.has(msg.info.id)) continue;
7603
- let tokens = 0;
7604
- for (const part of msg.parts || []) {
7605
- if (part.type === "text" && typeof part.text === "string") {
7606
- tokens += Math.round(part.text.length / 4);
7607
- } else if (part.type !== "text" && part.type !== "reasoning") {
7608
- tokens += Math.round(JSON.stringify(part).length / 4);
7697
+ for (const [permission, value] of Object.entries(permissionConfig)) {
7698
+ if (value === "ask" || value === "allow" || value === "deny") {
7699
+ rules.push({ permission, pattern: "*", action: value });
7700
+ continue;
7701
+ }
7702
+ for (const [pattern, action] of Object.entries(value)) {
7703
+ if (action === "ask" || action === "allow" || action === "deny") {
7704
+ rules.push({ permission, pattern, action });
7705
+ }
7609
7706
  }
7610
- }
7611
- visible.push({ ref, tokens, isUser: msg.info.role === "user" });
7612
- }
7613
- if (preserveN > 0) {
7614
- for (const m of visible.slice(-preserveN)) {
7615
- result.add(m.ref);
7616
7707
  }
7617
7708
  }
7618
- if (preserveTokens > 0) {
7619
- let tokenAccum = 0;
7620
- for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
7621
- result.add(visible[i].ref);
7622
- tokenAccum += visible[i].tokens;
7623
- }
7709
+ return rules;
7710
+ };
7711
+ var compressDisabledByOpencode = (...permissionConfigs) => {
7712
+ const match = findLastMatchingRule(
7713
+ getPermissionRules(permissionConfigs),
7714
+ (rule) => wildcardMatch("compress", rule.permission)
7715
+ );
7716
+ return match?.pattern === "*" && match.action === "deny";
7717
+ };
7718
+ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agentName) => {
7719
+ if (basePermission === "deny") {
7720
+ return "deny";
7624
7721
  }
7625
- return result;
7626
- }
7627
- function excludeProtectedRanges(ranges, protectedRefs) {
7628
- if (protectedRefs.size === 0) return ranges;
7629
- return ranges.filter(
7630
- (r) => !protectedRefs.has(r.startRef) && !protectedRefs.has(r.endRef)
7722
+ return compressDisabledByOpencode(
7723
+ hostPermissions.global,
7724
+ agentName ? hostPermissions.agents[agentName] : void 0
7725
+ ) ? "deny" : basePermission;
7726
+ };
7727
+ var hasExplicitToolPermission = (permissionConfig, tool6) => {
7728
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool6) : false;
7729
+ };
7730
+
7731
+ // lib/compress-permission.ts
7732
+ var compressPermission = (state, config) => {
7733
+ return state.compressPermission ?? config.compress.permission;
7734
+ };
7735
+ var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
7736
+ const activeAgent = getLastUserMessage(messages)?.info.agent;
7737
+ state.compressPermission = resolveEffectiveCompressPermission(
7738
+ config.compress.permission,
7739
+ hostPermissions,
7740
+ activeAgent
7631
7741
  );
7632
- }
7742
+ };
7633
7743
 
7634
7744
  // lib/messages/inject/inject.ts
7635
7745
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
@@ -7647,6 +7757,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7647
7757
  const lastMessage = findLastNonIgnoredMessage(messages);
7648
7758
  const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant");
7649
7759
  const { providerId, modelId } = getModelInfo(messages);
7760
+ config = applyCompressOverrides(config, providerId, modelId);
7650
7761
  const { overMaxLimit, overMinLimit, currentTokens, modelContextLimit } = isContextOverLimits(
7651
7762
  config,
7652
7763
  state,
@@ -9267,7 +9378,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9267
9378
  import { join as join3 } from "path";
9268
9379
  import { existsSync as existsSync3 } from "fs";
9269
9380
  import { homedir as homedir3 } from "os";
9270
- var LOG_VERSION = true ? "1.14.25-pr.351.82" : "dev";
9381
+ var LOG_VERSION = true ? "1.14.25-pr.351.88" : "dev";
9271
9382
  var LEVEL_RANK = {
9272
9383
  debug: 10,
9273
9384
  info: 20,
@@ -11708,7 +11819,7 @@ var server = (async (ctx) => {
11708
11819
  }
11709
11820
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11710
11821
  logger.info("ACP plugin initialized", {
11711
- version: true ? "1.14.25-pr.351.82" : "dev",
11822
+ version: true ? "1.14.25-pr.351.88" : "dev",
11712
11823
  workspace: ctx.directory,
11713
11824
  logLevel: logger.level,
11714
11825
  debug: config.debug,