opencode-acp 1.16.0-pr.297.127 → 1.16.0-pr.350.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +363 -57
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/decompress.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/utils.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/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) {
|
|
@@ -2635,6 +2702,16 @@ function resetOnCompaction(state) {
|
|
|
2635
2702
|
nextRef: 1
|
|
2636
2703
|
};
|
|
2637
2704
|
}
|
|
2705
|
+
function resolveEffectiveContextLimit(state, config) {
|
|
2706
|
+
if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
|
|
2707
|
+
return { limit: state.modelContextLimit, source: "model" };
|
|
2708
|
+
}
|
|
2709
|
+
const fallback = config.compress.contextLimitFallback;
|
|
2710
|
+
if (typeof fallback === "number" && fallback > 0) {
|
|
2711
|
+
return { limit: fallback, source: "fallback" };
|
|
2712
|
+
}
|
|
2713
|
+
return void 0;
|
|
2714
|
+
}
|
|
2638
2715
|
|
|
2639
2716
|
// lib/state/persistence.ts
|
|
2640
2717
|
function getDefaultStorageDir() {
|
|
@@ -4506,6 +4583,24 @@ var SessionStateRegistry = class {
|
|
|
4506
4583
|
hydrateModelLimitsFromClient(client) {
|
|
4507
4584
|
return this.catalog.hydrateFromClient(client);
|
|
4508
4585
|
}
|
|
4586
|
+
// [FIX #346] The init-time seed (above) is fire-and-forget and races
|
|
4587
|
+
// server readiness: in headless spawn+resume mode the provider-config
|
|
4588
|
+
// call can fail before the server is up, leaving the catalog empty for
|
|
4589
|
+
// the process's lifetime. During a request the server is guaranteed up
|
|
4590
|
+
// (we are inside its pipeline), so on a catalog miss we retry hydration
|
|
4591
|
+
// once per process before giving up (the fallback limit then applies).
|
|
4592
|
+
// The in-flight promise (not a boolean) lets concurrent callers await the
|
|
4593
|
+
// same hydration instead of skipping it.
|
|
4594
|
+
lazyHydration;
|
|
4595
|
+
async hydrateAndResolve(client, providerId, modelId) {
|
|
4596
|
+
const existing = this.catalog.resolve(providerId, modelId);
|
|
4597
|
+
if (existing !== void 0) {
|
|
4598
|
+
return existing;
|
|
4599
|
+
}
|
|
4600
|
+
this.lazyHydration ??= this.catalog.hydrateFromClient(client);
|
|
4601
|
+
await this.lazyHydration;
|
|
4602
|
+
return this.catalog.resolve(providerId, modelId);
|
|
4603
|
+
}
|
|
4509
4604
|
get(sessionId) {
|
|
4510
4605
|
return this.states.get(sessionId);
|
|
4511
4606
|
}
|
|
@@ -4599,7 +4694,8 @@ function createSessionState() {
|
|
|
4599
4694
|
modelID: void 0,
|
|
4600
4695
|
systemPromptTokens: void 0,
|
|
4601
4696
|
storageDir: void 0,
|
|
4602
|
-
qualityGateRetryPending: false
|
|
4697
|
+
qualityGateRetryPending: false,
|
|
4698
|
+
noContextLimitWarned: false
|
|
4603
4699
|
};
|
|
4604
4700
|
}
|
|
4605
4701
|
function resetSessionState(state) {
|
|
@@ -4642,6 +4738,7 @@ function resetSessionState(state) {
|
|
|
4642
4738
|
state.systemPromptTokens = void 0;
|
|
4643
4739
|
state.storageDir = void 0;
|
|
4644
4740
|
state.qualityGateRetryPending = false;
|
|
4741
|
+
state.noContextLimitWarned = false;
|
|
4645
4742
|
}
|
|
4646
4743
|
async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
|
|
4647
4744
|
if (state.sessionId === sessionId) {
|
|
@@ -6657,6 +6754,7 @@ function getModelInfo(messages) {
|
|
|
6657
6754
|
};
|
|
6658
6755
|
}
|
|
6659
6756
|
function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
|
|
6757
|
+
const effectiveLimit = resolveEffectiveContextLimit(state, config);
|
|
6660
6758
|
const parseLimitValue = (limit) => {
|
|
6661
6759
|
if (limit === void 0) {
|
|
6662
6760
|
return void 0;
|
|
@@ -6664,7 +6762,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
|
|
|
6664
6762
|
if (typeof limit === "number") {
|
|
6665
6763
|
return limit;
|
|
6666
6764
|
}
|
|
6667
|
-
if (!limit.endsWith("%") ||
|
|
6765
|
+
if (!limit.endsWith("%") || effectiveLimit === void 0) {
|
|
6668
6766
|
return void 0;
|
|
6669
6767
|
}
|
|
6670
6768
|
const parsedPercent = parseFloat(limit.slice(0, -1));
|
|
@@ -6673,7 +6771,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
|
|
|
6673
6771
|
}
|
|
6674
6772
|
const roundedPercent = Math.round(parsedPercent);
|
|
6675
6773
|
const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
|
|
6676
|
-
return Math.round(clampedPercent / 100 *
|
|
6774
|
+
return Math.round(clampedPercent / 100 * effectiveLimit.limit);
|
|
6677
6775
|
};
|
|
6678
6776
|
if (threshold === "max") {
|
|
6679
6777
|
const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
|
|
@@ -6724,11 +6822,12 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
|
6724
6822
|
if (!overMaxLimit) break;
|
|
6725
6823
|
}
|
|
6726
6824
|
}
|
|
6825
|
+
const effectiveLimit = resolveEffectiveContextLimit(state, config);
|
|
6727
6826
|
return {
|
|
6728
6827
|
overMaxLimit,
|
|
6729
6828
|
overMinLimit,
|
|
6730
6829
|
currentTokens,
|
|
6731
|
-
modelContextLimit:
|
|
6830
|
+
modelContextLimit: effectiveLimit?.limit
|
|
6732
6831
|
};
|
|
6733
6832
|
}
|
|
6734
6833
|
ensureBuiltinTriggerPolicyRegistered();
|
|
@@ -8807,8 +8906,9 @@ function createDecompressTool(factoryCtx) {
|
|
|
8807
8906
|
async execute(args, toolCtx) {
|
|
8808
8907
|
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
8809
8908
|
const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
|
|
8810
|
-
const
|
|
8811
|
-
|
|
8909
|
+
const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
|
|
8910
|
+
const contextUsageBefore = effectiveLimitBefore ? Math.round(
|
|
8911
|
+
getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
|
|
8812
8912
|
) : void 0;
|
|
8813
8913
|
const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
|
|
8814
8914
|
if (!resolved.ok) {
|
|
@@ -8872,8 +8972,9 @@ function createDecompressTool(factoryCtx) {
|
|
|
8872
8972
|
0,
|
|
8873
8973
|
ctx.state.stats.totalPruneTokens - restoredTokens
|
|
8874
8974
|
);
|
|
8875
|
-
const
|
|
8876
|
-
|
|
8975
|
+
const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
|
|
8976
|
+
const contextUsageAfter = effectiveLimitAfter ? Math.round(
|
|
8977
|
+
getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
|
|
8877
8978
|
) : void 0;
|
|
8878
8979
|
await finalizeDecompressSession(ctx);
|
|
8879
8980
|
const restoredContentPreview = buildRestoredContentPreview(
|
|
@@ -9530,7 +9631,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
9530
9631
|
import { join as join4 } from "path";
|
|
9531
9632
|
import { existsSync as existsSync4 } from "fs";
|
|
9532
9633
|
import { homedir as homedir3 } from "os";
|
|
9533
|
-
var LOG_VERSION = true ? "1.16.0-pr.
|
|
9634
|
+
var LOG_VERSION = true ? "1.16.0-pr.350.123" : "dev";
|
|
9534
9635
|
var LEVEL_RANK = {
|
|
9535
9636
|
debug: 10,
|
|
9536
9637
|
info: 20,
|
|
@@ -10354,6 +10455,8 @@ var MIN_OUTPUT_TOKENS = 1e3;
|
|
|
10354
10455
|
var KEEP_PREFIX_CHARS = 2e3;
|
|
10355
10456
|
var KEEP_SUFFIX_CHARS = 2e3;
|
|
10356
10457
|
var PROTECT_RECENT_MESSAGES = 3;
|
|
10458
|
+
var OUTPUT_RESERVE_TOKENS = 16384;
|
|
10459
|
+
var overheadErrorLogged = /* @__PURE__ */ new Set();
|
|
10357
10460
|
function parseGcThreshold(threshold, modelContextLimit) {
|
|
10358
10461
|
if (typeof threshold === "number") return threshold;
|
|
10359
10462
|
const str = threshold ?? "100%";
|
|
@@ -10362,10 +10465,26 @@ function parseGcThreshold(threshold, modelContextLimit) {
|
|
|
10362
10465
|
return modelContextLimit;
|
|
10363
10466
|
}
|
|
10364
10467
|
function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
10365
|
-
|
|
10468
|
+
const effective = resolveEffectiveContextLimit(state, config);
|
|
10469
|
+
if (!effective) return;
|
|
10366
10470
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
10367
10471
|
if (currentTokens === 0) return;
|
|
10368
|
-
const
|
|
10472
|
+
const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
|
|
10473
|
+
const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
|
|
10474
|
+
const threshold = Math.min(configuredThreshold, effective.limit - overhead);
|
|
10475
|
+
if (threshold <= 0) {
|
|
10476
|
+
const sessionKey = state.sessionId ?? "unknown";
|
|
10477
|
+
if (!overheadErrorLogged.has(sessionKey)) {
|
|
10478
|
+
overheadErrorLogged.add(sessionKey);
|
|
10479
|
+
logger.error("ACP: model context window too small to fit overhead", {
|
|
10480
|
+
session: state.sessionId,
|
|
10481
|
+
limit: effective.limit,
|
|
10482
|
+
contextLimitSource: effective.source,
|
|
10483
|
+
overhead
|
|
10484
|
+
});
|
|
10485
|
+
}
|
|
10486
|
+
return;
|
|
10487
|
+
}
|
|
10369
10488
|
if (currentTokens < threshold) return;
|
|
10370
10489
|
const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
|
|
10371
10490
|
const candidates = [];
|
|
@@ -10410,9 +10529,158 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
|
10410
10529
|
truncatedCount,
|
|
10411
10530
|
estimatedSavedTokens: Math.round(savedTokens),
|
|
10412
10531
|
currentTokens,
|
|
10413
|
-
threshold
|
|
10532
|
+
threshold,
|
|
10533
|
+
contextLimit: effective.limit,
|
|
10534
|
+
contextLimitSource: effective.source
|
|
10535
|
+
});
|
|
10536
|
+
}
|
|
10537
|
+
}
|
|
10538
|
+
|
|
10539
|
+
// lib/messages/enforce-budget.ts
|
|
10540
|
+
var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
|
|
10541
|
+
var TRUNCATION_MARKER2 = "[truncated for context space";
|
|
10542
|
+
var KEEP_PREFIX_CHARS2 = 2e3;
|
|
10543
|
+
var KEEP_SUFFIX_CHARS2 = 2e3;
|
|
10544
|
+
var PROTECT_RECENT_MESSAGES2 = 3;
|
|
10545
|
+
var MIN_CLEAR_TOKENS = 200;
|
|
10546
|
+
function resolveContextWindow(state) {
|
|
10547
|
+
if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
|
|
10548
|
+
return state.modelContextLimit;
|
|
10549
|
+
}
|
|
10550
|
+
return void 0;
|
|
10551
|
+
}
|
|
10552
|
+
function estimateWireTokens(state, messages) {
|
|
10553
|
+
const base = getCurrentTokenUsage(state, messages);
|
|
10554
|
+
if (base > 0) {
|
|
10555
|
+
let baseAssistant = -1;
|
|
10556
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10557
|
+
if (messages[i].info.role !== "assistant") continue;
|
|
10558
|
+
const tokens = messages[i].info.tokens;
|
|
10559
|
+
if ((tokens?.input || 0) <= 0 && (tokens?.output || 0) <= 0) continue;
|
|
10560
|
+
baseAssistant = i;
|
|
10561
|
+
break;
|
|
10562
|
+
}
|
|
10563
|
+
if (baseAssistant >= 0) {
|
|
10564
|
+
let additions = 0;
|
|
10565
|
+
for (let i = baseAssistant + 1; i < messages.length; i++) {
|
|
10566
|
+
additions += countAllMessageTokens(messages[i]);
|
|
10567
|
+
}
|
|
10568
|
+
return base + additions;
|
|
10569
|
+
}
|
|
10570
|
+
}
|
|
10571
|
+
let total = 0;
|
|
10572
|
+
for (const m of messages) total += countAllMessageTokens(m);
|
|
10573
|
+
return total + (state.systemPromptTokens ?? 0);
|
|
10574
|
+
}
|
|
10575
|
+
function enforceContextBudget(state, config, logger, messages) {
|
|
10576
|
+
const window = resolveContextWindow(state);
|
|
10577
|
+
if (window === void 0) return void 0;
|
|
10578
|
+
const configuredReserve = config.compress?.completionReserveTokens;
|
|
10579
|
+
const reserve = typeof configuredReserve === "number" && Number.isFinite(configuredReserve) && configuredReserve >= 0 ? configuredReserve : DEFAULT_COMPLETION_RESERVE_TOKENS;
|
|
10580
|
+
const budget = window - reserve;
|
|
10581
|
+
if (budget <= 0) return void 0;
|
|
10582
|
+
const estimatedTokens = estimateWireTokens(state, messages);
|
|
10583
|
+
if (estimatedTokens <= budget) {
|
|
10584
|
+
return {
|
|
10585
|
+
applied: false,
|
|
10586
|
+
window,
|
|
10587
|
+
reserve,
|
|
10588
|
+
budget,
|
|
10589
|
+
estimatedTokens,
|
|
10590
|
+
finalEstimate: estimatedTokens,
|
|
10591
|
+
truncatedCount: 0,
|
|
10592
|
+
clearedCount: 0
|
|
10593
|
+
};
|
|
10594
|
+
}
|
|
10595
|
+
const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
|
|
10596
|
+
const protectedTools = new Set(config.compress?.protectedTools ?? []);
|
|
10597
|
+
const candidates = [];
|
|
10598
|
+
for (let mi = 0; mi < protectedIndex; mi++) {
|
|
10599
|
+
if (mi === 0 && messages[mi].info.role === "user") continue;
|
|
10600
|
+
const msg = messages[mi];
|
|
10601
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
10602
|
+
for (const part of parts) {
|
|
10603
|
+
if (part?.type !== "tool") continue;
|
|
10604
|
+
if (part.state?.status !== "completed") continue;
|
|
10605
|
+
if (part.tool === "compress") continue;
|
|
10606
|
+
if (protectedTools.has(part.tool)) continue;
|
|
10607
|
+
const content = extractCompletedToolOutput(part);
|
|
10608
|
+
if (content === void 0) continue;
|
|
10609
|
+
if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
|
|
10610
|
+
const tokens = countTokens2(content);
|
|
10611
|
+
if (tokens <= 0) continue;
|
|
10612
|
+
candidates.push({ part, content, tokens, index: mi });
|
|
10613
|
+
}
|
|
10614
|
+
}
|
|
10615
|
+
let saved = 0;
|
|
10616
|
+
let truncatedCount = 0;
|
|
10617
|
+
let clearedCount = 0;
|
|
10618
|
+
const truncatable = candidates.filter(
|
|
10619
|
+
(c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
|
|
10620
|
+
).sort((a, b) => b.tokens - a.tokens);
|
|
10621
|
+
for (const c of truncatable) {
|
|
10622
|
+
if (estimatedTokens - saved <= budget) break;
|
|
10623
|
+
const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
|
|
10624
|
+
const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
|
|
10625
|
+
const truncated = prefix + `
|
|
10626
|
+
|
|
10627
|
+
...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
|
|
10628
|
+
|
|
10629
|
+
` + suffix;
|
|
10630
|
+
if (truncated.length >= c.content.length) continue;
|
|
10631
|
+
c.part.state.output = truncated;
|
|
10632
|
+
saved += c.tokens - countTokens2(truncated);
|
|
10633
|
+
truncatedCount++;
|
|
10634
|
+
}
|
|
10635
|
+
if (estimatedTokens - saved > budget) {
|
|
10636
|
+
const clearable = candidates.filter((c) => {
|
|
10637
|
+
const out = extractCompletedToolOutput(c.part);
|
|
10638
|
+
return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
|
|
10639
|
+
}).sort((a, b) => a.index - b.index);
|
|
10640
|
+
for (const c of clearable) {
|
|
10641
|
+
if (estimatedTokens - saved <= budget) break;
|
|
10642
|
+
const current = extractCompletedToolOutput(c.part);
|
|
10643
|
+
if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
|
|
10644
|
+
c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
|
|
10645
|
+
saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
|
|
10646
|
+
clearedCount++;
|
|
10647
|
+
}
|
|
10648
|
+
}
|
|
10649
|
+
const finalEstimate = Math.max(0, estimatedTokens - saved);
|
|
10650
|
+
if (truncatedCount > 0 || clearedCount > 0) {
|
|
10651
|
+
logger.warn("Context budget guard: pruned tool outputs to fit the request", {
|
|
10652
|
+
session: state.sessionId,
|
|
10653
|
+
estimatedTokens: Math.round(estimatedTokens),
|
|
10654
|
+
budget,
|
|
10655
|
+
window,
|
|
10656
|
+
reserve,
|
|
10657
|
+
truncatedCount,
|
|
10658
|
+
clearedCount,
|
|
10659
|
+
estimatedSavedTokens: Math.round(saved),
|
|
10660
|
+
finalEstimate: Math.round(finalEstimate)
|
|
10414
10661
|
});
|
|
10415
10662
|
}
|
|
10663
|
+
if (finalEstimate > budget) {
|
|
10664
|
+
logger.warn(
|
|
10665
|
+
"Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
|
|
10666
|
+
{
|
|
10667
|
+
session: state.sessionId,
|
|
10668
|
+
finalEstimate: Math.round(finalEstimate),
|
|
10669
|
+
budget,
|
|
10670
|
+
window
|
|
10671
|
+
}
|
|
10672
|
+
);
|
|
10673
|
+
}
|
|
10674
|
+
return {
|
|
10675
|
+
applied: truncatedCount > 0 || clearedCount > 0,
|
|
10676
|
+
window,
|
|
10677
|
+
reserve,
|
|
10678
|
+
budget,
|
|
10679
|
+
estimatedTokens,
|
|
10680
|
+
finalEstimate,
|
|
10681
|
+
truncatedCount,
|
|
10682
|
+
clearedCount
|
|
10683
|
+
};
|
|
10416
10684
|
}
|
|
10417
10685
|
|
|
10418
10686
|
// lib/commands/context.ts
|
|
@@ -11371,11 +11639,12 @@ function runBatchCleanup(state, config, logger, messages) {
|
|
|
11371
11639
|
mergedCount: 0,
|
|
11372
11640
|
savedTokens: 0
|
|
11373
11641
|
};
|
|
11374
|
-
|
|
11642
|
+
const effective = resolveEffectiveContextLimit(state, config);
|
|
11643
|
+
if (!effective) {
|
|
11375
11644
|
return noop;
|
|
11376
11645
|
}
|
|
11377
11646
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
11378
|
-
if (currentTokens <
|
|
11647
|
+
if (currentTokens < effective.limit) {
|
|
11379
11648
|
return noop;
|
|
11380
11649
|
}
|
|
11381
11650
|
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
@@ -11392,7 +11661,8 @@ function runBatchCleanup(state, config, logger, messages) {
|
|
|
11392
11661
|
mergedCount: result.mergedCount,
|
|
11393
11662
|
savedTokens: result.savedTokens,
|
|
11394
11663
|
currentTokens,
|
|
11395
|
-
contextLimit:
|
|
11664
|
+
contextLimit: effective.limit,
|
|
11665
|
+
contextLimitSource: effective.source
|
|
11396
11666
|
});
|
|
11397
11667
|
return {
|
|
11398
11668
|
tier: 3,
|
|
@@ -11426,11 +11696,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
|
11426
11696
|
input.model?.limit?.context
|
|
11427
11697
|
);
|
|
11428
11698
|
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
11699
|
if (!state || state.isSubAgent && !config.allowSubAgents) {
|
|
11435
11700
|
return;
|
|
11436
11701
|
}
|
|
@@ -11439,6 +11704,23 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
|
11439
11704
|
logger.info("Skipping DCP system prompt injection for internal agent");
|
|
11440
11705
|
return;
|
|
11441
11706
|
}
|
|
11707
|
+
if (input.model?.limit?.context) {
|
|
11708
|
+
const limit = input.model.limit.context;
|
|
11709
|
+
const providerID = input.model?.providerID;
|
|
11710
|
+
const modelID = input.model?.id;
|
|
11711
|
+
const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
|
|
11712
|
+
state.modelContextLimit = limit;
|
|
11713
|
+
if (providerID !== void 0) {
|
|
11714
|
+
state.modelProviderID = providerID;
|
|
11715
|
+
}
|
|
11716
|
+
if (modelID !== void 0) {
|
|
11717
|
+
state.modelID = modelID;
|
|
11718
|
+
}
|
|
11719
|
+
if (changed) {
|
|
11720
|
+
saveSessionState(state, logger).catch(() => {
|
|
11721
|
+
});
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11442
11724
|
const effectivePermission = compressPermission(state, config);
|
|
11443
11725
|
if (effectivePermission === "deny") {
|
|
11444
11726
|
return;
|
|
@@ -11483,10 +11765,17 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11483
11765
|
config
|
|
11484
11766
|
);
|
|
11485
11767
|
const requestModel = lastUserMessage.info.model;
|
|
11486
|
-
|
|
11768
|
+
let requestModelLimit = registry4.resolveModelLimit(
|
|
11487
11769
|
requestModel?.providerID,
|
|
11488
11770
|
requestModel?.modelID
|
|
11489
11771
|
);
|
|
11772
|
+
if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
|
|
11773
|
+
requestModelLimit = await registry4.hydrateAndResolve(
|
|
11774
|
+
client,
|
|
11775
|
+
requestModel.providerID,
|
|
11776
|
+
requestModel.modelID
|
|
11777
|
+
);
|
|
11778
|
+
}
|
|
11490
11779
|
const prevModelID = state.modelID;
|
|
11491
11780
|
if (requestModelLimit !== void 0) {
|
|
11492
11781
|
state.modelContextLimit = requestModelLimit;
|
|
@@ -11516,6 +11805,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11516
11805
|
});
|
|
11517
11806
|
}
|
|
11518
11807
|
await updatePerTurnState(state, logger, messages);
|
|
11808
|
+
if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
|
|
11809
|
+
state.noContextLimitWarned = true;
|
|
11810
|
+
logger.warn(
|
|
11811
|
+
'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.',
|
|
11812
|
+
{
|
|
11813
|
+
session: state.sessionId,
|
|
11814
|
+
model: `${requestModel.providerID}/${requestModel.modelID}`
|
|
11815
|
+
}
|
|
11816
|
+
);
|
|
11817
|
+
}
|
|
11519
11818
|
}
|
|
11520
11819
|
syncCompressPermissionState(state, config, hostPermissions, output.messages);
|
|
11521
11820
|
if (state.isSubAgent && !config.allowSubAgents) {
|
|
@@ -11541,10 +11840,11 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11541
11840
|
}
|
|
11542
11841
|
}
|
|
11543
11842
|
ensureBuiltinFiltersRegistered();
|
|
11843
|
+
const effectiveLimit = resolveEffectiveContextLimit(state, config);
|
|
11544
11844
|
applyMessageFilters(output.messages, config.messageFilters, logger, {
|
|
11545
11845
|
sessionId: state.sessionId ?? "",
|
|
11546
11846
|
isSubAgent: state.isSubAgent,
|
|
11547
|
-
modelContextLimit:
|
|
11847
|
+
modelContextLimit: effectiveLimit?.limit
|
|
11548
11848
|
});
|
|
11549
11849
|
cacheSystemPromptTokens(state, output.messages);
|
|
11550
11850
|
assignMessageRefs(state, output.messages);
|
|
@@ -11564,6 +11864,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
11564
11864
|
const prePruneTokens = getCurrentTokenUsage(state, output.messages);
|
|
11565
11865
|
prune(state, logger, config, output.messages);
|
|
11566
11866
|
truncateLargeToolOutputs(state, config, logger, output.messages);
|
|
11867
|
+
enforceContextBudget(state, config, logger, output.messages);
|
|
11567
11868
|
hideConsumedCompressCalls(state, output.messages);
|
|
11568
11869
|
assignMessageRefs(state, output.messages);
|
|
11569
11870
|
const compressionPriorities = buildPriorityMap(config, state, output.messages);
|
|
@@ -11595,14 +11896,31 @@ ${text}`);
|
|
|
11595
11896
|
stripStaleMetadata(output.messages);
|
|
11596
11897
|
dropEmptyMessages(output.messages);
|
|
11597
11898
|
const postTokens = getCurrentTokenUsage(state, output.messages);
|
|
11899
|
+
if (postTokens !== void 0 && effectiveLimit) {
|
|
11900
|
+
const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
|
|
11901
|
+
if (postTokens > budget) {
|
|
11902
|
+
logger.error(
|
|
11903
|
+
"ACP hard guard: context exceeds model budget after in-flight reduction",
|
|
11904
|
+
{
|
|
11905
|
+
session: state.sessionId,
|
|
11906
|
+
postTokens,
|
|
11907
|
+
budget,
|
|
11908
|
+
contextLimit: effectiveLimit.limit,
|
|
11909
|
+
contextLimitSource: effectiveLimit.source,
|
|
11910
|
+
hint: "request will likely be rejected; run /compact or start a new session"
|
|
11911
|
+
}
|
|
11912
|
+
);
|
|
11913
|
+
}
|
|
11914
|
+
}
|
|
11598
11915
|
logger.info("Chat transform complete", {
|
|
11599
11916
|
session: state.sessionId,
|
|
11600
11917
|
model: state.modelID,
|
|
11601
11918
|
messages: output.messages.length,
|
|
11602
11919
|
prePruneTokens,
|
|
11603
11920
|
postTokens,
|
|
11604
|
-
contextLimit:
|
|
11605
|
-
|
|
11921
|
+
contextLimit: effectiveLimit?.limit,
|
|
11922
|
+
contextLimitSource: effectiveLimit?.source,
|
|
11923
|
+
usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
|
|
11606
11924
|
nudged: state.nudges.shouldInjectThisTurn
|
|
11607
11925
|
});
|
|
11608
11926
|
if (state.sessionId) {
|
|
@@ -11634,12 +11952,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
11634
11952
|
path: { id: input.sessionID }
|
|
11635
11953
|
});
|
|
11636
11954
|
const messages = filterMessages(messagesResponse.data || messagesResponse);
|
|
11637
|
-
const state = await registry4.getOrCreate(
|
|
11638
|
-
client,
|
|
11639
|
-
input.sessionID,
|
|
11640
|
-
messages,
|
|
11641
|
-
config
|
|
11642
|
-
);
|
|
11955
|
+
const state = await registry4.getOrCreate(client, input.sessionID, messages, config);
|
|
11643
11956
|
syncCompressPermissionState(state, config, hostPermissions, messages);
|
|
11644
11957
|
const commandCtx = {
|
|
11645
11958
|
client,
|
|
@@ -11653,7 +11966,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
11653
11966
|
const sub = input.arguments?.trim().toLowerCase();
|
|
11654
11967
|
if (sub === "stats" || sub === "status" || sub === "") {
|
|
11655
11968
|
await handleStatsCommand(commandCtx);
|
|
11656
|
-
|
|
11969
|
+
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11657
11970
|
}
|
|
11658
11971
|
if (sub === "export" || sub.startsWith("export ")) {
|
|
11659
11972
|
const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
|
|
@@ -11661,16 +11974,11 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
11661
11974
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11662
11975
|
}
|
|
11663
11976
|
if (sub === "help") {
|
|
11664
|
-
await sendIgnoredMessage(
|
|
11665
|
-
client,
|
|
11666
|
-
input.sessionID,
|
|
11667
|
-
buildHelpText(),
|
|
11668
|
-
{},
|
|
11669
|
-
logger
|
|
11670
|
-
);
|
|
11977
|
+
await sendIgnoredMessage(client, input.sessionID, buildHelpText(), {}, logger);
|
|
11671
11978
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11672
11979
|
}
|
|
11673
11980
|
await handleContextCommand(commandCtx);
|
|
11981
|
+
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11674
11982
|
}
|
|
11675
11983
|
};
|
|
11676
11984
|
}
|
|
@@ -11741,9 +12049,7 @@ function createEventHandler(registry4, logger) {
|
|
|
11741
12049
|
return;
|
|
11742
12050
|
}
|
|
11743
12051
|
if (typeof part.callID === "string" && typeof part.messageID === "string") {
|
|
11744
|
-
timing.startsByCallId.delete(
|
|
11745
|
-
buildCompressionTimingKey(part.messageID, part.callID)
|
|
11746
|
-
);
|
|
12052
|
+
timing.startsByCallId.delete(buildCompressionTimingKey(part.messageID, part.callID));
|
|
11747
12053
|
}
|
|
11748
12054
|
};
|
|
11749
12055
|
}
|
|
@@ -12020,7 +12326,7 @@ var server = (async (ctx) => {
|
|
|
12020
12326
|
}
|
|
12021
12327
|
const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
|
|
12022
12328
|
logger.info("ACP plugin initialized", {
|
|
12023
|
-
version: true ? "1.16.0-pr.
|
|
12329
|
+
version: true ? "1.16.0-pr.350.123" : "dev",
|
|
12024
12330
|
workspace: ctx.directory,
|
|
12025
12331
|
logLevel: logger.level,
|
|
12026
12332
|
debug: config.debug,
|