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/README.md +42 -0
- package/README.zh-CN.md +42 -0
- package/dist/index.js +1197 -931
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +24 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +49 -1
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/package.json +1 -1
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/
|
|
6017
|
-
function
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
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
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
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
|
|
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
|
-
|
|
6331
|
-
|
|
6276
|
+
${guidance}
|
|
6277
|
+
${afterClose}`;
|
|
6278
|
+
}
|
|
6332
6279
|
|
|
6333
|
-
// lib/messages/
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
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
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
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
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
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
|
-
|
|
7225
|
-
continue;
|
|
7558
|
+
filtered.push(part);
|
|
7226
7559
|
}
|
|
7227
|
-
|
|
7228
|
-
|
|
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
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
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
|
-
|
|
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
|
|
7247
|
-
for (let i = 0; i <
|
|
7248
|
-
|
|
7249
|
-
|
|
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
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
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
|
-
|
|
7617
|
+
block.deactivatedByBlockId = void 0;
|
|
7261
7618
|
continue;
|
|
7262
7619
|
}
|
|
7263
|
-
const
|
|
7264
|
-
|
|
7265
|
-
|
|
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
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
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
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
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
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
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
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
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
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
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
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
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
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
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
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
}
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
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
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
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
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
11822
|
+
version: true ? "1.14.26" : "dev",
|
|
11557
11823
|
workspace: ctx.directory,
|
|
11558
11824
|
logLevel: logger.level,
|
|
11559
11825
|
debug: config.debug,
|