context-compress 2026.6.0 → 2026.7.0

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 (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +14 -0
  4. package/dist/bench/quality.d.ts +46 -0
  5. package/dist/bench/quality.d.ts.map +1 -0
  6. package/dist/bench/quality.js +118 -0
  7. package/dist/bench/quality.js.map +1 -0
  8. package/dist/bench/run.d.ts +2 -0
  9. package/dist/bench/run.d.ts.map +1 -0
  10. package/dist/bench/run.js +4 -0
  11. package/dist/bench/run.js.map +1 -0
  12. package/dist/cli/filter.d.ts.map +1 -1
  13. package/dist/cli/filter.js +13 -1
  14. package/dist/cli/filter.js.map +1 -1
  15. package/dist/cli/index.js +0 -0
  16. package/dist/cli/uninstall.d.ts.map +1 -1
  17. package/dist/cli/uninstall.js +4 -2
  18. package/dist/cli/uninstall.js.map +1 -1
  19. package/dist/config.d.ts +2 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +11 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/executor.d.ts.map +1 -1
  24. package/dist/executor.js +32 -8
  25. package/dist/executor.js.map +1 -1
  26. package/dist/format-filter.d.ts +63 -0
  27. package/dist/format-filter.d.ts.map +1 -0
  28. package/dist/format-filter.js +331 -0
  29. package/dist/format-filter.js.map +1 -0
  30. package/dist/logger.d.ts +4 -0
  31. package/dist/logger.d.ts.map +1 -1
  32. package/dist/logger.js +14 -1
  33. package/dist/logger.js.map +1 -1
  34. package/dist/runtime/index.d.ts +2 -2
  35. package/dist/runtime/index.d.ts.map +1 -1
  36. package/dist/runtime/index.js +37 -20
  37. package/dist/runtime/index.js.map +1 -1
  38. package/dist/server.bundle.mjs +479 -110
  39. package/dist/server.bundle.mjs.map +4 -4
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +5 -2
  42. package/dist/server.js.map +1 -1
  43. package/dist/stats.d.ts.map +1 -1
  44. package/dist/stats.js +19 -1
  45. package/dist/stats.js.map +1 -1
  46. package/dist/store.d.ts +1 -0
  47. package/dist/store.d.ts.map +1 -1
  48. package/dist/store.js +20 -2
  49. package/dist/store.js.map +1 -1
  50. package/dist/tools/batch-execute.d.ts.map +1 -1
  51. package/dist/tools/batch-execute.js +20 -2
  52. package/dist/tools/batch-execute.js.map +1 -1
  53. package/dist/util/auto-mode.d.ts +8 -0
  54. package/dist/util/auto-mode.d.ts.map +1 -1
  55. package/dist/util/auto-mode.js +40 -26
  56. package/dist/util/auto-mode.js.map +1 -1
  57. package/dist/util/fetch-code.d.ts.map +1 -1
  58. package/dist/util/fetch-code.js +2 -48
  59. package/dist/util/fetch-code.js.map +1 -1
  60. package/dist/util/html-to-markdown.d.ts +17 -0
  61. package/dist/util/html-to-markdown.d.ts.map +1 -0
  62. package/dist/util/html-to-markdown.js +66 -0
  63. package/dist/util/html-to-markdown.js.map +1 -0
  64. package/dist/util/intent-filter.d.ts +12 -3
  65. package/dist/util/intent-filter.d.ts.map +1 -1
  66. package/dist/util/intent-filter.js +61 -8
  67. package/dist/util/intent-filter.js.map +1 -1
  68. package/dist/util/regret.d.ts +54 -0
  69. package/dist/util/regret.d.ts.map +1 -0
  70. package/dist/util/regret.js +107 -0
  71. package/dist/util/regret.js.map +1 -0
  72. package/docs/quality-regression-report.md +20 -0
  73. package/package.json +2 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Single source of truth for the HTML→markdown conversion snippet.
3
+ *
4
+ * Returns a dependency-free JS snippet that assumes a `html` string variable is
5
+ * already in scope, converts it to markdown, and `console.log`s the result.
6
+ * Shared by the fetch tool (which runs it inside the sandbox subprocess) and its
7
+ * tests, so the two copies can never drift.
8
+ *
9
+ * NOTE: This is a regex pipeline, not a full HTML parser — a real parser cannot
10
+ * be used because the snippet must run in the dependency-free sandbox. To bound
11
+ * worst-case backtracking, callers MUST size-limit `html` before running this
12
+ * (see `buildFetchCode`, which caps at 10MB). All patterns use bounded character
13
+ * classes (`[^>]`, `[^"]`) or lazy quantifiers separated by required literals,
14
+ * so none exhibit catastrophic (exponential) backtracking.
15
+ */
16
+ export function htmlToMarkdownSnippet() {
17
+ return `
18
+ // Strip unwanted tags
19
+ let md = html
20
+ .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
21
+ .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "")
22
+ .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "")
23
+ .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "")
24
+ .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
25
+
26
+ // Convert headings
27
+ md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "# $1\\n");
28
+ md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "## $1\\n");
29
+ md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "### $1\\n");
30
+ md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "#### $1\\n");
31
+
32
+ // Convert code blocks
33
+ md = md.replace(/<pre[^>]*><code[^>]*>(.*?)<\\/code><\\/pre>/gis, "\`\`\`\\n$1\\n\`\`\`\\n");
34
+ md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, "\`$1\`");
35
+
36
+ // Convert links
37
+ md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
38
+
39
+ // Convert lists
40
+ md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
41
+
42
+ // Convert paragraphs
43
+ md = md.replace(/<p[^>]*>(.*?)<\\/p>/gis, "$1\\n\\n");
44
+ md = md.replace(/<br\\s*\\/?>/gi, "\\n");
45
+
46
+ // Strip remaining tags
47
+ md = md.replace(/<[^>]+>/g, "");
48
+
49
+ // Decode entities (&amp; LAST so we don't double-decode)
50
+ md = md.replace(/&lt;/g, "<")
51
+ .replace(/&gt;/g, ">")
52
+ .replace(/&quot;/g, '"')
53
+ .replace(/&#39;/g, "'")
54
+ .replace(/&apos;/g, "'")
55
+ .replace(/&nbsp;/g, " ")
56
+ .replace(/&#(\\d+);/g, (_, n) => { const c = parseInt(n, 10); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
57
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { const c = parseInt(h, 16); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
58
+ .replace(/&amp;/g, "&");
59
+
60
+ // Clean whitespace
61
+ md = md.replace(/\\n{3,}/g, "\\n\\n").trim();
62
+
63
+ console.log(md);
64
+ `;
65
+ }
66
+ //# sourceMappingURL=html-to-markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-markdown.js","sourceRoot":"","sources":["../../src/util/html-to-markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CP,CAAC;AACF,CAAC"}
@@ -7,9 +7,18 @@ interface IntentFilterDeps {
7
7
  tracker: SessionTracker;
8
8
  }
9
9
  /**
10
- * Index large output and return a compact summary keyed to `intent`.
11
- * For small output (<= config.intentSearchThreshold bytes), returns the
12
- * original output unchanged so callers don't pay for trivial filtering.
10
+ * Index large output and return a compact, *query-conditioned* summary keyed to
11
+ * `intent`. For small output (<= config.intentSearchThreshold bytes), returns
12
+ * the original output unchanged so callers don't pay for trivial filtering.
13
+ *
14
+ * Unlike a fixed-mode filter, this is variable-rate and query-aware (cf.
15
+ * ACC-RAG / AttnComp): the retrieval layer already produces query-focused
16
+ * excerpts, so instead of throwing them away and forcing a follow-up search()
17
+ * round-trip, we inline the top-ranked content up to `config.intentBudgetBytes`.
18
+ * Best-scoring sections get the most room; each is capped so one section can't
19
+ * crowd out the rest. Error/warning lines are always surfaced verbatim — they're
20
+ * usually the whole reason an intent was specified — so a summary never silently
21
+ * drops the one line the agent needed.
13
22
  */
14
23
  export declare function createIntentFilter(deps: IntentFilterDeps): (output: string, intent: string, sourceLabel: string) => string;
15
24
  export type ApplyIntentFilter = ReturnType<typeof createIntentFilter>;
@@ -1 +1 @@
1
- {"version":3,"file":"intent-filter.d.ts","sourceRoot":"","sources":["../../src/util/intent-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,UAAU,gBAAgB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,IAGtB,QAAQ,MAAM,EAAE,QAAQ,MAAM,EAAE,aAAa,MAAM,KAAG,MAAM,CAoB9F;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"intent-filter.d.ts","sourceRoot":"","sources":["../../src/util/intent-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,UAAU,gBAAgB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;CACxB;AASD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,IAGtB,QAAQ,MAAM,EAAE,QAAQ,MAAM,EAAE,aAAa,MAAM,KAAG,MAAM,CA0B9F;AAmCD,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -1,22 +1,43 @@
1
+ import { extractErrorLines } from "../format-filter.js";
1
2
  import { compactLabel } from "./label.js";
3
+ /** How many matching sections to consider (before the byte budget trims them). */
4
+ const INTENT_SEARCH_LIMIT = 6;
5
+ /** No single hit may consume more than this share of the budget. */
6
+ const PER_HIT_CAP = 700;
7
+ /** Minimum snippet bytes worth showing for a hit. */
8
+ const PER_HIT_FLOOR = 120;
2
9
  /**
3
- * Index large output and return a compact summary keyed to `intent`.
4
- * For small output (<= config.intentSearchThreshold bytes), returns the
5
- * original output unchanged so callers don't pay for trivial filtering.
10
+ * Index large output and return a compact, *query-conditioned* summary keyed to
11
+ * `intent`. For small output (<= config.intentSearchThreshold bytes), returns
12
+ * the original output unchanged so callers don't pay for trivial filtering.
13
+ *
14
+ * Unlike a fixed-mode filter, this is variable-rate and query-aware (cf.
15
+ * ACC-RAG / AttnComp): the retrieval layer already produces query-focused
16
+ * excerpts, so instead of throwing them away and forcing a follow-up search()
17
+ * round-trip, we inline the top-ranked content up to `config.intentBudgetBytes`.
18
+ * Best-scoring sections get the most room; each is capped so one section can't
19
+ * crowd out the rest. Error/warning lines are always surfaced verbatim — they're
20
+ * usually the whole reason an intent was specified — so a summary never silently
21
+ * drops the one line the agent needed.
6
22
  */
7
23
  export function createIntentFilter(deps) {
8
24
  const { config, store, tracker } = deps;
9
25
  return function applyIntentFilter(output, intent, sourceLabel) {
10
- if (Buffer.byteLength(output) <= config.intentSearchThreshold)
26
+ const outputBytes = Buffer.byteLength(output);
27
+ if (outputBytes <= config.intentSearchThreshold)
11
28
  return output;
12
29
  const indexed = store.index(output, sourceLabel);
13
- tracker.trackIndexed(Buffer.byteLength(output));
14
- const searchResults = store.search(intent, { limit: 3 });
30
+ tracker.trackIndexed(outputBytes);
31
+ const searchResults = store.search(intent, { limit: INTENT_SEARCH_LIMIT });
15
32
  const terms = store.getDistinctiveTerms(indexed.sourceId);
33
+ const errorLines = extractErrorLines(output);
16
34
  let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.\n`;
17
35
  filtered += `${searchResults.results.length} sections matched "${intent}":\n\n`;
18
- for (const hit of searchResults.results) {
19
- filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}\n`;
36
+ filtered += renderHits(searchResults.results, config.intentBudgetBytes);
37
+ if (errorLines.length > 0) {
38
+ filtered += `\n⚠ ${errorLines.length} error/warning line(s) in output:\n`;
39
+ filtered += errorLines.map((l) => ` ${l}`).join("\n");
40
+ filtered += "\n";
20
41
  }
21
42
  if (terms.length > 0 && config.compressionLevel !== "ultra") {
22
43
  filtered += `\nSearchable terms: ${terms.join(", ")}\n`;
@@ -25,4 +46,36 @@ export function createIntentFilter(deps) {
25
46
  return compactLabel(filtered, config.compressionLevel);
26
47
  };
27
48
  }
49
+ /**
50
+ * Render hits within a byte budget. Hits arrive best-first; each gets an even
51
+ * share of the remaining budget (capped and floored), and any unspent bytes
52
+ * roll forward to later hits. A hit skipped for lack of budget is still listed
53
+ * by title so the agent knows it exists and can search() for it.
54
+ */
55
+ function renderHits(hits, budget) {
56
+ if (hits.length === 0)
57
+ return " (no matching sections — try search() with different terms)\n";
58
+ const lines = [];
59
+ let remaining = budget;
60
+ for (let i = 0; i < hits.length; i++) {
61
+ const hit = hits[i];
62
+ const hitsLeft = hits.length - i;
63
+ const share = Math.min(PER_HIT_CAP, Math.floor(remaining / hitsLeft));
64
+ if (share < PER_HIT_FLOOR) {
65
+ lines.push(` - **${hit.title}** (search to view)`);
66
+ continue;
67
+ }
68
+ const snippet = clip(hit.snippet.trim(), share);
69
+ remaining -= Buffer.byteLength(snippet);
70
+ lines.push(` - **${hit.title}**: ${snippet}`);
71
+ }
72
+ return `${lines.join("\n")}\n`;
73
+ }
74
+ /** Trim a snippet to at most `max` bytes, marking truncation. */
75
+ function clip(s, max) {
76
+ if (Buffer.byteLength(s) <= max)
77
+ return s;
78
+ // Slice on characters; good enough for mostly-ASCII tool output.
79
+ return `${s.slice(0, Math.max(0, max - 1))}…`;
80
+ }
28
81
  //# sourceMappingURL=intent-filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"intent-filter.js","sourceRoot":"","sources":["../../src/util/intent-filter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAQ1C;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAsB;IACxD,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAExC,OAAO,SAAS,iBAAiB,CAAC,MAAc,EAAE,MAAc,EAAE,WAAmB;QACpF,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,qBAAqB;YAAE,OAAO,MAAM,CAAC;QAE7E,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QACjD,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAEhD,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAE1D,IAAI,QAAQ,GAAG,WAAW,OAAO,CAAC,WAAW,kBAAkB,WAAW,KAAK,CAAC;QAChF,QAAQ,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,sBAAsB,MAAM,QAAQ,CAAC;QAChF,KAAK,MAAM,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;YACzC,QAAQ,IAAI,SAAS,GAAG,CAAC,KAAK,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC;QACpE,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,gBAAgB,KAAK,OAAO,EAAE,CAAC;YAC7D,QAAQ,IAAI,uBAAuB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;QACzD,CAAC;QACD,QAAQ,IAAI,uEAAuE,CAAC;QACpF,OAAO,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACxD,CAAC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"intent-filter.js","sourceRoot":"","sources":["../../src/util/intent-filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ACON-style self-improving compression policy.
3
+ *
4
+ * ACON (Kang et al., 2026) optimizes a compression *guideline* in natural-language
5
+ * space by analyzing cases where the full context succeeds but the compressed
6
+ * context fails, then refines the guideline — no model fine-tuning. We can't run
7
+ * paired trajectories at runtime, but we can approximate the failure signal from
8
+ * the agent's own behavior: if a command is compressed aggressively and then
9
+ * *re-run almost immediately*, the aggressive summary probably hid something the
10
+ * agent needed. That's a "compression regret".
11
+ *
12
+ * The policy this drives is deliberately narrow and safe:
13
+ * - We only count a regret for a FAST re-run (default ≤ 30s) that followed an
14
+ * AGGRESSIVE compression. Balanced/conservative are never blamed — they
15
+ * rarely drop task-critical data, and normal edit→rerun loops (which are
16
+ * slower and usually ran under balanced) shouldn't be misread as regret.
17
+ * - The only adjustment is a one-step DOWNGRADE (aggressive → balanced) once a
18
+ * fingerprint's regret rate is high over enough samples. Downgrading only
19
+ * ever *reduces* compression, so a false positive costs a few tokens, never
20
+ * correctness. It never makes anything more aggressive on its own.
21
+ *
22
+ * State is a small JSON map at ~/.context-compress/regret.json, keyed by the same
23
+ * command fingerprint the auto-mode cache uses, so it persists across sessions.
24
+ */
25
+ import type { FilterMode } from "../filters.js";
26
+ export interface RegretOptions {
27
+ path?: string;
28
+ /** Injectable clock (epoch ms) for deterministic tests. */
29
+ now?: number;
30
+ /** Re-run window in ms; a re-run within this counts toward regret. */
31
+ windowMs?: number;
32
+ }
33
+ export interface RegretDecision {
34
+ /** Mode after any regret-driven adjustment. */
35
+ mode: FilterMode;
36
+ /** True when the chosen mode was downgraded due to regret. */
37
+ adjusted: boolean;
38
+ /** Current regret rate for this fingerprint (0–1). */
39
+ regretRate: number;
40
+ }
41
+ /**
42
+ * Record this compression decision, detect regret from a fast re-run, and return
43
+ * a possibly-downgraded mode. Call this once per auto-mode decision (including
44
+ * cache hits) so the re-run timeline stays accurate.
45
+ */
46
+ export declare function observeAndAdjust(fingerprint: string, chosenMode: FilterMode, opts?: RegretOptions): RegretDecision;
47
+ /** Read-only view of regret stats, for the `stats` tool / observability. */
48
+ export declare function regretSummary(opts?: RegretOptions): Array<{
49
+ fingerprint: string;
50
+ observations: number;
51
+ regrets: number;
52
+ regretRate: number;
53
+ }>;
54
+ //# sourceMappingURL=regret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regret.d.ts","sourceRoot":"","sources":["../../src/util/regret.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAyBhD,MAAM,WAAW,aAAa;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,+CAA+C;IAC/C,IAAI,EAAE,UAAU,CAAC;IACjB,8DAA8D;IAC9D,QAAQ,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;CACnB;AA0BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,UAAU,EACtB,IAAI,GAAE,aAAkB,GACtB,cAAc,CA8BhB;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,KAAK,CAAC;IAC9D,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACnB,CAAC,CAWD"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * ACON-style self-improving compression policy.
3
+ *
4
+ * ACON (Kang et al., 2026) optimizes a compression *guideline* in natural-language
5
+ * space by analyzing cases where the full context succeeds but the compressed
6
+ * context fails, then refines the guideline — no model fine-tuning. We can't run
7
+ * paired trajectories at runtime, but we can approximate the failure signal from
8
+ * the agent's own behavior: if a command is compressed aggressively and then
9
+ * *re-run almost immediately*, the aggressive summary probably hid something the
10
+ * agent needed. That's a "compression regret".
11
+ *
12
+ * The policy this drives is deliberately narrow and safe:
13
+ * - We only count a regret for a FAST re-run (default ≤ 30s) that followed an
14
+ * AGGRESSIVE compression. Balanced/conservative are never blamed — they
15
+ * rarely drop task-critical data, and normal edit→rerun loops (which are
16
+ * slower and usually ran under balanced) shouldn't be misread as regret.
17
+ * - The only adjustment is a one-step DOWNGRADE (aggressive → balanced) once a
18
+ * fingerprint's regret rate is high over enough samples. Downgrading only
19
+ * ever *reduces* compression, so a false positive costs a few tokens, never
20
+ * correctness. It never makes anything more aggressive on its own.
21
+ *
22
+ * State is a small JSON map at ~/.context-compress/regret.json, keyed by the same
23
+ * command fingerprint the auto-mode cache uses, so it persists across sessions.
24
+ */
25
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { dirname, join } from "node:path";
28
+ const DEFAULT_PATH = join(homedir(), ".context-compress", "regret.json");
29
+ const RERUN_WINDOW_MS = 30_000;
30
+ /**
31
+ * Absolute number of aggressive-mode fast re-runs before we downgrade. Using a
32
+ * count (not a decaying rate) gives hysteresis: once a fingerprint has proven
33
+ * aggressive is a bad fit, it stays downgraded instead of oscillating back the
34
+ * moment the re-runs stop (they stop *because* we downgraded).
35
+ */
36
+ const REGRET_MIN_COUNT = 3;
37
+ function load(path) {
38
+ if (!existsSync(path))
39
+ return {};
40
+ try {
41
+ return JSON.parse(readFileSync(path, "utf-8"));
42
+ }
43
+ catch {
44
+ return {};
45
+ }
46
+ }
47
+ function save(path, map) {
48
+ try {
49
+ mkdirSync(dirname(path), { recursive: true });
50
+ writeFileSync(path, JSON.stringify(map, null, 2));
51
+ }
52
+ catch {
53
+ /* best-effort */
54
+ }
55
+ }
56
+ function downgrade(mode) {
57
+ if (mode === "aggressive")
58
+ return "balanced";
59
+ if (mode === "balanced")
60
+ return "conservative";
61
+ return "conservative";
62
+ }
63
+ /**
64
+ * Record this compression decision, detect regret from a fast re-run, and return
65
+ * a possibly-downgraded mode. Call this once per auto-mode decision (including
66
+ * cache hits) so the re-run timeline stays accurate.
67
+ */
68
+ export function observeAndAdjust(fingerprint, chosenMode, opts = {}) {
69
+ const path = opts.path ?? DEFAULT_PATH;
70
+ const now = opts.now ?? Date.now();
71
+ const window = opts.windowMs ?? RERUN_WINDOW_MS;
72
+ const map = load(path);
73
+ const rec = map[fingerprint] ?? {
74
+ lastMode: chosenMode,
75
+ lastSeen: 0,
76
+ observations: 0,
77
+ regrets: 0,
78
+ };
79
+ // A fast re-run after an aggressive compression is our regret signal.
80
+ const isFastRerun = rec.lastSeen > 0 && now - rec.lastSeen <= window;
81
+ if (isFastRerun && rec.lastMode === "aggressive") {
82
+ rec.regrets++;
83
+ }
84
+ rec.observations++;
85
+ const regretRate = rec.observations > 0 ? rec.regrets / rec.observations : 0;
86
+ const shouldAdjust = chosenMode === "aggressive" && rec.regrets >= REGRET_MIN_COUNT;
87
+ const mode = shouldAdjust ? downgrade(chosenMode) : chosenMode;
88
+ rec.lastMode = mode;
89
+ rec.lastSeen = now;
90
+ map[fingerprint] = rec;
91
+ save(path, map);
92
+ return { mode, adjusted: mode !== chosenMode, regretRate };
93
+ }
94
+ /** Read-only view of regret stats, for the `stats` tool / observability. */
95
+ export function regretSummary(opts = {}) {
96
+ const map = load(opts.path ?? DEFAULT_PATH);
97
+ return Object.entries(map)
98
+ .map(([fingerprint, r]) => ({
99
+ fingerprint,
100
+ observations: r.observations,
101
+ regrets: r.regrets,
102
+ regretRate: r.observations > 0 ? r.regrets / r.observations : 0,
103
+ }))
104
+ .filter((r) => r.regrets > 0)
105
+ .sort((a, b) => b.regretRate - a.regretRate);
106
+ }
107
+ //# sourceMappingURL=regret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regret.js","sourceRoot":"","sources":["../../src/util/regret.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ <!-- Generated by `npm run bench:quality` — do not edit by hand. -->
2
+
3
+ # Quality-Regression Benchmark
4
+
5
+ | Case | Mode | Reduction | Survival | Valid JSON |
6
+ |------|------|-----------|----------|------------|
7
+ | pretty-json (unrecognized cmd) | conservative | 0% | 100% | — |
8
+ | pretty-json (unrecognized cmd) | balanced | 37% | 100% | ✓ |
9
+ | pretty-json (unrecognized cmd) | aggressive | 96% | 100% | — |
10
+ | ndjson stream | conservative | 0% | 100% | — |
11
+ | ndjson stream | balanced | 93% | 100% | — |
12
+ | ndjson stream | aggressive | 93% | 100% | — |
13
+ | app logs with an error | conservative | 0% | 100% | — |
14
+ | app logs with an error | balanced | 98% | 100% | — |
15
+ | app logs with an error | aggressive | 98% | 100% | — |
16
+ | git log | conservative | 0% | 100% | — |
17
+ | git log | balanced | 0% | 100% | — |
18
+ | git log | aggressive | 59% | 100% | — |
19
+
20
+ _Reduction = bytes removed. Survival = fraction of task-critical substrings still present after compression. Valid JSON = balanced-mode output still parses._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-compress",
3
- "version": "2026.6.0",
3
+ "version": "2026.7.0",
4
4
  "description": "Context-aware MCP server that compresses tool output for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -17,6 +17,7 @@
17
17
  "test": "node --import tsx --test tests/**/*.test.ts",
18
18
  "test:unit": "node --import tsx --test tests/unit/*.test.ts",
19
19
  "test:integration": "node --import tsx --test tests/integration/*.test.ts",
20
+ "bench:quality": "tsx src/bench/run.ts",
20
21
  "clean": "rm -rf dist dist-bin",
21
22
  "build:bin": "bun build --compile --target=bun-darwin-arm64 ./src/cli/lite.ts --outfile=./dist-bin/cc-lite-darwin-arm64 && bun build --compile --target=bun-darwin-x64 ./src/cli/lite.ts --outfile=./dist-bin/cc-lite-darwin-x64 && bun build --compile --target=bun-linux-x64 ./src/cli/lite.ts --outfile=./dist-bin/cc-lite-linux-x64 && bun build --compile --target=bun-linux-arm64 ./src/cli/lite.ts --outfile=./dist-bin/cc-lite-linux-arm64",
22
23
  "prepublishOnly": "npm run lint && npm run test && npm run build"