opencode-acp 1.14.26-pr.353.87 → 1.14.26

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
@@ -888,6 +888,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
888
888
  "compress.minContextLimit",
889
889
  "compress.modelMaxLimits",
890
890
  "compress.modelMinLimits",
891
+ "compress.providers",
891
892
  "compress.nudgeFrequency",
892
893
  "compress.minNudgeContextPercent",
893
894
  "compress.nudgeGrowthTokens",
@@ -931,7 +932,7 @@ function getConfigKeyPaths(obj, prefix = "") {
931
932
  for (const key of Object.keys(obj)) {
932
933
  const fullKey = prefix ? `${prefix}.${key}` : key;
933
934
  keys.push(fullKey);
934
- if (fullKey === "compress.modelMaxLimits" || fullKey === "compress.modelMinLimits" || fullKey === "messageFilters.filters") {
935
+ if (fullKey === "compress.modelMaxLimits" || fullKey === "compress.modelMinLimits" || fullKey === "compress.providers" || fullKey === "messageFilters.filters") {
935
936
  continue;
936
937
  }
937
938
  if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
@@ -1311,6 +1312,158 @@ function validateConfigTypes(config) {
1311
1312
  }
1312
1313
  validateModelLimits("compress.modelMaxLimits", compress.modelMaxLimits);
1313
1314
  validateModelLimits("compress.modelMinLimits", compress.modelMinLimits);
1315
+ const validatePercentField = (key, value) => {
1316
+ if (value === void 0) {
1317
+ return;
1318
+ }
1319
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
1320
+ errors.push({
1321
+ key,
1322
+ expected: "number (0-100)",
1323
+ actual: JSON.stringify(value)
1324
+ });
1325
+ }
1326
+ };
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) => {
1393
+ if (overrides === void 0) {
1394
+ return;
1395
+ }
1396
+ if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
1397
+ errors.push({ key: prefix, expected: "CompressModelOverrides", actual: typeof overrides });
1398
+ return;
1399
+ }
1400
+ const model = overrides;
1401
+ for (const field of Object.keys(model)) {
1402
+ const type = OVERRIDE_FIELD_TYPES[field];
1403
+ if (type === void 0) {
1404
+ errors.push({
1405
+ key: `${prefix}.${field}`,
1406
+ expected: "known override field (see CompressModelOverrides)",
1407
+ actual: "unknown field"
1408
+ });
1409
+ continue;
1410
+ }
1411
+ validateOverrideField(`${prefix}.${field}`, type, model[field]);
1412
+ }
1413
+ };
1414
+ const validateModelOverrides = validateOverrideObject;
1415
+ const validateProviderOverrides = (providers) => {
1416
+ if (providers === void 0) {
1417
+ return;
1418
+ }
1419
+ if (typeof providers !== "object" || providers === null || Array.isArray(providers)) {
1420
+ errors.push({
1421
+ key: "compress.providers",
1422
+ expected: "Record<string, ProviderOverrides>",
1423
+ actual: typeof providers
1424
+ });
1425
+ return;
1426
+ }
1427
+ for (const [providerId, providerValue] of Object.entries(providers)) {
1428
+ const prefix = `compress.providers.${providerId}`;
1429
+ if (typeof providerValue !== "object" || providerValue === null || Array.isArray(providerValue)) {
1430
+ errors.push({ key: prefix, expected: "ProviderOverrides", actual: typeof providerValue });
1431
+ continue;
1432
+ }
1433
+ const provider = providerValue;
1434
+ for (const field of Object.keys(provider)) {
1435
+ if (field === "models") {
1436
+ continue;
1437
+ }
1438
+ const type = OVERRIDE_FIELD_TYPES[field];
1439
+ if (type === void 0) {
1440
+ errors.push({
1441
+ key: `${prefix}.${field}`,
1442
+ expected: "known override field (see CompressModelOverrides)",
1443
+ actual: "unknown field"
1444
+ });
1445
+ continue;
1446
+ }
1447
+ validateOverrideField(`${prefix}.${field}`, type, provider[field]);
1448
+ }
1449
+ const models = provider.models;
1450
+ if (models === void 0) {
1451
+ continue;
1452
+ }
1453
+ if (typeof models !== "object" || models === null || Array.isArray(models)) {
1454
+ errors.push({
1455
+ key: `${prefix}.models`,
1456
+ expected: "Record<string, ModelOverrides>",
1457
+ actual: typeof models
1458
+ });
1459
+ continue;
1460
+ }
1461
+ for (const [modelId, modelValue] of Object.entries(models)) {
1462
+ validateModelOverrides(`${prefix}.models.${modelId}`, modelValue);
1463
+ }
1464
+ }
1465
+ };
1466
+ validateProviderOverrides(compress.providers);
1314
1467
  const validValues = ["ask", "allow", "deny"];
