opencode-acp 1.12.10-dev.1 → 1.13.1

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.
Files changed (44) hide show
  1. package/NOTICE +29 -0
  2. package/README.md +120 -40
  3. package/README.zh-CN.md +98 -24
  4. package/dist/index.js +933 -101
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  7. package/dist/lib/compress/quality-gate/algorithms/index.d.ts +3 -0
  8. package/dist/lib/compress/quality-gate/algorithms/index.d.ts.map +1 -0
  9. package/dist/lib/compress/quality-gate/evaluate.d.ts +8 -0
  10. package/dist/lib/compress/quality-gate/evaluate.d.ts.map +1 -0
  11. package/dist/lib/compress/quality-gate/index.d.ts +5 -0
  12. package/dist/lib/compress/quality-gate/index.d.ts.map +1 -0
  13. package/dist/lib/compress/quality-gate/registry.d.ts +6 -0
  14. package/dist/lib/compress/quality-gate/registry.d.ts.map +1 -0
  15. package/dist/lib/compress/quality-gate/types.d.ts +51 -0
  16. package/dist/lib/compress/quality-gate/types.d.ts.map +1 -0
  17. package/dist/lib/compress/range-utils.d.ts.map +1 -1
  18. package/dist/lib/compress/range.d.ts.map +1 -1
  19. package/dist/lib/compress/types.d.ts +5 -2
  20. package/dist/lib/compress/types.d.ts.map +1 -1
  21. package/dist/lib/config-validation.d.ts.map +1 -1
  22. package/dist/lib/config.d.ts +9 -0
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/hooks.d.ts.map +1 -1
  25. package/dist/lib/messages/inject/policy/index.d.ts +5 -0
  26. package/dist/lib/messages/inject/policy/index.d.ts.map +1 -0
  27. package/dist/lib/messages/inject/policy/registry.d.ts +8 -0
  28. package/dist/lib/messages/inject/policy/registry.d.ts.map +1 -0
  29. package/dist/lib/messages/inject/policy/types.d.ts +2 -0
  30. package/dist/lib/messages/inject/policy/types.d.ts.map +1 -0
  31. package/dist/lib/messages/inject/utils.d.ts +0 -9
  32. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  33. package/dist/lib/messages/utils.d.ts.map +1 -1
  34. package/dist/lib/prompts/compress-range.d.ts +1 -1
  35. package/dist/lib/prompts/compress-range.d.ts.map +1 -1
  36. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -1
  37. package/dist/lib/prompts/extensions/tool.d.ts +1 -1
  38. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -1
  39. package/dist/lib/prompts/system.d.ts +1 -1
  40. package/dist/lib/prompts/system.d.ts.map +1 -1
  41. package/dist/lib/ui/notification.d.ts.map +1 -1
  42. package/package.json +5 -2
  43. package/dist/lib/prompts/compression-rules.d.ts +0 -20
  44. package/dist/lib/prompts/compression-rules.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -926,7 +926,11 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
926
926
  "strategies.purgeErrors",
927
927
  "strategies.purgeErrors.enabled",
928
928
  "strategies.purgeErrors.turns",
929
- "strategies.purgeErrors.protectedTools"
929
+ "strategies.purgeErrors.protectedTools",
930
+ "qualityGate",
931
+ "qualityGate.enabled",
932
+ "qualityGate.algorithm",
933
+ "qualityGate.algorithms"
930
934
  ]);
