acp-kernel 0.0.13 → 0.0.15

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/README.md CHANGED
@@ -60,7 +60,6 @@ core.status(compressed, tokenCount, config); // context-usage report
60
60
  |--------|---------|
61
61
  | `truncateLargeToolOutputs` | Emergency context-threshold-gated truncation of large visible tool outputs (last-resort safety valve; summaries are never touched) |
62
62
  | `hideConsumedCompressCalls` | Hide historical compress tool-calls whose block is inactive |
63
- | `resolveKeepMarkers` | Expand `[[KEEP:mNNNNN]]` / rewrite `[[REF:mNNNNN\|desc]]` |
64
63
  | `buildStatusReport` / `buildRecap` | Context-usage report + block recap |
65
64
  | `mergeMarkedBlocks` / `collectOldGenBlocks` | Batch merge old-gen blocks into one summary |
66
65
  | `rebuildCompressionState` | Fork-recovery: replay historical compress calls |
@@ -80,7 +79,7 @@ The nudge system tells the model *when* to compress. It implements:
80
79
 
81
80
  ✅ **Engine complete** — 23 source modules, 167 tests, typecheck + build clean. 3-tier compression, growth-gated nudges, emergency truncation,, fork-recovery, batch merge, composable node pipeline. Ready for adapter authoring.
82
81
 
83
- > **Known limitation:** protected tool messages are excluded from *ref assignment* (marked `BLOCKED`) but, unlike opencode-acp's Bug 39, are **not** hard-excluded from an explicitly-referenced compress range. A model that names a range covering a `BLOCKED` message will compress it. This is an intentional simplification for the pure core; adapters that need hard-exclusion should pre-split ranges before calling `applyCompression`.
82
+ > **Protected tool messages:** protected tool calls (per `config.protectedTools`) and their paired tool-results are hard-excluded from compression they are dropped from the compressible set and from the new block's `effectiveMessageIds`, so they stay fully visible and are never folded into a summary. This matches opencode-acp's Bug 39 fix. The soft-protected recent zone (`preserveRecentMessages` / last user message) is handled separately: messages there are excluded from the range but do not fail it (an entirely-protected range fails with a clear error).
84
83
 
85
84
  ## License
86
85
 