1315
1468
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1316
1469
  errors.push({
@@ -1611,6 +1764,39 @@ function loadConfigFile(configPath) {
1611
1764
  return { data: null, parseError: error.message || "Failed to parse config" };
1612
1765
  }
1613
1766
  }
1767
+ function stripUndefined(value) {
1768
+ const result = {};
1769
+ for (const [key, entry] of Object.entries(value)) {
1770
+ if (entry !== void 0) {
1771
+ result[key] = entry;
1772
+ }
1773
+ }
1774
+ return result;
1775
+ }
1776
+ function mergeModelOverrides(base, override) {
1777
+ if (base === void 0) return override;
1778
+ if (override === void 0) return base;
1779
+ const merged = { ...base };
1780
+ for (const [modelId, model] of Object.entries(override)) {
1781
+ merged[modelId] = { ...base[modelId], ...stripUndefined(model) };
1782
+ }
1783
+ return merged;
1784
+ }
1785
+ function mergeProviderOverrides(base, override) {
1786
+ if (base === void 0) return override;
1787
+ if (override === void 0) return base;
1788
+ const merged = { ...base };
1789
+ for (const [providerId, provider] of Object.entries(override)) {
1790
+ const baseProvider = base[providerId];
1791
+ const mergedModels = mergeModelOverrides(baseProvider?.models, provider.models);
1792
+ merged[providerId] = {
1793
+ ...baseProvider,
1794
+ ...stripUndefined(provider),
1795
+ ...mergedModels !== void 0 ? { models: mergedModels } : {}
1796
+ };
1797
+ }
1798
+ return merged;
1799
+ }
1614
1800
  function mergeCompress(base, override) {
1615
1801
  if (!override) {
1616
1802
  return base;
@@ -1623,6 +1809,7 @@ function mergeCompress(base, override) {
1623
1809
  minContextLimit: override.minContextLimit ?? base.minContextLimit,
1624
1810
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1625
1811
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1812
+ providers: mergeProviderOverrides(base.providers, override.providers),
1626
1813
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1627
1814
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1628
1815
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -1673,6 +1860,22 @@ function deepCloneConfig(config) {
1673
1860
  ...config.compress,
1674
1861
  modelMaxLimits: { ...config.compress.modelMaxLimits },
1675
1862
  modelMinLimits: { ...config.compress.modelMinLimits },
1863
+ providers: config.compress.providers ? Object.fromEntries(
1864
+ Object.entries(config.compress.providers).map(([providerId, provider]) => [
1865
+ providerId,
1866
+ {
1867
+ ...provider,
1868
+ ...provider.models ? {
1869
+ models: Object.fromEntries(
1870
+ Object.entries(provider.models).map(([modelId, model]) => [
1871
+ modelId,
1872
+ { ...model }
1873
+ ])
1874
+ )
1875
+ } : {}
1876
+ }
1877
+ ])
1878
+ ) : void 0,
1676
1879
  protectedTools: [...config.compress.protectedTools]
1677
1880
  },
1678
1881
  gc: {
@@ -6013,580 +6216,111 @@ function truncate(text, maxChars) {
6013
6216
  ... [truncated, ${text.length} chars total]`;
6014
6217
  }
6015
6218
 
6016
- // lib/compress/range.ts
6017
- function buildSchema(maxSummaryLengthHard) {
6018
- return {
6019
- topic: tool2.schema.string().optional().describe(
6020
- "Fallback topic for entries without their own. Omit when each content entry specifies its own topic."
6021
- ),
6022
- content: tool2.schema.array(
6023
- tool2.schema.object({
6024
- topic: tool2.schema.string().optional().describe(
6025
- "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."
6026
- ),
6027
- startId: tool2.schema.string().describe(
6028
- "Message or block ID marking the beginning of range (e.g. m00001, b2)"
6029
- ),
6030
- endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
6031
- summary: tool2.schema.string().describe(
6032
- "Complete technical summary replacing all content in range. Keep only essential details (conclusions, file paths, decisions, exact values, etc.)."
6033
- )
6034
- })
6035
- ).describe(
6036
- "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."
6037
- ),
6038
- summaryMaxChars: tool2.schema.number().optional().describe(
6039
- `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.`
6040
- ),
6041
- dangerous: tool2.schema.boolean().optional().describe(
6042
- "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."
6043
- ),
6044
- acknowledgeRisk: tool2.schema.boolean().optional()
6045
- };
6046
- }
6047
- function createCompressRangeTool(factoryCtx) {
6048
- factoryCtx.prompts.reload();
6049
- const runtimePrompts = factoryCtx.prompts.getRuntimePrompts();
6050
- return tool2({
6051
- description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
6052
- args: buildSchema(factoryCtx.config.compress.maxSummaryLengthHard),
6053
- async execute(args, toolCtx) {
6054
- const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
6055
- const input = args;
6056
- validateArgs(input);
6057
- const maxLen = args.summaryMaxChars ?? ctx.config.compress.maxSummaryLengthHard;
6058
- for (const entry of input.content) {
6059
- if (entry.summary.length > maxLen) {
6060
- throw new Error(
6061
- `Summary too long (${entry.summary.length} chars, max ${maxLen}).
6062
- 1. If this summary is nearly the same size as the original content, it may not be worth compressing \u2014 skip it.
6063
- 2. Strip noise (failed attempts, verbose outputs) but keep project-critical details (file paths, decisions, exact values).
6064
- 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.`
6065
- );
6066
- }
6067
- }
6068
- const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
6069
- const { rawMessages, searchContext } = await prepareSession(
6070
- ctx,
6071
- toolCtx,
6072
- `Compress Range: ${input.topic ?? "(batch)"}`
6073
- );
6074
- const resolvedPlans = resolveRanges(input, searchContext, ctx.state, ctx.logger);
6075
- validateNonOverlapping(resolvedPlans);
6076
- const filteredPlans = resolvedPlans.map((plan) => ({
6077
- ...plan,
6078
- selection: filterProtectedToolMessages(
6079
- plan.selection,
6080
- searchContext,
6081
- ctx.config.compress.protectedTools,
6082
- ctx.config.protectedFilePatterns
6083
- )
6084
- })).map((plan) => ({
6085
- ...plan,
6086
- selection: filterLastUserMessage(
6087
- plan.selection,
6088
- searchContext,
6089
- ctx.state,
6090
- ctx.config.compress
6091
- )
6092
- })).map((plan) => ({
6093
- ...plan,
6094
- selection: filterProtectedRecentMessages(
6095
- plan.selection,
6096
- searchContext,
6097
- ctx.state,
6098
- ctx.config.compress
6099
- )
6100
- })).filter((plan) => plan.selection.messageIds.length > 0);
6101
- if (filteredPlans.length === 0) {
6102
- throw new Error(
6103
- "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
6104
- );
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)`);
6105
6253
  }
6106
- const minCompressRange = ctx.config.compress.minCompressRange;
6107
- if (minCompressRange > 0) {
6108
- let totalChars = 0;
6109
- const counted = /* @__PURE__ */ new Set();
6110
- for (const plan of filteredPlans) {
6111
- for (const messageId of plan.selection.messageIds) {
6112
- if (counted.has(messageId)) continue;
6113
- counted.add(messageId);
6114
- const rawMessage = searchContext.rawMessagesById.get(messageId);
6115
- if (rawMessage) {
6116
- totalChars += countMessageCharacters(rawMessage);
6117
- }
6118
- }
6119
- }
6120
- if (totalChars < minCompressRange) {
6121
- throw new Error(
6122
- `Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
6123
- );
6124
- }
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.`);
6125
6258
  }
6126
- const notifications = [];
6127
- let preparedPlans = [];
6128
- let totalCompressedMessages = 0;
6129
- for (const plan of filteredPlans) {
6130
- const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
6131
- validateSummaryPlaceholders(
6132
- parsedPlaceholders,
6133
- plan.selection.requiredBlockIds,
6134
- plan.selection.startReference,
6135
- plan.selection.endReference,
6136
- searchContext.summaryByBlockId,
6137
- ctx.logger
6138
- );
6139
- const injected = injectBlockPlaceholders(
6140
- plan.entry.summary,
6141
- parsedPlaceholders,
6142
- searchContext.summaryByBlockId,
6143
- plan.selection.startReference,
6144
- plan.selection.endReference
6145
- );
6146
- const summaryWithUsers = appendProtectedUserMessages(
6147
- injected.expandedSummary,
6148
- plan.selection,
6149
- searchContext,
6150
- ctx.state,
6151
- ctx.config.compress.protectUserMessages
6152
- );
6153
- const summaryWithPromptInfo = appendProtectedPromptInfo(
6154
- summaryWithUsers,
6155
- plan.selection,
6156
- searchContext,
6157
- ctx.state,
6158
- ctx.config.compress.protectTags
6159
- );
6160
- const summaryWithTools = await appendProtectedTools(
6161
- ctx.client,
6162
- ctx.state,
6163
- summaryWithPromptInfo,
6164
- plan.selection,
6165
- searchContext,
6166
- ctx.config.compress.protectedTools,
6167
- ctx.config.protectedFilePatterns
6168
- );
6169
- const completedSummary = appendMissingBlockSummaries(
6170
- summaryWithTools,
6171
- [],
6172
- searchContext.summaryByBlockId,
6173
- injected.consumedBlockIds
6174
- );
6175
- const boundaryConsumed = extractBoundaryConsumedBlocks2(
6176
- plan.selection.startReference,
6177
- plan.selection.endReference
6178
- );
6179
- const seenConsumed = /* @__PURE__ */ new Set();
6180
- const mergeConsumedBlockIds = [
6181
- ...plan.selection.requiredBlockIds,
6182
- ...boundaryConsumed
6183
- ].filter((id) => {
6184
- if (seenConsumed.has(id)) return false;
6185
- seenConsumed.add(id);
6186
- return true;
6187
- });
6188
- preparedPlans.push({
6189
- entry: plan.entry,
6190
- selection: plan.selection,
6191
- anchorMessageId: plan.anchorMessageId,
6192
- finalSummary: completedSummary.expandedSummary,
6193
- consumedBlockIds: mergeConsumedBlockIds
6194
- });
6195
- }
6196
- const partition = partitionPhantomPlans(
6197
- identifyPhantomPlans(
6198
- ctx.state,
6199
- preparedPlans.map((p) => ({
6200
- messageIds: p.selection.messageIds,
6201
- consumedBlockIds: p.consumedBlockIds
6202
- }))
6203
- ),
6204
- preparedPlans.length
6205
- );
6206
- let phantomSkipNotice = null;
6207
- if (partition.kind === "all-phantom") {
6208
- ctx.logger.warn("Batch compress: ALL entries phantom", {
6209
- totalCount: preparedPlans.length,
6210
- details: partition.details
6211
- });
6212
- throw new Error(buildPhantomErrorMessage(partition.details));
6213
- }
6214
- if (partition.kind === "partial") {
6215
- ctx.logger.warn("Batch compress: phantom entries detected", {
6216
- phantomCount: partition.dropIndices.length,
6217
- totalCount: preparedPlans.length,
6218
- phantomIndices: partition.dropIndices,
6219
- details: partition.details
6220
- });
6221
- const dropSet = new Set(partition.dropIndices);
6222
- preparedPlans = preparedPlans.filter((_, i) => !dropSet.has(i));
6223
- phantomSkipNotice = partition.notice;
6224
- }
6225
- const acknowledgeRisk = args.acknowledgeRisk === true;
6226
- const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
6227
- const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
6228
- const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
6229
- if (ignoredAcknowledgeRisk) {
6230
- ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
6231
- }
6232
- ctx.state.qualityGateRetryPending = false;
6233
- if (!bypassQuality) {
6234
- for (const plan of preparedPlans) {
6235
- const result = evaluatePreCommitQuality(
6236
- rawMessages,
6237
- plan.selection.messageIds,
6238
- plan.selection.messageTokenById,
6239
- plan.finalSummary,
6240
- ctx.config,
6241
- ctx.logger
6242
- );
6243
- if (result && !result.passed) {
6244
- ctx.state.qualityGateRetryPending = true;
6245
- throw buildQualityRejectionError(
6246
- {
6247
- startId: plan.entry.startId,
6248
- endId: plan.entry.endId,
6249
- summary: plan.finalSummary,
6250
- messageIds: plan.selection.messageIds,
6251
- messageTokenById: plan.selection.messageTokenById
6252
- },
6253
- result
6254
- );
6255
- }
6256
- }
6257
- }
6258
- const snapshot = snapshotCompressionState(ctx.state);
6259
- const runId = allocateRunId(ctx.state);
6260
- try {
6261
- for (const preparedPlan of preparedPlans) {
6262
- const blockId = allocateBlockId(ctx.state);
6263
- const keepResult = resolveKeepMarkers(
6264
- preparedPlan.finalSummary,
6265
- rawMessages,
6266
- ctx.state,
6267
- ctx.config
6268
- );
6269
- preparedPlan.finalSummary = keepResult.summary;
6270
- const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
6271
- const summaryTokens = countTokens2(storedSummary);
6272
- const applied = applyCompressionState(
6273
- ctx.state,
6274
- {
6275
- topic: preparedPlan.entry.topic ?? input.topic ?? "",
6276
- batchTopic: input.topic,
6277
- startId: preparedPlan.entry.startId,
6278
- endId: preparedPlan.entry.endId,
6279
- mode: "range",
6280
- runId,
6281
- compressMessageId: toolCtx.messageID,
6282
- compressCallId: callId,
6283
- summaryTokens
6284
- },
6285
- preparedPlan.selection,
6286
- preparedPlan.anchorMessageId,
6287
- blockId,
6288
- storedSummary,
6289
- preparedPlan.consumedBlockIds,
6290
- ctx.config.gc
6291
- );
6292
- totalCompressedMessages += applied.messageIds.length;
6293
- notifications.push({
6294
- blockId,
6295
- runId,
6296
- summary: preparedPlan.finalSummary,
6297
- summaryTokens
6298
- });
6299
- }
6300
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
6301
- } catch (error) {
6302
- restoreCompressionState(ctx.state, snapshot);
6303
- ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
6304
- throw error;
6305
- }
6306
- const skippedNote = phantomSkipNotice !== null ? `
6307
- \u26A0\uFE0F ${phantomSkipNotice}
6308
- ` : "";
6309
- const ackNote = ignoredAcknowledgeRisk ? `
6310
- \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.
6311
- ` : "";
6312
- return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}${ackNote}
6313
- 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.
6314
- \u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
6315
- }
6316
- });
6317
- }
6318
- function extractBoundaryConsumedBlocks2(startReference, endReference) {
6319
- const consumed = [];
6320
- const seen = /* @__PURE__ */ new Set();
6321
- for (const ref of [startReference, endReference]) {
6322
- if (ref.kind === "compressed-block" && ref.blockId !== void 0 && !seen.has(ref.blockId)) {
6323
- seen.add(ref.blockId);
6324
- consumed.push(ref.blockId);
6325
6259
  }
6326
6260
  }
6327
- return consumed;
6261
+ return lines.join("\n");
6328
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}
6329
6275
 