931
935
  function getConfigKeyPaths(obj, prefix = "") {
932
936
  const keys = [];
@@ -1554,7 +1558,10 @@ var defaultConfig = {
1554
1558
  autoUpdate: true,
1555
1559
  debug: false,
1556
1560
  pruneNotification: "detailed",
1557
- pruneNotificationType: "chat",
1561
+ // [FIX #20] Default to toast — chat-mode notifications inject an empty
1562
+ // user message that freezes the session on providers that reject empty
1563
+ // messages (zhipuai-lb code 1214). See lib/ui/notification.ts.
1564
+ pruneNotificationType: "toast",
1558
1565
  commands: {
1559
1566
  enabled: true,
1560
1567
  protectedTools: [...DEFAULT_PROTECTED_TOOLS]
@@ -1609,7 +1616,8 @@ var defaultConfig = {
1609
1616
  gc: {
1610
1617
  algorithm: "truncate",
1611
1618
  promotionThreshold: 5,
1612
- maxBlockAge: 15,
1619
+ maxBlockAge: Number.MAX_SAFE_INTEGER,
1620
+ // no-op: age-based deactivation removed (memory-loss fix)
1613
1621
  maxOldGenSummaryLength: 3e3,
1614
1622
  majorGcThresholdPercent: "100%",
1615
1623
  batchCleanup: {
@@ -1617,6 +1625,18 @@ var defaultConfig = {
1617
1625
  highThreshold: "75%",
1618
1626
  forceThreshold: "90%"
1619
1627
  }
1628
+ },
1629
+ qualityGate: {
1630
+ enabled: false,
1631
+ algorithm: "rouge-recall-v1",
1632
+ algorithms: {
1633
+ "rouge-recall-v1": {
1634
+ layer1MinChars: 200,
1635
+ layer1MinRetentionPct: 1,
1636
+ layer2MaxRougeF1: 0.05,
1637
+ layer2MaxTop20Recall: 0.2
1638
+ }
1639
+ }
1620
1640
  }
1621
1641
  };
1622
1642
  var GLOBAL_CONFIG_DIR = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
@@ -1814,6 +1834,11 @@ function deepCloneConfig(config) {
1814
1834
  gc: {
1815
1835
  ...config.gc,
1816
1836
  batchCleanup: { ...config.gc.batchCleanup }
1837
+ },
1838
+ qualityGate: {
1839
+ enabled: config.qualityGate.enabled,
1840
+ algorithm: config.qualityGate.algorithm,
1841
+ algorithms: { ...config.qualityGate.algorithms }
1817
1842
  }
1818
1843
  };
1819
1844
  }
@@ -1827,6 +1852,14 @@ function mergeGC(base, override) {
1827
1852
  batchCleanup: { ...base.batchCleanup, ...override.batchCleanup ?? {} }
1828
1853
  };
1829
1854
  }
1855
+ function mergeQualityGate(base, override) {
1856
+ if (!override) return base;
1857
+ return {
1858
+ enabled: override.enabled ?? base.enabled,
1859
+ algorithm: override.algorithm ?? base.algorithm,
1860
+ algorithms: { ...base.algorithms, ...override.algorithms ?? {} }
1861
+ };
1862
+ }
1830
1863
  function mergeLayer(config, data) {
1831
1864
  return {
1832
1865
  enabled: data.enabled ?? config.enabled,
@@ -1846,7 +1879,8 @@ function mergeLayer(config, data) {
1846
1879
  ],
1847
1880
  compress: mergeCompress(config.compress, data.compress),
1848
1881
  gc: mergeGC(config.gc, data.gc),
1849
- strategies: mergeStrategies(config.strategies, data.strategies)
1882
+ strategies: mergeStrategies(config.strategies, data.strategies),
1883
+ qualityGate: mergeQualityGate(config.qualityGate, data.qualityGate)
1850
1884
  };
1851
1885
  }
1852
1886
  function scheduleParseWarning(ctx, title, message) {
@@ -2131,20 +2165,27 @@ function countMessageCharacters(msg) {
2131
2165
 
2132
2166
  // lib/prompts/extensions/tool.ts
2133
2167
  var RANGE_FORMAT_EXTENSION = `
2168
+
2134
2169
  THE FORMAT OF COMPRESS
2135
2170
 
2136
2171
  \`\`\`
2137
2172
  {
2138
- topic: string, // Short label (3-5 words) - e.g., "Auth System Exploration"
2173
+ topic?: string, // OPTIONAL fallback topic for entries without their own.
2174
+ // Omit when every content entry specifies its own topic.
2139
2175
  content: [ // One or more ranges to compress
2140
2176
  {
2177
+ topic?: string, // OPTIONAL per-entry topic for this range.
2178
+ // Falls back to top-level topic.
2179
+ // Give each entry its own topic when compressing
2180
+ // unrelated ranges in one call.
2141
2181
  startId: string, // Boundary ID at range start: mNNNNN or bN
2142
2182
  endId: string, // Boundary ID at range end: mNNNNN or bN
2143
2183
  summary: string // Complete technical summary replacing all content in range
2144
2184
  }
2145
2185
  ]
2146
2186
  }
2147
- \`\`\``;
2187
+ \`\`\`
2188
+ Each entry needs a topic \u2014 either its own or the top-level fallback.`;
2148
2189
  var MESSAGE_FORMAT_EXTENSION = `
2149
2190
  THE FORMAT OF COMPRESS
2150
2191
 
@@ -3279,6 +3320,8 @@ var SoftIssue = class extends Error {
3279
3320
  this.kind = kind;
3280
3321
  this.messageId = messageId;
3281
3322
  }
3323
+ kind;
3324
+ messageId;
3282
3325
  };
3283
3326
  function validateArgs(args) {
3284
3327
  if (typeof args.topic !== "string" || args.topic.trim().length === 0) {
@@ -3969,9 +4012,7 @@ function applyPendingCompressionDurations(state) {
3969
4012
  // lib/compress/range-utils.ts
3970
4013
  var BLOCK_PLACEHOLDER_REGEX = /\(b(\d+)\)|\{block_(\d+)\}/gi;
3971
4014
  function validateArgs2(args) {
3972
- if (typeof args.topic !== "string" || args.topic.trim().length === 0) {
3973
- throw new Error("topic is required and must be a non-empty string");
3974
- }
4015
+ const hasTopLevelTopic = typeof args.topic === "string" && args.topic.trim().length > 0;
3975
4016
  if (!Array.isArray(args.content) || args.content.length === 0) {
3976
4017
  throw new Error("content is required and must be a non-empty array");
3977
4018
  }
@@ -3987,11 +4028,18 @@ function validateArgs2(args) {
3987
4028
  if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
3988
4029
  throw new Error(`${prefix}.summary is required and must be a non-empty string`);
3989
4030
  }
4031
+ const hasEntryTopic = typeof entry?.topic === "string" && entry.topic.trim().length > 0;
4032
+ if (!hasEntryTopic && !hasTopLevelTopic) {
4033
+ throw new Error(
4034
+ `${prefix} needs a topic \u2014 provide ${prefix}.topic or the top-level topic`
4035
+ );
4036
+ }
3990
4037
  }
3991
4038
  }
3992
4039
  function resolveRanges(args, searchContext, state) {
3993
4040
  return args.content.map((entry, index) => {
3994
4041
  const normalizedEntry = {
4042
+ topic: typeof entry.topic === "string" && entry.topic.trim().length > 0 ? entry.topic.trim() : void 0,
3995
4043
  startId: entry.startId.trim(),
3996
4044
  endId: entry.endId.trim(),
3997
4045
  summary: entry.summary
@@ -4185,8 +4233,8 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
4185
4233
  applyCompressionState(
4186
4234
  state,
4187
4235
  {
4188
- topic: input.topic,
4189
- batchTopic: input.topic,
4236
+ topic: plan.entry.topic ?? input.topic ?? "",
4237
+ batchTopic: typeof input.topic === "string" ? input.topic : void 0,
4190
4238
  startId: plan.entry.startId,
4191
4239
  endId: plan.entry.endId,
4192
4240
  mode: "range",
@@ -4253,7 +4301,7 @@ function rebuildMessageInvocation(state, input, searchContext, invocation, gcCon
4253
4301
  state,
4254
4302
  {
4255
4303
  topic: entry.topic,
4256
- batchTopic: input.topic,
4304
+ batchTopic: typeof input.topic === "string" ? input.topic : void 0,
4257
4305
  startId: entry.messageId,
4258
4306
  endId: entry.messageId,
4259
4307
  mode: "message",
@@ -5076,7 +5124,8 @@ async function sendCompressNotification(client, logger, config, state, sessionId
5076
5124
  newlyCompressedToolIds.push(toolId);
5077
5125
  }
5078
5126
  }
5079
- const topic = batchTopic ?? (entries.length === 1 ? state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ?? "(unknown topic)" : "(unknown topic)");
5127
+ const entryBlockTopics = entries.map((e) => state.prune.messages.blocksById.get(e.blockId)?.topic).filter((t) => typeof t === "string" && t.length > 0);
5128
+ const topic = batchTopic ?? (entries.length === 1 ? state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ?? "(unknown topic)" : entryBlockTopics.length > 0 ? entryBlockTopics.join(" \xB7 ") : "(unknown topic)");
5080
5129
  const contextTokensAfter = Math.max(
5081
5130
  0,
5082
5131
  contextTokensBefore - compressedTokens + summaryTokens
@@ -5131,20 +5180,22 @@ ${progressBar}`;
5131
5180
  \u2192 Compression (~${summaryTokensStr}): ${displaySummary}`;
5132
5181
  }
5133
5182
  }
5134
- if (config.pruneNotificationType === "toast") {
5135
- let toastMessage = message;
5136
- toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
5137
- await client.tui.showToast({
5138
- body: {
5139
- title: "ACP: Compress Notification",
5140
- message: toastMessage,
5141
- variant: "info",
5142
- duration: 5e3
5143
- }
5144
- });
5145
- return true;
5183
+ if (config.pruneNotificationType === "chat") {
5184
+ logger.warn(
5185
+ "compress.pruneNotificationType 'chat' is no longer supported (it injects an empty user message that causes provider 400 errors); falling back to toast. Set pruneNotificationType to 'toast' (or pruneNotification to 'off') to silence this warning.",
5186
+ { sessionId }
5187
+ );
5146
5188
  }
5147
- await sendIgnoredMessage(client, sessionId, message, params, logger);
5189
+ let toastMessage = message;
5190
+ toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
5191
+ await client.tui.showToast({
5192
+ body: {
5193
+ title: "ACP: Compress Notification",
5194
+ message: toastMessage,
5195
+ variant: "info",
5196
+ duration: 5e3
5197
+ }
5198
+ });
5148
5199
  return true;
5149
5200
  }
5150
5201
  async function sendIgnoredMessage(client, sessionID, text, params, logger) {
@@ -5178,6 +5229,737 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
5178
5229
  }
5179
5230
  }
5180
5231
 
5232
+ // lib/compress/quality-gate/registry.ts
5233
+ var registry = /* @__PURE__ */ new Map();
5234
+ function registerQualityGate(gate) {
5235
+ if (registry.has(gate.name)) {
5236
+ const existing = registry.get(gate.name);
5237
+ if (existing !== gate && existing.version !== gate.version) {
5238
+ throw new Error(
5239
+ `Quality gate "${gate.name}" already registered with version ${existing.version} (attempted ${gate.version})`
5240
+ );
5241
+ }
5242
+ }
5243
+ registry.set(gate.name, gate);
5244
+ }
5245
+ function getQualityGate(name) {
5246
+ return registry.get(name);
5247
+ }
5248
+
5249
+ // node_modules/context-compress-algorithms/dist/chunk-E6LYOQFY.js
5250
+ var ENGLISH_WORD_RE = /[a-z][a-z0-9_]+/g;
5251
+ var CJK_RE = /[\u4e00-\u9fff]/g;
5252
+ var FILE_PATH_RE = /(?:[a-zA-Z0-9_-]+\/){1,}[a-zA-Z0-9_-]+\.[a-zA-Z]{1,5}/g;
5253
+ var STOPWORDS = /* @__PURE__ */ new Set([
5254
+ "the",
5255
+ "that",
5256
+ "this",
5257
+ "these",
5258
+ "those",
5259
+ "there",
5260
+ "their",
5261
+ "them",
5262
+ "then",
5263
+ "than",
5264
+ "thats",
5265
+ "with",
5266
+ "will",
5267
+ "would",
5268
+ "could",
5269
+ "should",
5270
+ "from",
5271
+ "have",
5272
+ "has",
5273
+ "had",
5274
+ "having",
5275
+ "were",
5276
+ "what",
5277
+ "which",
5278
+ "when",
5279
+ "where",
5280
+ "while",
5281
+ "your",
5282
+ "yours",
5283
+ "theirs",
5284
+ "ours",
5285
+ "mine",
5286
+ "hers",
5287
+ "whose",
5288
+ "they",
5289
+ "them",
5290
+ "those",
5291
+ "these",
5292
+ "this",
5293
+ "that",
5294
+ "such",
5295
+ "some",
5296
+ "same",
5297
+ "other",
5298
+ "another",
5299
+ "each",
5300
+ "into",
5301
+ "onto",
5302
+ "upon",
5303
+ "over",
5304
+ "under",
5305
+ "between",
5306
+ "through",
5307
+ "during",
5308
+ "before",
5309
+ "after",
5310
+ "above",
5311
+ "below",
5312
+ "among",
5313
+ "across",
5314
+ "along",
5315
+ "around",
5316
+ "about",
5317
+ "because",
5318
+ "since",
5319
+ "unless",
5320
+ "although",
5321
+ "though",
5322
+ "whereas",
5323
+ "whether",
5324
+ "either",
5325
+ "neither",
5326
+ "both",
5327
+ "also",
5328
+ "only",
5329
+ "just",
5330
+ "very",
5331
+ "more",
5332
+ "most",
5333
+ "much",
5334
+ "many",
5335
+ "less",
5336
+ "least",
5337
+ "several",
5338
+ "enough",
5339
+ "make",
5340
+ "made",
5341
+ "makes",
5342
+ "making",
5343
+ "take",
5344
+ "took",
5345
+ "taken",
5346
+ "takes",
5347
+ "taking",
5348
+ "get",
5349
+ "got",
5350
+ "getting",
5351
+ "gets",
5352
+ "give",
5353
+ "gave",
5354
+ "given",
5355
+ "gives",
5356
+ "giving",
5357
+ "come",
5358
+ "came",
5359
+ "comes",
5360
+ "coming",
5361
+ "were",
5362
+ "been",
5363
+ "being",
5364
+ "have",
5365
+ "make",
5366
+ "want",
5367
+ "need",
5368
+ "using",
5369
+ "true",
5370
+ "false",
5371
+ "null",
5372
+ "undefined",
5373
+ "void",
5374
+ "return",
5375
+ "returns",
5376
+ "returning",
5377
+ "function",
5378
+ "const",
5379
+ "class",
5380
+ "interface",
5381
+ "type",
5382
+ "typeof",
5383
+ "instanceof",
5384
+ "import",
5385
+ "export",
5386
+ "require",
5387
+ "module",
5388
+ "default",
5389
+ "async",
5390
+ "await",
5391
+ "static",
5392
+ "public",
5393
+ "private",
5394
+ "protected",
5395
+ "readonly",
5396
+ "partial",
5397
+ "abstract",
5398
+ "virtual",
5399
+ "override",
5400
+ "final",
5401
+ "super",
5402
+ "value",
5403
+ "values",
5404
+ "param",
5405
+ "params",
5406
+ "name",
5407
+ "names",
5408
+ "test",
5409
+ "tests",
5410
+ "testing",
5411
+ "tested",
5412
+ "expect",
5413
+ "expected",
5414
+ "actual",
5415
+ "actuals",
5416
+ "result",
5417
+ "results",
5418
+ "output",
5419
+ "outputs",
5420
+ "input",
5421
+ "inputs",
5422
+ "data",
5423
+ "record",
5424
+ "records",
5425
+ "item",
5426
+ "items",
5427
+ "like",
5428
+ "want",
5429
+ "need",
5430
+ "used",
5431
+ "uses",
5432
+ "said",
5433
+ "say",
5434
+ "says",
5435
+ "went",
5436
+ "goes",
5437
+ "here",
5438
+ "there",
5439
+ "where",
5440
+ "when",
5441
+ "what",
5442
+ "who",
5443
+ "how",
5444
+ "why",
5445
+ "which",
5446
+ "whose",
5447
+ "whom",
5448
+ "yourself",
5449
+ "myself",
5450
+ "itself",
5451
+ "themselves",
5452
+ "ourselves",
5453
+ "himself",
5454
+ "herself",
5455
+ "yeah",
5456
+ "okay",
5457
+ "ok",
5458
+ "yes",
5459
+ "no",
5460
+ "not",
5461
+ "nor",
5462
+ "or",
5463
+ "and",
5464
+ "but",
5465
+ "if",
5466
+ "then",
5467
+ "else",
5468
+ "elif",
5469
+ "when",
5470
+ "while",
5471
+ "for",
5472
+ "to",
5473
+ "of",
5474
+ "in",
5475
+ "on",
5476
+ "at",
5477
+ "by",
5478
+ "with",
5479
+ "from",
5480
+ "into",
5481
+ "onto",
5482
+ "\u7684",
5483
+ "\u662F",
5484
+ "\u4E86",
5485
+ "\u5728",
5486
+ "\u548C",
5487
+ "\u4E0E",
5488
+ "\u6216",
5489
+ "\u4E5F",
5490
+ "\u90FD",
5491
+ "\u5C31",
5492
+ "\u8FD8",
5493
+ "\u53C8",
5494
+ "\u624D",
5495
+ "\u518D",
5496
+ "\u5DF2",
5497
+ "\u5C06",
5498
+ "\u4F1A",
5499
+ "\u80FD",
5500
+ "\u53EF",
5501
+ "\u53EF\u4EE5",
5502
+ "\u8981",
5503
+ "\u60F3",
5504
+ "\u9700\u8981",
5505
+ "\u5E94\u8BE5",
5506
+ "\u5FC5\u987B",
5507
+ "\u6CA1",
5508
+ "\u6CA1\u6709",
5509
+ "\u4E0D",
5510
+ "\u975E",
5511
+ "\u65E0",
5512
+ "\u83AB",
5513
+ "\u6211",
5514
+ "\u4F60",
5515
+ "\u4ED6",
5516
+ "\u5979",
5517
+ "\u5B83",
5518
+ "\u6211\u4EEC",
5519
+ "\u4F60\u4EEC",
5520
+ "\u4ED6\u4EEC",
5521
+ "\u5979\u4EEC",
5522
+ "\u5B83\u4EEC",
5523
+ "\u54B1",
5524
+ "\u54B1\u4EEC",
5525
+ "\u81EA\u5DF1",
5526
+ "\u8FD9",
5527
+ "\u90A3",
5528
+ "\u8FD9\u4E2A",
5529
+ "\u90A3\u4E2A",
5530
+ "\u8FD9\u4E9B",
5531
+ "\u90A3\u4E9B",
5532
+ "\u8FD9\u6837",
5533
+ "\u90A3\u6837",
5534
+ "\u8FD9\u91CC",
5535
+ "\u90A3\u91CC",
5536
+ "\u8FD9\u4E48",
5537
+ "\u90A3\u4E48",
5538
+ "\u4EC0\u4E48",
5539
+ "\u600E\u4E48",
5540
+ "\u4E3A\u4EC0\u4E48",
5541
+ "\u54EA",
5542
+ "\u54EA\u4E2A",
5543
+ "\u54EA\u4E9B",
5544
+ "\u54EA\u91CC",
5545
+ "\u600E\u6837",
5546
+ "\u591A\u5C11",
5547
+ "\u51E0",
5548
+ "\u591A",
5549
+ "\u5C11",
5550
+ "\u4E8E",
5551
+ "\u4ECE",
5552
+ "\u5411",
5553
+ "\u5F80",
5554
+ "\u5230",
5555
+ "\u81F3",
5556
+ "\u4E3A",
5557
+ "\u5BF9\u4E8E",
5558
+ "\u5173\u4E8E",
5559
+ "\u81F3\u4E8E",
5560
+ "\u7531\u4E8E",
5561
+ "\u56E0\u4E3A",
5562
+ "\u6240\u4EE5",
5563
+ "\u4F46\u662F",
5564
+ "\u4F46",
5565
+ "\u4E0D\u8FC7",
5566
+ "\u7136\u800C",
5567
+ "\u53EF\u662F",
5568
+ "\u53EA\u662F",
5569
+ "\u53EA\u6709",
5570
+ "\u9664\u4E86",
5571
+ "\u9664\u975E",
5572
+ "\u65E0\u8BBA",
5573
+ "\u4E0D\u7BA1",
5574
+ "\u5C3D\u7BA1",
5575
+ "\u867D\u7136",
5576
+ "\u867D\u8BF4",
5577
+ "\u5373\u4F7F",
5578
+ "\u5373\u4FBF",
5579
+ "\u54EA\u6015",
5580
+ "\u4E00\u65E6",
5581
+ "\u5982\u679C",
5582
+ "\u8981\u662F",
5583
+ "\u5047\u5982",
5584
+ "\u5047\u4F7F",
5585
+ "\u5018\u82E5",
5586
+ "\u4E4B",
5587
+ "\u5176",
5588
+ "\u5176\u4E2D",
5589
+ "\u5176\u4ED6",
5590
+ "\u5176\u5B83",
5591
+ "\u5176\u4F59",
5592
+ "\u53E6\u4E00",
5593
+ "\u53E6\u5916",
5594
+ "\u6B64\u5916",
5595
+ "\u5E76\u4E14",
5596
+ "\u5E76",
5597
+ "\u4E14",
5598
+ "\u7740",
5599
+ "\u8FC7",
5600
+ "\u5427",
5601
+ "\u5417",
5602
+ "\u5462",
5603
+ "\u554A",
5604
+ "\u54E6",
5605
+ "\u55EF",
5606
+ "\u5440",
5607
+ "\u54C7",
5608
+ "\u54C8",
5609
+ "\u561B",
5610
+ "\u54AF",
5611
+ "\u54DF"
5612
+ ]);
5613
+ var ZH_STOPWORD_BIGRAMS = /* @__PURE__ */ new Set([
5614
+ "\u6211\u4EEC",
5615
+ "\u4F60\u4EEC",
5616
+ "\u4ED6\u4EEC",
5617
+ "\u5979\u4EEC",
5618
+ "\u5B83\u4EEC",
5619
+ "\u54B1\u4EEC",
5620
+ "\u8FD9\u4E2A",
5621
+ "\u90A3\u4E2A",
5622
+ "\u8FD9\u4E9B",
5623
+ "\u90A3\u4E9B",
5624
+ "\u4EC0\u4E48",
5625
+ "\u600E\u4E48",
5626
+ "\u4E3A\u4EC0\u4E48",
5627
+ "\u5982\u4F55",
5628
+ "\u53EF\u4EE5",
5629
+ "\u5E94\u8BE5",
5630
+ "\u4F46\u662F",
5631
+ "\u56E0\u4E3A",
5632
+ "\u6240\u4EE5",
5633
+ "\u5982\u679C",
5634
+ "\u867D\u7136",
5635
+ "\u5373\u4F7F",
5636
+ "\u5C3D\u7BA1",
5637
+ "\u4E3A\u4E86",
5638
+ "\u7531\u4E8E",
5639
+ "\u4E0D\u4F46",
5640
+ "\u800C\u4E14",
5641
+ "\u5E76\u4E14",
5642
+ "\u6216\u8005",
5643
+ "\u8FD8\u662F",
5644
+ "\u4EE5\u53CA",
5645
+ "\u4EE5\u4E3A",
5646
+ "\u4E8E\u662F",
5647
+ "\u7136\u800C",
5648
+ "\u5176\u5B9E",
5649
+ "\u5C31\u662F",
5650
+ "\u53EA\u662F",
5651
+ "\u53EA\u6709",
5652
+ "\u9664\u4E86",
5653
+ "\u9664\u975E",
5654
+ "\u8FD9\u6837",
5655
+ "\u90A3\u6837",
5656
+ "\u8FD9\u4E48",
5657
+ "\u90A3\u4E48",
5658
+ "\u8FD9\u4E9B",
5659
+ "\u90A3\u4E9B",
5660
+ "\u8FD9\u91CC",
5661
+ "\u90A3\u91CC",
5662
+ "\u73B0\u5728",
5663
+ "\u4EE5\u540E",
5664
+ "\u4EE5\u524D",
5665
+ "\u4E4B\u540E",
5666
+ "\u4E4B\u524D",
5667
+ "\u7136\u540E",
5668
+ "\u5F53\u7136",
5669
+ "\u53EF\u80FD",
5670
+ "\u4E00\u4E9B",
5671
+ "\u8BB8\u591A",
5672
+ "\u975E\u5E38",
5673
+ "\u5341\u5206",
5674
+ "\u6BD4\u8F83",
5675
+ "\u66F4\u52A0",
5676
+ "\u6700\u4E3A",
5677
+ "\u4E5F\u662F",
5678
+ "\u8FD8\u662F",
5679
+ "\u5C31\u662F",
5680
+ "\u4E0D\u8FC7",
5681
+ "\u4E0D\u8981",
5682
+ "\u4E0D\u80FD",
5683
+ "\u4E0D\u4F1A",
5684
+ "\u6CA1\u6709",
5685
+ "\u4E0D\u662F",
5686
+ "\u4E0D\u7528",
5687
+ "\u4E0D\u5FC5",
5688
+ "\u4E00\u76F4",
5689
+ "\u5DF2\u7ECF",
5690
+ "\u6B63\u5728",
5691
+ "\u9A6C\u4E0A"
5692
+ ]);
5693
+ var DEFAULT_OPTS = {
5694
+ english: true,
5695
+ zhUnigrams: true,
5696
+ zhBigrams: true
5697
+ };
5698
+ function tokenize(text, opts = {}) {
5699
+ if (!text || typeof text !== "string") return [];
5700
+ const options = { ...DEFAULT_OPTS, ...opts };
5701
+ const tokens = [];
5702
+ const lower = text.toLowerCase();
5703
+ if (options.english) {
5704
+ const matches = lower.match(ENGLISH_WORD_RE);
5705
+ if (matches) {
5706
+ for (const w of matches) {
5707
+ if (w.length >= 4 && !STOPWORDS.has(w) && !/^\d+$/.test(w)) {
5708
+ tokens.push(w);
5709
+ }
5710
+ }
5711
+ }
5712
+ }
5713
+ if (options.zhUnigrams || options.zhBigrams) {
5714
+ const cjkChars = text.match(CJK_RE);
5715
+ if (cjkChars && cjkChars.length > 0) {
5716
+ const cjkStr = cjkChars.join("");
5717
+ if (options.zhUnigrams) {
5718
+ for (const c of cjkStr) {
5719
+ if (!STOPWORDS.has(c)) tokens.push(c);
5720
+ }
5721
+ }
5722
+ if (options.zhBigrams) {
5723
+ for (let i = 0; i < cjkStr.length - 1; i++) {
5724
+ const bg = cjkStr.slice(i, i + 2);
5725
+ if (!ZH_STOPWORD_BIGRAMS.has(bg) && !STOPWORDS.has(bg[0]) && !STOPWORDS.has(bg[1])) {
5726
+ tokens.push(bg);
5727
+ }
5728
+ }
5729
+ }
5730
+ }
5731
+ }
5732
+ return tokens;
5733
+ }
5734
+ function termFrequency(tokens) {
5735
+ const tf = /* @__PURE__ */ new Map();
5736
+ for (const t of tokens) {
5737
+ tf.set(t, (tf.get(t) ?? 0) + 1);
5738
+ }
5739
+ return tf;
5740
+ }
5741
+ function topKByTf(tokens, k) {
5742
+ if (tokens.length === 0 || k <= 0) return [];
5743
+ const tf = termFrequency(tokens);
5744
+ const sorted = [...tf.entries()].sort((a, b) => {
5745
+ if (b[1] !== a[1]) return b[1] - a[1];
5746
+ return a[0].localeCompare(b[0]);
5747
+ });
5748
+ return sorted.slice(0, k).map((e) => e[0]);
5749
+ }
5750
+ function extractFilePaths(text) {
5751
+ if (!text) return /* @__PURE__ */ new Set();
5752
+ const paths = /* @__PURE__ */ new Set();
5753
+ const matches = text.match(FILE_PATH_RE);
5754
+ if (matches) {
5755
+ for (const m of matches) paths.add(m);
5756
+ }
5757
+ return paths;
5758
+ }
5759
+ function rouge1Recall(summaryTokens, originalTokens) {
5760
+ if (originalTokens.length === 0) return 0;
5761
+ const summarySet = new Set(summaryTokens);
5762
+ const originalSet = new Set(originalTokens);
5763
+ let hit = 0;
5764
+ for (const t of originalSet) if (summarySet.has(t)) hit++;
5765
+ return hit / originalSet.size;
5766
+ }
5767
+ function rouge1Precision(summaryTokens, originalTokens) {
5768
+ if (summaryTokens.length === 0) return 0;
5769
+ const summarySet = new Set(summaryTokens);
5770
+ const originalSet = new Set(originalTokens);
5771
+ let hit = 0;
5772
+ for (const t of summarySet) if (originalSet.has(t)) hit++;
5773
+ return hit / summarySet.size;
5774
+ }
5775
+ function rouge1F1(summaryTokens, originalTokens) {
5776
+ const r = rouge1Recall(summaryTokens, originalTokens);
5777
+ const p = rouge1Precision(summaryTokens, originalTokens);
5778
+ if (r + p === 0) return 0;
5779
+ return 2 * r * p / (r + p);
5780
+ }
5781
+ function topKRecall(summaryTokens, originalTokens, k) {
5782
+ if (originalTokens.length === 0 || k <= 0) return 0;
5783
+ const top = topKByTf(originalTokens, k);
5784
+ if (top.length === 0) return 0;
5785
+ const summarySet = new Set(summaryTokens);
5786
+ let hit = 0;
5787
+ for (const t of top) if (summarySet.has(t)) hit++;
5788
+ return hit / top.length;
5789
+ }
5790
+ var DEFAULT_ROUGE_RECALL_V1_CONFIG = {
5791
+ layer1MinChars: 200,
5792
+ layer1MinRetentionPct: 1,
5793
+ layer2MaxRougeF1: 0.05,
5794
+ layer2MaxTop20Recall: 0.2
5795
+ };
5796
+ var TOP_K = 20;
5797
+ var ORIGINAL_TOKEN_ESTIMATE_CHARS_PER_TOKEN = 4;
5798
+ function resolveConfig(input) {
5799
+ if (!input || typeof input !== "object") return DEFAULT_ROUGE_RECALL_V1_CONFIG;
5800
+ const c = input;
5801
+ return {
5802
+ layer1MinChars: typeof c.layer1MinChars === "number" && c.layer1MinChars > 0 ? c.layer1MinChars : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer1MinChars,
5803
+ layer1MinRetentionPct: typeof c.layer1MinRetentionPct === "number" && c.layer1MinRetentionPct >= 0 ? c.layer1MinRetentionPct : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer1MinRetentionPct,
5804
+ layer2MaxRougeF1: typeof c.layer2MaxRougeF1 === "number" && c.layer2MaxRougeF1 >= 0 ? c.layer2MaxRougeF1 : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer2MaxRougeF1,
5805
+ layer2MaxTop20Recall: typeof c.layer2MaxTop20Recall === "number" && c.layer2MaxTop20Recall >= 0 ? c.layer2MaxTop20Recall : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer2MaxTop20Recall
5806
+ };
5807
+ }
5808
+ var rougeRecallV1 = {
5809
+ name: "rouge-recall-v1",
5810
+ version: "1.0.0",
5811
+ description: "Two-layer gate: length floor (L1) then ROUGE-1 F1 AND top-20 keyword recall (L2)",
5812
+ evaluate(ctx, rawConfig) {
5813
+ const cfg = resolveConfig(rawConfig);
5814
+ const summaryLen = ctx.summary.length;
5815
+ const originalChars = ctx.originalText.length;
5816
+ const retentionPct = ctx.block.compressedTokens > 0 ? summaryLen / (ctx.block.compressedTokens * ORIGINAL_TOKEN_ESTIMATE_CHARS_PER_TOKEN) * 100 : 0;
5817
+ const baseMetrics = [
5818
+ { name: "summaryLen", value: summaryLen },
5819
+ { name: "retentionPct", value: +retentionPct.toFixed(2), format: "percent" },
5820
+ { name: "originalTokens", value: ctx.block.compressedTokens }
5821
+ ];
5822
+ if (summaryLen < cfg.layer1MinChars || originalChars > 0 && retentionPct < cfg.layer1MinRetentionPct) {
5823
+ return {
5824
+ passed: false,
5825
+ layer: "L1-length",
5826
+ reason: `Summary too short: ${summaryLen} chars, ${retentionPct.toFixed(2)}% retention (threshold: ${cfg.layer1MinChars} chars OR ${cfg.layer1MinRetentionPct}% retention)`,
5827
+ metrics: baseMetrics
5828
+ };
5829
+ }
5830
+ if (ctx.originalText.length === 0) {
5831
+ return { passed: true, metrics: baseMetrics };
5832
+ }
5833
+ const summaryTokens = tokenize(ctx.summary);
5834
+ const originalTokens = tokenize(ctx.originalText);
5835
+ const rougeF1 = rouge1F1(summaryTokens, originalTokens);
5836
+ const rougeRecall = rouge1Recall(summaryTokens, originalTokens);
5837
+ const top20 = topKRecall(summaryTokens, originalTokens, TOP_K);
5838
+ const summaryPaths = extractFilePaths(ctx.summary);
5839
+ const originalPaths = extractFilePaths(ctx.originalText);
5840
+ const pathCoverage = originalPaths.size >= 5 ? [...summaryPaths].filter((p) => originalPaths.has(p)).length / originalPaths.size : -1;
5841
+ const contentMetrics = [
5842
+ ...baseMetrics,
5843
+ { name: "rougeF1", value: +rougeF1.toFixed(4), format: "ratio" },
5844
+ { name: "rougeRecall", value: +rougeRecall.toFixed(4), format: "ratio" },
5845
+ { name: "top20Recall", value: +top20.toFixed(4), format: "ratio" },
5846
+ { name: "nOriginalPaths", value: originalPaths.size },
5847
+ { name: "nSummaryPaths", value: summaryPaths.size }
5848
+ ];
5849
+ if (pathCoverage >= 0) {
5850
+ contentMetrics.push({ name: "pathCoverage", value: +pathCoverage.toFixed(3), format: "ratio" });
5851
+ }
5852
+ if (rougeF1 < cfg.layer2MaxRougeF1 && top20 < cfg.layer2MaxTop20Recall) {
5853
+ return {
5854
+ passed: false,
5855
+ layer: "L2-recall",
5856
+ reason: `Content coverage too low: rougeF1=${rougeF1.toFixed(3)}, top20Recall=${top20.toFixed(3)} (threshold: rougeF1<${cfg.layer2MaxRougeF1} AND top20<${cfg.layer2MaxTop20Recall})`,
5857
+ metrics: contentMetrics
5858
+ };
5859
+ }
5860
+ return { passed: true, metrics: contentMetrics };
5861
+ }
5862
+ };
5863
+
5864
+ // lib/compress/quality-gate/algorithms/index.ts
5865
+ function ensureBuiltinGatesRegistered() {
5866
+ registerQualityGate(rougeRecallV1);
5867
+ }
5868
+
5869
+ // lib/compress/quality-gate/evaluate.ts
5870
+ var CHARS_PER_TOKEN_ESTIMATE = 4;
5871
+ var TOOL_OUTPUT_MAX_CHARS = 1500;
5872
+ var TOOL_INPUT_MAX_CHARS = 500;
5873
+ function extractMessageText(parts) {
5874
+ if (!parts || !Array.isArray(parts)) return "";
5875
+ let text = "";
5876
+ for (const part of parts) {
5877
+ if (!part || typeof part !== "object") continue;
5878
+ if (part.type === "text") {
5879
+ text += part.text + "\n";
5880
+ } else if (part.type === "tool") {
5881
+ const state = part.state;
5882
+ const input = state.status === "completed" && typeof state.input === "object" ? JSON.stringify(state.input).slice(0, TOOL_INPUT_MAX_CHARS) : "";
5883
+ const output = state.status === "completed" && typeof state.output === "string" ? state.output.slice(0, TOOL_OUTPUT_MAX_CHARS) : state.status === "completed" && typeof state.output === "object" ? JSON.stringify(state.output).slice(0, TOOL_OUTPUT_MAX_CHARS) : "";
5884
+ text += `[tool:${part.tool}] ${input}
5885
+ ${output}
5886
+ `;
5887
+ }
5888
+ }
5889
+ return text;
5890
+ }
5891
+ function buildContext(block, rawMessages) {
5892
+ const directIds = block.directMessageIds;
5893
+ if (!directIds || directIds.length === 0) return null;
5894
+ const idToMsg = /* @__PURE__ */ new Map();
5895
+ for (const m of rawMessages) {
5896
+ const id = m?.info?.id;
5897
+ if (typeof id === "string") idToMsg.set(id, m);
5898
+ }
5899
+ const chunks = [];
5900
+ for (const id of directIds) {
5901
+ const m = idToMsg.get(id);
5902
+ if (!m) continue;
5903
+ chunks.push(extractMessageText(m.parts));
5904
+ }
5905
+ if (chunks.length === 0) return null;
5906
+ const originalText = chunks.join("\n");
5907
+ return {
5908
+ block,
5909
+ summary: block.summary ?? "",
5910
+ originalChunks: chunks,
5911
+ originalText,
5912
+ originalTokens: Math.ceil(originalText.length / CHARS_PER_TOKEN_ESTIMATE)
5913
+ };
5914
+ }
5915
+ function evaluateBlockQuality(state, rawMessages, entry, config, logger) {
5916
+ const qg = config.qualityGate;
5917
+ if (!qg || qg.enabled !== true) return null;
5918
+ ensureBuiltinGatesRegistered();
5919
+ const algoName = qg.algorithm;
5920
+ if (!algoName) {
5921
+ logger.warn("Quality gate enabled but no algorithm specified", {});
5922
+ return null;
5923
+ }
5924
+ const gate = getQualityGate(algoName);
5925
+ if (!gate) {
5926
+ logger.warn("Quality gate algorithm not found in registry", { algorithm: algoName });
5927
+ return null;
5928
+ }
5929
+ const block = state.prune.messages.blocksById.get(entry.blockId);
5930
+ if (!block) {
5931
+ logger.warn("Quality gate: block not found", { blockId: entry.blockId });
5932
+ return null;
5933
+ }
5934
+ const ctx = buildContext(block, rawMessages);
5935
+ if (!ctx) return null;
5936
+ const algoConfig = (qg.algorithms && qg.algorithms[algoName]) ?? {};
5937
+ try {
5938
+ return gate.evaluate(ctx, algoConfig);
5939
+ } catch (err) {
5940
+ logger.warn("Quality gate threw \u2014 treating as pass", {
5941
+ gate: gate.name,
5942
+ blockId: entry.blockId,
5943
+ error: err instanceof Error ? err.message : String(err)
5944
+ });
5945
+ return { passed: true, metrics: [] };
5946
+ }
5947
+ }
5948
+ function evaluateBatchQuality(state, rawMessages, entries, config, logger) {
5949
+ const failures = [];
5950
+ for (const entry of entries) {
5951
+ const result = evaluateBlockQuality(state, rawMessages, entry, config, logger);
5952
+ if (result && !result.passed) {
5953
+ failures.push({ blockId: entry.blockId, result });
5954
+ }
5955
+ }
5956
+ return {
5957
+ total: entries.length,
5958
+ passed: entries.length - failures.length,
5959
+ failures
5960
+ };
5961
+ }
5962
+
5181
5963
  // lib/compress/pipeline.ts
5182
5964
  function snapshotCompressionState(state) {
5183
5965
  return {
@@ -5226,6 +6008,27 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
5226
6008
  ctx.state.manualMode = ctx.state.manualMode ? "active" : false;
5227
6009
  applyPendingCompressionDurations(ctx.state);
5228
6010
  await saveSessionState(ctx.state, ctx.logger);
6011
+ if (entries.length > 0) {
6012
+ const qualityReport = evaluateBatchQuality(
6013
+ ctx.state,
6014
+ rawMessages,
6015
+ entries,
6016
+ ctx.config,
6017
+ ctx.logger
6018
+ );
6019
+ for (const failure of qualityReport.failures) {
6020
+ const metrics = Object.fromEntries(
6021
+ failure.result.metrics.map((m) => [m.name, m.value])
6022
+ );
6023
+ ctx.logger.warn("Compression quality gate FAILED", {
6024
+ blockId: failure.blockId,
6025
+ algorithm: ctx.config.qualityGate.algorithm,
6026
+ layer: failure.result.layer,
6027
+ reason: failure.result.reason,
6028
+ ...metrics
6029
+ });
6030
+ }
6031
+ }
5229
6032
  const params = getCurrentParams(ctx.state, rawMessages, ctx.logger);
5230
6033
  const sessionMessageIds = rawMessages.filter((msg) => !isIgnoredUserMessage(msg)).map((msg) => msg.info.id);
5231
6034
  const contextTokensBefore = getCurrentTokenUsage(ctx.state, rawMessages);
@@ -5578,9 +6381,14 @@ function createCompressMessageTool(ctx) {
5578
6381
  import { tool as tool3 } from "@opencode-ai/plugin";
5579
6382
  function buildSchema2(maxSummaryLengthHard) {
5580
6383
  return {
5581
- topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
6384
+ topic: tool3.schema.string().optional().describe(
6385
+ "Fallback topic for entries without their own. Omit when each content entry specifies its own topic."
6386
+ ),
5582
6387
  content: tool3.schema.array(
5583
6388
  tool3.schema.object({
6389
+ topic: tool3.schema.string().optional().describe(
6390
+ "Short label (3-5 words) for THIS range, e.g. 'Auth System Exploration'. Omit to use top-level topic. When compressing multiple unrelated ranges, give each its own topic for better quality."
6391
+ ),
5584
6392
  startId: tool3.schema.string().describe(
5585
6393
  "Message or block ID marking the beginning of range (e.g. m00001, b2)"
5586
6394
  ),
@@ -5590,7 +6398,7 @@ function buildSchema2(maxSummaryLengthHard) {
5590
6398
  )
5591
6399
  })
5592
6400
  ).describe(
5593
- "One or more ranges to compress, each with start/end boundaries and a summary"
6401
+ "One or more ranges to compress, each with start/end boundaries and a summary. When compressing multiple unrelated ranges in one call, give each its own topic."
5594
6402
  ),
5595
6403
  summaryMaxChars: tool3.schema.number().optional().describe(
5596
6404
  `Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
@@ -5624,7 +6432,7 @@ function createCompressRangeTool(ctx) {
5624
6432
  const { rawMessages, searchContext } = await prepareSession(
5625
6433
  ctx,
5626
6434
  toolCtx,
5627
- `Compress Range: ${input.topic}`
6435
+ `Compress Range: ${input.topic ?? "(batch)"}`
5628
6436
  );
5629
6437
  const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
5630
6438
  validateNonOverlapping(resolvedPlans);
@@ -5766,7 +6574,7 @@ function createCompressRangeTool(ctx) {
5766
6574
  const applied = applyCompressionState(
5767
6575
  ctx.state,
5768
6576
  {
5769
- topic: input.topic,
6577
+ topic: preparedPlan.entry.topic ?? input.topic ?? "",
5770
6578
  batchTopic: input.topic,
5771
6579
  startId: preparedPlan.entry.startId,
5772
6580
  endId: preparedPlan.entry.endId,
@@ -5791,7 +6599,13 @@ function createCompressRangeTool(ctx) {
5791
6599
  summaryTokens
5792
6600
  });
5793
6601
  }
5794
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
6602
+ await finalizeSession(
6603
+ ctx,
6604
+ toolCtx,
6605
+ rawMessages,
6606
+ notifications,
6607
+ input.topic
6608
+ );
5795
6609
  } catch (error) {
5796
6610
  restoreCompressionState(ctx.state, snapshot);
5797
6611
  throw error;
@@ -6195,7 +7009,7 @@ var dropEmptyMessages = (messages) => {
6195
7009
  for (let i = messages.length - 1; i >= 0; i--) {
6196
7010
  const parts = Array.isArray(messages[i].parts) ? messages[i].parts : [];
6197
7011
  const isEmpty = parts.every(
6198
- (part) => part.type === "text" && (typeof part.text !== "string" || part.text.trim().length === 0)
7012
+ (part) => part.type === "text" && (typeof part.text !== "string" || part.text.trim().length === 0 || part.ignored === true)
6199
7013
  );
6200
7014
  if (isEmpty) {
6201
7015
  messages.splice(i, 1);
@@ -6248,7 +7062,7 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
6248
7062
  }
6249
7063
  }
6250
7064
  const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
6251
- if (gcConfig && usageRatio > 0.5) {
7065
+ if (gcConfig && usageRatio > 0.9) {
6252
7066
  const promotionThreshold = gcConfig.promotionThreshold;
6253
7067
  const agingBlocks = [];
6254
7068
  for (const blockId of activeBlockIds) {
@@ -6266,10 +7080,10 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
6266
7080
  }
6267
7081
  if (agingBlocks.length > 0) {
6268
7082
  lines.push("");
6269
- lines.push("\u26A0\uFE0F Block aging warning \u2014 these blocks may be truncated by GC soon:");
7083
+ lines.push("\u26A0\uFE0F Block aging warning \u2014 context near limit, these blocks may be truncated by last-resort GC:");
6270
7084
  lines.push(...agingBlocks);
6271
7085
  lines.push(
6272
- "To preserve important content: use the compress tool to re-summarize these blocks into new concise ones. Unhandled blocks will be auto-truncated."
7086
+ "Re-summarize these blocks into concise new ones to preserve key facts. At 100% context, oversized blocks are auto-truncated as a last resort."
6273
7087
  );
6274
7088
  }
6275
7089
  }
@@ -6363,6 +7177,62 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
6363
7177
  return refs;
6364
7178
  }
6365
7179
 
7180
+ // node_modules/context-compress-algorithms/dist/chunk-UX2UW5FY.js
7181
+ var NUDGE_GROWTH_FLOOR = 6e3;
7182
+ var NUDGE_GROWTH_CAP = 5e4;
7183
+ var NUDGE_GROWTH_RATIO = 0.05;
7184
+ function computeShouldNudge(params) {
7185
+ const { currentTokens, overMinLimit, overMaxLimit } = params;
7186
+ if (currentTokens === void 0) {
7187
+ return { shouldNudge: false, tipsVariant: null };
7188
+ }
7189
+ if (params.lastNudgeTokens === void 0) {
7190
+ return { shouldNudge: false, tipsVariant: null };
7191
+ }
7192
+ const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
7193
+ const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
7194
+ if (!shouldNudge) {
7195
+ return { shouldNudge: false, tipsVariant: null };
7196
+ }
7197
+ const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
7198
+ return { shouldNudge: true, tipsVariant };
7199
+ }
7200
+ function resolveAdaptiveNudgeGrowth(modelContextLimit) {
7201
+ if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
7202
+ return Math.min(
7203
+ NUDGE_GROWTH_CAP,
7204
+ Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
7205
+ );
7206
+ }
7207
+ var defaultTriggerPolicy = {
7208
+ name: "context-compress-algorithms-trigger",
7209
+ version: "1.0.0",
7210
+ description: "Growth-only cadence: nudge when context growth since last nudge exceeds adaptive threshold, or when over max limit.",
7211
+ computeShouldNudge,
7212
+ resolveAdaptiveNudgeGrowth
7213
+ };
7214
+
7215
+ // lib/messages/inject/policy/registry.ts
7216
+ var registry2 = /* @__PURE__ */ new Map();
7217
+ var defaultPolicy = null;
7218
+ function registerTriggerPolicy2(policy) {
7219
+ if (!policy.name) {
7220
+ throw new Error("TriggerPolicy must have a name");
7221
+ }
7222
+ registry2.set(policy.name, policy);
7223
+ if (!defaultPolicy) {
7224
+ defaultPolicy = policy;
7225
+ }
7226
+ }
7227
+ function getDefaultTriggerPolicy() {
7228
+ return defaultPolicy;
7229
+ }
7230
+
7231
+ // lib/messages/inject/policy/index.ts
7232
+ function ensureBuiltinTriggerPolicyRegistered() {
7233
+ registerTriggerPolicy2(defaultTriggerPolicy);
7234
+ }
7235
+
6366
7236
  // lib/messages/inject/utils.ts
6367
7237
  var MESSAGE_MODE_NUDGE_PRIORITY = "high";
6368
7238
  function getNudgeFrequency(config) {
@@ -6475,31 +7345,20 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6475
7345
  modelContextLimit: state.modelContextLimit
6476
7346
  };
6477
7347
  }
6478
- function computeShouldNudge(params) {
6479
- const { currentTokens, overMinLimit, overMaxLimit } = params;
6480
- if (currentTokens === void 0) {
6481
- return { shouldNudge: false, tipsVariant: null };
6482
- }
6483
- if (params.lastNudgeTokens === void 0) {
7348
+ ensureBuiltinTriggerPolicyRegistered();
7349
+ function computeShouldNudge2(params) {
7350
+ const policy = getDefaultTriggerPolicy();
7351
+ if (!policy) {
6484
7352
  return { shouldNudge: false, tipsVariant: null };
6485
7353
  }
6486
- const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
6487
- const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
6488
- if (!shouldNudge) {
6489
- return { shouldNudge: false, tipsVariant: null };
6490
- }
6491
- const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
6492
- return { shouldNudge: true, tipsVariant };
7354
+ return policy.computeShouldNudge(params);
6493
7355
  }
6494
- var NUDGE_GROWTH_FLOOR = 6e3;
6495
- var NUDGE_GROWTH_CAP = 5e4;
6496
- var NUDGE_GROWTH_RATIO = 0.05;
6497
- function resolveAdaptiveNudgeGrowth(modelContextLimit) {
6498
- if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
6499
- return Math.min(
6500
- NUDGE_GROWTH_CAP,
6501
- Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
6502
- );
7356
+ function resolveAdaptiveNudgeGrowth2(modelContextLimit) {
7357
+ const policy = getDefaultTriggerPolicy();
7358
+ if (!policy) {
7359
+ return 6e3;
7360
+ }
7361
+ return policy.resolveAdaptiveNudgeGrowth(modelContextLimit);
6503
7362
  }
6504
7363
  function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
6505
7364
  if (anchorMessageIndex < 0) {
@@ -7075,7 +7934,7 @@ ${lines2.join("\n")}`;
7075
7934
  ${lines.join("\n")}`;
7076
7935
  }
7077
7936
 
7078
- // lib/prompts/compression-rules.ts
7937
+ // node_modules/context-compress-algorithms/dist/chunk-ZRHPFN6B.js
7079
7938
  var COMPRESS_PHILOSOPHY = `Compression Philosophy:
7080
7939
  - All compression serves the primary task, but be frugal.
7081
7940
  - Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
@@ -7251,7 +8110,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7251
8110
  }
7252
8111
  }
7253
8112
  const suffixMessage = createSuffixMessage(messages);
7254
- const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
8113
+ const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth2(modelContextLimit);
7255
8114
  const growthFloor = Math.max(
7256
8115
  config.compress?.minNudgeGrowthFloor ?? 5e3,
7257
8116
  (config.compress?.minNudgeGrowthRatio ?? 0.45) * nudgeGrowthTokens
@@ -7266,7 +8125,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7266
8125
  const hasPendingNudge = state.nudges.lastNudgeShownTokens !== void 0;
7267
8126
  const effectiveThreshold = hasPendingNudge ? Math.floor(nudgeGrowthTokens / 2) : nudgeGrowthTokens;
7268
8127
  const growthReference = state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens;
7269
- const decision = computeShouldNudge({
8128
+ const decision = computeShouldNudge2({
7270
8129
  currentTokens,
7271
8130
  modelContextLimit,
7272
8131
  overMinLimit,
@@ -8000,7 +8859,7 @@ function buildSchema3() {
8000
8859
  function extractMessageId(m) {
8001
8860
  return m.id ?? m.messageId ?? "";
8002
8861
  }
8003
- function extractMessageText(m) {
8862
+ function extractMessageText2(m) {
8004
8863
  const msg = m;
8005
8864
  const role = msg.role || msg.type || "unknown";
8006
8865
  const content = typeof msg.content === "string" ? msg.content : typeof msg.text === "string" ? msg.text : JSON.stringify(msg.content || msg.text || "");
@@ -8053,7 +8912,7 @@ function createDecompressTool(ctx) {
8053
8912
  }
8054
8913
  }
8055
8914
  const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
8056
- const lines2 = blockMessages.map(extractMessageText);
8915
+ const lines2 = blockMessages.map(extractMessageText2);
8057
8916
  const { writeFile: writeFile3 } = await import("fs/promises");
8058
8917
  const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : activeBlocks[0]?.summary ?? "(no content available)";
8059
8918
  await writeFile3(targetPath, fileContent, "utf-8");
@@ -8774,7 +9633,7 @@ TOOLS
8774
9633
 
8775
9634
  You have five context-management tools:
8776
9635
 
8777
- - \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Example: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`.
9636
+ - \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
8778
9637
  - \`decompress\` \u2014 Restore a previously compressed block's full original content, optionally to a file for large blocks. Use when a summary lacks the exact detail you need. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\`.
8779
9638
  - \`search_context\` \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: \`search_context({ query: "auth token refresh" })\`.
8780
9639
  - \`prune\` \u2014 Remove old tool outputs by tool type, keeping only recent calls. Unlike compress (which creates summaries), prune directly strips outputs. Use for disposable outputs like old todowrite states or edit echoes. Example: \`prune({ toolType: "todowrite", keepLatest: 3 })\`.
@@ -8873,6 +9732,16 @@ Rules:
8873
9732
  BATCHING
8874
9733
  When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
8875
9734
 
9735
+ When the ranges cover unrelated topics, give each entry its own \`topic\` for better summary quality \u2014 do not force unrelated content under a single shared topic. Omit the top-level \`topic\` when every entry has its own. Use the top-level \`topic\` only as a fallback when entries don't specify one.
9736
+
9737
+ \`\`\`
9738
+ compress({ content: [
9739
+ { topic: "Auth System Exploration", startId: "m00010", endId: "m00050", summary: "..." },
9740
+ { topic: "Bug Hunt", startId: "m00060", endId: "m00080", summary: "..." },
9741
+ { topic: "Deployment", startId: "m00090", endId: "m00110", summary: "..." },
9742
+ ]})
9743
+ \`\`\`
9744
+
8876
9745
  KEEP AND REF MARKERS
8877
9746
  When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
8878
9747
 
@@ -10567,46 +11436,9 @@ function createSystemPromptHandler(state, logger, config, prompts) {
10567
11436
  };
10568
11437
  }
10569
11438
  function runMajorGC(state, config, logger, messages) {
10570
- const maxBlockAge = config.gc.maxBlockAge ?? 15;
10571
- let agedOutCount = 0;
10572
- let agedOutTokens = 0;
10573
- const now = Date.now();
10574
- for (const [blockId, block] of state.prune.messages.blocksById) {
10575
- if (!block.active) continue;
10576
- const age = block.survivedCount ?? 0;
10577
- if (age > maxBlockAge) {
10578
- block.active = false;
10579
- block.deactivatedAt = now;
10580
- block.deactivatedByBlockId = void 0;
10581
- state.prune.messages.activeBlockIds.delete(Number(blockId));
10582
- const anchorMapped = state.prune.messages.activeByAnchorMessageId.get(block.anchorMessageId);
10583
- if (anchorMapped === Number(blockId)) {
10584
- state.prune.messages.activeByAnchorMessageId.delete(block.anchorMessageId);
10585
- }
10586
- agedOutCount++;
10587
- agedOutTokens += block.summaryTokens ?? Math.round(block.summary.length / 4);
10588
- }
10589
- }
10590
- if (agedOutCount > 0) {
10591
- logger.info("Major GC: deactivated aged-out blocks", {
10592
- agedOutCount,
10593
- agedOutTokens,
10594
- maxBlockAge
10595
- });
10596
- saveSessionState(state, logger).catch(() => {
10597
- });
10598
- }
10599
11439
  if (!state.modelContextLimit) return;
10600
11440
  const currentTokens = getCurrentTokenUsage(state, messages);
10601
- const oversizedThreshold = config.gc.maxOldGenSummaryLength * 2;
10602
- let hasOversizedBlocks = false;
10603
- for (const [, block] of state.prune.messages.blocksById) {
10604
- if (block.active && block.summary.length > oversizedThreshold) {
10605
- hasOversizedBlocks = true;
10606
- break;
10607
- }
10608
- }
10609
- if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc) && !hasOversizedBlocks) return;
11441
+ if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc)) return;
10610
11442
  const oldBlocks = [];
10611
11443
  for (const [blockId, block] of state.prune.messages.blocksById) {
10612
11444
  if (!block.active) continue;