opencode-acp 1.16.0 → 1.17.0-pr.383.130
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 +28 -0
- package/README.zh-CN.md +27 -0
- package/dist/index.js +445 -80
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +16 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/gc/merge.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/enforce-budget.d.ts +65 -0
- package/dist/lib/messages/enforce-budget.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +1 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/query.d.ts +16 -0
- package/dist/lib/messages/query.d.ts.map +1 -1
- package/dist/lib/messages/truncate-tools.d.ts +1 -0
- package/dist/lib/messages/truncate-tools.d.ts.map +1 -1
- package/dist/lib/prompts/system.d.ts +1 -1
- package/dist/lib/prompts/system.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts +2 -0
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +6 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts +16 -0
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -890,6 +890,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
890
890
|
"compress.modelMaxLimits",
|
|
891
891
|
"compress.modelMinLimits",
|
|
892
892
|
"compress.providers",
|
|
893
|
+
"compress.contextLimitFallback",
|
|
893
894
|
"compress.nudgeFrequency",
|
|
894
895
|
"compress.minNudgeContextPercent",
|
|
895
896
|
"compress.nudgeGrowthTokens",
|
|
@@ -913,6 +914,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
913
914
|
"compress.reasoning",
|
|
914
915
|
"compress.reasoning.drop",
|
|
915
916
|
"compress.reasoning.threshold",
|
|
917
|
+
"compress.completionReserveTokens",
|
|
916
918
|
"gc",
|
|
917
919
|
"gc.algorithm",
|
|
918
920
|
"gc.promotionThreshold",
|
|
@@ -964,7 +966,11 @@ function validateConfigTypes(config) {
|
|
|
964
966
|
errors.push({ key: "storagePath", expected: "string", actual: typeof config.storagePath });
|
|
965
967
|
}
|
|
966
968
|
if (config.allowSubAgents !== void 0 && typeof config.allowSubAgents !== "boolean") {
|
|
967
|
-
errors.push({
|
|
969
|
+
errors.push({
|
|
970
|
+
key: "allowSubAgents",
|
|
971
|
+
expected: "boolean",
|
|
972
|
+
actual: typeof config.allowSubAgents
|
|
973
|
+
});
|
|
968
974
|
}
|
|
969
975
|
if (config.pruneNotification !== void 0) {
|
|
970
976
|
const validValues = ["off", "minimal", "detailed"];
|
|
@@ -1293,6 +1299,20 @@ function validateConfigTypes(config) {
|
|
|
1293
1299
|
}
|
|
1294
1300
|
}
|
|
1295
1301
|
}
|
|
1302
|
+
if (compress.completionReserveTokens !== void 0 && typeof compress.completionReserveTokens !== "number") {
|
|
1303
|
+
errors.push({
|
|
1304
|
+
key: "compress.completionReserveTokens",
|
|
1305
|
+
expected: "number",
|
|
1306
|
+
actual: typeof compress.completionReserveTokens
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
if (typeof compress.completionReserveTokens === "number" && compress.completionReserveTokens < 0) {
|
|
1310
|
+
errors.push({
|
|
1311
|
+
key: "compress.completionReserveTokens",
|
|
1312
|
+
expected: "non-negative number (>= 0)",
|
|
1313
|
+
actual: `${compress.completionReserveTokens}`
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1296
1316
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1297
1317
|
errors.push({
|
|
1298
1318
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1411,12 +1431,20 @@ function validateConfigTypes(config) {
|
|
|
1411
1431
|
break;
|
|
1412
1432
|
case "nudgeForce":
|
|
1413
1433
|
if (value !== "strong" && value !== "soft") {
|
|
1414
|
-
errors.push({
|
|
1434
|
+
errors.push({
|
|
1435
|
+
key,
|
|
1436
|
+
expected: "'strong' | 'soft'",
|
|
1437
|
+
actual: JSON.stringify(value)
|
|
1438
|
+
});
|
|
1415
1439
|
}
|
|
1416
1440
|
break;
|
|
1417
1441
|
case "stringArray":
|
|
1418
1442
|
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
1419
|
-
errors.push({
|
|
1443
|
+
errors.push({
|
|
1444
|
+
key,
|
|
1445
|
+
expected: "string[]",
|
|
1446
|
+
actual: JSON.stringify(value)
|
|
1447
|
+
});
|
|
1420
1448
|
}
|
|
1421
1449
|
break;
|
|
1422
1450
|
case "reasoningConfig":
|
|
@@ -1451,7 +1479,11 @@ function validateConfigTypes(config) {
|
|
|
1451
1479
|
return;
|
|
1452
1480
|
}
|
|
1453
1481
|
if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
|
|
1454
|
-
errors.push({
|
|
1482
|
+
errors.push({
|
|
1483
|
+
key: prefix,
|
|
1484
|
+
expected: "CompressModelOverrides",
|
|
1485
|
+
actual: typeof overrides
|
|
1486
|
+
});
|
|
1455
1487
|
return;
|
|
1456
1488
|
}
|
|
1457
1489
|
const model = overrides;
|
|
@@ -1484,7 +1516,11 @@ function validateConfigTypes(config) {
|
|
|
1484
1516
|
for (const [providerId, providerValue] of Object.entries(providers)) {
|
|
1485
1517
|
const prefix = `compress.providers.${providerId}`;
|
|
1486
1518
|
if (typeof providerValue !== "object" || providerValue === null || Array.isArray(providerValue)) {
|
|
1487
|
-
errors.push({
|
|
1519
|
+
errors.push({
|
|
1520
|
+
key: prefix,
|
|
1521
|
+
expected: "ProviderOverrides",
|
|
1522
|
+
actual: typeof providerValue
|
|
1523
|
+
});
|
|
1488
1524
|
continue;
|
|
1489
1525
|
}
|
|
1490
1526
|
const provider = providerValue;
|
|
@@ -1521,6 +1557,20 @@ function validateConfigTypes(config) {
|
|
|
1521
1557
|
}
|
|
1522
1558
|
};
|
|
1523
1559
|
validateProviderOverrides(compress.providers);
|
|
1560
|
+
if (compress.contextLimitFallback !== void 0 && typeof compress.contextLimitFallback !== "number") {
|
|
1561
|
+
errors.push({
|
|
1562
|
+
key: "compress.contextLimitFallback",
|
|
1563
|
+
expected: "number",
|
|
1564
|
+
actual: typeof compress.contextLimitFallback
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
if (typeof compress.contextLimitFallback === "number" && compress.contextLimitFallback < 0) {
|
|
1568
|
+
errors.push({
|
|
1569
|
+
key: "compress.contextLimitFallback",
|
|
1570
|
+
expected: "non-negative number (0 disables the fallback)",
|
|
1571
|
+
actual: `${compress.contextLimitFallback}`
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1524
1574
|
const validValues = ["ask", "allow", "deny"];
|
|
1525
1575
|
if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
|
|
1526
1576
|
errors.push({
|
|
@@ -1606,13 +1656,22 @@ function validateConfigTypes(config) {
|
|
|
1606
1656
|
});
|
|
1607
1657
|
} else {
|
|
1608
1658
|
if (gc.batchCleanup.lowThreshold !== void 0) {
|
|
1609
|
-
validateBatchThreshold(
|
|
1659
|
+
validateBatchThreshold(
|
|
1660
|
+
"gc.batchCleanup.lowThreshold",
|
|
1661
|
+
gc.batchCleanup.lowThreshold
|
|
1662
|
+
);
|
|
1610
1663
|
}
|
|
1611
1664
|
if (gc.batchCleanup.highThreshold !== void 0) {
|
|
1612
|
-
validateBatchThreshold(
|
|
1665
|
+
validateBatchThreshold(
|
|
1666
|
+
"gc.batchCleanup.highThreshold",
|
|
1667
|
+
gc.batchCleanup.highThreshold
|
|
1668
|
+
);
|
|
1613
1669
|
}
|
|
1614
1670
|
if (gc.batchCleanup.forceThreshold !== void 0) {
|
|
1615
|
-
validateBatchThreshold(
|
|
1671
|
+
validateBatchThreshold(
|
|
1672
|
+
"gc.batchCleanup.forceThreshold",
|
|
1673
|
+
gc.batchCleanup.forceThreshold
|
|
1674
|
+
);
|
|
1616
1675
|
}
|
|
1617
1676
|
}
|
|
1618
1677
|
}
|
|
@@ -1702,6 +1761,7 @@ var defaultConfig = {
|
|
|
1702
1761
|
summaryBuffer: true,
|
|
1703
1762
|
maxContextLimit: "80%",
|
|
1704
1763
|
minContextLimit: "80%",
|
|
1764
|
+
contextLimitFallback: 128e3,
|
|
1705
1765
|
nudgeFrequency: 5,
|
|
1706
1766
|
minNudgeContextPercent: 5,
|
|
1707
1767
|
iterationNudgeThreshold: 15,
|
|
@@ -1872,6 +1932,7 @@ function mergeCompress(base, override) {
|
|
|
1872
1932
|
modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
|
|
1873
1933
|
modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
|
|
1874
1934
|
providers: mergeProviderOverrides(base.providers, override.providers),
|
|
1935
|
+
contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
|
|
1875
1936
|
nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
|
|
1876
1937
|
minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
|
|
1877
1938
|
nudgeGrowthTokens: override.nudgeGrowthTokens,
|
|
@@ -1895,7 +1956,8 @@ function mergeCompress(base, override) {
|
|
|
1895
1956
|
reasoning: {
|
|
1896
1957
|
drop: override.reasoning?.drop ?? base.reasoning?.drop ?? DEFAULT_COMPRESS_REASONING.drop,
|
|
1897
1958
|
threshold: override.reasoning?.threshold ?? base.reasoning?.threshold ?? DEFAULT_COMPRESS_REASONING.threshold
|
|
1898
|
-
}
|
|
1959
|
+
},
|
|
1960
|
+
completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
|
|
1899
1961
|
};
|
|
1900
1962
|
}
|
|
1901
1963
|
function mergeCommands(base, override) {
|
|
@@ -1933,10 +1995,9 @@ function deepCloneConfig(config) {
|
|
|
1933
1995
|
...provider,
|
|
1934
1996
|
...provider.models ? {
|
|
1935
1997
|
models: Object.fromEntries(
|
|
1936
|
-
Object.entries(provider.models).map(
|
|
1937
|
-
modelId,
|
|
1938
|
-
|
|
1939
|
-
])
|
|
1998
|
+
Object.entries(provider.models).map(
|
|
1999
|
+
([modelId, model]) => [modelId, { ...model }]
|
|
2000
|
+
)
|
|
1940
2001
|
)
|
|
1941
2002
|
} : {}
|
|
1942
2003
|
}
|
|
@@ -2010,8 +2071,14 @@ function mergeLayer(config, data) {
|
|
|
2010
2071
|
],
|
|
2011
2072
|
compress: mergeCompress(config.compress, data.compress),
|
|
2012
2073
|
gc: mergeGC(config.gc, data.gc),
|
|
2013
|
-
qualityGate: mergeQualityGate(
|
|
2014
|
-
|
|
2074
|
+
qualityGate: mergeQualityGate(
|
|
2075
|
+
config.qualityGate,
|
|
2076
|
+
data.qualityGate
|
|
2077
|
+
),
|
|
2078
|
+
messageFilters: mergeMessageFilters(
|
|
2079
|
+
config.messageFilters,
|
|
2080
|
+
data.messageFilters
|
|
2081
|
+
)
|
|
2015
2082
|
};
|
|
2016
2083
|
}
|
|
2017
2084
|
function scheduleParseWarning(ctx, title, message) {
|
|
@@ -2159,6 +2226,56 @@ var messageHasCompressAttempt = (message) => {
|
|
|
2159
2226
|
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
2160
2227
|
return parts.some((part) => part.type === "tool" && part.tool === "compress");
|
|
2161
2228
|
};
|
|
2229
|
+
var isCaptureOnlyCompress = (message) => {
|
|
2230
|
+
if (!isMessageWithInfo(message)) {
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
if (message.info.role !== "assistant") {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
2237
|
+
let sawBoundary = false;
|
|
2238
|
+
for (const part of parts) {
|
|
2239
|
+
if (!(part.type === "tool" && part.tool === "compress")) {
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
for (const startId of extractCompressBoundaryIds(part.state?.input)) {
|
|
2243
|
+
sawBoundary = true;
|
|
2244
|
+
if (/^b\d+$/i.test(startId)) {
|
|
2245
|
+
return false;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return sawBoundary;
|
|
2250
|
+
};
|
|
2251
|
+
function extractCompressBoundaryIds(rawInput) {
|
|
2252
|
+
let content = [];
|
|
2253
|
+
if (typeof rawInput === "string") {
|
|
2254
|
+
try {
|
|
2255
|
+
const parsed = JSON.parse(rawInput);
|
|
2256
|
+
const c = parsed?.content;
|
|
2257
|
+
content = Array.isArray(c) ? c : [];
|
|
2258
|
+
} catch {
|
|
2259
|
+
return [];
|
|
2260
|
+
}
|
|
2261
|
+
} else if (rawInput && typeof rawInput === "object") {
|
|
2262
|
+
const c = rawInput.content;
|
|
2263
|
+
content = Array.isArray(c) ? c : [];
|
|
2264
|
+
}
|
|
2265
|
+
const ids = [];
|
|
2266
|
+
for (const entry of content) {
|
|
2267
|
+
if (!entry || typeof entry !== "object") {
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
const { startId, endId } = entry;
|
|
2271
|
+
for (const sid of [startId, endId]) {
|
|
2272
|
+
if (typeof sid === "string" && sid.trim() !== "") {
|
|
2273
|
+
ids.push(sid.trim());
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return ids;
|
|
2278
|
+
}
|
|
2162
2279
|
var isIgnoredUserMessage = (message) => {
|
|
2163
2280
|
if (!isMessageWithInfo(message)) {
|
|
2164
2281
|
return false;
|
|
@@ -2635,6 +2752,16 @@ function resetOnCompaction(state) {
|
|
|
2635
2752
|
nextRef: 1
|
|
2636
2753
|
};
|
|
2637
2754
|
}
|
|
2755
|
+
function resolveEffectiveContextLimit(state, config) {
|
|
2756
|
+
if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
|
|
2757
|
+
return { limit: state.modelContextLimit, source: "model" };
|
|
2758
|
+
}
|
|
2759
|
+
const fallback = config.compress.contextLimitFallback;
|
|
2760
|
+
if (typeof fallback === "number" && fallback > 0) {
|
|
2761
|
+
return { limit: fallback, source: "fallback" };
|
|
2762
|
+
}
|
|
2763
|
+
return void 0;
|
|
2764
|
+
}
|
|
2638
2765
|
|
|
2639
2766
|
// lib/state/persistence.ts
|
|
2640
2767
|
function getDefaultStorageDir() {
|
|
@@ -4506,6 +4633,24 @@ var SessionStateRegistry = class {
|
|
|
4506
4633
|
hydrateModelLimitsFromClient(client) {
|
|
4507
4634
|
return this.catalog.hydrateFromClient(client);
|
|
4508
4635
|
}
|
|
4636
|
+
// [FIX #346] The init-time seed (above) is fire-and-forget and races
|
|
4637
|
+
// server readiness: in headless spawn+resume mode the provider-config
|
|
4638
|
+
// call can fail before the server is up, leaving the catalog empty for
|
|
4639
|
+
// the process's lifetime. During a request the server is guaranteed up
|
|
4640
|
+
// (we are inside its pipeline), so on a catalog miss we retry hydration
|
|
4641
|
+
// once per process before giving up (the fallback limit then applies).
|
|
4642
|
+
// The in-flight promise (not a boolean) lets concurrent callers await the
|
|
4643
|
+
// same hydration instead of skipping it.
|
|
4644
|
+
lazyHydration;
|
|
4645
|
+
async hydrateAndResolve(client, providerId, modelId) {
|
|
4646
|
+
const existing = this.catalog.resolve(providerId, modelId);
|
|
4647
|
+
if (existing !== void 0) {
|
|
4648
|
+
return existing;
|
|
4649
|
+
}
|
|
4650
|
+
this.lazyHydration ??= this.catalog.hydrateFromClient(client);
|
|
4651
|
+
await this.lazyHydration;
|
|
4652
|
+
return this.catalog.resolve(providerId, modelId);
|
|
4653
|
+
}
|
|
4509
4654
|
get(sessionId) {
|
|
4510
4655
|
return this.states.get(sessionId);
|
|
4511
4656
|
}
|
|
@@ -4599,7 +4744,8 @@ function createSessionState() {
|
|
|
4599
4744
|
modelID: void 0,
|
|
4600
4745
|
systemPromptTokens: void 0,
|
|
4601
4746
|
storageDir: void 0,
|
|
4602
|
-
qualityGateRetryPending: false
|
|
4747
|
+
qualityGateRetryPending: false,
|
|
4748
|
+
noContextLimitWarned: false
|
|
4603
4749
|
};
|
|
4604
4750
|
}
|
|
4605
4751
|
function resetSessionState(state) {
|
|
@@ -4642,6 +4788,7 @@ function resetSessionState(state) {
|
|
|
4642
4788
|
state.systemPromptTokens = void 0;
|
|
4643
4789
|
state.storageDir = void 0;
|
|
4644
4790
|
state.qualityGateRetryPending = false;
|
|
4791
|
+
state.noContextLimitWarned = false;
|
|
4645
4792
|
}
|
|
4646
4793
|
async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
|
|
4647
4794
|
if (state.sessionId === sessionId) {
|
|
@@ -6657,6 +6804,7 @@ function getModelInfo(messages) {
|
|
|
6657
6804
|
};
|
|
6658
6805
|
}
|
|
6659
6806
|
function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
|
|
6807
|
+
const effectiveLimit = resolveEffectiveContextLimit(state, config);
|
|
6660
6808
|
const parseLimitValue = (limit) => {
|
|
6661
6809
|
if (limit === void 0) {
|
|
6662
6810
|
return void 0;
|
|
@@ -6664,7 +6812,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
|
|
|
6664
6812
|
if (typeof limit === "number") {
|
|
6665
6813
|
return limit;
|
|
6666
6814
|
}
|
|
6667
|
-
if (!limit.endsWith("%") ||
|
|
6815
|
+
if (!limit.endsWith("%") || effectiveLimit === void 0) {
|
|
6668
6816
|
return void 0;
|
|
6669
6817
|
}
|
|
6670
6818
|
const parsedPercent = parseFloat(limit.slice(0, -1));
|
|
@@ -6673,7 +6821,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
|
|
|
6673
6821
|
}
|
|
6674
6822
|
const roundedPercent = Math.round(parsedPercent);
|
|
6675
6823
|
const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
|
|
6676
|
-
return Math.round(clampedPercent / 100 *
|
|
6824
|
+
return Math.round(clampedPercent / 100 * effectiveLimit.limit);
|
|
6677
6825
|
};
|
|
6678
6826
|
if (threshold === "max") {
|
|
6679
6827
|
const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
|
|
@@ -6724,11 +6872,12 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
|
6724
6872
|
if (!overMaxLimit) break;
|
|
6725
6873
|
}
|
|
6726
6874
|
}
|
|
6875
|
+
const effectiveLimit = resolveEffectiveContextLimit(state, config);
|
|
6727
6876
|
return {
|
|
6728
6877
|
overMaxLimit,
|
|
6729
6878
|
overMinLimit,
|
|
6730
6879
|
currentTokens,
|
|
6731
|
-
modelContextLimit:
|
|
6880
|
+
modelContextLimit: effectiveLimit?.limit
|
|
6732
6881
|
};
|
|
6733
6882
|
}
|
|
6734
6883
|
ensureBuiltinTriggerPolicyRegistered();
|
|
@@ -6931,6 +7080,7 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
6931
7080
|
let summaryTokens = 0;
|
|
6932
7081
|
let messageTokens = 0;
|
|
6933
7082
|
let protectedTokens = 0;
|
|
7083
|
+
let reasoningTokens = 0;
|
|
6934
7084
|
const perMessage = [];
|
|
6935
7085
|
const perTool = [];
|
|
6936
7086
|
const perCode = [];
|
|
@@ -6984,6 +7134,10 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
6984
7134
|
summaryTokens += summaryPartTokens;
|
|
6985
7135
|
toolTypeMap.set(toolName, (toolTypeMap.get(toolName) || 0) + toolPartTokens);
|
|
6986
7136
|
if (!msgToolName) msgToolName = toolName;
|
|
7137
|
+
} else if (part.type === "reasoning" && typeof part.text === "string") {
|
|
7138
|
+
const tokens = Math.round(part.text.length / 4);
|
|
7139
|
+
msgTotal += tokens;
|
|
7140
|
+
reasoningTokens += tokens;
|
|
6987
7141
|
}
|
|
6988
7142
|
}
|
|
6989
7143
|
if (isProtected && !isSummary) {
|
|
@@ -7011,7 +7165,8 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
7011
7165
|
textTokens: Math.max(0, messageTokens - codeTokens),
|
|
7012
7166
|
systemTokens,
|
|
7013
7167
|
protectedTokens,
|
|
7014
|
-
|
|
7168
|
+
reasoningTokens,
|
|
7169
|
+
total: systemTokens + toolTokens + summaryTokens + messageTokens + reasoningTokens,
|
|
7015
7170
|
largestRanges: perMessage.slice(0, 15),
|
|
7016
7171
|
largestToolRanges: perTool.slice(0, 15),
|
|
7017
7172
|
largestCodeRanges: perCode.slice(0, 5),
|
|
@@ -7034,13 +7189,10 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
|
|
|
7034
7189
|
if (!ref) continue;
|
|
7035
7190
|
const rn = parseInt(ref.slice(1), 10);
|
|
7036
7191
|
if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
|
|
7037
|
-
|
|
7192
|
+
const tokens2 = Math.round(countMessageCharacters(msg) / 4);
|
|
7038
7193
|
const tools = /* @__PURE__ */ new Set();
|
|
7039
7194
|
for (const part of msg.parts || []) {
|
|
7040
|
-
if (part.type
|
|
7041
|
-
tokens2 += Math.round(part.text.length / 4);
|
|
7042
|
-
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
7043
|
-
tokens2 += Math.round(JSON.stringify(part).length / 4);
|
|
7195
|
+
if (part.type !== "text" && part.type !== "reasoning") {
|
|
7044
7196
|
const toolName = part?.tool;
|
|
7045
7197
|
const callID = part?.callID;
|
|
7046
7198
|
if (toolName && callID) {
|
|
@@ -7061,15 +7213,13 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
|
|
|
7061
7213
|
protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
|
|
7062
7214
|
continue;
|
|
7063
7215
|
}
|
|
7064
|
-
|
|
7216
|
+
const tokens = Math.round(countMessageCharacters(msg) / 4);
|
|
7065
7217
|
let isTool = false;
|
|
7066
7218
|
let hasMeaningfulPart = false;
|
|
7067
7219
|
for (const part of msg.parts || []) {
|
|
7068
7220
|
if (part.type === "text" && typeof part.text === "string") {
|
|
7069
|
-
tokens += Math.round(part.text.length / 4);
|
|
7070
7221
|
if (part.text.trim().length > 0) hasMeaningfulPart = true;
|
|
7071
7222
|
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
7072
|
-
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
7073
7223
|
isTool = true;
|
|
7074
7224
|
hasMeaningfulPart = true;
|
|
7075
7225
|
}
|
|
@@ -7893,8 +8043,11 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7893
8043
|
state.nudges.iterationNudgeAnchors.clear();
|
|
7894
8044
|
state.nudges.lastNudgeShownTokens = void 0;
|
|
7895
8045
|
state.nudges.lastToolOutputNudgeTokens = void 0;
|
|
7896
|
-
|
|
7897
|
-
|
|
8046
|
+
const captureOnly = isCaptureOnlyCompress(lastCompressMsg);
|
|
8047
|
+
if (!captureOnly) {
|
|
8048
|
+
state.nudges.lastTier2NudgeTokens = currentTokens;
|
|
8049
|
+
state.nudges.lastTier3NudgeTokens = currentTokens;
|
|
8050
|
+
}
|
|
7898
8051
|
const currentTurnHasSuccessfulCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
|
|
7899
8052
|
if (currentTurnHasSuccessfulCompress && wasNudgeTriggered && !state.nudges.compressBaselineSet) {
|
|
7900
8053
|
const baseline = state.nudges.lastPerMessageNudgeTokens;
|
|
@@ -8206,7 +8359,7 @@ This is an efficiency nudge to compress early and keep context lean \u2014 not a
|
|
|
8206
8359
|
${COMPRESS_PHILOSOPHY}` : "";
|
|
8207
8360
|
const sysPart = composition.systemTokens > 0 ? `${fmt(composition.systemTokens)} system (${pct2(composition.systemTokens)}%) | ` : "";
|
|
8208
8361
|
let breakdown = `${efficiencyNote}
|
|
8209
|
-
Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%)${growthStr}`;
|
|
8362
|
+
Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%) | ${fmt(composition.reasoningTokens)} reasoning (${pct2(composition.reasoningTokens)}%)${growthStr}`;
|
|
8210
8363
|
const compressibleTokens = composition.total - composition.systemTokens - composition.protectedTokens - composition.summaryTokens;
|
|
8211
8364
|
if (composition.protectedTokens > 0) {
|
|
8212
8365
|
breakdown += `
|
|
@@ -8807,8 +8960,9 @@ function createDecompressTool(factoryCtx) {
|
|
|
8807
8960
|
async execute(args, toolCtx) {
|
|
8808
8961
|
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
8809
8962
|
const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
|
|
8810
|
-
const
|
|
8811
|
-
|
|
8963
|
+
const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
|
|
8964
|
+
const contextUsageBefore = effectiveLimitBefore ? Math.round(
|
|
8965
|
+
getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
|
|
8812
8966
|
) : void 0;
|
|
8813
8967
|
const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
|
|
8814
8968
|
if (!resolved.ok) {
|
|
@@ -8872,8 +9026,9 @@ function createDecompressTool(factoryCtx) {
|
|
|
8872
9026
|
0,
|
|
8873
9027
|
ctx.state.stats.totalPruneTokens - restoredTokens
|
|
8874
9028
|
);
|
|
8875
|
-
const
|
|
8876
|
-
|
|
9029
|
+
const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
|
|
9030
|
+
const contextUsageAfter = effectiveLimitAfter ? Math.round(
|
|
9031
|
+
getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
|
|
8877
9032
|
) : void 0;
|
|
8878
9033
|
await finalizeDecompressSession(ctx);
|
|
8879
9034
|
const restoredContentPreview = buildRestoredContentPreview(
|
|
@@ -9094,6 +9249,7 @@ function collectVisibleMessages(rawMessages, ctx) {
|
|
|
9094
9249
|
const ref = byRawId.get(msgId);
|
|
9095
9250
|
if (!ref) return;
|
|
9096
9251
|
let tokens = 0;
|
|
9252
|
+
let reasoning = 0;
|
|
9097
9253
|
let toolName = "";
|
|
9098
9254
|
for (const part of msg.parts || []) {
|
|
9099
9255
|
if (part.type === "text" && typeof part.text === "string") {
|
|
@@ -9104,10 +9260,12 @@ function collectVisibleMessages(rawMessages, ctx) {
|
|
|
9104
9260
|
if (!toolName) {
|
|
9105
9261
|
toolName = part?.tool || "unknown";
|
|
9106
9262
|
}
|
|
9263
|
+
} else if (part.type === "reasoning" && typeof part.text === "string") {
|
|
9264
|
+
reasoning += Math.round(part.text.length / 4);
|
|
9107
9265
|
}
|
|
9108
9266
|
}
|
|
9109
|
-
if (tokens > 0) {
|
|
9110
|
-
result.push({ ref, tokens, tool: toolName || "text", index: idx });
|
|
9267
|
+
if (tokens > 0 || reasoning > 0) {
|
|
9268
|
+
result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning });
|
|
9111
9269
|
}
|
|
9112
9270
|
});
|
|
9113
9271
|
return {
|
|
@@ -9129,14 +9287,16 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
|
|
|
9129
9287
|
} else {
|
|
9130
9288
|
const totalTool = visibleMessages.filter((m) => m.tool !== "text" && m.tool !== "step-finish").reduce((s, m) => s + m.tokens, 0);
|
|
9131
9289
|
const totalText = visibleMessages.filter((m) => m.tool === "text").reduce((s, m) => s + m.tokens, 0);
|
|
9132
|
-
const
|
|
9290
|
+
const totalReasoning = visibleMessages.reduce((s, m) => s + m.reasoning, 0);
|
|
9291
|
+
const total = systemTokens + totalTool + totalText + summaryTokens + totalReasoning;
|
|
9133
9292
|
const sysPct = pct(systemTokens, total);
|
|
9134
9293
|
const toolPct = pct(totalTool, total);
|
|
9135
9294
|
const textPct = pct(totalText, total);
|
|
9136
9295
|
const summaryPct = pct(summaryTokens, total);
|
|
9296
|
+
const reasoningPct = pct(totalReasoning, total);
|
|
9137
9297
|
lines.push("CONTEXT BREAKDOWN");
|
|
9138
9298
|
lines.push(
|
|
9139
|
-
` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
|
|
9299
|
+
` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%) | ${formatTokens(totalReasoning)} reasoning (${reasoningPct}%)`
|
|
9140
9300
|
);
|
|
9141
9301
|
const topTypes = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
|
|
9142
9302
|
if (topTypes.length > 0) {
|
|
@@ -9247,22 +9407,23 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
|
|
|
9247
9407
|
if (toolFilter) {
|
|
9248
9408
|
filtered = filtered.filter((m) => m.tool === toolFilter);
|
|
9249
9409
|
}
|
|
9410
|
+
const sizeOf = (m) => m.tokens + m.reasoning;
|
|
9250
9411
|
if (sort === "time") {
|
|
9251
9412
|
filtered.sort((a, b) => a.index - b.index);
|
|
9252
9413
|
} else if (sort === "tool") {
|
|
9253
|
-
filtered.sort((a, b) => a.tool.localeCompare(b.tool) || b
|
|
9414
|
+
filtered.sort((a, b) => a.tool.localeCompare(b.tool) || sizeOf(b) - sizeOf(a));
|
|
9254
9415
|
} else {
|
|
9255
|
-
filtered.sort((a, b) => b
|
|
9416
|
+
filtered.sort((a, b) => sizeOf(b) - sizeOf(a));
|
|
9256
9417
|
}
|
|
9257
|
-
const totalTokens = filtered.reduce((s, m) => s + m
|
|
9258
|
-
const allTokens = visibleMessages.reduce((s, m) => s + m
|
|
9418
|
+
const totalTokens = filtered.reduce((s, m) => s + sizeOf(m), 0);
|
|
9419
|
+
const allTokens = visibleMessages.reduce((s, m) => s + sizeOf(m), 0);
|
|
9259
9420
|
const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${filtered.length} msgs`;
|
|
9260
9421
|
lines.push(header);
|
|
9261
9422
|
lines.push(`Sorted by ${sort}`);
|
|
9262
9423
|
lines.push("");
|
|
9263
9424
|
const shown = filtered.slice(0, limit);
|
|
9264
9425
|
for (const m of shown) {
|
|
9265
|
-
lines.push(` ${m.ref} (${formatTokens(m
|
|
9426
|
+
lines.push(` ${m.ref} (${formatTokens(sizeOf(m))}) ${m.tool}`);
|
|
9266
9427
|
}
|
|
9267
9428
|
if (filtered.length > shown.length) {
|
|
9268
9429
|
lines.push("");
|
|
@@ -9530,7 +9691,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
9530
9691
|
import { join as join4 } from "path";
|
|
9531
9692
|
import { existsSync as existsSync4 } from "fs";
|
|
9532
9693
|
import { homedir as homedir3 } from "os";
|
|
9533
|
-
var LOG_VERSION = true ? "1.
|
|
9694
|
+
var LOG_VERSION = true ? "1.17.0-pr.383.130" : "dev";
|
|
9534
9695
|
var LEVEL_RANK = {
|
|
9535
9696
|
debug: 10,
|
|
9536
9697
|
info: 20,
|
|
@@ -9826,13 +9987,14 @@ CONTEXT BREAKDOWN
|
|
|
9826
9987
|
|
|
9827
9988
|
When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
|
|
9828
9989
|
|
|
9829
|
-
Breakdown:
|
|
9990
|
+
Breakdown: 4.2K system (21%) | 8.0K tool (40%) | 2.0K summaries (10%) | 2.6K code (13%) | 2.2K text (11%) | 1.0K reasoning (5%)
|
|
9830
9991
|
|
|
9831
9992
|
- "system" = system prompt tokens (AGENTS.md, tool definitions \u2014 not compressible)
|
|
9832
9993
|
- "tool" = tool call outputs (largest category \u2014 compress first when consumed)
|
|
9833
9994
|
- "summaries" = existing compression block summaries (already compressed; do not re-compress standalone)
|
|
9834
9995
|
- "code" = messages containing code blocks
|
|
9835
9996
|
- "text" = plain text messages
|
|
9997
|
+
- "reasoning" = model thinking blocks (counted to match real API usage; freed when their message is compressed)
|
|
9836
9998
|
|
|
9837
9999
|
Below the breakdown, the system lists compressible ranges grouped by conversation turn. All listed ranges should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct. Compress the largest ranges first when the current step no longer needs them.
|
|
9838
10000
|
|
|
@@ -10354,6 +10516,8 @@ var MIN_OUTPUT_TOKENS = 1e3;
|
|
|
10354
10516
|
var KEEP_PREFIX_CHARS = 2e3;
|
|
10355
10517
|
var KEEP_SUFFIX_CHARS = 2e3;
|
|
10356
10518
|
var PROTECT_RECENT_MESSAGES = 3;
|
|
10519
|
+
var OUTPUT_RESERVE_TOKENS = 16384;
|
|
10520
|
+
var overheadErrorLogged = /* @__PURE__ */ new Set();
|
|
10357
10521
|
function parseGcThreshold(threshold, modelContextLimit) {
|
|
10358
10522
|
if (typeof threshold === "number") return threshold;
|
|
10359
10523
|
const str = threshold ?? "100%";
|
|
@@ -10362,10 +10526,26 @@ function parseGcThreshold(threshold, modelContextLimit) {
|
|
|
10362
10526
|
return modelContextLimit;
|
|
10363
10527
|
}
|
|
10364
10528
|
function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
10365
|
-
|
|
10529
|
+
const effective = resolveEffectiveContextLimit(state, config);
|
|
10530
|
+
if (!effective) return;
|
|
10366
10531
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
10367
10532
|
if (currentTokens === 0) return;
|
|
10368
|
-
const
|
|
10533
|
+
const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
|
|
10534
|
+
const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
|
|
10535
|
+
const threshold = Math.min(configuredThreshold, effective.limit - overhead);
|
|
10536
|
+
if (threshold <= 0) {
|
|
10537
|
+
const sessionKey = state.sessionId ?? "unknown";
|
|
10538
|
+
if (!overheadErrorLogged.has(sessionKey)) {
|
|
10539
|
+
overheadErrorLogged.add(sessionKey);
|
|
10540
|
+
logger.error("ACP: model context window too small to fit overhead", {
|
|
10541
|
+
session: state.sessionId,
|
|
10542
|
+
limit: effective.limit,
|
|
10543
|
+
contextLimitSource: effective.source,
|
|
10544
|
+
overhead
|
|
10545
|
+
});
|
|
10546
|
+
}
|
|
10547
|
+
return;
|
|
10548
|
+
}
|
|
10369
10549
|
if (currentTokens < threshold) return;
|
|
10370
10550
|
const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
|
|
10371
10551
|
const candidates = [];
|
|
@@ -10410,9 +10590,158 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
|
10410
10590
|
truncatedCount,
|
|
10411
10591
|
estimatedSavedTokens: Math.round(savedTokens),
|
|
10412
10592
|
currentTokens,
|
|
10413
|
-
threshold
|
|
10593
|
+
threshold,
|
|
10594
|
+
contextLimit: effective.limit,
|
|
10595
|
+
contextLimitSource: effective.source
|
|
10596
|
+
});
|
|
10597
|
+
}
|
|
10598
|
+
}
|
|
10599
|
+
|
|
10600
|
+
// lib/messages/enforce-budget.ts
|
|
10601
|
+
var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
|
|
10602
|
+
var TRUNCATION_MARKER2 = "[truncated for context space";
|
|
10603
|
+
var KEEP_PREFIX_CHARS2 = 2e3;
|
|
10604
|
+
var KEEP_SUFFIX_CHARS2 = 2e3;
|
|
10605
|
+
var PROTECT_RECENT_MESSAGES2 = 3;
|
|
10606
|
+
var MIN_CLEAR_TOKENS = 200;
|
|
10607
|
+
function resolveContextWindow(state) {
|
|
10608
|
+
if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
|
|
10609
|
+
return state.modelContextLimit;
|
|
10610
|
+
}
|
|
10611
|
+
return void 0;
|
|
10612
|
+
}
|
|
10613
|
+
function estimateWireTokens(state, messages) {
|
|
10614
|
+
const base = getCurrentTokenUsage(state, messages);
|
|
10615
|
+
if (base > 0) {
|
|
10616
|
+
let baseAssistant = -1;
|
|
10617
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10618
|
+
if (messages[i].info.role !== "assistant") continue;
|
|
10619
|
+
const tokens = messages[i].info.tokens;
|
|
10620
|
+
if ((tokens?.input || 0) <= 0 && (tokens?.output || 0) <= 0) continue;
|
|
10621
|
+
baseAssistant = i;
|
|
10622
|
+
break;
|
|
10623
|
+
}
|
|
10624
|
+
if (baseAssistant >= 0) {
|
|
10625
|
+
let additions = 0;
|
|
10626
|
+
for (let i = baseAssistant + 1; i < messages.length; i++) {
|
|
10627
|
+
additions += countAllMessageTokens(messages[i]);
|
|
10628
|
+
}
|
|
10629
|
+
return base + additions;
|
|
10630
|
+
}
|
|
10631
|
+
}
|
|
10632
|
+
let total = 0;
|
|
10633
|
+
for (const m of messages) total += countAllMessageTokens(m);
|
|
10634
|
+
return total + (state.systemPromptTokens ?? 0);
|
|
10635
|
+
}
|
|
10636
|
+
function enforceContextBudget(state, config, logger, messages) {
|
|
10637
|
+
const window = resolveContextWindow(state);
|
|
10638
|
+
if (window === void 0) return void 0;
|
|
10639
|
+
const configuredReserve = config.compress?.completionReserveTokens;
|
|
10640
|
+
const reserve = typeof configuredReserve === "number" && Number.isFinite(configuredReserve) && configuredReserve >= 0 ? configuredReserve : DEFAULT_COMPLETION_RESERVE_TOKENS;
|
|
10641
|
+
const budget = window - reserve;
|
|
10642
|
+
if (budget <= 0) return void 0;
|
|
10643
|
+
const estimatedTokens = estimateWireTokens(state, messages);
|
|
10644
|
+
if (estimatedTokens <= budget) {
|
|
10645
|
+
return {
|
|
10646
|
+
applied: false,
|
|
10647
|
+
window,
|
|
10648
|
+
reserve,
|
|
10649
|
+
budget,
|
|
10650
|
+
estimatedTokens,
|
|
10651
|
+
finalEstimate: estimatedTokens,
|
|
10652
|
+
truncatedCount: 0,
|
|
10653
|
+
clearedCount: 0
|
|
10654
|
+
};
|
|
10655
|
+
}
|
|
10656
|
+
const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
|
|
10657
|
+
const protectedTools = new Set(config.compress?.protectedTools ?? []);
|
|
10658
|
+
const candidates = [];
|
|
10659
|
+
for (let mi = 0; mi < protectedIndex; mi++) {
|
|
10660
|
+
if (mi === 0 && messages[mi].info.role === "user") continue;
|
|
10661
|
+
const msg = messages[mi];
|
|
10662
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
10663
|
+
for (const part of parts) {
|
|
10664
|
+
if (part?.type !== "tool") continue;
|
|
10665
|
+
if (part.state?.status !== "completed") continue;
|
|
10666
|
+
if (part.tool === "compress") continue;
|
|
10667
|
+
if (protectedTools.has(part.tool)) continue;
|
|
10668
|
+
const content = extractCompletedToolOutput(part);
|
|
10669
|
+
if (content === void 0) continue;
|
|
10670
|
+
if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
|
|
10671
|
+
const tokens = countTokens2(content);
|
|
10672
|
+
if (tokens <= 0) continue;
|
|
10673
|
+
candidates.push({ part, content, tokens, index: mi });
|
|
10674
|
+
}
|
|
10675
|
+
}
|
|
10676
|
+
let saved = 0;
|
|
10677
|
+
let truncatedCount = 0;
|
|
10678
|
+
let clearedCount = 0;
|
|
10679
|
+
const truncatable = candidates.filter(
|
|
10680
|
+
(c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
|
|
10681
|
+
).sort((a, b) => b.tokens - a.tokens);
|
|
10682
|
+
for (const c of truncatable) {
|
|
10683
|
+
if (estimatedTokens - saved <= budget) break;
|
|
10684
|
+
const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
|
|
10685
|
+
const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
|
|
10686
|
+
const truncated = prefix + `
|
|
10687
|
+
|
|
10688
|
+
...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
|
|
10689
|
+
|
|
10690
|
+
` + suffix;
|
|
10691
|
+
if (truncated.length >= c.content.length) continue;
|
|
10692
|
+
c.part.state.output = truncated;
|
|
10693
|
+
saved += c.tokens - countTokens2(truncated);
|
|
10694
|
+
truncatedCount++;
|
|
10695
|
+
}
|
|
10696
|
+
if (estimatedTokens - saved > budget) {
|
|
10697
|
+
const clearable = candidates.filter((c) => {
|
|
10698
|
+
const out = extractCompletedToolOutput(c.part);
|
|
10699
|
+
return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
|
|
10700
|
+
}).sort((a, b) => a.index - b.index);
|
|
10701
|
+
for (const c of clearable) {
|
|
10702
|
+
if (estimatedTokens - saved <= budget) break;
|
|
10703
|
+
const current = extractCompletedToolOutput(c.part);
|
|
10704
|
+
if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
|
|
10705
|
+
c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
|
|
10706
|
+
saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
|
|
10707
|
+
clearedCount++;
|
|
10708
|
+
}
|
|
10709
|
+
}
|
|
10710
|
+
const finalEstimate = Math.max(0, estimatedTokens - saved);
|
|
10711
|
+
if (truncatedCount > 0 || clearedCount > 0) {
|
|
10712
|
+
logger.warn("Context budget guard: pruned tool outputs to fit the request", {
|
|
10713
|
+
session: state.sessionId,
|
|
10714
|
+
estimatedTokens: Math.round(estimatedTokens),
|
|
10715
|
+
budget,
|
|
10716
|
+
window,
|
|
10717
|
+
reserve,
|
|
10718
|
+
truncatedCount,
|
|
10719
|
+
clearedCount,
|
|
10720
|
+
estimatedSavedTokens: Math.round(saved),
|
|
10721
|
+
finalEstimate: Math.round(finalEstimate)
|
|
10414
10722
|
});
|
|
10415
10723
|
}
|
|
10724
|
+
if (finalEstimate > budget) {
|
|
10725
|
+
logger.warn(
|
|
10726
|
+
"Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
|
|
10727
|
+
{
|
|
10728
|
+
session: state.sessionId,
|
|
10729
|
+
finalEstimate: Math.round(finalEstimate),
|
|
10730
|
+
budget,
|
|
10731
|
+
window
|
|
10732
|
+
}
|
|
10733
|
+
);
|
|
10734
|
+
}
|
|
10735
|
+
return {
|
|
10736
|
+
applied: truncatedCount > 0 || clearedCount > 0,
|
|
10737
|
+
window,
|
|
10738
|
+
reserve,
|
|
10739
|
+
budget,
|
|
10740
|
+
estimatedTokens,
|
|
10741
|
+
finalEstimate,
|
|
10742
|
+
truncatedCount,
|
|
10743
|
+
clearedCount
|
|
10744
|
+
};
|
|
10416
10745
|
}
|
|
10417
10746
|
|
|
10418
10747
|
// lib/commands/context.ts
|
|
@@ -11371,11 +11700,12 @@ function runBatchCleanup(state, config, logger, messages) {
|
|
|
11371
11700
|
mergedCount: 0,
|
|
11372
11701
|
savedTokens: 0
|
|
11373
11702
|
};
|
|
11374
|
-
|
|
11703
|
+
const effective = resolveEffectiveContextLimit(state, config);
|
|
11704
|
+
if (!effective) {
|
|
11375
11705
|
return noop;
|
|
11376
11706
|
}
|
|
11377
11707
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
11378
|
-
if (currentTokens <
|
|
11708
|
+
if (currentTokens < effective.limit) {
|
|
11379
11709
|
return noop;
|
|
11380
11710
|
}
|
|
11381
11711
|
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
@@ -11392,7 +11722,8 @@ function runBatchCleanup(state, config, logger, messages) {
|
|
|
11392
11722
|
mergedCount: result.mergedCount,
|
|
11393
11723
|
savedTokens: result.savedTokens,
|
|
11394
11724
|
currentTokens,
|
|
11395
|
-
contextLimit:
|
|
11725
|
+
contextLimit: effective.limit,
|
|
11726
|
+
contextLimitSource: effective.source
|
|
11396
11727
|
});
|
|
11397
11728
|
return {
|
|
11398
11729
|
tier: 3,
|
|
@@ -11426,11 +11757,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
|
11426
11757
|
input.model?.limit?.context
|
|
11427
11758
|
);
|
|
11428
11759
|
const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
|
|
11429
|
-
if (state && input.model?.limit?.context) {
|
|
11430
|
-
state.modelContextLimit = input.model.limit.context;
|
|
11431
|
-
state.modelProviderID = input.model?.providerID;
|
|
11432
|
-
state.modelID = input.model?.id;
|
|
11433
|
-
}
|
|
11434
11760
|
if (!state || state.isSubAgent && !config.allowSubAgents) {
|
|
11435
11761
|
return;
|
|
11436
11762
|
}
|
|
@@ -11439,6 +11765,23 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
|
11439
11765
|
logger.info("Skipping DCP system prompt injection for internal agent");
|
|
11440
11766
|
return;
|
|
11441
11767
|
}
|
|
11768
|
+
if (input.model?.limit?.context) {
|
|
11769
|
+
const limit = input.model.limit.context;
|
|
11770
|
+
const providerID = input.model?.providerID;
|
|
11771
|
+
const modelID = input.model?.id;
|
|
11772
|
+
const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
|
|
11773
|
+
state.modelContextLimit = limit;
|
|
11774
|
+
if (providerID !== void 0) {
|
|
11775
|
+
state.modelProviderID = providerID;
|
|
11776
|
+
}
|
|
11777
|
+
if (modelID !== void 0) {
|
|
11778
|
+
state.modelID = modelID;
|
|
11779
|
+
}
|
|
11780
|
+
if (changed) {
|
|
11781
|
+
saveSessionState(state, logger).catch(() => {
|
|
11782
|
+
});
|
|
11783
|
+
}
|
|
11784
|
+
}
|
|
11442
11785
|
const effectivePermission = compressPermission(state, config);
|
|
11443
11786
|
if (effectivePermission === "deny") {
|
|
11444
11787
|
return;
|
|
@@ -11483,10 +11826,17 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11483
11826
|
config
|
|
11484
11827
|
);
|
|
11485
11828
|
const requestModel = lastUserMessage.info.model;
|
|
11486
|
-
|
|
11829
|
+
let requestModelLimit = registry4.resolveModelLimit(
|
|
11487
11830
|
requestModel?.providerID,
|
|
11488
11831
|
requestModel?.modelID
|
|
11489
11832
|
);
|
|
11833
|
+
if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
|
|
11834
|
+
requestModelLimit = await registry4.hydrateAndResolve(
|
|
11835
|
+
client,
|
|
11836
|
+
requestModel.providerID,
|
|
11837
|
+
requestModel.modelID
|
|
11838
|
+
);
|
|
11839
|
+
}
|
|
11490
11840
|
const prevModelID = state.modelID;
|
|
11491
11841
|
if (requestModelLimit !== void 0) {
|
|
11492
11842
|
state.modelContextLimit = requestModelLimit;
|
|
@@ -11516,6 +11866,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11516
11866
|
});
|
|
11517
11867
|
}
|
|
11518
11868
|
await updatePerTurnState(state, logger, messages);
|
|
11869
|
+
if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
|
|
11870
|
+
state.noContextLimitWarned = true;
|
|
11871
|
+
logger.warn(
|
|
11872
|
+
'Model reports no context window and the catalog has no entry for it; all percentage thresholds (min/max/emergency, GC) and the context-budget guard are disabled. Set the model limit in opencode.json (e.g. "limit": {"context": 262144, "output": 16384}) to enable them (also fixes the 32000 max_tokens fallback); an absolute compress.maxContextLimit in acp.jsonc only enables proactive nudges, not the guard.',
|
|
11873
|
+
{
|
|
11874
|
+
session: state.sessionId,
|
|
11875
|
+
model: `${requestModel.providerID}/${requestModel.modelID}`
|
|
11876
|
+
}
|
|
11877
|
+
);
|
|
11878
|
+
}
|
|
11519
11879
|
}
|
|
11520
11880
|
syncCompressPermissionState(state, config, hostPermissions, output.messages);
|
|
11521
11881
|
if (state.isSubAgent && !config.allowSubAgents) {
|
|
@@ -11541,10 +11901,11 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11541
11901
|
}
|
|
11542
11902
|
}
|
|
11543
11903
|
ensureBuiltinFiltersRegistered();
|
|
11904
|
+
const effectiveLimit = resolveEffectiveContextLimit(state, config);
|
|
11544
11905
|
applyMessageFilters(output.messages, config.messageFilters, logger, {
|
|
11545
11906
|
sessionId: state.sessionId ?? "",
|
|
11546
11907
|
isSubAgent: state.isSubAgent,
|
|
11547
|
-
modelContextLimit:
|
|
11908
|
+
modelContextLimit: effectiveLimit?.limit
|
|
11548
11909
|
});
|
|
11549
11910
|
cacheSystemPromptTokens(state, output.messages);
|
|
11550
11911
|
assignMessageRefs(state, output.messages);
|
|
@@ -11564,6 +11925,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11564
11925
|
const prePruneTokens = getCurrentTokenUsage(state, output.messages);
|
|
11565
11926
|
prune(state, logger, config, output.messages);
|
|
11566
11927
|
truncateLargeToolOutputs(state, config, logger, output.messages);
|
|
11928
|
+
enforceContextBudget(state, config, logger, output.messages);
|
|
11567
11929
|
hideConsumedCompressCalls(state, output.messages);
|
|
11568
11930
|
assignMessageRefs(state, output.messages);
|
|
11569
11931
|
const compressionPriorities = buildPriorityMap(config, state, output.messages);
|
|
@@ -11595,14 +11957,31 @@ ${text}`);
|
|
|
11595
11957
|
stripStaleMetadata(output.messages);
|
|
11596
11958
|
dropEmptyMessages(output.messages);
|
|
11597
11959
|
const postTokens = getCurrentTokenUsage(state, output.messages);
|
|
11960
|
+
if (postTokens !== void 0 && effectiveLimit) {
|
|
11961
|
+
const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
|
|
11962
|
+
if (postTokens > budget) {
|
|
11963
|
+
logger.error(
|
|
11964
|
+
"ACP hard guard: context exceeds model budget after in-flight reduction",
|
|
11965
|
+
{
|
|
11966
|
+
session: state.sessionId,
|
|
11967
|
+
postTokens,
|
|
11968
|
+
budget,
|
|
11969
|
+
contextLimit: effectiveLimit.limit,
|
|
11970
|
+
contextLimitSource: effectiveLimit.source,
|
|
11971
|
+
hint: "request will likely be rejected; run /compact or start a new session"
|
|
11972
|
+
}
|
|
11973
|
+
);
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11598
11976
|
logger.info("Chat transform complete", {
|
|
11599
11977
|
session: state.sessionId,
|
|
11600
11978
|
model: state.modelID,
|
|
11601
11979
|
messages: output.messages.length,
|
|
11602
11980
|
prePruneTokens,
|
|
11603
11981
|
postTokens,
|
|
11604
|
-
contextLimit:
|
|
11605
|
-
|
|
11982
|
+
contextLimit: effectiveLimit?.limit,
|
|
11983
|
+
contextLimitSource: effectiveLimit?.source,
|
|
11984
|
+
usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
|
|
11606
11985
|
nudged: state.nudges.shouldInjectThisTurn
|
|
11607
11986
|
});
|
|
11608
11987
|
if (state.sessionId) {
|
|
@@ -11634,12 +12013,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
11634
12013
|
path: { id: input.sessionID }
|
|
11635
12014
|
});
|
|
11636
12015
|
const messages = filterMessages(messagesResponse.data || messagesResponse);
|
|
11637
|
-
const state = await registry4.getOrCreate(
|
|
11638
|
-
client,
|
|
11639
|
-
input.sessionID,
|
|
11640
|
-
messages,
|
|
11641
|
-
config
|
|
11642
|
-
);
|
|
12016
|
+
const state = await registry4.getOrCreate(client, input.sessionID, messages, config);
|
|
11643
12017
|
syncCompressPermissionState(state, config, hostPermissions, messages);
|
|
11644
12018
|
const commandCtx = {
|
|
11645
12019
|
client,
|
|
@@ -11653,7 +12027,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
11653
12027
|
const sub = input.arguments?.trim().toLowerCase();
|
|
11654
12028
|
if (sub === "stats" || sub === "status" || sub === "") {
|
|
11655
12029
|
await handleStatsCommand(commandCtx);
|
|
11656
|
-
|
|
12030
|
+
return;
|
|
11657
12031
|
}
|
|
11658
12032
|
if (sub === "export" || sub.startsWith("export ")) {
|
|
11659
12033
|
const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
|
|
@@ -11661,17 +12035,10 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
11661
12035
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11662
12036
|
}
|
|
11663
12037
|
if (sub === "help") {
|
|
11664
|
-
await sendIgnoredMessage(
|
|
11665
|
-
client,
|
|
11666
|
-
input.sessionID,
|
|
11667
|
-
buildHelpText(),
|
|
11668
|
-
{},
|
|
11669
|
-
logger
|
|
11670
|
-
);
|
|
12038
|
+
await sendIgnoredMessage(client, input.sessionID, buildHelpText(), {}, logger);
|
|
11671
12039
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11672
12040
|
}
|
|
11673
12041
|
await handleContextCommand(commandCtx);
|
|
11674
|
-
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11675
12042
|
}
|
|
11676
12043
|
};
|
|
11677
12044
|
}
|
|
@@ -11742,9 +12109,7 @@ function createEventHandler(registry4, logger) {
|
|
|
11742
12109
|
return;
|
|
11743
12110
|
}
|
|
11744
12111
|
if (typeof part.callID === "string" && typeof part.messageID === "string") {
|
|
11745
|
-
timing.startsByCallId.delete(
|
|
11746
|
-
buildCompressionTimingKey(part.messageID, part.callID)
|
|
11747
|
-
);
|
|
12112
|
+
timing.startsByCallId.delete(buildCompressionTimingKey(part.messageID, part.callID));
|
|
11748
12113
|
}
|
|
11749
12114
|
};
|
|
11750
12115
|
}
|
|
@@ -12021,7 +12386,7 @@ var server = (async (ctx) => {
|
|
|
12021
12386
|
}
|
|
12022
12387
|
const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
|
|
12023
12388
|
logger.info("ACP plugin initialized", {
|
|
12024
|
-
version: true ? "1.
|
|
12389
|
+
version: true ? "1.17.0-pr.383.130" : "dev",
|
|
12025
12390
|
workspace: ctx.directory,
|
|
12026
12391
|
logLevel: logger.level,
|
|
12027
12392
|
debug: config.debug,
|