6330
- // lib/compress/decompress.ts
6331
- import { tool as tool3 } from "@opencode-ai/plugin";
6276
+ ${guidance}
6277
+ ${afterClose}`;
6278
+ }
6332
6279
 
6333
- // lib/messages/prune.ts
6334
- var prune = (state, logger, config, messages) => {
6335
- filterCompressedRanges(state, messages);
6336
- stripStepMarkers(messages);
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}`;
6337
6288
  };
6338
- var MAX_STEP_FINISH_REASON = 50;
6339
- var stripStepMarkers = (messages) => {
6340
- for (const msg of messages) {
6341
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
6342
- let changed = false;
6343
- const filtered = [];
6344
- for (const part of parts) {
6345
- if (part.type === "step-start") {
6346
- changed = true;
6347
- continue;
6348
- }
6349
- if (part.type === "step-finish") {
6350
- const reason = part.reason;
6351
- if (typeof reason === "string" && reason.length > MAX_STEP_FINISH_REASON) {
6352
- const truncated = reason.slice(0, MAX_STEP_FINISH_REASON) + "...";
6353
- if (truncated !== reason) {
6354
- filtered.push({ ...part, reason: truncated });
6355
- changed = true;
6356
- continue;
6357
- }
6358
- }
6359
- }
6360
- filtered.push(part);
6361
- }
6362
- if (changed) {
6363
- msg.parts = filtered;
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
6364
6303
  }
6365
- }
6366
- };
6367
- var filterCompressedRanges = (state, messages) => {
6368
- if (state.prune.messages.byMessageId.size === 0) {
6369
- return;
6370
- }
6371
- const survive = messages.map((msg) => {
6372
- const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id);
6373
- if (!pruneEntry || pruneEntry.activeBlockIds.length === 0) {
6374
- return true;
6375
- }
6376
- return false;
6377
- });
6378
- const firstUserIdx = messages.findIndex((msg) => msg.info.role === "user");
6379
- if (firstUserIdx >= 0) {
6380
- survive[firstUserIdx] = true;
6381
- }
6382
- const result = [];
6383
- for (let i = 0; i < messages.length; i++) {
6384
- if (survive[i]) {
6385
- result.push(messages[i]);
6386
- }
6387
- }
6388
- messages.length = 0;
6389
- messages.push(...result);
6390
- };
6391
-
6392
- // lib/messages/sync.ts
6393
- function sortBlocksByCreation(a, b) {
6394
- const createdAtDiff = a.createdAt - b.createdAt;
6395
- if (createdAtDiff !== 0) {
6396
- return createdAtDiff;
6397
- }
6398
- return a.blockId - b.blockId;
6399
- }
6400
- var syncCompressionBlocks = (state, logger, messages) => {
6401
- const messagesState = state.prune.messages;
6402
- if (!messagesState?.blocksById?.size) {
6403
- return;
6404
- }
6405
- const messageIds = new Set(messages.map((msg) => msg.info.id));
6406
- const previousActiveBlockIds = new Set(
6407
- Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
6408
- );
6409
- messagesState.activeBlockIds.clear();
6410
- messagesState.activeByAnchorMessageId.clear();
6411
- const now = Date.now();
6412
- const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
6413
- for (const block of orderedBlocks) {
6414
- if (block.deactivatedByUser || block.deactivatedByUserDeep) {
6415
- block.active = false;
6416
- if (block.deactivatedAt === void 0) {
6417
- block.deactivatedAt = now;
6418
- }
6419
- block.deactivatedByBlockId = void 0;
6420
- continue;
6421
- }
6422
- for (const consumedBlockId of block.consumedBlockIds) {
6423
- if (!messagesState.activeBlockIds.has(consumedBlockId)) {
6424
- continue;
6425
- }
6426
- const consumedBlock = messagesState.blocksById.get(consumedBlockId);
6427
- if (consumedBlock) {
6428
- consumedBlock.active = false;
6429
- consumedBlock.deactivatedAt = now;
6430
- consumedBlock.deactivatedByBlockId = block.blockId;
6431
- const mappedBlockId = messagesState.activeByAnchorMessageId.get(
6432
- consumedBlock.anchorMessageId
6433
- );
6434
- if (mappedBlockId === consumedBlock.blockId) {
6435
- messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
6436
- }
6437
- }
6438
- messagesState.activeBlockIds.delete(consumedBlockId);
6439
- }
6440
- block.active = true;
6441
- block.deactivatedAt = void 0;
6442
- block.deactivatedByBlockId = void 0;
6443
- messagesState.activeBlockIds.add(block.blockId);
6444
- if (messageIds.has(block.anchorMessageId)) {
6445
- messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
6446
- }
6447
- }
6448
- for (const entry of messagesState.byMessageId.values()) {
6449
- const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
6450
- entry.allBlockIds = allBlockIds;
6451
- entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
6452
- }
6453
- const nextActiveBlockIds = messagesState.activeBlockIds;
6454
- let deactivatedCount = 0;
6455
- let reactivatedCount = 0;
6456
- for (const blockId of previousActiveBlockIds) {
6457
- if (!nextActiveBlockIds.has(blockId)) {
6458
- deactivatedCount++;
6459
- }
6460
- }
6461
- for (const blockId of nextActiveBlockIds) {
6462
- if (!previousActiveBlockIds.has(blockId)) {
6463
- reactivatedCount++;
6464
- }
6465
- }
6466
- if (deactivatedCount > 0 || reactivatedCount > 0) {
6467
- logger.info("Synced compress block state", {
6468
- deactivatedCount,
6469
- reactivatedCount
6470
- });
6471
- }
6472
- };
6473
-
6474
- // lib/host-permissions.ts
6475
- var findLastMatchingRule = (rules, predicate) => {
6476
- for (let index = rules.length - 1; index >= 0; index -= 1) {
6477
- const rule = rules[index];
6478
- if (rule && predicate(rule)) {
6479
- return rule;
6480
- }
6481
- }
6482
- return void 0;
6483
- };
6484
- var wildcardMatch = (value, pattern) => {
6485
- const normalizedValue = value.replaceAll("\\", "/");
6486
- let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
6487
- if (escaped.endsWith(" .*")) {
6488
- escaped = escaped.slice(0, -3) + "( .*)?";
6489
- }
6490
- const flags = process.platform === "win32" ? "si" : "s";
6491
- return new RegExp(`^${escaped}$`, flags).test(normalizedValue);
6492
- };
6493
- var getPermissionRules = (permissionConfigs) => {
6494
- const rules = [];
6495
- for (const permissionConfig of permissionConfigs) {
6496
- if (!permissionConfig) {
6497
- continue;
6498
- }
6499
- for (const [permission, value] of Object.entries(permissionConfig)) {
6500
- if (value === "ask" || value === "allow" || value === "deny") {
6501
- rules.push({ permission, pattern: "*", action: value });
6502
- continue;
6503
- }
6504
- for (const [pattern, action] of Object.entries(value)) {
6505
- if (action === "ask" || action === "allow" || action === "deny") {
6506
- rules.push({ permission, pattern, action });
6507
- }
6508
- }
6509
- }
6510
- }
6511
- return rules;
6512
- };
6513
- var compressDisabledByOpencode = (...permissionConfigs) => {
6514
- const match = findLastMatchingRule(
6515
- getPermissionRules(permissionConfigs),
6516
- (rule) => wildcardMatch("compress", rule.permission)
6517
- );
6518
- return match?.pattern === "*" && match.action === "deny";
6519
- };
6520
- var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agentName) => {
6521
- if (basePermission === "deny") {
6522
- return "deny";
6523
- }
6524
- return compressDisabledByOpencode(
6525
- hostPermissions.global,
6526
- agentName ? hostPermissions.agents[agentName] : void 0
6527
- ) ? "deny" : basePermission;
6528
- };
6529
- var hasExplicitToolPermission = (permissionConfig, tool6) => {
6530
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool6) : false;
6531
- };
6532
-
6533
- // lib/compress-permission.ts
6534
- var compressPermission = (state, config) => {
6535
- return state.compressPermission ?? config.compress.permission;
6536
- };
6537
- var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
6538
- const activeAgent = getLastUserMessage(messages)?.info.agent;
6539
- state.compressPermission = resolveEffectiveCompressPermission(
6540
- config.compress.permission,
6541
- hostPermissions,
6542
- activeAgent
6543
- );
6544
- };
6545
-
6546
- // lib/messages/utils.ts
6547
- import { createHash } from "crypto";
6548
- var SUMMARY_ID_HASH_LENGTH = 16;
6549
- var DCP_PAIRED_TAG_REGEX = /<(?:dcp|acp)[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
6550
- var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
6551
- var generateStableId = (prefix, seed) => {
6552
- const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
6553
- return `${prefix}_${hash}`;
6554
- };
6555
- var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
6556
- const baseInfo = baseMessage.info;
6557
- const now = Date.now();
6558
- const deterministicSeed = stableSeed?.trim() || baseInfo.id;
6559
- const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
6560
- const partId = generateStableId("prt_dcp_summary", deterministicSeed);
6561
- const parts = [
6562
- {
6563
- id: partId,
6564
- sessionID: baseInfo.sessionID,
6565
- messageID: messageId,
6566
- type: "text",
6567
- text: content,
6568
- synthetic: true
6569
- }
6570
- ];
6571
- if (role === "assistant") {
6572
- const isAssistant = baseInfo.role === "assistant";
6573
- const assistantBase = isAssistant ? baseInfo : void 0;
6574
- const userModel = !isAssistant ? baseInfo.model : void 0;
6575
- const info2 = {
6576
- id: messageId,
6577
- sessionID: baseInfo.sessionID,
6578
- role: "assistant",
6579
- time: { created: now },
6580
- parentID: assistantBase?.parentID ?? "",
6581
- modelID: assistantBase?.modelID ?? userModel?.modelID ?? "",
6582
- providerID: assistantBase?.providerID ?? userModel?.providerID ?? "",
6583
- mode: assistantBase?.mode ?? "code",
6584
- agent: baseInfo.agent ?? "code",
6585
- path: assistantBase?.path ?? { cwd: "", root: "" },
6586
- cost: 0,
6587
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
6588
- };
6589
- return { info: info2, parts };
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 };
6590
6324
  }
