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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +14 -0
- package/dist/bench/quality.d.ts +46 -0
- package/dist/bench/quality.d.ts.map +1 -0
- package/dist/bench/quality.js +118 -0
- package/dist/bench/quality.js.map +1 -0
- package/dist/bench/run.d.ts +2 -0
- package/dist/bench/run.d.ts.map +1 -0
- package/dist/bench/run.js +4 -0
- package/dist/bench/run.js.map +1 -0
- package/dist/cli/filter.d.ts.map +1 -1
- package/dist/cli/filter.js +13 -1
- package/dist/cli/filter.js.map +1 -1
- package/dist/cli/index.js +0 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +4 -2
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +32 -8
- package/dist/executor.js.map +1 -1
- package/dist/format-filter.d.ts +63 -0
- package/dist/format-filter.d.ts.map +1 -0
- package/dist/format-filter.js +331 -0
- package/dist/format-filter.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +14 -1
- package/dist/logger.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +37 -20
- package/dist/runtime/index.js.map +1 -1
- package/dist/server.bundle.mjs +479 -110
- package/dist/server.bundle.mjs.map +4 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -2
- package/dist/server.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +19 -1
- package/dist/stats.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +20 -2
- package/dist/store.js.map +1 -1
- package/dist/tools/batch-execute.d.ts.map +1 -1
- package/dist/tools/batch-execute.js +20 -2
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/util/auto-mode.d.ts +8 -0
- package/dist/util/auto-mode.d.ts.map +1 -1
- package/dist/util/auto-mode.js +40 -26
- package/dist/util/auto-mode.js.map +1 -1
- package/dist/util/fetch-code.d.ts.map +1 -1
- package/dist/util/fetch-code.js +2 -48
- package/dist/util/fetch-code.js.map +1 -1
- package/dist/util/html-to-markdown.d.ts +17 -0
- package/dist/util/html-to-markdown.d.ts.map +1 -0
- package/dist/util/html-to-markdown.js +66 -0
- package/dist/util/html-to-markdown.js.map +1 -0
- package/dist/util/intent-filter.d.ts +12 -3
- package/dist/util/intent-filter.d.ts.map +1 -1
- package/dist/util/intent-filter.js +61 -8
- package/dist/util/intent-filter.js.map +1 -1
- package/dist/util/regret.d.ts +54 -0
- package/dist/util/regret.d.ts.map +1 -0
- package/dist/util/regret.js +107 -0
- package/dist/util/regret.js.map +1 -0
- package/docs/quality-regression-report.md +20 -0
- 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 (& LAST so we don't double-decode)
|
|
50
|
+
md = md.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/"/g, '"')
|
|
53
|
+
.replace(/'/g, "'")
|
|
54
|
+
.replace(/'/g, "'")
|
|
55
|
+
.replace(/ /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(/&/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
|
|
11
|
-
* For small output (<= config.intentSearchThreshold bytes), returns
|
|
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;
|
|
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
|
|
4
|
-
* For small output (<= config.intentSearchThreshold bytes), returns
|
|
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
|
-
|
|
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(
|
|
14
|
-
const searchResults = store.search(intent, { limit:
|
|
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
|
-
|
|
19
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"intent-filter.js","sourceRoot":"","sources":["../../src/util/intent-filter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAIxD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAQ1C,kFAAkF;AAClF,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAC9B,oEAAoE;AACpE,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,qDAAqD;AACrD,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;;;;;;;;;GAaG;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,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,WAAW,IAAI,MAAM,CAAC,qBAAqB;YAAE,OAAO,MAAM,CAAC;QAE/D,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QACjD,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;QAElC,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;QAC3E,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1D,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAE7C,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,QAAQ,IAAI,UAAU,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAExE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,QAAQ,IAAI,OAAO,UAAU,CAAC,MAAM,qCAAqC,CAAC;YAC1E,QAAQ,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,QAAQ,IAAI,IAAI,CAAC;QAClB,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;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,IAAiB,EAAE,MAAc;IACpD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,gEAAgE,CAAC;IAE/F,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,SAAS,GAAG,MAAM,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC;QACtE,IAAI,KAAK,GAAG,aAAa,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,KAAK,qBAAqB,CAAC,CAAC;YACpD,SAAS;QACV,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;QAChD,SAAS,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,KAAK,OAAO,OAAO,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AAChC,CAAC;AAED,iEAAiE;AACjE,SAAS,IAAI,CAAC,CAAS,EAAE,GAAW;IACnC,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC1C,iEAAiE;IACjE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAC/C,CAAC"}
|
|
@@ -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.
|
|
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"
|