@@ -1 +1 @@
1
- {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAcpB,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAwN7D;AAwqBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAcpB,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAwN7D;AAmrBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * DO NOT modify the wording — it is the result of extensive tuning.
6
6
  */
7
- export declare const COMPRESS_PHILOSOPHY = "Compression Philosophy:\n- All compression serves the primary task, but be frugal.\n- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.\n- Compress by need, not by percentage.\n- Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.\n- Curate summaries like a well-structured document. User prompts, compressed tool outputs, code, logs, or skill-call intermediate results that are critically important should be preserved \u2014 not by exempting them from compression, but by embedding them in the summary via [[KEEP:mNNNNN]] (auto-expanded verbatim) and [[REF:mNNNNN|description]] (compact link).";
8
- export declare const HOW_TO_COMPRESS_RULES = "HOW TO COMPRESS\n\nWhen you call `compress`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.\n\nKEEP VERBATIM \u2014 never paraphrase or abbreviate these:\n- Full file paths with line numbers, directory prefix on every mention (`lib/hooks.ts:347`, `src/index.ts:12-18`, `gatenet_v3/model.py:45`). Never abbreviate to a bare filename (`hooks.ts`, `model.py`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.\n- Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. `kv_keys += define_gate * a_key[i](emb)` is more useful than \"see model_kvnet.py\").\n- Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).\n- Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not \"X is worse\" alone (write \"1.76\u00D7 PPL gap because KV store is static\", not \"KVNet underperforms\").\n- Decisions and their rationale (\"chose X over Y because Z\" \u2014 the \"because\" is load-bearing; without it the decision looks arbitrary).\n- Constraints discovered (\"must support Node 22\", \"no new dependencies\", \"AGENTS.md forbids `as any`\").\n- Exact values: versions, config keys, thresholds, magic numbers.\n- User intent \u2014 quote short user messages verbatim. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Mark them clearly as past quotes (e.g., \"User said: ...\"), not as current directives. Losing these changes the task itself.\n- The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., \"initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause\"). Losing the goal or its evolution makes all subsequent work appear unmotivated.\n- Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.\n- Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.\n- Message refs of key anchors (`m00420`, `m00510\u2013m00520`) \u2014 they let you or a later reader jump back via decompress to the exact original.\n\nDROP \u2014 extract the signal, discard the vessel:\n- Verbose logs (build/test/`npm` output) once you have captured the error line or the result.\n- Duplicate file reads once the needed content is recorded.\n- Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).\n- Dead-end exploration \u2014 but PRESERVE the lesson in one line: \"tried X, failed because Y\".\n- Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).\n- Repeated status checks (`git status`, `ls`) once state is known.\n\nFor each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: \"probe script at /path/probe_kvnet.py\". Good: \"probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention.\" This lets a later decompress target the right block by relevance, not by guessing locations.\n\nKEEP MARKERS: `[[KEEP:mNNNNN]]` expands original message content into the summary (truncated to a max length). Do NOT use KEEP for verbose command output, diagnostic scripts, log dumps, or any content whose value is in the conclusion rather than the raw output \u2014 summarize these or use `[[REF:mNNNNN|desc]` instead.\n\nPRIORITY \u2014 when the summary must be compact, preserve in this order:\n1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).\n2. Decisions and rationale.\n3. Exact technical artifacts: paths, signatures, errors, values.\n4. Conclusions and key findings.\n5. Lessons learned: what failed and why.\n\nWrite dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.";
7
+ export declare const COMPRESS_PHILOSOPHY = "Compression Philosophy:\n- All compression serves the primary task, but be frugal.\n- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.\n- Compress by need, not by percentage.\n- Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.";
8
+ export declare const HOW_TO_COMPRESS_RULES = "HOW TO COMPRESS\n\nWhen you call `compress`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.\n\nKEEP VERBATIM \u2014 never paraphrase or abbreviate these:\n- Full file paths with line numbers, directory prefix on every mention (`lib/hooks.ts:347`, `src/index.ts:12-18`, `gatenet_v3/model.py:45`). Never abbreviate to a bare filename (`hooks.ts`, `model.py`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.\n- Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. `kv_keys += define_gate * a_key[i](emb)` is more useful than \"see model_kvnet.py\").\n- Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).\n- Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not \"X is worse\" alone (write \"1.76\u00D7 PPL gap because KV store is static\", not \"KVNet underperforms\").\n- Decisions and their rationale (\"chose X over Y because Z\" \u2014 the \"because\" is load-bearing; without it the decision looks arbitrary).\n- Constraints discovered (\"must support Node 22\", \"no new dependencies\", \"AGENTS.md forbids `as any`\").\n- Exact values: versions, config keys, thresholds, magic numbers.\n- User intent \u2014 quote short user messages verbatim. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Mark them clearly as past quotes (e.g., \"User said: ...\"), not as current directives. Losing these changes the task itself.\n- The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., \"initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause\"). Losing the goal or its evolution makes all subsequent work appear unmotivated.\n- Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.\n- Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.\n- Message refs of key anchors (`m00420`, `m00510\u2013m00520`) \u2014 they let you or a later reader jump back via decompress to the exact original.\n\nDROP \u2014 extract the signal, discard the vessel:\n- Verbose logs (build/test/`npm` output) once you have captured the error line or the result.\n- Duplicate file reads once the needed content is recorded.\n- Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).\n- Dead-end exploration \u2014 but PRESERVE the lesson in one line: \"tried X, failed because Y\".\n- Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).\n- Repeated status checks (`git status`, `ls`) once state is known.\n\nFor each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: \"probe script at /path/probe_kvnet.py\". Good: \"probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention.\" This lets a later decompress target the right block by relevance, not by guessing locations.\n\nPRIORITY \u2014 when the summary must be compact, preserve in this order:\n1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).\n2. Decisions and rationale.\n3. Exact technical artifacts: paths, signatures, errors, values.\n4. Conclusions and key findings.\n5. Lessons learned: what failed and why.\n\nWrite dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.";
9
9
  export declare const TIER2_DISTILL_RULES = "TIER 2 COMPRESSION \u2014 DISTILLATION\n\nYou are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.\n\nKEEP \u2014 these are the only things that survive distillation:\n- Decisions and their rationale (\"chose X over Y because Z\" \u2014 the \"because\" is load-bearing).\n- Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.\n- Key lessons: what failed and why (\"tried X, failed because Y\"). These prevent repeating mistakes.\n- Critical constraints discovered (\"must support Node 22\", \"AGENTS.md forbids as any\").\n- Design decisions with architectural impact (\"chose compress-as-anchor over synthetic messages because prefix cache\").\n- Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: \"[SUPERSEDED by PR #NNN]\" or \"[OBSOLETE: deleted in vX.Y.Z]\". Do NOT keep the obsolete content's details \u2014 just the marker and reason.\n- Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., \"fixed filterCompressedRanges in prune.ts\", \"added SessionStateRegistry in state.ts\". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.\n- Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line (\"explored X, not viable because Y\"). Do not keep the exploration process.\n\nDROP \u2014 these were useful during the work but are no longer needed:\n- Exact line numbers, diffs, verbose function signatures, full code listings.\n- Build/deploy process details, test execution steps.\n- Review process details (who reviewed, what rounds, test counts).\n- Verbose logs, command output, intermediate debugging steps.\n\nFORMAT:\n- Start each distilled block with a source header line:\n `Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]`\n Example: `Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]`\n- 3-5 bullet points per source block, each a self-contained fact.\n- Dense, scannable \u2014 no narrative prose.\n- Start with the outcome, not the process: \"v1.13.0 shipped (7 PRs bundled)\" not \"implemented 7 PRs then reviewed then merged\".\n- Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.\n\nSIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by \"[no actionable content].\"";
10
10
  export declare const TIER3_CONDENSE_RULES = "TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION\n\nYou are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.\n\nPRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:\n1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.\n2. Open work (PRs/issues still pending) \u2014 these may need follow-up.\n3. Key decisions with architectural impact (\"chose X over Y because Z\").\n4. Critical constraints (\"must support Node 22\").\nDrop everything else. Tier 3 is a lookup index, not a knowledge base.\n\nFORMAT:\n- Start with a source header line:\n `Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]`\n- Output 1-3 facts per source block. Each fact is a single line: subject + outcome.\n- No explanations, no rationale, no process \u2014 just the fact.\n- Format: \"[PR/Issue/Version] \u2014 [outcome in \u22648 words]\"\n- Merge related facts from different source blocks if they concern the same topic.\n\nEXAMPLES:\n- \"v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)\"\n- \"PR #196 merged \u2014 preserve-first-user (supersedes #169)\"\n- \"Bug 1214 fixed \u2014 compress consumed all user messages\"\n- \"Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection\"\n- \"Constraint: AGENTS.md forbids as any \u2014 never suppress types\"\n\nDROP:\n- Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.\n- Lessons learned (\"tried X, failed because Y\") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.\n- Design rationale details \u2014 keep the decision, drop the \"because\" unless it's a critical constraint.\n- Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note \"[N blocks obsolete]\" in the summary.\n\nSIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \u00D7 40 tokens. If a source block has only one trivial fact, output just the header + one line.";
11
11
  //# sourceMappingURL=compression-rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compression-rules.d.ts","sourceRoot":"","sources":["../src/compression-rules.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,mBAAmB,64BAKuU,CAAC;AAExW,eAAO,MAAM,qBAAqB,qgKAqC8R,CAAC;AAEjU,eAAO,MAAM,mBAAmB,mxFA6ByN,CAAC;AAE1P,eAAO,MAAM,oBAAoB,0nEAgC4K,CAAC"}
1
+ {"version":3,"file":"compression-rules.d.ts","sourceRoot":"","sources":["../src/compression-rules.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,mBAAmB,giBAIqR,CAAC;AAEtT,eAAO,MAAM,qBAAqB,isJAmC8R,CAAC;AAEjU,eAAO,MAAM,mBAAmB,mxFA6ByN,CAAC;AAE1P,eAAO,MAAM,oBAAoB,0nEAgC4K,CAAC"}
package/dist/index.d.ts CHANGED
@@ -14,8 +14,6 @@ export type { NudgeVoice, RenderedNudge } from "./nudge-text.js";
14
14
  export { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES, TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES } from "./compression-rules.js";
15
15
  export { truncateLargeToolOutputs } from "./truncate-tools.js";
16
16
  export type { TruncateOptions, TruncateResult } from "./truncate-tools.js";
17
- export { resolveKeepMarkers } from "./keep-markers.js";
18
- export type { KeepMarkerResult } from "./keep-markers.js";
19
17
  export { parseBlockIdArg, findBlocksOverlappingMessages, findActiveAncestor, deactivateBlock, buildRestoredContentPreview, collectBlockContent, } from "./decompress.js";
20
18
  export type { DeactivateOptions, CollectedContentResult, CollectContentOptions } from "./decompress.js";
21
19
  export { buildStatusReport, buildRecap } from "./report.js";
@@ -25,8 +23,9 @@ export type { HideConsumedResult } from "./hide-consumed.js";
25
23
  export { rebuildCompressionState } from "./rebuild.js";
26
24
  export type { RebuildResult, RebuildPorts } from "./rebuild.js";
27
25
  export { renderVisibleRefs, renderRefsNode } from "./render-refs.js";
28
- export { searchBlocks } from "./search.js";
29
- export type { SearchResult, SearchOptions } from "./search.js";
26
+ export { searchBlocks, searchBlocksAsync, blockDocs, messageDocs } from "./search.js";
27
+ export type { SearchResult, SearchOptions, SearchAlgorithm, AsyncSearchAlgorithm, AnySearchAlgorithm, SearchDoc, SearchDocKind, ScoredBlock, MessageRole, RoleWeights, MessageInput } from "./search.js";
28
+ export { DEFAULT_ALGORITHM, DEFAULT_ROLE_WEIGHTS, registerSearchAlgorithm, getSearchAlgorithm, listSearchAlgorithms } from "./search.js";
30
29
  export { isMessageProtected, matchToolPattern } from "./protected.js";
31
30
  export { runPipeline, makeIO, type PipelineNode, type PipelineContext, type NodeIO, type NodeEffects, } from "./pipeline.js";
32
31
  export * from "./filter/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EACV,KAAK,EACL,eAAe,EACf,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC3F,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC/H,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EACH,eAAe,EACf,6BAA6B,EAC7B,kBAAkB,EAClB,eAAe,EACf,2BAA2B,EAC3B,mBAAmB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,WAAW,EACX,MAAM,EACN,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,WAAW,GACjB,MAAM,eAAe,CAAC;AACvB,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EACV,KAAK,EACL,eAAe,EACf,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC3F,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC/H,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EACH,eAAe,EACf,6BAA6B,EAC7B,kBAAkB,EAClB,eAAe,EACf,2BAA2B,EAC3B,mBAAmB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtF,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzM,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACzI,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,WAAW,EACX,MAAM,EACN,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,WAAW,GACjB,MAAM,eAAe,CAAC;AACvB,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -706,7 +706,7 @@ var renderRefsNode = {
706
706
 
707
707
  // src/protected.ts
708
708
  var ALWAYS_PROTECTED_TOOLS = ["compress"];
709
- var NEVER_PRESERVE_RECENT_TOOLS = ["decompress"];
709
+ var NEVER_PRESERVE_RECENT_TOOLS = ["decompress", "search_context"];
710
710
  function isNeverPreserveRecent(msg) {
711
711
  if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result") {
712
712
  return false;
@@ -1300,6 +1300,12 @@ function applySingleRange(input) {
1300
1300
  input.messages,
1301
1301
  input.config
1302
1302
  );
1303
+ if (filteredIds.length < directMessageIds.length) {
1304
+ const kept = new Set(filteredIds);
1305
+ for (const id of directMessageIds) {
1306
+ if (!kept.has(id)) effectiveMessageIds.delete(id);
1307
+ }
1308
+ }
1303
1309
  const protectedRefs = input.protectedMessageIds;
1304
1310
  const hitProtectedRaw = protectedRefs ? filteredIds.filter((id) => {
1305
1311
  const ref = input.state.messageRefs.byRaw[id];
@@ -1614,8 +1620,7 @@ var COMPRESS_PHILOSOPHY = `Compression Philosophy:
1614
1620
  - All compression serves the primary task, but be frugal.
1615
1621
  - Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
1616
1622
  - Compress by need, not by percentage.
1617
- - Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.
1618
- - Curate summaries like a well-structured document. User prompts, compressed tool outputs, code, logs, or skill-call intermediate results that are critically important should be preserved \u2014 not by exempting them from compression, but by embedding them in the summary via [[KEEP:mNNNNN]] (auto-expanded verbatim) and [[REF:mNNNNN|description]] (compact link).`;
1623
+ - Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
1619
1624
  var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
1620
1625
 
1621
1626
  When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.
@@ -1644,8 +1649,6 @@ DROP \u2014 extract the signal, discard the vessel:
1644
1649
 
1645
1650
  For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
1646
1651
 
1647
- KEEP MARKERS: \`[[KEEP:mNNNNN]]\` expands original message content into the summary (truncated to a max length). Do NOT use KEEP for verbose command output, diagnostic scripts, log dumps, or any content whose value is in the conclusion rather than the raw output \u2014 summarize these or use \`[[REF:mNNNNN|desc]\` instead.
1648
-
1649
1652
  PRIORITY \u2014 when the summary must be compact, preserve in this order:
1650
1653
  1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
1651
1654
  2. Decisions and rationale.
@@ -1893,65 +1896,6 @@ function renderNudgeText(decision) {
1893
1896
  };
1894
1897
  }
1895
1898
 
1896
- // src/keep-markers.ts
1897
- var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
1898
- var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
1899
- function resolveKeepMarkers(summary, messages, state, maxChars = 2e3) {
1900
- const messageByRef = /* @__PURE__ */ new Map();
1901
- for (const message of messages) {
1902
- const ref = refForRaw(state.messageRefs, message.id);
1903
- if (ref) messageByRef.set(ref, message);
1904
- }
1905
- let expandedCount = 0;
1906
- let refCount = 0;
1907
- const unresolvedRefs = [];
1908
- const expanded = summary.replace(KEEP_REGEX, (match, ref) => {
1909
- const normalized = normalizeRef(ref);
1910
- const message = normalized ? messageByRef.get(normalized) : void 0;
1911
- if (!message) {
1912
- unresolvedRefs.push(ref);
1913
- return match;
1914
- }
1915
- expandedCount++;
1916
- return formatKeptMessage(message, normalized, maxChars);
1917
- }).replace(REF_REGEX, (_match, ref, desc) => {
1918
- const normalized = normalizeRef(ref);
1919
- const message = normalized ? messageByRef.get(normalized) : void 0;
1920
- if (!message) {
1921
- unresolvedRefs.push(ref);
1922
- return _match;
1923
- }
1924
- refCount++;
1925
- return `[\u2192 ${normalized}: ${desc.trim()}]`;
1926
- });
1927
- return { summary: expanded, expandedCount, refCount, unresolvedRefs };
1928
- }
1929
- function normalizeRef(ref) {
1930
- const match = /^m0*(\d{1,5})$/.exec(ref.trim().toLowerCase());
1931
- if (!match || match[1] === void 0) return null;
1932
- return `m${match[1].padStart(5, "0")}`;
1933
- }
1934
- function formatKeptMessage(message, ref, maxChars) {
1935
- const label = labelFor(message);
1936
- const body = truncate(message.text ?? "[empty message]", maxChars);
1937
- return `
1938
- --- [${ref}: ${label}] ---
1939
- ${body}
1940
- --- end ---
1941
- `;
1942
- }
1943
- function labelFor(message) {
1944
- if (message.contentType === "tool-call" || message.contentType === "tool-result") {
1945
- return message.toolName ?? "tool";
1946
- }
1947
- return message.role;
1948
- }
1949
- function truncate(text, maxChars) {
1950
- if (text.length <= maxChars) return text;
1951
- return text.slice(0, maxChars) + `
1952
- ... [truncated, ${text.length} chars total]`;
1953
- }
1954
-
1955
1899
  // src/decompress.ts
1956
1900
  function parseBlockIdArg(arg) {
1957
1901
  const normalized = arg.trim().toLowerCase();
@@ -2394,44 +2338,291 @@ function extractRanges(input, callId) {
2394
2338
  return ranges;
2395
2339
  }
2396
2340
 
2397
- // src/search.ts
2398
- function searchBlocks(state, query, options = {}) {
2341
+ // src/search/algorithms/substring.ts
2342
+ var substringAlgorithm = {
2343
+ name: "substring",
2344
+ description: "Exact substring counting (original baseline). Predictable, no normalization.",
2345
+ score(docs, query) {
2346
+ const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 0);
2347
+ if (terms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
2348
+ return docs.map((d) => {
2349
+ const haystack = d.text.toLowerCase();
2350
+ let score = 0;
2351
+ for (const term of terms) score += countOccurrences2(haystack, term);
2352
+ return { ref: d.ref, score };
2353
+ });
2354
+ }
2355
+ };
2356
+ function countOccurrences2(haystack, needle) {
2357
+ if (!needle) return 0;
2358
+ return haystack.split(needle).length - 1;
2359
+ }
2360
+
2361
+ // src/search/stemmer.ts
2362
+ function stem(word) {
2363
+ let w = word;
2364
+ if (w.length <= 3) return w;
2365
+ if (w.endsWith("ies")) w = w.slice(0, -3) + "y";
2366
+ else if (w.endsWith("ses") || w.endsWith("xes") || w.endsWith("zes")) w = w.slice(0, -2);
2367
+ else if (w.endsWith("ches") || w.endsWith("shes")) w = w.slice(0, -2);
2368
+ else if (w.endsWith("s") && !w.endsWith("ss")) w = w.slice(0, -1);
2369
+ if (w.endsWith("ing") && w.length > 5) w = w.slice(0, -3);
2370
+ if (w.endsWith("ed") && w.length > 4) w = w.slice(0, -2);
2371
+ if (w.endsWith("ation") && w.length > 6) w = w.slice(0, -3);
2372
+ else if (w.endsWith("tion") && w.length > 5) w = w.slice(0, -4) + "t";
2373
+ else if (w.endsWith("ion") && w.length > 4) w = w.slice(0, -3);
2374
+ if (w.endsWith("ment") && w.length > 6) w = w.slice(0, -4);
2375
+ if (w.endsWith("ness") && w.length > 6) w = w.slice(0, -4);
2376
+ if (w.endsWith("ly") && w.length > 4) w = w.slice(0, -2);
2377
+ return w;
2378
+ }
2379
+
2380
+ // src/search/tokenizer.ts
2381
+ var CJK = /[\u3400-\u9fff\uf900-\ufaff\u3040-\u30ff\uac00-\ud7af]/;
2382
+ var CJK_RUN = new RegExp(`${CJK.source}+`, "g");
2383
+ var LATIN_WORD = /[a-z][a-z0-9_]*[a-z0-9]|[a-z0-9]/g;
2384
+ function tokenize(text, opts = {}) {
2385
+ const lower = text.toLowerCase();
2386
+ const tokens = [];
2387
+ const latin = lower.match(LATIN_WORD) ?? [];
2388
+ for (let w of latin) {
2389
+ if (w.length >= 2) {
2390
+ if (opts.stem) w = stem(w);
2391
+ tokens.push(w);
2392
+ }
2393
+ }
2394
+ const cjkRuns = lower.match(CJK_RUN) ?? [];
2395
+ for (const run of cjkRuns) {
2396
+ if (run.length === 1) {
2397
+ tokens.push(run);
2398
+ } else {
2399
+ for (let i = 0; i < run.length - 1; i++) tokens.push(run.slice(i, i + 2));
2400
+ for (const ch of run) tokens.push(ch);
2401
+ }
2402
+ }
2403
+ return tokens;
2404
+ }
2405
+ function charBigrams(text) {
2406
+ const grams = [];
2407
+ for (let i = 0; i < text.length - 1; i++) {
2408
+ const pair = text.slice(i, i + 2);
2409
+ if (pair.trim().length === pair.length) grams.push(pair);
2410
+ }
2411
+ return grams;
2412
+ }
2413
+ function tfMap(text, stem2) {
2414
+ const m = /* @__PURE__ */ new Map();
2415
+ for (const t of tokenize(text, { stem: stem2 })) m.set(t, (m.get(t) ?? 0) + 1);
2416
+ return m;
2417
+ }
2418
+
2419
+ // src/search/algorithms/bm25.ts
2420
+ var bm25Algorithm = {
2421
+ name: "bm25",
2422
+ description: "BM25 with stemming + CJK bigram tokenization. IR-standard relevance ranking.",
2423
+ score(docs, query) {
2424
+ const N = docs.length;
2425
+ const k1 = 1.2;
2426
+ const b = 0.75;
2427
+ const parsed = docs.map((d) => {
2428
+ const text = d.text;
2429
+ const tf = tfMap(text, true);
2430
+ let len = 0;
2431
+ for (const v of tf.values()) len += v;
2432
+ return { id: d.ref, tf, len };
2433
+ });
2434
+ const avgdl = parsed.reduce((s, d) => s + d.len, 0) / (N || 1);
2435
+ const qTerms = tokenize(query, { stem: true });
2436
+ if (qTerms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
2437
+ const idf = /* @__PURE__ */ new Map();
2438
+ for (const t of new Set(qTerms)) {
2439
+ let df = 0;
2440
+ for (const d of parsed) if (d.tf.has(t)) df++;
2441
+ idf.set(t, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
2442
+ }
2443
+ return parsed.map((d) => {
2444
+ let score = 0;
2445
+ for (const t of qTerms) {
2446
+ const f = d.tf.get(t) ?? 0;
2447
+ if (f === 0) continue;
2448
+ const idfT = idf.get(t) ?? 0;
2449
+ score += idfT * (f * (k1 + 1)) / (f + k1 * (1 - b + b * d.len / (avgdl || 1)));
2450
+ }
2451
+ return { ref: d.id, score };
2452
+ });
2453
+ }
2454
+ };
2455
+
2456
+ // src/search/algorithms/fuzzy.ts
2457
+ var fuzzyAlgorithm = {
2458
+ name: "fuzzy",
2459
+ description: "Character bigram overlap. Typo-tolerant, script-agnostic, high recall.",
2460
+ score(docs, query) {
2461
+ const qTokens = query.toLowerCase().split(/[\s,]+/).filter((t) => t.length >= 4);
2462
+ if (qTokens.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
2463
+ const qGrams = /* @__PURE__ */ new Set();
2464
+ for (const t of qTokens) for (const g of charBigrams(t)) qGrams.add(g);
2465
+ if (qGrams.size === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
2466
+ return docs.map((d) => {
2467
+ const haystack = d.text.toLowerCase();
2468
+ const docGrams = new Set(charBigrams(haystack));
2469
+ let hits = 0;
2470
+ for (const g of qGrams) if (docGrams.has(g)) hits++;
2471
+ return { ref: d.ref, score: hits / qGrams.size };
2472
+ });
2473
+ }
2474
+ };
2475
+
2476
+ // src/search/algorithms/hybrid.ts
2477
+ var W_BM25 = 0.7;
2478
+ var W_FUZZY = 0.3;
2479
+ var hybridAlgorithm = {
2480
+ name: "hybrid",
2481
+ description: "Weighted BM25(stem) + fuzzy n-gram. Default \u2014 best precision + recall.",
2482
+ score(docs, query) {
2483
+ const bm = bm25Algorithm.score(docs, query);
2484
+ const fz = fuzzyAlgorithm.score(docs, query);
2485
+ const maxBm = Math.max(...bm.map((r) => r.score), 1e-9);
2486
+ const maxFz = Math.max(...fz.map((r) => r.score), 1e-9);
2487
+ const bmMap = new Map(bm.map((r) => [r.ref, r.score / maxBm]));
2488
+ const fzMap = new Map(fz.map((r) => [r.ref, r.score / maxFz]));
2489
+ return docs.map((d) => ({
2490
+ ref: d.ref,
2491
+ score: W_BM25 * (bmMap.get(d.ref) ?? 0) + W_FUZZY * (fzMap.get(d.ref) ?? 0)
2492
+ }));
2493
+ }
2494
+ };
2495
+
2496
+ // src/search/registry.ts
2497
+ var registry2 = /* @__PURE__ */ new Map();
2498
+ function registerSearchAlgorithm(algo) {
2499
+ registry2.set(algo.name, algo);
2500
+ }
2501
+ function getSearchAlgorithm(name) {
2502
+ return registry2.get(name);
2503
+ }
2504
+ function listSearchAlgorithms() {
2505
+ return [...registry2.values()];
2506
+ }
2507
+ registerSearchAlgorithm(substringAlgorithm);
2508
+ registerSearchAlgorithm(bm25Algorithm);
2509
+ registerSearchAlgorithm(fuzzyAlgorithm);
2510
+ registerSearchAlgorithm(hybridAlgorithm);
2511
+
2512
+ // src/search/types.ts
2513
+ var DEFAULT_ROLE_WEIGHTS = {
2514
+ user: 1.5,
2515
+ assistant: 1,
2516
+ tool: 0.6,
2517
+ block: 1
2518
+ };
2519
+ var DEFAULT_ALGORITHM = "hybrid";
2520
+
2521
+ // src/search/index.ts
2522
+ function blockDocs(state) {
2523
+ return state.blocks.map((b) => ({
2524
+ kind: "block",
2525
+ ref: b.blockId,
2526
+ text: `${b.topic ?? ""} ${b.summary ?? ""}`,
2527
+ title: b.topic ?? b.blockId,
2528
+ blockId: b.blockId,
2529
+ tier: b.tier ?? 1,
2530
+ tokens: b.compressedTokens
2531
+ }));
2532
+ }
2533
+ function messageDocs(msgs) {
2534
+ return msgs.map((m) => ({
2535
+ kind: "message",
2536
+ ref: m.ref,
2537
+ text: m.text,
2538
+ title: `${m.role}: ${m.text.slice(0, 60)}`,
2539
+ role: m.role,
2540
+ blockId: m.blockId,
2541
+ tier: m.tier,
2542
+ tokens: m.tokens
2543
+ }));
2544
+ }
2545
+ function applyRoleWeight(scored, docs, rw) {
2546
+ if (docs.length === 0) return scored;
2547
+ const docByRef = new Map(docs.map((d) => [d.ref, d]));
2548
+ return scored.map((s) => {
2549
+ const doc = docByRef.get(s.ref);
2550
+ if (!doc) return s;
2551
+ const w = doc.kind === "message" ? doc.role === "user" ? rw.user : doc.role === "assistant" ? rw.assistant : rw.tool : rw.block;
2552
+ return { ref: s.ref, score: s.score * w };
2553
+ });
2554
+ }
2555
+ function runSearch(docs, query, options) {
2399
2556
  const limit = options.limit ?? 10;
2400
2557
  const previewLength = options.previewLength ?? 200;
2401
- const minScore = options.minScore ?? 1;
2402
- const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 0);
2403
- if (terms.length === 0) return [];
2404
- const active = state.blocks.filter((b) => b.active);
2405
- const scored = [];
2406
- for (const block of active) {
2407
- const text = (block.summary || "").toLowerCase();
2408
- const topic = (block.topic || "").toLowerCase();
2409
- const haystack = topic + " " + text;
2410
- let score = 0;
2411
- for (const term of terms) {
2412
- score += countOccurrences2(haystack, term);
2413
- }
2414
- if (score >= minScore) {
2415
- scored.push({
2416
- blockId: block.blockId,
2417
- tier: block.tier ?? 1,
2418
- score,
2419
- topic: block.topic ?? "(no topic)",
2420
- preview: (block.summary || "").slice(0, previewLength),
2421
- block
2422
- });
2423
- }
2558
+ const minScore = options.minScore ?? 0.01;
2559
+ const algoName = options.algorithm ?? DEFAULT_ALGORITHM;
2560
+ const rw = { ...DEFAULT_ROLE_WEIGHTS, ...options.roleWeights };
2561
+ const algo = getSearchAlgorithm(algoName);
2562
+ if (!algo) return [];
2563
+ if (docs.length === 0) return [];
2564
+ const scoredOrPromise = algo.score(docs, query);
2565
+ const buildResults = (weighted) => {
2566
+ const byRef = new Map(docs.map((d) => [d.ref, d]));
2567
+ return weighted.map((s) => {
2568
+ const doc = byRef.get(s.ref);
2569
+ if (!doc) return null;
2570
+ return {
2571
+ kind: doc.kind,
2572
+ ref: doc.ref,
2573
+ blockId: doc.blockId,
2574
+ tier: doc.tier ?? 1,
2575
+ score: s.score,
2576
+ title: doc.title,
2577
+ preview: makePreview(doc.text, query, previewLength),
2578
+ role: doc.role,
2579
+ tokens: doc.tokens
2580
+ };
2581
+ }).filter((r) => r !== null && r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
2582
+ };
2583
+ if (scoredOrPromise instanceof Promise) {
2584
+ return scoredOrPromise.then((raw) => buildResults(applyRoleWeight(raw, docs, rw)));
2424
2585
  }
2425
- scored.sort((a, b) => b.score - a.score);
2426
- return scored.slice(0, limit);
2586
+ return buildResults(applyRoleWeight(scoredOrPromise, docs, rw));
2427
2587
  }
2428
- function countOccurrences2(haystack, needle) {
2429
- if (needle.length === 0) return 0;
2430
- return haystack.split(needle).length - 1;
2588
+ function searchBlocks(docs, query, options = {}) {
2589
+ const result = runSearch(docs, query, options);
2590
+ if (result instanceof Promise) {
2591
+ throw new Error(
2592
+ `searchBlocks: algorithm "${options.algorithm ?? DEFAULT_ALGORITHM}" is async (e.g. semantic). Use searchBlocksAsync() instead.`
2593
+ );
2594
+ }
2595
+ return result;
2596
+ }
2597
+ async function searchBlocksAsync(docs, query, options = {}) {
2598
+ return await runSearch(docs, query, options);
2599
+ }
2600
+ function makePreview(text, query, len) {
2601
+ if (!text) return "";
2602
+ const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 1);
2603
+ if (terms.length === 0) return text.slice(0, len);
2604
+ const lower = text.toLowerCase();
2605
+ let hitIdx = -1;
2606
+ for (const term of terms) {
2607
+ const idx = lower.indexOf(term);
2608
+ if (idx >= 0) {
2609
+ hitIdx = idx;
2610
+ break;
2611
+ }
2612
+ }
2613
+ if (hitIdx < 0) return text.slice(0, len);
2614
+ const half = Math.max(0, Math.floor(len / 2) - 10);
2615
+ const start = Math.max(0, hitIdx - half);
2616
+ const end = Math.min(text.length, start + len);
2617
+ const prefix = start > 0 ? "\u2026" : "";
2618
+ const suffix = end < text.length ? "\u2026" : "";
2619
+ return prefix + text.slice(start, end).trim() + suffix;
2431
2620
  }
2432
2621
  export {
2433
2622
  BLOCKED_REF,
2434
2623
  COMPRESS_PHILOSOPHY,
2624
+ DEFAULT_ALGORITHM,
2625
+ DEFAULT_ROLE_WEIGHTS,
2435
2626
  HOW_TO_COMPRESS_RULES,
2436
2627
  SUMMARY_HEADER,
2437
2628
  TIER2_DISTILL_RULES,
@@ -2443,6 +2634,7 @@ export {
2443
2634
  applyMessageFilters,
2444
2635
  assignRefs,
2445
2636
  blockById,
2637
+ blockDocs,
2446
2638
  buildRecap,
2447
2639
  buildRestoredContentPreview,
2448
2640
  buildStatusReport,
@@ -2461,14 +2653,17 @@ export {
2461
2653
  findBlocksOverlappingMessages,
2462
2654
  formatRanges,
2463
2655
  getMessageFilter,
2656
+ getSearchAlgorithm,
2464
2657
  hideConsumedCompressCalls,
2465
2658
  highestActiveTier,
2466
2659
  highestUsedIndex,
2467
2660
  indexToRef,
2468
2661
  isMessageProtected,
2469
2662
  listMessageFilters,
2663
+ listSearchAlgorithms,
2470
2664
  makeIO,
2471
2665
  matchToolPattern,
2666
+ messageDocs,
2472
2667
  parseBlockIdArg,
2473
2668
  parseBoundary,
2474
2669
  prune,
@@ -2477,13 +2672,14 @@ export {
2477
2672
  refForRaw,
2478
2673
  refToIndex,
2479
2674
  registerMessageFilter,
2675
+ registerSearchAlgorithm,
2480
2676
  renderNudgeText,
2481
2677
  renderRefsNode,
2482
2678
  renderVisibleRefs,
2483
2679
  resolveBoundaries,
2484
- resolveKeepMarkers,
2485
2680
  runPipeline,
2486
2681
  searchBlocks,
2682
+ searchBlocksAsync,
2487
2683
  syncBlocks,
2488
2684
  truncateLargeToolOutputs,
2489
2685
  validateConfig