6591
6325
  const userInfo = baseInfo;
6592
6326
  const info = {
@@ -6717,67 +6451,6 @@ var dropEmptyMessages = (messages) => {
6717
6451
  return removed;
6718
6452
  };
6719
6453
 
6720
- // lib/prompts/extensions/nudge.ts
6721
- function buildCompressedBlockGuidance(state, context) {
6722
- const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
6723
- const blockCount = activeBlockIds.length;
6724
- const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6725
- const totalSummaryTokens = blocksForStats.reduce((s, b) => s + (b.summaryTokens ?? 0), 0);
6726
- const totalSummaryDisplay = totalSummaryTokens >= 1e3 ? `${(totalSummaryTokens / 1e3).toFixed(1)}K` : String(totalSummaryTokens);
6727
- const lastBlock = blocksForStats.length > 0 ? blocksForStats.reduce((latest, b) => b.createdAt > latest.createdAt ? b : latest) : null;
6728
- const ageStr = lastBlock ? formatAge(lastBlock.createdAt) : "never";
6729
- const lines = [
6730
- `- Compressed blocks: ${blockCount} (${totalSummaryDisplay} summary, last ${ageStr}). Use acp_status for details.`
6731
- ];
6732
- if (blockCount > 50) {
6733
- const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
6734
- const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
6735
- const visibleMessageIds = context?.visibleMessageIds;
6736
- const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
6737
- if (visibleOldBlocks.length > 5) {
6738
- const blocksWithRef = visibleOldBlocks.map((block) => {
6739
- const ref = state.messageIds.byRawId.get(block.anchorMessageId);
6740
- return ref ? { block, ref } : null;
6741
- }).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
6742
- const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6743
- const totalK = Math.max(1, Math.round(totalTokens / 1e3));
6744
- const targets = [];
6745
- const chunkSize = Math.ceil(blocksWithRef.length / 3);
6746
- for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
6747
- const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
6748
- if (chunk.length < 2) continue;
6749
- const startRef = chunk[0].ref;
6750
- const endRef = chunk[chunk.length - 1].ref;
6751
- const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6752
- const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
6753
- targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
6754
- }
6755
- if (targets.length > 0) {
6756
- lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
6757
- lines.push(...targets);
6758
- lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write your summary normally.`);
6759
- }
6760
- }
6761
- }
6762
- return lines.join("\n");
6763
- }
6764
- function appendGuidanceToDcpTag(nudgeText, guidance) {
6765
- if (!guidance.trim()) {
6766
- return nudgeText;
6767
- }
6768
- const closeTag = "</dcp-system-reminder>";
6769
- const closeTagIndex = nudgeText.lastIndexOf(closeTag);
6770
- if (closeTagIndex === -1) {
6771
- return nudgeText;
6772
- }
6773
- const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd();
6774
- const afterClose = nudgeText.slice(closeTagIndex);
6775
- return `${beforeClose}
6776
-
6777
- ${guidance}
6778
- ${afterClose}`;
6779
- }
6780
-
6781
6454
  // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
6782
6455
  var NUDGE_GROWTH_FLOOR = 6e3;
6783
6456
  var NUDGE_GROWTH_CAP = 5e4;
@@ -6898,6 +6571,12 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6898
6571
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6899
6572
  return Math.round(clampedPercent / 100 * state.modelContextLimit);
6900
6573
  };
6574
+ if (threshold === "max") {
6575
+ const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
6576
+ if (nestedLimit !== void 0) {
6577
+ return parseLimitValue(nestedLimit);
6578
+ }
6579
+ }
6901
6580
  const modelLimits = threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits;
6902
6581
  if (modelLimits && providerId !== void 0 && modelId !== void 0) {
6903
6582
  const providerModelId = `${providerId}/${modelId}`;
@@ -6957,6 +6636,52 @@ function computeShouldNudge2(params) {
6957
6636
  return policy.computeShouldNudge(params);
6958
6637
  }
6959
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 {};
6643
+ }
6644
+ const providerEntry = config.compress?.providers?.[providerId];
6645
+ if (!providerEntry) {
6646
+ return {};
6647
+ }
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
+ }
6960
6685
  function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
6961
6686
  if (anchorMessageIndex < 0) {
6962
6687
  return false;
@@ -7149,337 +6874,872 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
7149
6874
  if (!msgToolName) msgToolName = toolName;
7150
6875
  }
7151
6876
  }
7152
- if (isProtected && !isSummary) {
7153
- protectedTokens += msgTotal;
7154
- }
7155
- if (!isSummary) {
7156
- const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
7157
- if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
7158
- if (msgTool > 500) perTool.push({ ref, tokens: msgTool, tool: msgToolName });
7159
- if (msgCode > 300) perCode.push({ ref, tokens: msgCode });
7160
- if (msgText > 500 && msgCode === 0) perText.push({ ref, tokens: msgText });
6877
+ if (isProtected && !isSummary) {
6878
+ protectedTokens += msgTotal;
6879
+ }
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 });
6886
+ }
6887
+ }
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
+ };
6909
+ }
6910
+ function refNum(ref) {
6911
+ const n = parseInt(ref.slice(1), 10);
6912
+ return Number.isNaN(n) ? -1 : n;
6913
+ }
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] });
6950
+ continue;
6951
+ }
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
+ }
6964
+ }
6965
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
6966
+ lastUserRefIdx.length = 0;
6967
+ lastUserRefIdx.push(msgInfo.length);
6968
+ }
6969
+ msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
6970
+ }
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;
6975
+ }
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;
6987
+ }
6988
+ const hasGap = info.refNum > prevRefNum + 1;
6989
+ if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
6990
+ groups.push(cur);
6991
+ cur = null;
6992
+ }
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;
7015
+ }
7016
+ }
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);
7042
+ }
7043
+ }
7044
+ }
7045
+ if (pcur) protectedGroups.push(pcur);
7046
+ return {
7047
+ compressible: groups.filter((g) => g.tokens > 0),
7048
+ protected: protectedGroups
7049
+ };
7050
+ }
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;
7055
+ }
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 [];
7062
+ }
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;
7081
+ }
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")}`;
7094
+ }
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
+ });
7113
+ }
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
+ });
7131
+ }
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);
7148
+ }
7149
+ } else {
7150
+ merged.push({ ...entry });
7151
+ }
7152
+ }
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}`;
7157
+ }
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}`;
7160
+ }
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")}`;
7165
+ }
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
+ }
7185
+ }
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);
7191
+ }
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;
7198
+ }
7199
+ }
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)
7206
+ );
7207
+ }
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
+ };
7239
+ }
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
+ );
7259
+ }
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);
7315
+ }
7316
+ }
7317
+ }
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;
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.`;
7513
+ }
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);
7161
7523
  }
7162
7524
  }
7163
- perMessage.sort((a, b) => b.tokens - a.tokens);
7164
- perTool.sort((a, b) => b.tokens - a.tokens);
7165
- perCode.sort((a, b) => b.tokens - a.tokens);
7166
- perText.sort((a, b) => b.tokens - a.tokens);
7167
- const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens);
7168
- const systemTokens = state?.systemPromptTokens !== void 0 && state.systemPromptTokens > 0 ? state.systemPromptTokens : estimateSystemPromptTokens(messages);
7169
- return {
7170
- toolTokens,
7171
- codeTokens,
7172
- summaryTokens,
7173
- messageTokens,
7174
- textTokens: Math.max(0, messageTokens - codeTokens),
7175
- systemTokens,
7176
- protectedTokens,
7177
- total: systemTokens + toolTokens + summaryTokens + messageTokens,
7178
- largestRanges: perMessage.slice(0, 15),
7179
- largestToolRanges: perTool.slice(0, 15),
7180
- largestCodeRanges: perCode.slice(0, 5),
7181
- largestMessageRanges: perText.slice(0, 5),
7182
- toolTypeBreakdown
7183
- };
7184
- }
7185
- function refNum(ref) {
7186
- const n = parseInt(ref.slice(1), 10);
7187
- return Number.isNaN(n) ? -1 : n;
7525
+ return consumed;
7188
7526
  }
7189
- function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = [], protectedZoneRefs) {
7190
- const msgInfo = [];
7191
- const protectedMsgInfo = [];
7192
- const lastUserRefIdx = [];
7193
- for (let mi = 0; mi < messages.length; mi++) {
7194
- const msg = messages[mi];
7195
- if (isSyntheticMessage(msg)) continue;
7196
- const ref = state.messageIds.byRawId.get(msg.info.id);
7197
- if (!ref) continue;
7198
- const rn = parseInt(ref.slice(1), 10);
7199
- if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
7200
- let tokens2 = 0;
7201
- const tools = /* @__PURE__ */ new Set();
7202
- for (const part of msg.parts || []) {
7203
- if (part.type === "text" && typeof part.text === "string") {
7204
- tokens2 += Math.round(part.text.length / 4);
7205
- } else if (part.type !== "text" && part.type !== "reasoning") {
7206
- tokens2 += Math.round(JSON.stringify(part).length / 4);
7207
- const toolName = part?.tool;
7208
- const callID = part?.callID;
7209
- if (toolName && callID) {
7210
- if (isToolNameProtected(toolName, protectedTools)) {
7211
- tools.add(toolName);
7212
- } else if (protectedFilePatterns.length > 0) {
7213
- const filePaths = getFilePathsFromParameters(
7214
- toolName,
7215
- part?.state?.input
7216
- );
7217
- if (isFilePathProtected(filePaths, protectedFilePatterns)) {
7218
- tools.add(toolName);
7219
- }
7220
- }
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;
7221
7555
  }
7222
7556
  }
7223
7557
  }
7224
- protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
7225
- continue;
7558
+ filtered.push(part);
7226
7559
  }
7227
- let tokens = 0;
7228
- let isTool = false;
7229
- let hasMeaningfulPart = false;
7230
- for (const part of msg.parts || []) {
7231
- if (part.type === "text" && typeof part.text === "string") {
7232
- tokens += Math.round(part.text.length / 4);
7233
- if (part.text.trim().length > 0) hasMeaningfulPart = true;
7234
- } else if (part.type !== "text" && part.type !== "reasoning") {
7235
- tokens += Math.round(JSON.stringify(part).length / 4);
7236
- isTool = true;
7237
- hasMeaningfulPart = true;
7238
- }
7560
+ if (changed) {
7561
+ msg.parts = filtered;
7239
7562
  }
7240
- if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
7241
- lastUserRefIdx.length = 0;
7242
- lastUserRefIdx.push(msgInfo.length);
7563
+ }
7564
+ };
7565
+ var filterCompressedRanges = (state, messages) => {
7566
+ if (state.prune.messages.byMessageId.size === 0) {
7567
+ return;
7568
+ }
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;
7243
7573
  }
7244
- msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
7574
+ return false;
7575
+ });
7576
+ const firstUserIdx = messages.findIndex((msg) => msg.info.role === "user");
7577
+ if (firstUserIdx >= 0) {
7578
+ survive[firstUserIdx] = true;
7245
7579
  }
7246
- const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
7247
- for (let i = 0; i < msgInfo.length; i++) {
7248
- const info = msgInfo[i];
7249
- info.effectiveTokens = i !== lastUserIdx && info.meaningful ? info.tokens : 0;
7580
+ const result = [];
7581
+ for (let i = 0; i < messages.length; i++) {
7582
+ if (survive[i]) {
7583
+ result.push(messages[i]);
7584
+ }
7250
7585
  }
7251
- const groups = [];
7252
- let cur = null;
7253
- let prevRefNum = -2;
7254
- for (const info of msgInfo) {
7255
- if (protectedZoneRefs?.has(info.ref)) {
7256
- if (cur) {
7257
- groups.push(cur);
7258
- cur = 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;
7259
7616
  }
7260
- prevRefNum = info.refNum;
7617
+ block.deactivatedByBlockId = void 0;
7261
7618
  continue;
7262
7619
  }
7263
- const hasGap = info.refNum > prevRefNum + 1;
7264
- if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
7265
- groups.push(cur);
7266
- cur = null;
7267
- }
7268
- prevRefNum = info.refNum;
7269
- if (!cur) {
7270
- cur = {
7271
- startRef: info.ref,
7272
- endRef: info.ref,
7273
- count: 1,
7274
- tokens: info.tokens,
7275
- effectiveTokens: info.effectiveTokens,
7276
- toolPct: info.isTool ? 100 : 0,
7277
- textPct: info.isTool ? 0 : 100
7278
- };
7279
- } else {
7280
- cur.endRef = info.ref;
7281
- cur.count++;
7282
- cur.tokens += info.tokens;
7283
- cur.effectiveTokens += info.effectiveTokens;
7284
- if (info.isTool) {
7285
- cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
7286
- } else {
7287
- cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
7620
+ for (const consumedBlockId of block.consumedBlockIds) {
7621
+ if (!messagesState.activeBlockIds.has(consumedBlockId)) {
7622
+ continue;
7288
7623
  }
7289
- cur.textPct = 100 - cur.toolPct;
7290
- }
7291
- }
7292
- if (cur) groups.push(cur);
7293
- const protectedGroups = [];
7294
- let pcur = null;
7295
- let pPrevRefNum = -2;
7296
- for (const info of protectedMsgInfo) {
7297
- const hasGap = info.refNum > pPrevRefNum + 1;
7298
- if (pcur && hasGap) {
7299
- protectedGroups.push(pcur);
7300
- pcur = null;
7301
- }
7302
- pPrevRefNum = info.refNum;
7303
- if (!pcur) {
7304
- pcur = {
7305
- startRef: info.ref,
7306
- endRef: info.ref,
7307
- count: 1,
7308
- tokens: info.tokens,
7309
- tools: [...info.tools]
7310
- };
7311
- } else {
7312
- pcur.endRef = info.ref;
7313
- pcur.count++;
7314
- pcur.tokens += info.tokens;
7315
- for (const t of info.tools) {
7316
- if (!pcur.tools.includes(t)) pcur.tools.push(t);
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
+ }
7317
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);
7318
7644
  }
7319
7645
  }
7320
- if (pcur) protectedGroups.push(pcur);
7321
- return {
7322
- compressible: groups.filter((g) => g.tokens > 0),
7323
- protected: protectedGroups
7324
- };
7325
- }
7326
- var EFFECTIVE_MIN_COMPRESSIBLE_TOKENS = 1250;
7327
- function resolveEffectiveFloor(config) {
7328
- const minChars = config.compress?.minCompressRange ?? 5e3;
7329
- return minChars > 0 ? Math.floor(minChars / 4) : 0;
7330
- }
7331
- function filterRecommendedRanges(compressible, _protectedRanges, options) {
7332
- const { logger } = options;
7333
- const log = logger?.debug.bind(logger);
7334
- if (compressible.length === 0) {
7335
- log?.("filterRecommendedRanges: no compressible ranges, returning empty");
7336
- 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));
7337
7650
  }
7338
- const floor = options.minEffectiveTokens ?? EFFECTIVE_MIN_COMPRESSIBLE_TOKENS;
7339
- const kept = compressible.filter((r) => {
7340
- const effective = r.effectiveTokens ?? r.tokens;
7341
- return effective > 0 && effective >= floor;
7342
- });
7343
- const result = kept.map(
7344
- (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7345
- );
7346
- log?.("filterRecommendedRanges: effective-token floor applied", {
7347
- inputRanges: compressible.length,
7348
- outputRanges: result.length,
7349
- floor,
7350
- dropped: compressible.filter((r) => {
7351
- const effective = r.effectiveTokens ?? r.tokens;
7352
- return effective <= 0 || effective < floor;
7353
- }).map((r) => `${r.startRef}\u2013${r.endRef} (${r.effectiveTokens ?? r.tokens} eff tokens)`)
7354
- });
7355
- return result;
7356
- }
7357
- function formatCompressibleRanges(ranges, protectedRanges) {
7358
- const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
7359
- if (!protectedRanges || protectedRanges.length === 0) {
7360
- if (ranges.length === 0) return "";
7361
- const lines2 = ranges.map((r) => {
7362
- const eff = r.effectiveTokens ?? r.tokens;
7363
- const size = eff < r.tokens ? `${fmt(eff)} effective of ${fmt(r.tokens)}` : fmt(r.tokens);
7364
- const suffix = r.dangerous ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7365
- return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${size} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
7366
- });
7367
- return `Compressible ranges (oldest first):
7368
- ${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
+ }
7369
7658
  }
