opencode-acp 1.14.25-pr.348.72 → 1.14.25-pr.350.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -908,8 +908,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
908
908
  "compress.preserveRecentMessages",
909
909
  "compress.preserveRecentTokens",
910
910
  "compress.preserveLastUserMessage",
911
- "compress.overflowGuard",
912
- "compress.overflowGuardReserve",
913
911
  "gc",
914
912
  "gc.algorithm",
915
913
  "gc.promotionThreshold",
@@ -1263,27 +1261,6 @@ function validateConfigTypes(config) {
1263
1261
  actual: typeof compress.preserveLastUserMessage
1264
1262
  });
1265
1263
  }
1266
- if (compress.overflowGuard !== void 0 && typeof compress.overflowGuard !== "boolean") {
1267
- errors.push({
1268
- key: "compress.overflowGuard",
1269
- expected: "boolean",
1270
- actual: typeof compress.overflowGuard
1271
- });
1272
- }
1273
- if (compress.overflowGuardReserve !== void 0 && typeof compress.overflowGuardReserve !== "number") {
1274
- errors.push({
1275
- key: "compress.overflowGuardReserve",
1276
- expected: "number",
1277
- actual: typeof compress.overflowGuardReserve
1278
- });
1279
- }
1280
- if (typeof compress.overflowGuardReserve === "number" && compress.overflowGuardReserve < 0) {
1281
- errors.push({
1282
- key: "compress.overflowGuardReserve",
1283
- expected: "non-negative number (>= 0)",
1284
- actual: `${compress.overflowGuardReserve}`
1285
- });
1286
- }
1287
1264
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1288
1265
  errors.push({
1289
1266
  key: "compress.iterationNudgeThreshold",
@@ -1529,9 +1506,7 @@ var defaultConfig = {
1529
1506
  lastSegmentSoftBlock: true,
1530
1507
  preserveRecentMessages: 5,
1531
1508
  preserveRecentTokens: 5e3,
1532
- preserveLastUserMessage: true,
1533
- overflowGuard: true,
1534
- overflowGuardReserve: 32768
1509
+ preserveLastUserMessage: true
1535
1510
  },
1536
1511
  gc: {
1537
1512
  algorithm: "truncate",
@@ -1668,8 +1643,7 @@ function mergeCompress(base, override) {
1668
1643
  preserveRecentMessages: override.preserveRecentMessages ?? base.preserveRecentMessages,
1669
1644
  preserveRecentTokens: override.preserveRecentTokens ?? base.preserveRecentTokens,
1670
1645
  preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage,
1671
- overflowGuard: override.overflowGuard ?? base.overflowGuard,
1672
- overflowGuardReserve: override.overflowGuardReserve ?? base.overflowGuardReserve
1646
+ completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1673
1647
  };
1674
1648
  }
1675
1649
  function mergeCommands(base, override) {
@@ -4329,8 +4303,7 @@ function createSessionState() {
4329
4303
  modelID: void 0,
4330
4304
  systemPromptTokens: void 0,
4331
4305
  qualityGateRetryPending: false,
4332
- uncalibratedWindowTransforms: 0,
4333
- uncalibratedWindowWarned: false
4306
+ noContextLimitWarned: false
4334
4307
  };
4335
4308
  }
4336
4309
  function resetSessionState(state) {
@@ -4372,8 +4345,7 @@ function resetSessionState(state) {
4372
4345
  state.modelID = void 0;
4373
4346
  state.systemPromptTokens = void 0;
4374
4347
  state.qualityGateRetryPending = false;
4375
- state.uncalibratedWindowTransforms = 0;
4376
- state.uncalibratedWindowWarned = false;
4348
+ state.noContextLimitWarned = false;
4377
4349
  }
4378
4350
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config) {
4379
4351
  if (state.sessionId === sessionId) {
@@ -6420,66 +6392,159 @@ var filterCompressedRanges = (state, messages) => {
6420
6392
  messages.push(...result);
6421
6393
  };
6422
6394
 
6423
- // lib/prompts/extensions/nudge.ts
6424
- function buildCompressedBlockGuidance(state, context) {
6425
- const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
6426
- const blockCount = activeBlockIds.length;
6427
- const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6428
- const totalSummaryTokens = blocksForStats.reduce((s, b) => s + (b.summaryTokens ?? 0), 0);
6429
- const totalSummaryDisplay = totalSummaryTokens >= 1e3 ? `${(totalSummaryTokens / 1e3).toFixed(1)}K` : String(totalSummaryTokens);
6430
- const lastBlock = blocksForStats.length > 0 ? blocksForStats.reduce((latest, b) => b.createdAt > latest.createdAt ? b : latest) : null;
6431
- const ageStr = lastBlock ? formatAge(lastBlock.createdAt) : "never";
6432
- const lines = [
6433
- `- Compressed blocks: ${blockCount} (${totalSummaryDisplay} summary, last ${ageStr}). Use acp_status for details.`
6434
- ];
6435
- if (blockCount > 50) {
6436
- const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
6437
- const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
6438
- const visibleMessageIds = context?.visibleMessageIds;
6439
- const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
6440
- if (visibleOldBlocks.length > 5) {
6441
- const blocksWithRef = visibleOldBlocks.map((block) => {
6442
- const ref = state.messageIds.byRawId.get(block.anchorMessageId);
6443
- return ref ? { block, ref } : null;
6444
- }).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
6445
- const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6446
- const totalK = Math.max(1, Math.round(totalTokens / 1e3));
6447
- const targets = [];
6448
- const chunkSize = Math.ceil(blocksWithRef.length / 3);
6449
- for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
6450
- const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
6451
- if (chunk.length < 2) continue;
6452
- const startRef = chunk[0].ref;
6453
- const endRef = chunk[chunk.length - 1].ref;
6454
- const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6455
- const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
6456
- targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
6395
+ // lib/messages/sync.ts
6396
+ function sortBlocksByCreation(a, b) {
6397
+ const createdAtDiff = a.createdAt - b.createdAt;
6398
+ if (createdAtDiff !== 0) {
6399
+ return createdAtDiff;
6400
+ }
6401
+ return a.blockId - b.blockId;
6402
+ }
6403
+ var syncCompressionBlocks = (state, logger, messages) => {
6404
+ const messagesState = state.prune.messages;
6405
+ if (!messagesState?.blocksById?.size) {
6406
+ return;
6407
+ }
6408
+ const messageIds = new Set(messages.map((msg) => msg.info.id));
6409
+ const previousActiveBlockIds = new Set(
6410
+ Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
6411
+ );
6412
+ messagesState.activeBlockIds.clear();
6413
+ messagesState.activeByAnchorMessageId.clear();
6414
+ const now = Date.now();
6415
+ const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
6416
+ for (const block of orderedBlocks) {
6417
+ if (block.deactivatedByUser || block.deactivatedByUserDeep) {
6418
+ block.active = false;
6419
+ if (block.deactivatedAt === void 0) {
6420
+ block.deactivatedAt = now;
6457
6421
  }
6458
- if (targets.length > 0) {
6459
- lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
6460
- lines.push(...targets);
6461
- lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write your summary normally.`);
6422
+ block.deactivatedByBlockId = void 0;
6423
+ continue;
6424
+ }
6425
+ for (const consumedBlockId of block.consumedBlockIds) {
6426
+ if (!messagesState.activeBlockIds.has(consumedBlockId)) {
6427
+ continue;
6428
+ }
6429
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId);
6430
+ if (consumedBlock) {
6431
+ consumedBlock.active = false;
6432
+ consumedBlock.deactivatedAt = now;
6433
+ consumedBlock.deactivatedByBlockId = block.blockId;
6434
+ const mappedBlockId = messagesState.activeByAnchorMessageId.get(
6435
+ consumedBlock.anchorMessageId
6436
+ );
6437
+ if (mappedBlockId === consumedBlock.blockId) {
6438
+ messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
6439
+ }
6462
6440
  }
6441
+ messagesState.activeBlockIds.delete(consumedBlockId);
6442
+ }
6443
+ block.active = true;
6444
+ block.deactivatedAt = void 0;
6445
+ block.deactivatedByBlockId = void 0;
6446
+ messagesState.activeBlockIds.add(block.blockId);
6447
+ if (messageIds.has(block.anchorMessageId)) {
6448
+ messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
6463
6449
  }
6464
6450
  }
6465
- return lines.join("\n");
6466
- }
6467
- function appendGuidanceToDcpTag(nudgeText, guidance) {
6468
- if (!guidance.trim()) {
6469
- return nudgeText;
6451
+ for (const entry of messagesState.byMessageId.values()) {
6452
+ const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
6453
+ entry.allBlockIds = allBlockIds;
6454
+ entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
6470
6455
  }
6471
- const closeTag = "</dcp-system-reminder>";
6472
- const closeTagIndex = nudgeText.lastIndexOf(closeTag);
6473
- if (closeTagIndex === -1) {
6474
- return nudgeText;
6456
+ const nextActiveBlockIds = messagesState.activeBlockIds;
6457
+ let deactivatedCount = 0;
6458
+ let reactivatedCount = 0;
6459
+ for (const blockId of previousActiveBlockIds) {
6460
+ if (!nextActiveBlockIds.has(blockId)) {
6461
+ deactivatedCount++;
6462
+ }
6475
6463
  }
6476
- const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd();
6477
- const afterClose = nudgeText.slice(closeTagIndex);
6478
- return `${beforeClose}
6464
+ for (const blockId of nextActiveBlockIds) {
6465
+ if (!previousActiveBlockIds.has(blockId)) {
6466
+ reactivatedCount++;
6467
+ }
6468
+ }
6469
+ if (deactivatedCount > 0 || reactivatedCount > 0) {
6470
+ logger.info("Synced compress block state", {
6471
+ deactivatedCount,
6472
+ reactivatedCount
6473
+ });
6474
+ }
6475
+ };
6479
6476
 
6480
- ${guidance}
6481
- ${afterClose}`;
6482
- }
6477
+ // lib/host-permissions.ts
6478
+ var findLastMatchingRule = (rules, predicate) => {
6479
+ for (let index = rules.length - 1; index >= 0; index -= 1) {
6480
+ const rule = rules[index];
6481
+ if (rule && predicate(rule)) {
6482
+ return rule;
6483
+ }
6484
+ }
6485
+ return void 0;
6486
+ };
6487
+ var wildcardMatch = (value, pattern) => {
6488
+ const normalizedValue = value.replaceAll("\\", "/");
6489
+ let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
6490
+ if (escaped.endsWith(" .*")) {
6491
+ escaped = escaped.slice(0, -3) + "( .*)?";
6492
+ }
6493
+ const flags = process.platform === "win32" ? "si" : "s";
6494
+ return new RegExp(`^${escaped}$`, flags).test(normalizedValue);
6495
+ };
6496
+ var getPermissionRules = (permissionConfigs) => {
6497
+ const rules = [];
6498
+ for (const permissionConfig of permissionConfigs) {
6499
+ if (!permissionConfig) {
6500
+ continue;
6501
+ }
6502
+ for (const [permission, value] of Object.entries(permissionConfig)) {
6503
+ if (value === "ask" || value === "allow" || value === "deny") {
6504
+ rules.push({ permission, pattern: "*", action: value });
6505
+ continue;
6506
+ }
6507
+ for (const [pattern, action] of Object.entries(value)) {
6508
+ if (action === "ask" || action === "allow" || action === "deny") {
6509
+ rules.push({ permission, pattern, action });
6510
+ }
6511
+ }
6512
+ }
6513
+ }
6514
+ return rules;
6515
+ };
6516
+ var compressDisabledByOpencode = (...permissionConfigs) => {
6517
+ const match = findLastMatchingRule(
6518
+ getPermissionRules(permissionConfigs),
6519
+ (rule) => wildcardMatch("compress", rule.permission)
6520
+ );
6521
+ return match?.pattern === "*" && match.action === "deny";
6522
+ };
6523
+ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agentName) => {
6524
+ if (basePermission === "deny") {
6525
+ return "deny";
6526
+ }
6527
+ return compressDisabledByOpencode(
6528
+ hostPermissions.global,
6529
+ agentName ? hostPermissions.agents[agentName] : void 0
6530
+ ) ? "deny" : basePermission;
6531
+ };
6532
+ var hasExplicitToolPermission = (permissionConfig, tool6) => {
6533
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool6) : false;
6534
+ };
6535
+
6536
+ // lib/compress-permission.ts
6537
+ var compressPermission = (state, config) => {
6538
+ return state.compressPermission ?? config.compress.permission;
6539
+ };
6540
+ var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
6541
+ const activeAgent = getLastUserMessage(messages)?.info.agent;
6542
+ state.compressPermission = resolveEffectiveCompressPermission(
6543
+ config.compress.permission,
6544
+ hostPermissions,
6545
+ activeAgent
6546
+ );
6547
+ };
6483
6548
 
6484
6549
  // lib/messages/utils.ts
6485
6550
  import { createHash } from "crypto";
@@ -6655,51 +6720,112 @@ var dropEmptyMessages = (messages) => {
6655
6720
  return removed;
6656
6721
  };
6657
6722
 
6658
- // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
6659
- var NUDGE_GROWTH_FLOOR = 6e3;
6660
- var NUDGE_GROWTH_CAP = 5e4;
6661
- var NUDGE_GROWTH_RATIO = 0.05;
6662
- function computeShouldNudge(params) {
6663
- const { currentTokens, overMinLimit, overMaxLimit } = params;
6664
- if (currentTokens === void 0) {
6665
- return { shouldNudge: false, tipsVariant: null };
6666
- }
6667
- if (params.lastNudgeTokens === void 0) {
6668
- return { shouldNudge: false, tipsVariant: null };
6669
- }
6670
- const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
6671
- const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
6672
- if (!shouldNudge) {
6673
- return { shouldNudge: false, tipsVariant: null };
6674
- }
6675
- const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
6676
- return { shouldNudge: true, tipsVariant };
6677
- }
6678
- function resolveAdaptiveNudgeGrowth(modelContextLimit) {
6679
- if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
6680
- return Math.min(
6681
- NUDGE_GROWTH_CAP,
6682
- Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
6683
- );
6684
- }
6685
- var defaultTriggerPolicy = {
6686
- name: "context-compress-algorithms-trigger",
6687
- version: "1.0.0",
6688
- description: "Growth-only cadence: nudge when context growth since last nudge exceeds adaptive threshold, or when over max limit.",
6689
- computeShouldNudge,
6690
- resolveAdaptiveNudgeGrowth
6691
- };
6692
-
6693
- // lib/messages/inject/policy/registry.ts
6694
- var registry2 = /* @__PURE__ */ new Map();
6695
- var defaultPolicy = null;
6696
- function registerTriggerPolicy2(policy) {
6697
- if (!policy.name) {
6698
- throw new Error("TriggerPolicy must have a name");
6699
- }
6700
- registry2.set(policy.name, policy);
6701
- if (!defaultPolicy) {
6702
- defaultPolicy = policy;
6723
+ // lib/prompts/extensions/nudge.ts
6724
+ function buildCompressedBlockGuidance(state, context) {
6725
+ const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
6726
+ const blockCount = activeBlockIds.length;
6727
+ const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6728
+ const totalSummaryTokens = blocksForStats.reduce((s, b) => s + (b.summaryTokens ?? 0), 0);
6729
+ const totalSummaryDisplay = totalSummaryTokens >= 1e3 ? `${(totalSummaryTokens / 1e3).toFixed(1)}K` : String(totalSummaryTokens);
6730
+ const lastBlock = blocksForStats.length > 0 ? blocksForStats.reduce((latest, b) => b.createdAt > latest.createdAt ? b : latest) : null;
6731
+ const ageStr = lastBlock ? formatAge(lastBlock.createdAt) : "never";
6732
+ const lines = [
6733
+ `- Compressed blocks: ${blockCount} (${totalSummaryDisplay} summary, last ${ageStr}). Use acp_status for details.`
6734
+ ];
6735
+ if (blockCount > 50) {
6736
+ const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
6737
+ const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
6738
+ const visibleMessageIds = context?.visibleMessageIds;
6739
+ const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
6740
+ if (visibleOldBlocks.length > 5) {
6741
+ const blocksWithRef = visibleOldBlocks.map((block) => {
6742
+ const ref = state.messageIds.byRawId.get(block.anchorMessageId);
6743
+ return ref ? { block, ref } : null;
6744
+ }).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
6745
+ const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6746
+ const totalK = Math.max(1, Math.round(totalTokens / 1e3));
6747
+ const targets = [];
6748
+ const chunkSize = Math.ceil(blocksWithRef.length / 3);
6749
+ for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
6750
+ const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
6751
+ if (chunk.length < 2) continue;
6752
+ const startRef = chunk[0].ref;
6753
+ const endRef = chunk[chunk.length - 1].ref;
6754
+ const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
6755
+ const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
6756
+ targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
6757
+ }
6758
+ if (targets.length > 0) {
6759
+ lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
6760
+ lines.push(...targets);
6761
+ lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write your summary normally.`);
6762
+ }
6763
+ }
6764
+ }
6765
+ return lines.join("\n");
6766
+ }
6767
+ function appendGuidanceToDcpTag(nudgeText, guidance) {
6768
+ if (!guidance.trim()) {
6769
+ return nudgeText;
6770
+ }
6771
+ const closeTag = "</dcp-system-reminder>";
6772
+ const closeTagIndex = nudgeText.lastIndexOf(closeTag);
6773
+ if (closeTagIndex === -1) {
6774
+ return nudgeText;
6775
+ }
6776
+ const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd();
6777
+ const afterClose = nudgeText.slice(closeTagIndex);
6778
+ return `${beforeClose}
6779
+
6780
+ ${guidance}
6781
+ ${afterClose}`;
6782
+ }
6783
+
6784
+ // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
6785
+ var NUDGE_GROWTH_FLOOR = 6e3;
6786
+ var NUDGE_GROWTH_CAP = 5e4;
6787
+ var NUDGE_GROWTH_RATIO = 0.05;
6788
+ function computeShouldNudge(params) {
6789
+ const { currentTokens, overMinLimit, overMaxLimit } = params;
6790
+ if (currentTokens === void 0) {
6791
+ return { shouldNudge: false, tipsVariant: null };
6792
+ }
6793
+ if (params.lastNudgeTokens === void 0) {
6794
+ return { shouldNudge: false, tipsVariant: null };
6795
+ }
6796
+ const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
6797
+ const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
6798
+ if (!shouldNudge) {
6799
+ return { shouldNudge: false, tipsVariant: null };
6800
+ }
6801
+ const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
6802
+ return { shouldNudge: true, tipsVariant };
6803
+ }
6804
+ function resolveAdaptiveNudgeGrowth(modelContextLimit) {
6805
+ if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
6806
+ return Math.min(
6807
+ NUDGE_GROWTH_CAP,
6808
+ Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
6809
+ );
6810
+ }
6811
+ var defaultTriggerPolicy = {
6812
+ name: "context-compress-algorithms-trigger",
6813
+ version: "1.0.0",
6814
+ description: "Growth-only cadence: nudge when context growth since last nudge exceeds adaptive threshold, or when over max limit.",
6815
+ computeShouldNudge,
6816
+ resolveAdaptiveNudgeGrowth
6817
+ };
6818
+
6819
+ // lib/messages/inject/policy/registry.ts
6820
+ var registry2 = /* @__PURE__ */ new Map();
6821
+ var defaultPolicy = null;
6822
+ function registerTriggerPolicy2(policy) {
6823
+ if (!policy.name) {
6824
+ throw new Error("TriggerPolicy must have a name");
6825
+ }
6826
+ registry2.set(policy.name, policy);
6827
+ if (!defaultPolicy) {
6828
+ defaultPolicy = policy;
6703
6829
  }
6704
6830
  }
6705
6831
  function getDefaultTriggerPolicy() {
@@ -7358,291 +7484,6 @@ function excludeProtectedRanges(ranges, protectedRefs) {
7358
7484
  );
7359
7485
  }
7360
7486
 
7361
- // lib/messages/prune-to-fit.ts
7362
- var WIRE_SAFETY_MARGIN = 8192;
7363
- var CLEAR_PLACEHOLDER = "[cleared by ACP overflow guard \u2014 re-run tool if needed]";
7364
- var MIN_CLEAR_TOKENS = 500;
7365
- function resolveKnownWindow(config, state, providerId, modelId) {
7366
- if (state.modelContextLimit !== void 0) {
7367
- return state.modelContextLimit;
7368
- }
7369
- const maxLimits = config.compress.modelMaxLimits;
7370
- if (maxLimits && providerId !== void 0 && modelId !== void 0) {
7371
- const perModel = maxLimits[`${providerId}/${modelId}`];
7372
- if (typeof perModel === "number") return perModel;
7373
- }
7374
- const global = config.compress.maxContextLimit;
7375
- if (typeof global === "number") return global;
7376
- return void 0;
7377
- }
7378
- function estimateWireTokens(state, messages) {
7379
- const base = getCurrentTokenUsage(state, messages);
7380
- if (base > 0) {
7381
- let trailing = 0;
7382
- const lastAsst = [...messages].reverse().find((m) => m.info.role === "assistant");
7383
- if (lastAsst) {
7384
- const parts = Array.isArray(lastAsst.parts) ? lastAsst.parts : [];
7385
- for (let i = parts.length - 1; i >= 0; i--) {
7386
- const p = parts[i];
7387
- if (p?.type !== "tool") break;
7388
- if (p.state.status !== "completed") break;
7389
- trailing += countTokens2(extractCompletedToolOutput(p) ?? "");
7390
- }
7391
- }
7392
- return base + trailing + WIRE_SAFETY_MARGIN;
7393
- }
7394
- let total = 0;
7395
- for (const msg of messages) {
7396
- total += countAllMessageTokens(msg);
7397
- }
7398
- return total + (state.systemPromptTokens ?? 0) + WIRE_SAFETY_MARGIN;
7399
- }
7400
- function pruneToFit(state, config, logger, messages) {
7401
- if (!config.compress.overflowGuard) return;
7402
- const knownWindow = resolveKnownWindow(config, state, state.modelProviderID, state.modelID);
7403
- if (knownWindow === void 0) return;
7404
- const reserve = config.compress.overflowGuardReserve ?? 32768;
7405
- const safeBudget = knownWindow - reserve;
7406
- if (safeBudget <= 0) return;
7407
- const estimate = estimateWireTokens(state, messages);
7408
- if (estimate <= safeBudget) return;
7409
- const protectedRefs = computeProtectedRefs(messages, state, config.compress);
7410
- const lastNonIgnored = findLastNonIgnoredMessage(messages);
7411
- const lastMsgId = lastNonIgnored?.message.info.id;
7412
- const protectedTools = config.compress.protectedTools;
7413
- const protectedFilePatterns = config.protectedFilePatterns;
7414
- let freed = 0;
7415
- let clearedCount = 0;
7416
- for (const msg of messages) {
7417
- if (estimate - freed <= safeBudget) break;
7418
- if (msg.info.id === lastMsgId) continue;
7419
- if (msg.info.role === "user") continue;
7420
- const ref = state.messageIds.byRawId.get(msg.info.id);
7421
- if (ref && protectedRefs.has(ref)) continue;
7422
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
7423
- for (const part of parts) {
7424
- if (estimate - freed <= safeBudget) break;
7425
- if (part?.type !== "tool") continue;
7426
- const toolState = part.state;
7427
- if (toolState.status !== "completed") continue;
7428
- const content = extractCompletedToolOutput(part);
7429
- if (content === void 0) continue;
7430
- if (content === CLEAR_PLACEHOLDER) continue;
7431
- if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
7432
- if (isToolNameProtected(part.tool, protectedTools)) continue;
7433
- if (protectedFilePatterns.length > 0) {
7434
- const filePaths = getFilePathsFromParameters(part.tool, toolState.input);
7435
- if (isFilePathProtected(filePaths, protectedFilePatterns)) continue;
7436
- }
7437
- const outputTokens = countTokens2(content);
7438
- if (outputTokens < MIN_CLEAR_TOKENS) continue;
7439
- toolState.output = CLEAR_PLACEHOLDER;
7440
- freed += outputTokens - countTokens2(CLEAR_PLACEHOLDER);
7441
- clearedCount++;
7442
- }
7443
- }
7444
- const after = estimate - freed;
7445
- const detail = {
7446
- session: state.sessionId,
7447
- estimate,
7448
- safeBudget,
7449
- knownWindow,
7450
- reserve,
7451
- clearedCount,
7452
- freedTokens: Math.round(freed),
7453
- afterTokens: Math.round(after)
7454
- };
7455
- if (clearedCount === 0) {
7456
- logger.error("ACP overflow guard: over window but no clearable tool outputs", detail);
7457
- return;
7458
- }
7459
- if (after <= safeBudget) {
7460
- logger.warn("ACP overflow guard: cleared tool outputs to fit context window", detail);
7461
- } else {
7462
- logger.error(
7463
- "ACP overflow guard: cleared tool outputs but context STILL exceeds window",
7464
- detail
7465
- );
7466
- }
7467
- }
7468
-
7469
- // lib/messages/uncalibrated-window.ts
7470
- var UNCALIBRATED_WINDOW_WARN_THRESHOLD = 3;
7471
- function trackUncalibratedWindow(state, logger) {
7472
- if (state.modelContextLimit === void 0) {
7473
- state.uncalibratedWindowTransforms++;
7474
- if (state.uncalibratedWindowTransforms >= UNCALIBRATED_WINDOW_WARN_THRESHOLD && !state.uncalibratedWindowWarned) {
7475
- state.uncalibratedWindowWarned = true;
7476
- logger.warn(
7477
- "Model reports no context window \u2014 ACP percentage thresholds are disabled",
7478
- {
7479
- session: state.sessionId,
7480
- provider: state.modelProviderID,
7481
- model: state.modelID,
7482
- transforms: state.uncalibratedWindowTransforms,
7483
- hint: 'set the model\'s `limit` in opencode.json (e.g. {"context": 262144, "output": 16384}) or use absolute compress.maxContextLimit / compress.minContextLimit in acp.jsonc; the request-side overflow guard also needs a known window to fire'
7484
- }
7485
- );
7486
- }
7487
- } else {
7488
- state.uncalibratedWindowTransforms = 0;
7489
- }
7490
- }
7491
-
7492
- // lib/messages/sync.ts
7493
- function sortBlocksByCreation(a, b) {
7494
- const createdAtDiff = a.createdAt - b.createdAt;
7495
- if (createdAtDiff !== 0) {
7496
- return createdAtDiff;
7497
- }
7498
- return a.blockId - b.blockId;
7499
- }
7500
- var syncCompressionBlocks = (state, logger, messages) => {
7501
- const messagesState = state.prune.messages;
7502
- if (!messagesState?.blocksById?.size) {
7503
- return;
7504
- }
7505
- const messageIds = new Set(messages.map((msg) => msg.info.id));
7506
- const previousActiveBlockIds = new Set(
7507
- Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
7508
- );
7509
- messagesState.activeBlockIds.clear();
7510
- messagesState.activeByAnchorMessageId.clear();
7511
- const now = Date.now();
7512
- const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
7513
- for (const block of orderedBlocks) {
7514
- if (block.deactivatedByUser || block.deactivatedByUserDeep) {
7515
- block.active = false;
7516
- if (block.deactivatedAt === void 0) {
7517
- block.deactivatedAt = now;
7518
- }
7519
- block.deactivatedByBlockId = void 0;
7520
- continue;
7521
- }
7522
- for (const consumedBlockId of block.consumedBlockIds) {
7523
- if (!messagesState.activeBlockIds.has(consumedBlockId)) {
7524
- continue;
7525
- }
7526
- const consumedBlock = messagesState.blocksById.get(consumedBlockId);
7527
- if (consumedBlock) {
7528
- consumedBlock.active = false;
7529
- consumedBlock.deactivatedAt = now;
7530
- consumedBlock.deactivatedByBlockId = block.blockId;
7531
- const mappedBlockId = messagesState.activeByAnchorMessageId.get(
7532
- consumedBlock.anchorMessageId
7533
- );
7534
- if (mappedBlockId === consumedBlock.blockId) {
7535
- messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
7536
- }
7537
- }
7538
- messagesState.activeBlockIds.delete(consumedBlockId);
7539
- }
7540
- block.active = true;
7541
- block.deactivatedAt = void 0;
7542
- block.deactivatedByBlockId = void 0;
7543
- messagesState.activeBlockIds.add(block.blockId);
7544
- if (messageIds.has(block.anchorMessageId)) {
7545
- messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
7546
- }
7547
- }
7548
- for (const entry of messagesState.byMessageId.values()) {
7549
- const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
7550
- entry.allBlockIds = allBlockIds;
7551
- entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
7552
- }
7553
- const nextActiveBlockIds = messagesState.activeBlockIds;
7554
- let deactivatedCount = 0;
7555
- let reactivatedCount = 0;
7556
- for (const blockId of previousActiveBlockIds) {
7557
- if (!nextActiveBlockIds.has(blockId)) {
7558
- deactivatedCount++;
7559
- }
7560
- }
7561
- for (const blockId of nextActiveBlockIds) {
7562
- if (!previousActiveBlockIds.has(blockId)) {
7563
- reactivatedCount++;
7564
- }
7565
- }
7566
- if (deactivatedCount > 0 || reactivatedCount > 0) {
7567
- logger.info("Synced compress block state", {
7568
- deactivatedCount,
7569
- reactivatedCount
7570
- });
7571
- }
7572
- };
7573
-
7574
- // lib/host-permissions.ts
7575
- var findLastMatchingRule = (rules, predicate) => {
7576
- for (let index = rules.length - 1; index >= 0; index -= 1) {
7577
- const rule = rules[index];
7578
- if (rule && predicate(rule)) {
7579
- return rule;
7580
- }
7581
- }
7582
- return void 0;
7583
- };
7584
- var wildcardMatch = (value, pattern) => {
7585
- const normalizedValue = value.replaceAll("\\", "/");
7586
- let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
7587
- if (escaped.endsWith(" .*")) {
7588
- escaped = escaped.slice(0, -3) + "( .*)?";
7589
- }
7590
- const flags = process.platform === "win32" ? "si" : "s";
7591
- return new RegExp(`^${escaped}$`, flags).test(normalizedValue);
7592
- };
7593
- var getPermissionRules = (permissionConfigs) => {
7594
- const rules = [];
7595
- for (const permissionConfig of permissionConfigs) {
7596
- if (!permissionConfig) {
7597
- continue;
7598
- }
7599
- for (const [permission, value] of Object.entries(permissionConfig)) {
7600
- if (value === "ask" || value === "allow" || value === "deny") {
7601
- rules.push({ permission, pattern: "*", action: value });
7602
- continue;
7603
- }
7604
- for (const [pattern, action] of Object.entries(value)) {
7605
- if (action === "ask" || action === "allow" || action === "deny") {
7606
- rules.push({ permission, pattern, action });
7607
- }
7608
- }
7609
- }
7610
- }
7611
- return rules;
7612
- };
7613
- var compressDisabledByOpencode = (...permissionConfigs) => {
7614
- const match = findLastMatchingRule(
7615
- getPermissionRules(permissionConfigs),
7616
- (rule) => wildcardMatch("compress", rule.permission)
7617
- );
7618
- return match?.pattern === "*" && match.action === "deny";
7619
- };
7620
- var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agentName) => {
7621
- if (basePermission === "deny") {
7622
- return "deny";
7623
- }
7624
- return compressDisabledByOpencode(
7625
- hostPermissions.global,
7626
- agentName ? hostPermissions.agents[agentName] : void 0
7627
- ) ? "deny" : basePermission;
7628
- };
7629
- var hasExplicitToolPermission = (permissionConfig, tool6) => {
7630
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool6) : false;
7631
- };
7632
-
7633
- // lib/compress-permission.ts
7634
- var compressPermission = (state, config) => {
7635
- return state.compressPermission ?? config.compress.permission;
7636
- };
7637
- var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
7638
- const activeAgent = getLastUserMessage(messages)?.info.agent;
7639
- state.compressPermission = resolveEffectiveCompressPermission(
7640
- config.compress.permission,
7641
- hostPermissions,
7642
- activeAgent
7643
- );
7644
- };
7645
-
7646
7487
  // lib/messages/inject/inject.ts
7647
7488
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
7648
7489
  function createSuffixMessage(messages) {
@@ -9272,7 +9113,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9272
9113
  import { join as join3 } from "path";
9273
9114
  import { existsSync as existsSync3 } from "fs";
9274
9115
  import { homedir as homedir3 } from "os";
9275
- var LOG_VERSION = true ? "1.14.25-pr.348.72" : "dev";
9116
+ var LOG_VERSION = true ? "1.14.25-pr.350.73" : "dev";
9276
9117
  var LEVEL_RANK = {
9277
9118
  debug: 10,
9278
9119
  info: 20,
@@ -10157,6 +9998,148 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10157
9998
  }
10158
9999
  }
10159
10000
 
10001
+ // lib/messages/enforce-budget.ts
10002
+ var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
10003
+ var TRUNCATION_MARKER2 = "[truncated for context space";
10004
+ var KEEP_PREFIX_CHARS2 = 2e3;
10005
+ var KEEP_SUFFIX_CHARS2 = 2e3;
10006
+ var PROTECT_RECENT_MESSAGES2 = 3;
10007
+ var MIN_CLEAR_TOKENS = 200;
10008
+ function resolveContextWindow(state) {
10009
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
10010
+ return state.modelContextLimit;
10011
+ }
10012
+ return void 0;
10013
+ }
10014
+ function estimateWireTokens(state, messages) {
10015
+ const base = getCurrentTokenUsage(state, messages);
10016
+ if (base > 0) {
10017
+ let lastAssistant = -1;
10018
+ for (let i = messages.length - 1; i >= 0; i--) {
10019
+ if (messages[i].info.role === "assistant") {
10020
+ lastAssistant = i;
10021
+ break;
10022
+ }
10023
+ }
10024
+ let additions = 0;
10025
+ for (let i = lastAssistant + 1; i < messages.length; i++) {
10026
+ additions += countAllMessageTokens(messages[i]);
10027
+ }
10028
+ return base + additions;
10029
+ }
10030
+ let total = 0;
10031
+ for (const m of messages) total += countAllMessageTokens(m);
10032
+ return total + (state.systemPromptTokens ?? 0);
10033
+ }
10034
+ function enforceContextBudget(state, config, logger, messages) {
10035
+ const window = resolveContextWindow(state);
10036
+ if (window === void 0) return void 0;
10037
+ const reserve = config.compress?.completionReserveTokens ?? DEFAULT_COMPLETION_RESERVE_TOKENS;
10038
+ const budget = window - reserve;
10039
+ if (budget <= 0) return void 0;
10040
+ const estimatedTokens = estimateWireTokens(state, messages);
10041
+ if (estimatedTokens <= budget) {
10042
+ return {
10043
+ applied: false,
10044
+ window,
10045
+ reserve,
10046
+ budget,
10047
+ estimatedTokens,
10048
+ finalEstimate: estimatedTokens,
10049
+ truncatedCount: 0,
10050
+ clearedCount: 0
10051
+ };
10052
+ }
10053
+ const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
10054
+ const protectedTools = new Set(config.compress?.protectedTools ?? []);
10055
+ const candidates = [];
10056
+ for (let mi = 0; mi < protectedIndex; mi++) {
10057
+ if (mi === 0 && messages[mi].info.role === "user") continue;
10058
+ const msg = messages[mi];
10059
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10060
+ for (const part of parts) {
10061
+ if (part?.type !== "tool") continue;
10062
+ if (part.state?.status !== "completed") continue;
10063
+ if (part.tool === "compress") continue;
10064
+ if (protectedTools.has(part.tool)) continue;
10065
+ const content = extractCompletedToolOutput(part);
10066
+ if (content === void 0) continue;
10067
+ if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10068
+ const tokens = countTokens2(content);
10069
+ if (tokens <= 0) continue;
10070
+ candidates.push({ part, content, tokens, index: mi });
10071
+ }
10072
+ }
10073
+ let saved = 0;
10074
+ let truncatedCount = 0;
10075
+ let clearedCount = 0;
10076
+ const truncatable = candidates.filter(
10077
+ (c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
10078
+ ).sort((a, b) => b.tokens - a.tokens);
10079
+ for (const c of truncatable) {
10080
+ if (estimatedTokens - saved <= budget) break;
10081
+ const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
10082
+ const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
10083
+ const truncated = prefix + `
10084
+
10085
+ ...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
10086
+
10087
+ ` + suffix;
10088
+ c.part.state.output = truncated;
10089
+ saved += c.tokens - countTokens2(truncated);
10090
+ truncatedCount++;
10091
+ }
10092
+ if (estimatedTokens - saved > budget) {
10093
+ const clearable = candidates.filter((c) => {
10094
+ const out = extractCompletedToolOutput(c.part);
10095
+ return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
10096
+ }).sort((a, b) => a.index - b.index);
10097
+ for (const c of clearable) {
10098
+ if (estimatedTokens - saved <= budget) break;
10099
+ const current = extractCompletedToolOutput(c.part);
10100
+ if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10101
+ c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
10102
+ saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
10103
+ clearedCount++;
10104
+ }
10105
+ }
10106
+ const finalEstimate = Math.max(0, estimatedTokens - saved);
10107
+ if (truncatedCount > 0 || clearedCount > 0) {
10108
+ logger.warn("Context budget guard: pruned tool outputs to fit the request", {
10109
+ session: state.sessionId,
10110
+ estimatedTokens: Math.round(estimatedTokens),
10111
+ budget,
10112
+ window,
10113
+ reserve,
10114
+ truncatedCount,
10115
+ clearedCount,
10116
+ estimatedSavedTokens: Math.round(saved),
10117
+ finalEstimate: Math.round(finalEstimate)
10118
+ });
10119
+ }
10120
+ if (finalEstimate > budget) {
10121
+ logger.warn(
10122
+ "Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
10123
+ {
10124
+ session: state.sessionId,
10125
+ finalEstimate: Math.round(finalEstimate),
10126
+ budget,
10127
+ window
10128
+ }
10129
+ );
10130
+ }
10131
+ return {
10132
+ applied: truncatedCount > 0 || clearedCount > 0,
10133
+ window,
10134
+ reserve,
10135
+ budget,
10136
+ estimatedTokens,
10137
+ finalEstimate,
10138
+ truncatedCount,
10139
+ clearedCount
10140
+ };
10141
+ }
10142
+
10160
10143
  // lib/commands/context.ts
10161
10144
  function analyzeTokens(state, messages) {
10162
10145
  const breakdown = {
@@ -11258,7 +11241,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11258
11241
  });
11259
11242
  }
11260
11243
  await updatePerTurnState(state, logger, messages);
11261
- trackUncalibratedWindow(state, logger);
11244
+ if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
11245
+ state.noContextLimitWarned = true;
11246
+ logger.warn(
11247
+ '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}) or set an absolute compress.maxContextLimit in acp.jsonc.',
11248
+ {
11249
+ session: state.sessionId,
11250
+ model: `${requestModel.providerID}/${requestModel.modelID}`
11251
+ }
11252
+ );
11253
+ }
11262
11254
  }
11263
11255
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11264
11256
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11289,7 +11281,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11289
11281
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11290
11282
  prune(state, logger, config, output.messages);
11291
11283
  truncateLargeToolOutputs(state, config, logger, output.messages);
11292
- pruneToFit(state, config, logger, output.messages);
11284
+ enforceContextBudget(state, config, logger, output.messages);
11293
11285
  hideConsumedCompressCalls(state, output.messages);
11294
11286
  assignMessageRefs(state, output.messages);
11295
11287
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11715,7 +11707,7 @@ var server = (async (ctx) => {
11715
11707
  }
11716
11708
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11717
11709
  logger.info("ACP plugin initialized", {
11718
- version: true ? "1.14.25-pr.348.72" : "dev",
11710
+ version: true ? "1.14.25-pr.350.73" : "dev",
11719
11711
  workspace: ctx.directory,
11720
11712
  logLevel: logger.level,
11721
11713
  debug: config.debug,