7370
- const entries = [];
7371
- for (const r of ranges) {
7372
- entries.push({
7373
- startRef: r.startRef,
7374
- endRef: r.endRef,
7375
- startNum: refNum(r.startRef),
7376
- endNum: refNum(r.endRef),
7377
- count: r.count,
7378
- tokens: r.tokens,
7379
- toolPct: r.toolPct,
7380
- textPct: r.textPct,
7381
- compressibleTokens: r.effectiveTokens ?? r.tokens,
7382
- compressibleCount: r.count,
7383
- protectedTokens: 0,
7384
- protectedCount: 0,
7385
- protectedTools: [],
7386
- dangerous: r.dangerous ?? false
7387
- });
7659
+ for (const blockId of nextActiveBlockIds) {
7660
+ if (!previousActiveBlockIds.has(blockId)) {
7661
+ reactivatedCount++;
7662
+ }
7388
7663
  }
7389
- for (const r of protectedRanges) {
7390
- entries.push({
7391
- startRef: r.startRef,
7392
- endRef: r.endRef,
7393
- startNum: refNum(r.startRef),
7394
- endNum: refNum(r.endRef),
7395
- count: r.count,
7396
- tokens: r.tokens,
7397
- toolPct: 0,
7398
- textPct: 0,
7399
- compressibleTokens: 0,
7400
- compressibleCount: 0,
7401
- protectedTokens: r.tokens,
7402
- protectedCount: r.count,
7403
- protectedTools: [...r.tools],
7404
- dangerous: false
7664
+ if (deactivatedCount > 0 || reactivatedCount > 0) {
7665
+ logger.info("Synced compress block state", {
7666
+ deactivatedCount,
7667
+ reactivatedCount
7405
7668
  });
7406
7669
  }
7407
- entries.sort((a, b) => a.startNum - b.startNum);
7408
- const merged = [];
7409
- for (const entry of entries) {
7410
- const last = merged[merged.length - 1];
7411
- if (last && entry.startNum <= last.endNum + 1) {
7412
- last.endRef = entry.endRef;
7413
- last.endNum = Math.max(last.endNum, entry.endNum);
7414
- last.count += entry.count;
7415
- last.tokens += entry.tokens;
7416
- last.compressibleTokens += entry.compressibleTokens;
7417
- last.compressibleCount += entry.compressibleCount;
7418
- last.protectedTokens += entry.protectedTokens;
7419
- last.protectedCount += entry.protectedCount;
7420
- if (entry.dangerous) last.dangerous = true;
7421
- for (const t of entry.protectedTools) {
7422
- if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
7423
- }
7424
- } else {
7425
- 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;
7426
7678
  }
7427
7679
  }
7428
- const lines = merged.map((e) => {
7429
- const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7430
- if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
7431
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
7432
- }
7433
- if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
7434
- 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;
7435
7696
  }
7436
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
7437
- });
7438
- return `Compressible ranges (oldest first):
7439
- ${lines.join("\n")}`;
7440
- }
7441
- function computeProtectedRefs(messages, state, compress) {
7442
- if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
7443
- const preserveN = compress.preserveRecentMessages ?? 5;
7444
- const preserveTokens = compress.preserveRecentTokens ?? 5e3;
7445
- const result = /* @__PURE__ */ new Set();
7446
- const visible = [];
7447
- for (const msg of messages) {
7448
- if (isSyntheticMessage(msg)) continue;
7449
- if (isIgnoredUserMessage(msg)) continue;
7450
- const ref = state.messageIds.byRawId.get(msg.info.id);
7451
- if (!ref) continue;
7452
- if (state.prune.messages.byMessageId.has(msg.info.id)) continue;
7453
- let tokens = 0;
7454
- for (const part of msg.parts || []) {
7455
- if (part.type === "text" && typeof part.text === "string") {
7456
- tokens += Math.round(part.text.length / 4);
7457
- } else if (part.type !== "text" && part.type !== "reasoning") {
7458
- 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
+ }
7459
7706
  }
7460
- }
7461
- visible.push({ ref, tokens, isUser: msg.info.role === "user" });
7462
- }
7463
- if (preserveN > 0) {
7464
- for (const m of visible.slice(-preserveN)) {
7465
- result.add(m.ref);
7466
7707
  }
7467
7708
  }
7468
- if (preserveTokens > 0) {
7469
- let tokenAccum = 0;
7470
- for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
7471
- result.add(visible[i].ref);
7472
- tokenAccum += visible[i].tokens;
7473
- }
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";
7474
7721
  }
7475
- return result;
7476
- }
7477
- function excludeProtectedRanges(ranges, protectedRefs) {
7478
- if (protectedRefs.size === 0) return ranges;
7479
- return ranges.filter(
7480
- (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
7481
7741
  );
7482
- }
7742
+ };
7483
7743
 
7484
7744
  // lib/messages/inject/inject.ts
7485
7745
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
@@ -7497,6 +7757,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7497
7757
  const lastMessage = findLastNonIgnoredMessage(messages);
7498
7758
  const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant");
7499
7759
  const { providerId, modelId } = getModelInfo(messages);
7760
+ config = applyCompressOverrides(config, providerId, modelId);
7500
7761
  const { overMaxLimit, overMinLimit, currentTokens, modelContextLimit } = isContextOverLimits(
7501
7762
  config,
7502
7763
  state,
@@ -7639,11 +7900,16 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7639
7900
  overMinLimit,
7640
7901
  overMaxLimit,
7641
7902
  lastNudgeTokens: growthReference,
7642
- minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 5,
7903
+ minNudgeContextPercent: resolveMinNudgeContextPercent(config, providerId, modelId) ?? DEFAULT_MIN_NUDGE_CONTEXT_PERCENT,
7643
7904
  nudgeGrowthTokens: effectiveThreshold
7644
7905
  });
7645
7906
  const growthSinceBaseline = currentTokens !== void 0 && growthReference !== void 0 ? currentTokens - growthReference : void 0;
7646
- const minNudgeFloorTokens = modelContextLimit !== void 0 ? Math.round((config.compress?.minNudgeContextPercent ?? 5) / 100 * modelContextLimit) : void 0;
7907
+ const minNudgeFloorTokens = resolveMinNudgeFloorTokens(
7908
+ config,
7909
+ modelContextLimit,
7910
+ providerId,
7911
+ modelId
7912
+ );
7647
7913
  const overMinNudgeFloor = minNudgeFloorTokens === void 0 || currentTokens === void 0 || currentTokens >= minNudgeFloorTokens;
7648
7914
  const nudgeAllowed = emergencyOverride || decision.shouldNudge && (overMaxLimit || overMinNudgeFloor) && growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
7649
7915
  const effectiveTipsVariant = emergencyOverride ? "maxLimit" : decision.tipsVariant;
@@ -9112,7 +9378,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9112
9378
  import { join as join3 } from "path";
9113
9379
  import { existsSync as existsSync3 } from "fs";
9114
9380
  import { homedir as homedir3 } from "os";
9115
- var LOG_VERSION = true ? "1.14.26-pr.353.87" : "dev";
9381
+ var LOG_VERSION = true ? "1.14.26" : "dev";
9116
9382
  var LEVEL_RANK = {
9117
9383
  debug: 10,
9118
9384
  info: 20,
@@ -11553,7 +11819,7 @@ var server = (async (ctx) => {
11553
11819
  }
11554
11820
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11555
11821
  logger.info("ACP plugin initialized", {
11556
- version: true ? "1.14.26-pr.353.87" : "dev",
11822
+ version: true ? "1.14.26" : "dev",
11557
11823
  workspace: ctx.directory,
11558
11824
  logLevel: logger.level,
11559
11825
  debug: config.debug,