acp-kernel 0.0.63-pr.249.95 → 0.0.63-pr.250.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-6TAK7DSI.js → chunk-T6YQEHNL.js} +37 -52
- package/dist/chunk-T6YQEHNL.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/nudge-text.d.ts +16 -1
- package/dist/nudge-text.d.ts.map +1 -1
- package/dist/panel/index.js +1 -1
- package/dist/persist/index.d.ts +1 -1
- package/dist/persist/index.d.ts.map +1 -1
- package/dist/persist/index.js +7 -18
- package/dist/persist/index.js.map +1 -1
- package/dist/persist/store.d.ts +0 -26
- package/dist/persist/store.d.ts.map +1 -1
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6TAK7DSI.js.map +0 -1
|
@@ -169,15 +169,17 @@ function resolvePrompts(overrides, options = {}) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// src/nudge-text.ts
|
|
172
|
-
function efficiencyNote(prompts) {
|
|
173
|
-
|
|
172
|
+
function efficiencyNote(prompts, includeGuidance) {
|
|
173
|
+
const note = "This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.";
|
|
174
|
+
return includeGuidance ? `${note}
|
|
174
175
|
|
|
175
|
-
${prompts.compressPhilosophy}
|
|
176
|
+
${prompts.compressPhilosophy}` : note;
|
|
176
177
|
}
|
|
177
|
-
function emergencyHeader(prompts) {
|
|
178
|
-
|
|
178
|
+
function emergencyHeader(prompts, includeGuidance) {
|
|
179
|
+
const header = "\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.";
|
|
180
|
+
return includeGuidance ? `${header}
|
|
179
181
|
|
|
180
|
-
${prompts.compressPhilosophy}
|
|
182
|
+
${prompts.compressPhilosophy}` : header;
|
|
181
183
|
}
|
|
182
184
|
function formatK(n) {
|
|
183
185
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
@@ -286,7 +288,8 @@ function formatRanges(compressible, protectedRanges) {
|
|
|
286
288
|
return `Compressible ranges (${merged.length}, oldest first):
|
|
287
289
|
${lines.join("\n")}`;
|
|
288
290
|
}
|
|
289
|
-
function renderNudgeText(decision, prompts = defaultPrompts) {
|
|
291
|
+
function renderNudgeText(decision, prompts = defaultPrompts, options = {}) {
|
|
292
|
+
const includeGuidance = options.includeGuidance ?? true;
|
|
290
293
|
const breakdownStr = formatBreakdown(decision.contextBreakdown);
|
|
291
294
|
const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
|
|
292
295
|
const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
|
|
@@ -298,55 +301,37 @@ function renderNudgeText(decision, prompts = defaultPrompts) {
|
|
|
298
301
|
const endId = targets[targets.length - 1]?.blockId ?? "b5";
|
|
299
302
|
const voice = isEmergency ? "emergency" : "gentle";
|
|
300
303
|
const triggerLine = isEmergency ? `[EMERGENCY \u2014 TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"}] Context limit reached \u2014 distill NOW into a denser summary to reclaim tokens.` : `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
text: [
|
|
304
|
-
efficiencyNote(prompts),
|
|
305
|
-
"",
|
|
306
|
-
breakdownStr,
|
|
307
|
-
"",
|
|
308
|
-
triggerLine,
|
|
309
|
-
isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
|
|
310
|
-
blockList,
|
|
311
|
-
`Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
|
|
312
|
-
"",
|
|
313
|
-
prompts.howToCompressRules,
|
|
314
|
-
"",
|
|
315
|
-
isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules
|
|
316
|
-
].join("\n")
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
if (isEmergency) {
|
|
320
|
-
return {
|
|
321
|
-
voice: "emergency",
|
|
322
|
-
text: [
|
|
323
|
-
emergencyHeader(prompts),
|
|
324
|
-
"",
|
|
325
|
-
breakdownStr,
|
|
326
|
-
"",
|
|
327
|
-
prompts.howToCompressRules,
|
|
328
|
-
"",
|
|
329
|
-
`{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
|
|
330
|
-
"Only use IDs from visible messages above. Compress older work first.",
|
|
331
|
-
"",
|
|
332
|
-
rangesStr
|
|
333
|
-
].join("\n")
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
return {
|
|
337
|
-
voice: "gentle",
|
|
338
|
-
text: [
|
|
339
|
-
efficiencyNote(prompts),
|
|
304
|
+
const parts2 = [
|
|
305
|
+
efficiencyNote(prompts, includeGuidance),
|
|
340
306
|
"",
|
|
341
307
|
breakdownStr,
|
|
342
308
|
"",
|
|
343
|
-
|
|
309
|
+
triggerLine,
|
|
310
|
+
isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
|
|
311
|
+
blockList,
|
|
312
|
+
`Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`
|
|
313
|
+
];
|
|
314
|
+
if (includeGuidance) {
|
|
315
|
+
parts2.push("", prompts.howToCompressRules, "", isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules);
|
|
316
|
+
}
|
|
317
|
+
return { voice, text: parts2.join("\n") };
|
|
318
|
+
}
|
|
319
|
+
if (isEmergency) {
|
|
320
|
+
const parts2 = [emergencyHeader(prompts, includeGuidance), "", breakdownStr];
|
|
321
|
+
if (includeGuidance) parts2.push("", prompts.howToCompressRules);
|
|
322
|
+
parts2.push(
|
|
344
323
|
"",
|
|
345
|
-
|
|
324
|
+
`{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
|
|
325
|
+
"Only use IDs from visible messages above. Compress older work first.",
|
|
346
326
|
"",
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
327
|
+
rangesStr
|
|
328
|
+
);
|
|
329
|
+
return { voice: "emergency", text: parts2.join("\n") };
|
|
330
|
+
}
|
|
331
|
+
const parts = [efficiencyNote(prompts, includeGuidance), "", breakdownStr];
|
|
332
|
+
if (includeGuidance) parts.push("", prompts.howToCompressRules);
|
|
333
|
+
parts.push("", rangesStr, "", `\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`);
|
|
334
|
+
return { voice: "gentle", text: parts.join("\n") };
|
|
350
335
|
}
|
|
351
336
|
|
|
352
337
|
// src/viable.ts
|
|
@@ -372,4 +357,4 @@ export {
|
|
|
372
357
|
VIABLE_RANGE_MIN_TOKENS,
|
|
373
358
|
viableRanges
|
|
374
359
|
};
|
|
375
|
-
//# sourceMappingURL=chunk-
|
|
360
|
+
//# sourceMappingURL=chunk-T6YQEHNL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tokenize.ts","../src/compression-rules.ts","../src/prompts.ts","../src/nudge-text.ts","../src/viable.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\n\nexport function defaultCountTokens(text: string): number {\n if (!text) return 0;\n // CJK chars tokenize ~1:1 (chars/4 badly underestimates them). Count them\n // directly, then estimate the non-CJK remainder with chars/4 so digits,\n // punctuation, and symbols in code/JSON are not dropped to zero.\n const cjk = text.match(/[\\u4e00-\\u9fff\\u3040-\\u30ff\\uac00-\\ud7af]/g);\n const cjkCount = cjk?.length ?? 0;\n return cjkCount + Math.ceil((text.length - cjkCount) / 4);\n}\n\nexport function estimateMessageTokens(text: string | undefined): number {\n return defaultCountTokens(text ?? \"\");\n}\n\n/** Guarded contribution of a host-projected thinking payload to the metered\n * message size. Non-finite or non-positive values are treated as absent (0),\n * so a bad host value can never poison the accounting. */\nexport function thinkingTokenValue(thinking: number | undefined): number {\n return typeof thinking === \"number\" &&\n Number.isFinite(thinking) &&\n thinking > 0\n ? thinking\n : 0;\n}\n\n/** Total metered size of a core message: visible text plus any host-projected\n * thinking payload (CoreMessage.thinkingTokens). Every per-message counting\n * site (range recommendations, block compressedTokens, status reports, context\n * breakdown) goes through this so all surfaces share one caliber. */\nexport function countMessageTokens(\n message: { text?: string; thinkingTokens?: number },\n countTokens: TokenCountFn = defaultCountTokens,\n): number {\n return (\n countTokens(message.text ?? \"\") + thinkingTokenValue(message.thinkingTokens)\n );\n}\n\nexport function estimateTokensFast(text: string): number {\n if (!text) return 0;\n return Math.ceil(text.length / 4);\n}\n\nexport type TokenCountFn = (text: string) => number;\n\nconst BPE_SIZE_GUARD = 100_000;\n\nexport function createBpeTokenizer(): TokenCountFn {\n try {\n const mod = require(\"@anthropic-ai/tokenizer\");\n const bpeCount = mod.countTokens ?? mod.default?.countTokens;\n if (typeof bpeCount !== \"function\") return defaultCountTokens;\n return (text: string) => {\n if (text.length > BPE_SIZE_GUARD) return defaultCountTokens(text);\n try {\n return bpeCount(text);\n } catch {\n return defaultCountTokens(text);\n }\n };\n } catch {\n return defaultCountTokens;\n }\n}\n","/**\n * Compression rule texts — VERBATIM copy from context-compress-algorithms (MIT, ours).\n * These were tuned over months of production use.\n *\n * 2026-09-06 amendment (owner-approved; billion-context-pi#309 incident): write-side\n * quote-fidelity rules merged INTO the tuned text. Verbatim user quotes now require\n * a message ref; recorded task state is labeled as history; unicode written directly.\n * Motivation: session 01a071dc blocks b60/b61 stored a fabricated\n * `user verbatim '合并了 下一个'` as a live \"CURRENT TASK\", causing loop relapses.\n */\n\nexport 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 — the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;\n\nexport 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. The summary records the PAST as of this block's creation: label recorded task state as history (\"TASK AS OF THIS BLOCK: ...\") — never as a live instruction, so a later reader treats it as settled context, not something to re-execute. Write plain text with real unicode characters; never copy \\\\uXXXX escape sequences or JSON-escaped fragments out of tool output.\n\nKEEP VERBATIM — 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\\`) — 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 — 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 — you need the literal string to grep for it later).\n- Key details from reports and analyses — not just the conclusion. Keep the comparison numbers and the mechanism, not \"X is worse\" alone (write \"1.76× PPL gap because KV store is static\", not \"KVNet underperforms\").\n- Decisions and their rationale (\"chose X over Y because Z\" — 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 — quote short user messages verbatim ONLY WITH their message ref, e.g. \\`User said (m00132): \"ship it tonight\"\\`. Without a verifiable ref, paraphrase (\\`user previously asked (paraphrased): ...\\`) — this is the one exception to the verbatim rule above; never present a reconstructed or half-remembered phrase as a verbatim quote. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Quotes are historical records, never current directives. Losing these changes the task itself.\n- The user's overall goal and any changes to it — 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 → 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 — 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 — losing these changes what work appears to remain.\n- Message refs of key anchors (\\`m00420\\`, \\`m00510–m00520\\`) — they let you or a later reader jump back via decompress to the exact original.\n\nDROP — 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 — search hits, agent return values, successful tool outputs — 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 — 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 — 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 — 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 — not narrative prose. If the range spans distinct concerns (request → findings → 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.`;\n\nexport const TIER2_DISTILL_RULES = `TIER 2 COMPRESSION — 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 — these are the only things that survive distillation:\n- Decisions and their rationale (\"chose X over Y because Z\" — 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- User quotes and task state only as attributed history: keep the source ref with any user quote; never carry a tier-1 \"CURRENT TASK\" claim forward as a live directive — relabel it \"TASK AS OF THIS BLOCK\".\n- Whether content is OBSOLETE or SUPERSEDED — mark with one line: \"[SUPERSEDED by PR #NNN]\" or \"[OBSOLETE: deleted in vX.Y.Z]\". Do NOT keep the obsolete content's details — just the marker and reason.\n- Function/class/type names and module paths that are the SUBJECT of the work — e.g., \"fixed filterCompressedRanges in prune.ts\", \"added SessionStateRegistry in state.ts\". Not exact line numbers or full signatures — 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 — 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→YK tok, Zx). [original topic]\\`\n Example: \\`Source: b5+b7 (56K+44K→268 tok, 375x). [Tool-result recap + publish]\\`\n- 3-5 bullet points per source block, each a self-contained fact.\n- Dense, scannable — 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 — 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].\"`;\n\nexport const TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION — 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 — when a source block has more facts than the size target allows, keep in this order:\n1. Shipped outcomes (versions released, PRs merged) — these are permanent record.\n2. Open work (PRs/issues still pending) — 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→YK 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 — just the fact.\n- Format: \"[PR/Issue/Version] — [outcome in ≤8 words]\"\n- Merge related facts from different source blocks if they concern the same topic.\n\nEXAMPLES:\n- \"v1.13.0 shipped — quality gate + GC fix (7 PRs)\"\n- \"PR #196 merged — preserve-first-user (supersedes #169)\"\n- \"Bug 1214 fixed — compress consumed all user messages\"\n- \"Chose compress-as-anchor — prefix cache benefit over synthetic injection\"\n- \"Constraint: AGENTS.md forbids as any — 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\") — drop UNLESS the failure is likely to recur and the block is <30 days old.\n- Design rationale details — keep the decision, drop the \"because\" unless it's a critical constraint.\n- Anything marked [OBSOLETE] or [SUPERSEDED] — 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 ≈ N × 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;\n","import {\n COMPRESS_PHILOSOPHY,\n HOW_TO_COMPRESS_RULES,\n TIER2_DISTILL_RULES,\n TIER3_CONDENSE_RULES,\n} from \"./compression-rules.js\";\n\n/**\n * Overridable prompt text consumed by the kernel's nudge renderer and, via the\n * adapter, the system prompt. Every field here is LOAD-BEARING: these rules\n * were tuned over months of production use and are quality-critical. Overriding\n * them can degrade summary quality (loss of paths / signatures / decisions →\n * broken retrieval), so {@link resolvePrompts} requires `{ acknowledgeRisk: true }`.\n *\n * Surface-level text (summary section headers, status-report chrome, tool\n * descriptions) is intentionally NOT part of this interface — it is owned by\n * the adapter or a later \"prompt-set format\" layer and is safe to customize\n * freely. See DESIGN.md for the load-bearing vs surface classification.\n */\nexport interface Prompts {\n /** Core compression philosophy. Embedded in the system prompt + every nudge. */\n compressPhilosophy: string;\n /** Rules the model follows when writing a tier-1 summary. */\n howToCompressRules: string;\n /** Rules for tier-2 distillation of existing summaries. */\n tier2DistillRules: string;\n /** Rules for tier-3 ultra-condensation of distilled summaries. */\n tier3CondenseRules: string;\n}\n\n/**\n * The kernel's canonical prompt values (verbatim from compression-rules.ts).\n * Frozen so a buggy caller cannot mutate the shared singleton and corrupt\n * every other consumer of {@link defaultPrompts}.\n */\nexport const defaultPrompts: Prompts = Object.freeze({\n compressPhilosophy: COMPRESS_PHILOSOPHY,\n howToCompressRules: HOW_TO_COMPRESS_RULES,\n tier2DistillRules: TIER2_DISTILL_RULES,\n tier3CondenseRules: TIER3_CONDENSE_RULES,\n}) as Prompts;\n\nexport interface ResolvePromptsOptions {\n /**\n * Must be `true` to override any prompt field. Every {@link Prompts} field is\n * load-bearing; overriding without acknowledging the quality risk is a\n * programming error and throws.\n */\n acknowledgeRisk?: boolean;\n}\n\n/**\n * Merge prompt overrides onto the kernel defaults. All fields are load-bearing,\n * so ANY override requires `{ acknowledgeRisk: true }`.\n *\n * Only `string`-valued overrides take effect: an explicit `undefined`/`null` or\n * a wrong type is silently dropped (never clobbers a good default), so a\n * malformed partial never degrades the canonical rules. Resolve once at host\n * startup, then pass the resulting {@link Prompts} to {@link renderNudgeText}\n * and to the adapter's system-prompt composition so both layers stay consistent.\n *\n * To keep guidance in the system prompt but drop it from every nudge (hosts that\n * deliver these texts once at startup), do NOT override fields here — that would\n * change both surfaces at once. Pass `{ includeGuidance: false }` to\n * {@link renderNudgeText} instead; the system-prompt path is unaffected by it.\n */\nexport function resolvePrompts(\n overrides?: Partial<Prompts>,\n options: ResolvePromptsOptions = {},\n): Prompts {\n const clean: Partial<Prompts> = {};\n if (overrides) {\n for (const [key, value] of Object.entries(overrides)) {\n if (typeof value === \"string\") {\n (clean as Record<string, unknown>)[key] = value;\n }\n }\n }\n const keys = Object.keys(clean) as (keyof Prompts)[];\n if (keys.length > 0 && !options.acknowledgeRisk) {\n throw new Error(\n `resolvePrompts: overriding compression rules requires { acknowledgeRisk: true }. ` +\n `Overridden keys: ${keys.join(\", \")}. These rules are quality-critical (tuned over months of production use); ` +\n `changing them can degrade summary quality and break retrieval (summaries may lose paths, signatures, decisions).`,\n );\n }\n return { ...defaultPrompts, ...clean };\n}\n","import type { NudgeDecision, CompressibleRange, ProtectedRange, ContextBreakdown, CompressionBlock } from \"./types.js\";\nimport { defaultPrompts } from \"./prompts.js\";\nimport type { Prompts } from \"./prompts.js\";\n\nexport type NudgeVoice = \"gentle\" | \"emergency\";\n\nexport interface RenderedNudge {\n voice: NudgeVoice;\n text: string;\n}\n\nexport interface RenderNudgeOptions {\n /**\n * Embed the load-bearing guidance texts (compressPhilosophy, howToCompressRules,\n * and the active tier's distill/condense rules) in the nudge body. Defaults to\n * true, so existing callers render byte-identical output. Set to false when the\n * host already delivers these same texts once via its system prompt or tool\n * descriptions — re-billing them on every injection is pure repetition. Only\n * the four guidance texts are dropped; per-injection dynamic content\n * (breakdown, trigger line, ranges, tier target blocks) survives. The\n * system-prompt composition path reads {@link Prompts} directly and is\n * unaffected by this flag — this is how a host keeps guidance in the system\n * prompt while slimming every nudge.\n */\n includeGuidance?: boolean;\n}\n\nfunction efficiencyNote(prompts: Prompts, includeGuidance: boolean): string {\n const note = \"This is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full.\";\n return includeGuidance ? `${note}\\n\\n${prompts.compressPhilosophy}` : note;\n}\n\nfunction emergencyHeader(prompts: Prompts, includeGuidance: boolean): string {\n const header = \"⚠️ Context limit reached — compress now. Prioritize consumed tool outputs.\";\n return includeGuidance ? `${header}\\n\\n${prompts.compressPhilosophy}` : header;\n}\n\nfunction formatK(n: number): string {\n if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;\n return `${n}`;\n}\n\nfunction formatBreakdown(bd?: ContextBreakdown): string {\n if (!bd) return \"\";\n const parts: string[] = [];\n if (bd.system > 0) parts.push(`${formatK(bd.system)} system`);\n if (bd.tool > 0) parts.push(`${formatK(bd.tool)} tool`);\n if (bd.summaries > 0) parts.push(`${formatK(bd.summaries)} summaries`);\n if (bd.code > 0) parts.push(`${formatK(bd.code)} code`);\n if (bd.text > 0) parts.push(`${formatK(bd.text)} text`);\n const growth = bd.growth > 0 ? `\\n+${formatK(bd.growth)} since last nudge` : \"\";\n return `Context breakdown: ${parts.join(\" | \")}${growth}`;\n}\n\n\n\nfunction formatTierTargetBlocks(blocks: CompressionBlock[]): string {\n if (blocks.length === 0) {\n return \"Target blocks: (none — no tier blocks found)\";\n }\n const lines = blocks.map((b) => {\n const summaryTokens = Math.ceil((b.summary ?? \"\").length / 4);\n const topic = b.topic ? ` \"${b.topic}\"` : \"\";\n return ` ${b.blockId} ${b.effectiveMessageIds.length} msgs ${formatK(b.compressedTokens)}→${formatK(summaryTokens)}${topic}`;\n });\n return `Target ${blocks[0]!.tier === 1 ? \"tier-1\" : \"tier-2\"} blocks to distill (${blocks.length}):\\n${lines.join(\"\\n\")}`;\n}\n\nexport function formatRanges(compressible: CompressibleRange[], protectedRanges: ProtectedRange[]): string {\n if (compressible.length === 0 && protectedRanges.length === 0) {\n return \"[No specific ranges detected — compress any consumed content.]\";\n }\n\n // Merge compressible + protected into a single oldest-first list, mirroring\n // opencode-acp's formatCompressibleRanges. Splitting them into two sections\n // lost the time order and hid overlaps; a range can be partly compressible\n // and partly protected, which only the merged view shows correctly.\n interface Merged {\n startRef: string; endRef: string; startNum: number; endNum: number;\n count: number; tokens: number;\n compressibleTokens: number; compressibleCount: number;\n protectedTokens: number; protectedCount: number; protectedTools: string[];\n toolPct: number; textPct: number; dangerous: boolean;\n }\n const refNum = (ref: string): number => {\n const m = ref.match(/\\d+/);\n return m ? parseInt(m[0], 10) : 0;\n };\n const entries: Merged[] = [];\n for (const r of compressible) {\n entries.push({\n startRef: r.startRef, endRef: r.endRef, startNum: refNum(r.startRef), endNum: refNum(r.endRef),\n count: r.count, tokens: r.tokens, toolPct: r.toolPct, textPct: r.textPct,\n compressibleTokens: r.tokens, compressibleCount: r.count,\n protectedTokens: 0, protectedCount: 0, protectedTools: [], dangerous: r.dangerous ?? false,\n });\n }\n for (const r of protectedRanges) {\n entries.push({\n startRef: r.startRef, endRef: r.endRef, startNum: refNum(r.startRef), endNum: refNum(r.endRef),\n count: r.count, tokens: r.tokens, toolPct: 0, textPct: 0,\n compressibleTokens: 0, compressibleCount: 0,\n protectedTokens: r.tokens, protectedCount: r.count, protectedTools: [...r.tools], dangerous: false,\n });\n }\n entries.sort((a, b) => a.startNum - b.startNum);\n // Merge adjacent/overlapping ranges (gap ≤ 1 ref).\n const merged: Merged[] = [];\n for (const e of entries) {\n const last = merged[merged.length - 1];\n if (last && e.startNum <= last.endNum + 1) {\n last.endRef = e.endRef;\n last.endNum = Math.max(last.endNum, e.endNum);\n last.count += e.count;\n last.tokens += e.tokens;\n last.compressibleTokens += e.compressibleTokens;\n last.compressibleCount += e.compressibleCount;\n last.protectedTokens += e.protectedTokens;\n last.protectedCount += e.protectedCount;\n if (e.dangerous) last.dangerous = true;\n for (const t of e.protectedTools) {\n if (!last.protectedTools.includes(t)) last.protectedTools.push(t);\n }\n } else {\n merged.push({ ...e });\n }\n }\n const lines = merged.map((e) => {\n const suffix = e.dangerous && e.compressibleTokens > 0 ? \" ⚠️ NOT recommended unless you are certain.\" : \"\";\n if (e.protectedTokens > 0 && e.compressibleTokens === 0) {\n return ` ${e.startRef}–${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(\", \")} — not compressible]${suffix}`;\n }\n if (e.protectedTokens > 0 && e.compressibleTokens > 0) {\n return ` ${e.startRef}–${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(\", \")}]${suffix}`;\n }\n return ` ${e.startRef}–${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;\n });\n return `Compressible ranges (${merged.length}, oldest first):\\n${lines.join(\"\\n\")}`;\n}\n\nexport function renderNudgeText(\n decision: NudgeDecision,\n prompts: Prompts = defaultPrompts,\n options: RenderNudgeOptions = {},\n): RenderedNudge {\n const includeGuidance = options.includeGuidance ?? true;\n const breakdownStr = formatBreakdown(decision.contextBreakdown);\n const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);\n const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;\n\n if (decision.tier !== null && decision.tier >= 2) {\n const isT2 = decision.tier === 2;\n const targets = decision.tierTargetBlocks ?? [];\n const blockList = formatTierTargetBlocks(targets);\n const startId = targets[0]?.blockId ?? \"b1\";\n const endId = targets[targets.length - 1]?.blockId ?? \"b5\";\n const voice: NudgeVoice = isEmergency ? \"emergency\" : \"gentle\";\n const triggerLine = isEmergency\n ? `[EMERGENCY — TIER ${decision.tier} ${isT2 ? \"DISTILLATION\" : \"CONDENSATION\"}] Context limit reached — distill NOW into a denser summary to reclaim tokens.`\n : `[TIER ${decision.tier} ${isT2 ? \"DISTILLATION\" : \"CONDENSATION\"} TRIGGER]`;\n const parts = [\n efficiencyNote(prompts, includeGuidance),\n \"\",\n breakdownStr,\n \"\",\n triggerLine,\n isT2\n ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well — apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.`\n : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well — apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,\n blockList,\n `Example: compress({ content: [{ startId: \"${startId}\", endId: \"${endId}\", summary: \"...\" }] })`,\n ];\n if (includeGuidance) {\n parts.push(\"\", prompts.howToCompressRules, \"\", isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules);\n }\n return { voice, text: parts.join(\"\\n\") };\n }\n\n if (isEmergency) {\n const parts = [emergencyHeader(prompts, includeGuidance), \"\", breakdownStr];\n if (includeGuidance) parts.push(\"\", prompts.howToCompressRules);\n parts.push(\n \"\",\n `{ \"topic\": \"...\", \"content\": [{ \"startId\": \"<ID>\", \"endId\": \"<ID>\", \"summary\": \"...\" }] }`,\n \"Only use IDs from visible messages above. Compress older work first.\",\n \"\",\n rangesStr,\n );\n return { voice: \"emergency\", text: parts.join(\"\\n\") };\n }\n\n const parts = [efficiencyNote(prompts, includeGuidance), \"\", breakdownStr];\n if (includeGuidance) parts.push(\"\", prompts.howToCompressRules);\n parts.push(\"\", rangesStr, \"\", `💡 Compress all ranges in one call (pass multiple content entries: \\`content: [{...}, {...}]\\`).`);\n return { voice: \"gentle\", text: parts.join(\"\\n\") };\n}\n","/** Minimum size for a compressible range to be worth recommending. Ranges\n * below this are fragmented leftovers (a 16-token ack, a one-line tool\n * result): the model cannot write a meaningful >=50-char summary for them,\n * and a batched compress call that includes one gets atomically rejected\n * (the kernel validates the whole batch). Observed in the wild: a 14-range\n * recommendation list containing a 16-token range → every batch attempt\n * failed with \"Summary too short\". Apply on every surface that recommends\n * ranges: the injected nudge, acp_status, and the /acp panel. */\nexport const VIABLE_RANGE_MIN_TOKENS = 200;\n\nexport function viableRanges<T extends { tokens: number }>(ranges: T[]): T[] {\n return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);\n}\n"],"mappings":";AAAA,SAAS,qBAAqB;AAE9B,IAAMA,WAAU,cAAc,YAAY,GAAG;AAEtC,SAAS,mBAAmB,MAAsB;AACvD,MAAI,CAAC,KAAM,QAAO;AAIlB,QAAM,MAAM,KAAK,MAAM,4CAA4C;AACnE,QAAM,WAAW,KAAK,UAAU;AAChC,SAAO,WAAW,KAAK,MAAM,KAAK,SAAS,YAAY,CAAC;AAC1D;AASO,SAAS,mBAAmB,UAAsC;AACvE,SAAO,OAAO,aAAa,YACzB,OAAO,SAAS,QAAQ,KACxB,WAAW,IACT,WACA;AACN;AAMO,SAAS,mBACd,SACA,cAA4B,oBACpB;AACR,SACE,YAAY,QAAQ,QAAQ,EAAE,IAAI,mBAAmB,QAAQ,cAAc;AAE/E;AAEO,SAAS,mBAAmB,MAAsB;AACvD,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,KAAK,KAAK,SAAS,CAAC;AAClC;AAIA,IAAM,iBAAiB;AAEhB,SAAS,qBAAmC;AACjD,MAAI;AACF,UAAM,MAAMC,SAAQ,yBAAyB;AAC7C,UAAM,WAAW,IAAI,eAAe,IAAI,SAAS;AACjD,QAAI,OAAO,aAAa,WAAY,QAAO;AAC3C,WAAO,CAAC,SAAiB;AACvB,UAAI,KAAK,SAAS,eAAgB,QAAO,mBAAmB,IAAI;AAChE,UAAI;AACF,eAAO,SAAS,IAAI;AAAA,MACtB,QAAQ;AACN,eAAO,mBAAmB,IAAI;AAAA,MAChC;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACxDO,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAM5B,IAAM,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqC9B,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgC5B,IAAM,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACnD7B,IAAM,iBAA0B,OAAO,OAAO;AAAA,EACnD,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,mBAAmB;AAAA,EACnB,oBAAoB;AACtB,CAAC;AA0BM,SAAS,eACd,WACA,UAAiC,CAAC,GACzB;AACT,QAAM,QAA0B,CAAC;AACjC,MAAI,WAAW;AACb,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACpD,UAAI,OAAO,UAAU,UAAU;AAC7B,QAAC,MAAkC,GAAG,IAAI;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,MAAI,KAAK,SAAS,KAAK,CAAC,QAAQ,iBAAiB;AAC/C,UAAM,IAAI;AAAA,MACR,qGACsB,KAAK,KAAK,IAAI,CAAC;AAAA,IAEvC;AAAA,EACF;AACA,SAAO,EAAE,GAAG,gBAAgB,GAAG,MAAM;AACvC;;;AC5DA,SAAS,eAAe,SAAkB,iBAAkC;AAC1E,QAAM,OAAO;AACb,SAAO,kBAAkB,GAAG,IAAI;AAAA;AAAA,EAAO,QAAQ,kBAAkB,KAAK;AACxE;AAEA,SAAS,gBAAgB,SAAkB,iBAAkC;AAC3E,QAAM,SAAS;AACf,SAAO,kBAAkB,GAAG,MAAM;AAAA;AAAA,EAAO,QAAQ,kBAAkB,KAAK;AAC1E;AAEA,SAAS,QAAQ,GAAmB;AAClC,MAAI,KAAK,IAAM,QAAO,IAAI,IAAI,KAAM,QAAQ,CAAC,CAAC;AAC9C,SAAO,GAAG,CAAC;AACb;AAEA,SAAS,gBAAgB,IAA+B;AACtD,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,QAAkB,CAAC;AACzB,MAAI,GAAG,SAAS,EAAG,OAAM,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC,SAAS;AAC5D,MAAI,GAAG,OAAO,EAAG,OAAM,KAAK,GAAG,QAAQ,GAAG,IAAI,CAAC,OAAO;AACtD,MAAI,GAAG,YAAY,EAAG,OAAM,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC,YAAY;AACrE,MAAI,GAAG,OAAO,EAAG,OAAM,KAAK,GAAG,QAAQ,GAAG,IAAI,CAAC,OAAO;AACtD,MAAI,GAAG,OAAO,EAAG,OAAM,KAAK,GAAG,QAAQ,GAAG,IAAI,CAAC,OAAO;AACtD,QAAM,SAAS,GAAG,SAAS,IAAI;AAAA,GAAM,QAAQ,GAAG,MAAM,CAAC,sBAAsB;AAC7E,SAAO,sBAAsB,MAAM,KAAK,KAAK,CAAC,GAAG,MAAM;AACzD;AAIA,SAAS,uBAAuB,QAAoC;AAClE,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM;AAC9B,UAAM,gBAAgB,KAAK,MAAM,EAAE,WAAW,IAAI,SAAS,CAAC;AAC5D,UAAM,QAAQ,EAAE,QAAQ,MAAM,EAAE,KAAK,MAAM;AAC3C,WAAO,KAAK,EAAE,OAAO,KAAK,EAAE,oBAAoB,MAAM,UAAU,QAAQ,EAAE,gBAAgB,CAAC,SAAI,QAAQ,aAAa,CAAC,GAAG,KAAK;AAAA,EAC/H,CAAC;AACD,SAAO,UAAU,OAAO,CAAC,EAAG,SAAS,IAAI,WAAW,QAAQ,uBAAuB,OAAO,MAAM;AAAA,EAAO,MAAM,KAAK,IAAI,CAAC;AACzH;AAEO,SAAS,aAAa,cAAmC,iBAA2C;AACzG,MAAI,aAAa,WAAW,KAAK,gBAAgB,WAAW,GAAG;AAC7D,WAAO;AAAA,EACT;AAaA,QAAM,SAAS,CAAC,QAAwB;AACtC,UAAM,IAAI,IAAI,MAAM,KAAK;AACzB,WAAO,IAAI,SAAS,EAAE,CAAC,GAAG,EAAE,IAAI;AAAA,EAClC;AACA,QAAM,UAAoB,CAAC;AAC3B,aAAW,KAAK,cAAc;AAC5B,YAAQ,KAAK;AAAA,MACX,UAAU,EAAE;AAAA,MAAU,QAAQ,EAAE;AAAA,MAAQ,UAAU,OAAO,EAAE,QAAQ;AAAA,MAAG,QAAQ,OAAO,EAAE,MAAM;AAAA,MAC7F,OAAO,EAAE;AAAA,MAAO,QAAQ,EAAE;AAAA,MAAQ,SAAS,EAAE;AAAA,MAAS,SAAS,EAAE;AAAA,MACjE,oBAAoB,EAAE;AAAA,MAAQ,mBAAmB,EAAE;AAAA,MACnD,iBAAiB;AAAA,MAAG,gBAAgB;AAAA,MAAG,gBAAgB,CAAC;AAAA,MAAG,WAAW,EAAE,aAAa;AAAA,IACvF,CAAC;AAAA,EACH;AACA,aAAW,KAAK,iBAAiB;AAC/B,YAAQ,KAAK;AAAA,MACX,UAAU,EAAE;AAAA,MAAU,QAAQ,EAAE;AAAA,MAAQ,UAAU,OAAO,EAAE,QAAQ;AAAA,MAAG,QAAQ,OAAO,EAAE,MAAM;AAAA,MAC7F,OAAO,EAAE;AAAA,MAAO,QAAQ,EAAE;AAAA,MAAQ,SAAS;AAAA,MAAG,SAAS;AAAA,MACvD,oBAAoB;AAAA,MAAG,mBAAmB;AAAA,MAC1C,iBAAiB,EAAE;AAAA,MAAQ,gBAAgB,EAAE;AAAA,MAAO,gBAAgB,CAAC,GAAG,EAAE,KAAK;AAAA,MAAG,WAAW;AAAA,IAC/F,CAAC;AAAA,EACH;AACA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAE9C,QAAM,SAAmB,CAAC;AAC1B,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,OAAO,OAAO,SAAS,CAAC;AACrC,QAAI,QAAQ,EAAE,YAAY,KAAK,SAAS,GAAG;AACzC,WAAK,SAAS,EAAE;AAChB,WAAK,SAAS,KAAK,IAAI,KAAK,QAAQ,EAAE,MAAM;AAC5C,WAAK,SAAS,EAAE;AAChB,WAAK,UAAU,EAAE;AACjB,WAAK,sBAAsB,EAAE;AAC7B,WAAK,qBAAqB,EAAE;AAC5B,WAAK,mBAAmB,EAAE;AAC1B,WAAK,kBAAkB,EAAE;AACzB,UAAI,EAAE,UAAW,MAAK,YAAY;AAClC,iBAAW,KAAK,EAAE,gBAAgB;AAChC,YAAI,CAAC,KAAK,eAAe,SAAS,CAAC,EAAG,MAAK,eAAe,KAAK,CAAC;AAAA,MAClE;AAAA,IACF,OAAO;AACL,aAAO,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,IACtB;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM;AAC9B,UAAM,SAAS,EAAE,aAAa,EAAE,qBAAqB,IAAI,2DAAiD;AAC1G,QAAI,EAAE,kBAAkB,KAAK,EAAE,uBAAuB,GAAG;AACvD,aAAO,KAAK,EAAE,QAAQ,SAAI,EAAE,MAAM,KAAK,EAAE,KAAK,UAAU,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE,eAAe,KAAK,IAAI,CAAC,4BAAuB,MAAM;AAAA,IACnJ;AACA,QAAI,EAAE,kBAAkB,KAAK,EAAE,qBAAqB,GAAG;AACrD,aAAO,KAAK,EAAE,QAAQ,SAAI,EAAE,MAAM,KAAK,EAAE,KAAK,UAAU,QAAQ,EAAE,MAAM,CAAC,KAAK,QAAQ,EAAE,kBAAkB,CAAC,mBAAmB,QAAQ,EAAE,eAAe,CAAC,eAAe,EAAE,eAAe,KAAK,IAAI,CAAC,IAAI,MAAM;AAAA,IAC9M;AACA,WAAO,KAAK,EAAE,QAAQ,SAAI,EAAE,MAAM,KAAK,EAAE,KAAK,UAAU,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,YAAY,EAAE,OAAO,KAAK,MAAM;AAAA,EAC9H,CAAC;AACD,SAAO,wBAAwB,OAAO,MAAM;AAAA,EAAqB,MAAM,KAAK,IAAI,CAAC;AACnF;AAEO,SAAS,gBACd,UACA,UAAmB,gBACnB,UAA8B,CAAC,GAChB;AACf,QAAM,kBAAkB,QAAQ,mBAAmB;AACnD,QAAM,eAAe,gBAAgB,SAAS,gBAAgB;AAC9D,QAAM,YAAY,aAAa,SAAS,oBAAoB,SAAS,mBAAmB,CAAC,CAAC;AAC1F,QAAM,cAAc,CAAC,CAAC,SAAS,WAAW,qBAAqB,CAAC,CAAC,SAAS,WAAW;AAErF,MAAI,SAAS,SAAS,QAAQ,SAAS,QAAQ,GAAG;AAChD,UAAM,OAAO,SAAS,SAAS;AAC/B,UAAM,UAAU,SAAS,oBAAoB,CAAC;AAC9C,UAAM,YAAY,uBAAuB,OAAO;AAChD,UAAM,UAAU,QAAQ,CAAC,GAAG,WAAW;AACvC,UAAM,QAAQ,QAAQ,QAAQ,SAAS,CAAC,GAAG,WAAW;AACtD,UAAM,QAAoB,cAAc,cAAc;AACtD,UAAM,cAAc,cAChB,0BAAqB,SAAS,IAAI,IAAI,OAAO,iBAAiB,cAAc,wFAC5E,SAAS,SAAS,IAAI,IAAI,OAAO,iBAAiB,cAAc;AACpE,UAAMC,SAAQ;AAAA,MACZ,eAAe,SAAS,eAAe;AAAA,MACvC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OACI,kbACA;AAAA,MACJ;AAAA,MACA,6CAA6C,OAAO,cAAc,KAAK;AAAA,IACzE;AACA,QAAI,iBAAiB;AACnB,MAAAA,OAAM,KAAK,IAAI,QAAQ,oBAAoB,IAAI,OAAO,QAAQ,oBAAoB,QAAQ,kBAAkB;AAAA,IAC9G;AACA,WAAO,EAAE,OAAO,MAAMA,OAAM,KAAK,IAAI,EAAE;AAAA,EACzC;AAEA,MAAI,aAAa;AACf,UAAMA,SAAQ,CAAC,gBAAgB,SAAS,eAAe,GAAG,IAAI,YAAY;AAC1E,QAAI,gBAAiB,CAAAA,OAAM,KAAK,IAAI,QAAQ,kBAAkB;AAC9D,IAAAA,OAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,WAAO,EAAE,OAAO,aAAa,MAAMA,OAAM,KAAK,IAAI,EAAE;AAAA,EACtD;AAEA,QAAM,QAAQ,CAAC,eAAe,SAAS,eAAe,GAAG,IAAI,YAAY;AACzE,MAAI,gBAAiB,OAAM,KAAK,IAAI,QAAQ,kBAAkB;AAC9D,QAAM,KAAK,IAAI,WAAW,IAAI,yGAAkG;AAChI,SAAO,EAAE,OAAO,UAAU,MAAM,MAAM,KAAK,IAAI,EAAE;AACnD;;;AC3LO,IAAM,0BAA0B;AAEhC,SAAS,aAA2C,QAAkB;AAC3E,SAAO,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,uBAAuB;AACjE;","names":["require","require","parts"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { resolveBoundaries, parseBoundary, BoundaryNotFoundError, visibleBlockAn
|
|
|
11
11
|
export { defaultCountTokens, estimateTokensFast, createBpeTokenizer, countMessageTokens, } from "./tokenize.js";
|
|
12
12
|
export type { TokenCountFn } from "./tokenize.js";
|
|
13
13
|
export { renderNudgeText, formatRanges } from "./nudge-text.js";
|
|
14
|
-
export type { NudgeVoice, RenderedNudge } from "./nudge-text.js";
|
|
14
|
+
export type { NudgeVoice, RenderedNudge, RenderNudgeOptions } from "./nudge-text.js";
|
|
15
15
|
export { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES, TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES, } from "./compression-rules.js";
|
|
16
16
|
export { defaultPrompts, resolvePrompts } from "./prompts.js";
|
|
17
17
|
export type { Prompts, ResolvePromptsOptions } from "./prompts.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,cAAc,qBAAqB,CAAC;AACpC,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,EACL,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
package/dist/nudge-text.d.ts
CHANGED
|
@@ -5,6 +5,21 @@ export interface RenderedNudge {
|
|
|
5
5
|
voice: NudgeVoice;
|
|
6
6
|
text: string;
|
|
7
7
|
}
|
|
8
|
+
export interface RenderNudgeOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Embed the load-bearing guidance texts (compressPhilosophy, howToCompressRules,
|
|
11
|
+
* and the active tier's distill/condense rules) in the nudge body. Defaults to
|
|
12
|
+
* true, so existing callers render byte-identical output. Set to false when the
|
|
13
|
+
* host already delivers these same texts once via its system prompt or tool
|
|
14
|
+
* descriptions — re-billing them on every injection is pure repetition. Only
|
|
15
|
+
* the four guidance texts are dropped; per-injection dynamic content
|
|
16
|
+
* (breakdown, trigger line, ranges, tier target blocks) survives. The
|
|
17
|
+
* system-prompt composition path reads {@link Prompts} directly and is
|
|
18
|
+
* unaffected by this flag — this is how a host keeps guidance in the system
|
|
19
|
+
* prompt while slimming every nudge.
|
|
20
|
+
*/
|
|
21
|
+
includeGuidance?: boolean;
|
|
22
|
+
}
|
|
8
23
|
export declare function formatRanges(compressible: CompressibleRange[], protectedRanges: ProtectedRange[]): string;
|
|
9
|
-
export declare function renderNudgeText(decision: NudgeDecision, prompts?: Prompts): RenderedNudge;
|
|
24
|
+
export declare function renderNudgeText(decision: NudgeDecision, prompts?: Prompts, options?: RenderNudgeOptions): RenderedNudge;
|
|
10
25
|
//# sourceMappingURL=nudge-text.d.ts.map
|
package/dist/nudge-text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudge-text.d.ts","sourceRoot":"","sources":["../src/nudge-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAsC,MAAM,YAAY,CAAC;AAEvH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEhD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"nudge-text.d.ts","sourceRoot":"","sources":["../src/nudge-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAsC,MAAM,YAAY,CAAC;AAEvH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEhD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA2CD,wBAAgB,YAAY,CAAC,YAAY,EAAE,iBAAiB,EAAE,EAAE,eAAe,EAAE,cAAc,EAAE,GAAG,MAAM,CAsEzG;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,aAAa,EACvB,OAAO,GAAE,OAAwB,EACjC,OAAO,GAAE,kBAAuB,GAC/B,aAAa,CAmDf"}
|
package/dist/panel/index.js
CHANGED
package/dist/persist/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
* - the store never deletes files; cleanup is a downstream decision
|
|
15
15
|
*/
|
|
16
16
|
export { StateStore, flatFileNameFor } from "./store.js";
|
|
17
|
-
export type { PersistedEnvelope, PersistLogger,
|
|
17
|
+
export type { PersistedEnvelope, PersistLogger, StateStoreOptions } from "./store.js";
|
|
18
18
|
export { mergeCompressionState } from "./state-merge.js";
|
|
19
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/persist/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/persist/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/persist/index.js
CHANGED
|
@@ -16,7 +16,6 @@ var StateStore = class {
|
|
|
16
16
|
legacyFn;
|
|
17
17
|
relPathFn;
|
|
18
18
|
validateFn;
|
|
19
|
-
codec;
|
|
20
19
|
retryAttempts;
|
|
21
20
|
retryBaseMs;
|
|
22
21
|
retryMaxMs;
|
|
@@ -39,7 +38,6 @@ var StateStore = class {
|
|
|
39
38
|
this.relPathFn = opts.relPath;
|
|
40
39
|
this.legacyFn = opts.legacy;
|
|
41
40
|
this.validateFn = opts.validate ?? defaultValidate;
|
|
42
|
-
this.codec = opts.codec;
|
|
43
41
|
this.retryAttempts = Math.max(1, opts.retryAttempts ?? 6);
|
|
44
42
|
this.retryBaseMs = Math.max(1, opts.retryBaseMs ?? 50);
|
|
45
43
|
this.retryMaxMs = Math.max(this.retryBaseMs, opts.retryMaxMs ?? 1600);
|
|
@@ -96,13 +94,13 @@ var StateStore = class {
|
|
|
96
94
|
return false;
|
|
97
95
|
}
|
|
98
96
|
const file = this.resolvePath(id, payload);
|
|
99
|
-
const data = this.
|
|
97
|
+
const data = JSON.stringify(this.envelope(id, payload));
|
|
100
98
|
let lastErr;
|
|
101
99
|
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
|
|
102
100
|
const tmp = this.tempPath(file);
|
|
103
101
|
try {
|
|
104
102
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
-
fs.writeFileSync(tmp, data);
|
|
103
|
+
fs.writeFileSync(tmp, data, "utf8");
|
|
106
104
|
fs.renameSync(tmp, file);
|
|
107
105
|
lastErr = void 0;
|
|
108
106
|
break;
|
|
@@ -127,7 +125,7 @@ var StateStore = class {
|
|
|
127
125
|
for (let attempt = 0; attempt < this.retryAttempts && spillPath === null; attempt++) {
|
|
128
126
|
try {
|
|
129
127
|
fs.mkdirSync(path.dirname(spill), { recursive: true });
|
|
130
|
-
fs.writeFileSync(spill, data);
|
|
128
|
+
fs.writeFileSync(spill, data, "utf8");
|
|
131
129
|
spillPath = spill;
|
|
132
130
|
} catch (e) {
|
|
133
131
|
if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;
|
|
@@ -238,13 +236,13 @@ var StateStore = class {
|
|
|
238
236
|
throw e;
|
|
239
237
|
}
|
|
240
238
|
const file = this.resolvePath(id, payload);
|
|
241
|
-
const data = this.
|
|
239
|
+
const data = JSON.stringify(this.envelope(id, payload));
|
|
242
240
|
let lastErr;
|
|
243
241
|
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
|
|
244
242
|
const tmp = this.tempPath(file);
|
|
245
243
|
try {
|
|
246
244
|
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
247
|
-
await fsp.writeFile(tmp, data);
|
|
245
|
+
await fsp.writeFile(tmp, data, "utf8");
|
|
248
246
|
await fsp.rename(tmp, file);
|
|
249
247
|
this.discovered.set(id, file);
|
|
250
248
|
this.clearFailure(id);
|
|
@@ -265,7 +263,7 @@ var StateStore = class {
|
|
|
265
263
|
for (let attempt = 0; attempt < this.retryAttempts && spillPath === null; attempt++) {
|
|
266
264
|
try {
|
|
267
265
|
await fsp.mkdir(path.dirname(spill), { recursive: true });
|
|
268
|
-
await fsp.writeFile(spill, data);
|
|
266
|
+
await fsp.writeFile(spill, data, "utf8");
|
|
269
267
|
spillPath = spill;
|
|
270
268
|
} catch (e) {
|
|
271
269
|
if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;
|
|
@@ -281,13 +279,6 @@ var StateStore = class {
|
|
|
281
279
|
envelope(id, payload) {
|
|
282
280
|
return { version: this.version, savedAt: Date.now(), id, payload };
|
|
283
281
|
}
|
|
284
|
-
/** Serialize an envelope for disk, applying the optional codec. Strings
|
|
285
|
-
* are written as UTF-8 (the fs default); Buffer results pass through as
|
|
286
|
-
* raw bytes. */
|
|
287
|
-
serialize(id, payload) {
|
|
288
|
-
const json = JSON.stringify(this.envelope(id, payload));
|
|
289
|
-
return this.codec ? this.codec.encode(json) : json;
|
|
290
|
-
}
|
|
291
282
|
/** Absolute path for a record: custom relPath (guarded against path
|
|
292
283
|
* escape) or the flat hash default. */
|
|
293
284
|
resolvePath(id, payload) {
|
|
@@ -354,9 +345,7 @@ var StateStore = class {
|
|
|
354
345
|
readEnvelope(file) {
|
|
355
346
|
let parsed;
|
|
356
347
|
try {
|
|
357
|
-
|
|
358
|
-
const text = this.codec ? this.codec.decode(buf) : buf.toString("utf8");
|
|
359
|
-
parsed = JSON.parse(text);
|
|
348
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
360
349
|
} catch (e) {
|
|
361
350
|
if (e.code !== "ENOENT") {
|
|
362
351
|
this.log("warn", `[persist] skipping corrupt file ${rel(file, this.dir)}: ${errText(e)}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/persist/store.ts","../../src/persist/state-merge.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/** Logger callback. Downstream routes it wherever it wants (file, stderr). */\nexport type PersistLogger = (level: \"info\" | \"warn\" | \"error\", msg: string) => void;\n\n/**\n * On-disk record shape. The store owns the envelope (version stamp, save\n * time, id) so atomicity and discovery can rely on it; the payload schema\n * is entirely downstream-owned. `payload` must be JSON-serializable.\n */\nexport interface PersistedEnvelope<T> {\n version: number;\n savedAt: number;\n id: string;\n payload: T;\n}\n\n/** A legacy (pre-envelope) record adopted on load. `version`/`savedAt`\n * preserve the source record's own stamps when present. */\nexport interface LegacyAdoption<T> {\n id: string;\n payload: T;\n version?: number;\n savedAt?: number;\n}\n\n/**\n * Optional record codec: transforms serialized envelopes between the store\n * and disk (e.g. compress and/or encrypt at rest). The store is\n * codec-agnostic — it hands `encode` the exact JSON string it would have\n * written and passes raw file bytes to `decode`, which must return that same\n * JSON string or throw, in which case the file is treated as corrupt\n * (skipped + logged) like any unreadable record. Format detection (magic\n * bytes, plaintext fallback for unencoded legacy files) is the codec's job,\n * so mixed trees of legacy plaintext and encoded files load fine.\n */\nexport interface StateStoreCodec {\n encode(data: string): Buffer | string;\n decode(buf: Buffer): string;\n}\n\nexport interface StateStoreOptions<T> {\n /**\n * Storage root. The kernel deliberately has NO default location: the\n * downstream decides where state lives (CLI dir, XDG data dir, plugin\n * dir, temp dir in tests). All records live under this directory.\n */\n dir: string;\n /**\n * Schema version stamped into every envelope. Owned by the downstream;\n * bump it when the payload shape changes. The store itself never\n * rejects a record over its version — migration policy belongs to the\n * reader.\n */\n version: number;\n /** Debounce window for scheduleSave, ms. Default 500. */\n debounceMs?: number;\n /** Default true. When false, all writes are silent no-ops and loads\n * return empty results. */\n enabled?: boolean;\n log?: PersistLogger;\n /**\n * Relative path (under `dir`) for a record, possibly namespaced into\n * subdirectories (e.g. `openai/host-hash.json`). Default: flat\n * `<sha256(id)[:24]>.json`.\n *\n * Because the path may depend on payload fields the store only learns\n * at write time, single-record loads resolve namespaced files only\n * after loadAll() has discovered them (or the store itself wrote\n * them). The flat default name is always checked as a fallback, so\n * downstreams using a custom relPath should call loadAll() at boot.\n */\n relPath?: (id: string, payload: T) => string;\n /**\n * Adopt records written by an older, pre-envelope schema. Receives the\n * parsed JSON of any file that failed the envelope-shape check; return\n * an adoption to load it as an envelope, or null to skip it. Adopted\n * records are re-persisted in the current envelope format on the next\n * dirty write — files migrate organically, and old files keep loading\n * (same policy billion-context's proxy used for its v1→v3 migration).\n */\n legacy?: (parsed: unknown) => LegacyAdoption<T> | null;\n /**\n * Payload validation on load. Return false to skip a record (foreign\n * schema, corrupt content). Default: envelope-shape check only\n * (string id, non-null payload).\n */\n validate?: (envelope: PersistedEnvelope<T>) => boolean;\n /**\n * Optional codec applied around every write (canonical + spill) and\n * read. See StateStoreCodec. Default: none — files are plain UTF-8 JSON.\n */\n codec?: StateStoreCodec;\n /**\n * Transient-write retry policy (Windows + hostile temp environments).\n * The whole write cycle — mkdir, temp write, rename — is retried with\n * exponential backoff when it fails with a TRANSIENT code: EPERM/EBUSY/\n * EACCES (a lock held by AV scan, search indexer, SMB) or ENOENT/ENOTDIR\n * (the temp file or its directory vanished between create and rename —\n * observed on CI Windows runners where a sweeper deletes freshly\n * created files under %TEMP%). The delay after attempt i is\n * min(retryBaseMs * 2^i, retryMaxMs). Defaults (6 attempts, 50ms base,\n * 1600ms cap) give a ~1.5s window per write path — long enough for most\n * locks to release, short enough not to stall a sync flush. When the\n * window is exhausted the write spills to a side file (see spillPath)\n * instead of dropping the data.\n */\n retryAttempts?: number;\n retryBaseMs?: number;\n retryMaxMs?: number;\n}\n\n/**\n * Crash-safe, debounce-coalescing JSON state store. Mechanism only — lifted\n * from billion-context's proxy SessionStore and generalized:\n *\n * - atomic writes: temp file + rename, so a crash mid-write never leaves a\n * truncated record (readers see either the old or the new file)\n * - whole-cycle retries with exponential backoff on transient fs failures:\n * Windows locks (EPERM/EBUSY/EACCES from AV/indexer/SMB) and vanishing\n * files/dirs (ENOENT/ENOTDIR — a sweeper deleting fresh temp files, seen\n * on CI Windows runners); when the window is exhausted the record spills\n * to a side file (`<name>.fb.json`) instead of being dropped, so a lock\n * held by AV/indexer/SMB never silently loses session data\n * - per-id serialization so concurrent writeNow calls never interleave\n * temp-file names or reorders writes\n * - debounced scheduleSave coalesces bursts into one write; the record is\n * built at WRITE time from a builder, so late mutations are picked up\n * - optional record codec (compress/encrypt at rest): encode on every write,\n * decode on every read; a decode failure is corrupt-file semantics\n * - loadAll skips `.tmp-*` orphans, corrupt JSON, and records whose\n * filename does not match their id — one bad file never blocks boot; it\n * reconciles a canonical record against its spill by savedAt (freshest wins)\n *\n * The store never deletes a record's data. On a successful canonical write it\n * removes a now-stale spill of the SAME id (a duplicate, not distinct data).\n * Session cleanup is a downstream policy decision (kernel position: persisted\n * state should not be deleted opportunistically).\n */\nexport class StateStore<T> {\n readonly enabled: boolean;\n private readonly dir: string;\n private readonly version: number;\n private readonly debounceMs: number;\n private readonly log: PersistLogger;\n private readonly legacyFn?: (parsed: unknown) => LegacyAdoption<T> | null;\n private readonly relPathFn?: (id: string, payload: T) => string;\n private readonly validateFn: (envelope: PersistedEnvelope<T>) => boolean;\n private readonly codec?: StateStoreCodec;\n private readonly retryAttempts: number;\n private readonly retryBaseMs: number;\n private readonly retryMaxMs: number;\n private readonly timers = new Map<string, NodeJS.Timeout>();\n private readonly pending = new Map<string, () => T>();\n private readonly writeChains = new Map<string, Promise<void>>();\n /** id → absolute path, populated by writes and loadAll. */\n private readonly discovered = new Map<string, string>();\n /** id → cumulative write-failure count, for rate-limited alerting. */\n private readonly failCounts = new Map<string, number>();\n /** Monotonic counter for unique temp filenames within a process. */\n private tmpSeq = 0;\n\n constructor(opts: StateStoreOptions<T>) {\n this.dir = opts.dir;\n this.version = opts.version;\n this.debounceMs = opts.debounceMs ?? 500;\n this.enabled = opts.enabled ?? true;\n this.log = opts.log ?? ((_level, _msg) => {});\n this.relPathFn = opts.relPath;\n this.legacyFn = opts.legacy;\n this.validateFn = opts.validate ?? defaultValidate;\n this.codec = opts.codec;\n this.retryAttempts = Math.max(1, opts.retryAttempts ?? 6);\n this.retryBaseMs = Math.max(1, opts.retryBaseMs ?? 50);\n this.retryMaxMs = Math.max(this.retryBaseMs, opts.retryMaxMs ?? 1600);\n }\n\n /** Debounced save. Coalesces bursts; the builder runs at write time, so\n * the freshest state is always persisted. Never throws. */\n scheduleSave(id: string, build: () => T): void {\n if (!this.enabled) return;\n this.pending.set(id, build);\n const existing = this.timers.get(id);\n if (existing) return;\n const timer = setTimeout(() => {\n this.timers.delete(id);\n const builder = this.pending.get(id);\n this.pending.delete(id);\n if (!builder) return;\n // Errors are logged inside writeInner; swallowing here keeps a\n // failed timer write from becoming an unhandledRejection.\n void this.writeNow(id, builder).catch(() => {});\n }, this.debounceMs);\n timer.unref?.();\n this.timers.set(id, timer);\n }\n\n /** Immediate save, serialized per id. Rejects on write failure; a\n * failing write never breaks the chain for the next one. */\n async writeNow(id: string, build: () => T): Promise<void> {\n if (!this.enabled) return;\n // The previous promise may reject (disk full, EPERM) — catch so our\n // chain doesn't break, then run our own write.\n const prev = this.writeChains.get(id) ?? Promise.resolve();\n const next = prev.catch(() => {}).then(() => this.writeInner(id, build));\n this.writeChains.set(id, next);\n // Clean up the chain entry once settled so the Map doesn't grow. The\n // .catch() is load-bearing: .finally() returns a derived promise that\n // nobody else holds — when the chain rejects it would surface as an\n // unhandledRejection and crash the host on default Node settings.\n next\n .finally(() => {\n if (this.writeChains.get(id) === next) this.writeChains.delete(id);\n })\n .catch(() => {});\n return next;\n }\n\n /** Synchronous flush for one id. Used where the caller cannot await\n * (memory eviction, sync shutdown paths). Cancels any pending debounce\n * timer. Returns true on success, false on failure — callers that use\n * the result to drop in-memory state must NOT drop it on failure. */\n flushSync(id: string, build: () => T): boolean {\n if (!this.enabled) return true;\n const timer = this.timers.get(id);\n if (timer) {\n clearTimeout(timer);\n this.timers.delete(id);\n this.pending.delete(id);\n }\n let payload: T;\n try {\n payload = build();\n } catch (e) {\n this.log(\"error\", `[persist] builder failed for ${id}: ${errText(e)}`);\n return false;\n }\n const file = this.resolvePath(id, payload);\n const data = this.serialize(id, payload);\n let lastErr: unknown;\n for (let attempt = 0; attempt < this.retryAttempts; attempt++) {\n // Fresh temp name + mkdir per attempt — same reasoning as the\n // async writeInner loop (delete-pending tombstones, swept dirs).\n const tmp = this.tempPath(file);\n try {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(tmp, data);\n fs.renameSync(tmp, file);\n lastErr = undefined;\n break;\n } catch (e) {\n lastErr = e;\n try {\n fs.unlinkSync(tmp);\n } catch {\n // temp never created or already gone\n }\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n // sync sleep (Atomics.wait) — backoff grows per attempt\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, this.backoffMs(attempt));\n }\n }\n if (!lastErr) {\n this.discovered.set(id, file);\n this.clearFailure(id);\n this.removeSpillSync(file);\n return true;\n }\n // The canonical write outlived the retry window (Windows lock held by\n // AV/indexer/SMB). Spill to a side file so the data still lands on\n // disk instead of being dropped — loadAll picks the freshest of the\n // canonical and the spill, with its own transient retry window.\n const spill = this.spillPathFor(file);\n let spillPath: string | null = null;\n for (let attempt = 0; attempt < this.retryAttempts && spillPath === null; attempt++) {\n try {\n fs.mkdirSync(path.dirname(spill), { recursive: true });\n fs.writeFileSync(spill, data);\n spillPath = spill;\n } catch (e) {\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, this.backoffMs(attempt));\n }\n }\n if (spillPath) this.discovered.set(id, spillPath);\n this.recordFailure(id, lastErr, spillPath);\n return spillPath !== null;\n }\n\n /** Load one record. Checks the discovered path (from a prior\n * write/loadAll), an optional relative-path hint, and the flat default\n * name. Returns null when absent, disabled, corrupt, or rejected by\n * validate. The hint covers namespaced records the store has not\n * discovered (e.g. an evicted session re-requested with its meta,\n * where the path depends on data the store cannot reconstruct from\n * the id alone). */\n loadSync(id: string, hint?: string): PersistedEnvelope<T> | null {\n if (!this.enabled) return null;\n const candidates = [\n this.discovered.get(id),\n hint ? path.join(this.dir, hint) : undefined,\n path.join(this.dir, flatFileNameFor(id)),\n ];\n for (const file of candidates) {\n if (!file) continue;\n const envelope = this.readEnvelope(file);\n if (envelope && envelope.id === id) return envelope;\n }\n return null;\n }\n\n /** Load every record under dir. Populates the discovery map (enables\n * loadSync for namespaced relPaths). Skips corrupt files, `.tmp-*`\n * orphans, and records whose filename does not match their id — one\n * bad file never blocks boot. Never throws. */\n async loadAll(): Promise<Map<string, PersistedEnvelope<T>>> {\n const out = new Map<string, PersistedEnvelope<T>>();\n if (!this.enabled) return out;\n const files = await this.walkJsonFiles(this.dir);\n for (const file of files) {\n const envelope = this.readEnvelope(file);\n if (!envelope) continue;\n const base = path.basename(file);\n const relBase = path.basename(this.relPathOf(envelope.id, envelope.payload));\n const flatBase = flatFileNameFor(envelope.id);\n // A file is a valid record for its envelope id when its name is the\n // canonical name OR a spill (`.fb`) of the canonical name. This lets\n // a spilled record (written while the canonical was locked) be\n // discovered and reconciled against the canonical by savedAt.\n let owner: string | null = null;\n if (base === relBase || base === flatBase) {\n owner = envelope.id;\n } else if (base.endsWith(\".fb.json\")) {\n const canonicalBase = `${base.slice(0, -\".fb.json\".length)}.json`;\n if (canonicalBase === relBase || canonicalBase === flatBase) {\n owner = envelope.id;\n }\n }\n if (owner === null) {\n this.log(\"warn\", `[persist] skipping ${rel(file, this.dir)}: filename does not match record id`);\n continue;\n }\n const existing = out.get(owner);\n if (!existing || envelope.savedAt >= existing.savedAt) {\n out.set(owner, envelope);\n this.discovered.set(owner, file);\n }\n }\n return out;\n }\n\n /** Whether a debounced write is pending for an id. */\n hasPending(id: string): boolean {\n return this.timers.has(id);\n }\n\n /** Ids with a pending debounced write. */\n pendingIds(): string[] {\n return [...this.timers.keys()];\n }\n\n /** Flush every pending debounced write immediately, then drain in-flight\n * writes. For graceful shutdown (SIGTERM/SIGINT). Never rejects. */\n async flushAll(): Promise<void> {\n const ids = this.pendingIds();\n for (const timer of this.timers.values()) clearTimeout(timer);\n this.timers.clear();\n const builders = new Map<string, () => T>();\n for (const id of ids) {\n const build = this.pending.get(id);\n if (build) builders.set(id, build);\n this.pending.delete(id);\n }\n await Promise.all(\n [...builders.entries()].map(([id, build]) =>\n this.writeNow(id, build).catch((e) => {\n this.log(\"error\", `[persist] shutdown flush failed for ${id}: ${errText(e)}`);\n }),\n ),\n );\n // Drain writes whose timer fired earlier but are still mid-flight.\n await Promise.allSettled([...this.writeChains.values()]);\n }\n\n /** Cancel all pending debounced writes without flushing (tests). */\n cancelAll(): void {\n for (const timer of this.timers.values()) clearTimeout(timer);\n this.timers.clear();\n this.pending.clear();\n }\n\n private async writeInner(id: string, build: () => T): Promise<void> {\n let payload: T;\n try {\n payload = build();\n } catch (e) {\n this.log(\"error\", `[persist] builder failed for ${id}: ${errText(e)}`);\n throw e;\n }\n const file = this.resolvePath(id, payload);\n const data = this.serialize(id, payload);\n let lastErr: unknown;\n for (let attempt = 0; attempt < this.retryAttempts; attempt++) {\n // Fresh temp name per attempt: on Windows a failed cleanup can\n // leave the previous name delete-pending, and creating over a\n // tombstoned name fails immediately — reusing it would burn every\n // remaining attempt before the write even happens. mkdir runs per\n // attempt too: a sweeper may have deleted the directory after the\n // previous cycle created it.\n const tmp = this.tempPath(file);\n try {\n await fsp.mkdir(path.dirname(file), { recursive: true });\n await fsp.writeFile(tmp, data);\n await fsp.rename(tmp, file);\n this.discovered.set(id, file);\n this.clearFailure(id);\n await this.removeSpill(file);\n return;\n } catch (e) {\n lastErr = e;\n // Clean up the .tmp orphan so the next write doesn't collide\n // with it.\n await fsp.unlink(tmp).catch(() => {});\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n const { promise, resolve } = Promise.withResolvers<void>();\n setTimeout(resolve, this.backoffMs(attempt));\n await promise;\n }\n }\n // The canonical write outlived the retry window (Windows lock held\n // by AV/indexer/SMB, or a sweeper keeps deleting the fresh temp).\n // Spill to a side file so the data still lands on disk instead of\n // being dropped — loadAll picks the freshest of the canonical and\n // the spill. The spill write gets its own transient retry window for\n // the same reason (observed failing with ENOENT alongside the\n // canonical on CI Windows runners).\n const spill = this.spillPathFor(file);\n let spillPath: string | null = null;\n for (let attempt = 0; attempt < this.retryAttempts && spillPath === null; attempt++) {\n try {\n await fsp.mkdir(path.dirname(spill), { recursive: true });\n await fsp.writeFile(spill, data);\n spillPath = spill;\n } catch (e) {\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n const { promise, resolve } = Promise.withResolvers<void>();\n setTimeout(resolve, this.backoffMs(attempt));\n await promise;\n }\n }\n if (spillPath) this.discovered.set(id, spillPath);\n this.recordFailure(id, lastErr, spillPath);\n throw lastErr;\n }\n\n private envelope(id: string, payload: T): PersistedEnvelope<T> {\n return { version: this.version, savedAt: Date.now(), id, payload };\n }\n\n /** Serialize an envelope for disk, applying the optional codec. Strings\n * are written as UTF-8 (the fs default); Buffer results pass through as\n * raw bytes. */\n private serialize(id: string, payload: T): Buffer | string {\n const json = JSON.stringify(this.envelope(id, payload));\n return this.codec ? this.codec.encode(json) : json;\n }\n\n /** Absolute path for a record: custom relPath (guarded against path\n * escape) or the flat hash default. */\n private resolvePath(id: string, payload: T): string {\n return path.join(this.dir, this.relPathOf(id, payload));\n }\n\n private relPathOf(id: string, payload: T): string {\n const custom = this.relPathFn?.(id, payload);\n if (!custom) return flatFileNameFor(id);\n const rel = path.normalize(custom);\n if (path.isAbsolute(rel) || rel.split(/[\\\\/]+/).includes(\"..\")) {\n this.log(\"warn\", `[persist] relPath for ${id} escapes dir; using flat name`);\n return flatFileNameFor(id);\n }\n return rel;\n }\n\n /** Unique temp file next to the destination (same dir ⇒ same volume ⇒\n * rename is atomic). Prefixed `.tmp-` so loadAll skips orphans. */\n private tempPath(dest: string): string {\n const seq = this.tmpSeq++;\n return path.join(path.dirname(dest), `.tmp-${path.basename(dest, \".json\")}-${process.pid}-${seq}`);\n }\n\n private backoffMs(attempt: number): number {\n return Math.min(this.retryBaseMs * 2 ** attempt, this.retryMaxMs);\n }\n\n /** Side file for a record whose canonical write keeps failing: the\n * canonical name with a `.fb` (fallback) infix, e.g. `a.json` →\n * `a.fb.json`. One slot per id, overwritten on each spill, so a stuck\n * lock never accumulates files. Ends in `.json` so loadAll discovers it. */\n private spillPathFor(file: string): string {\n const base = path.basename(file);\n const dot = base.lastIndexOf(\".\");\n const stem = dot > 0 ? base.slice(0, dot) : base;\n const ext = dot > 0 ? base.slice(dot) : \".json\";\n return path.join(path.dirname(file), `${stem}.fb${ext}`);\n }\n\n /** Remove a stale spill after a successful canonical write (best-effort). */\n private async removeSpill(file: string): Promise<void> {\n await fsp.unlink(this.spillPathFor(file)).catch(() => {});\n }\n\n private removeSpillSync(file: string): void {\n try {\n fs.unlinkSync(this.spillPathFor(file));\n } catch {\n // no spill or still locked — loadAll reconciles by savedAt\n }\n }\n\n /** Rate-limited failure alerting: log on the first failure and at each\n * power-of-two count (1,2,4,8,…), so a long lock yields ~log2(N) lines\n * instead of one per write. Includes the spill path so the operator can\n * see where the data landed. */\n private recordFailure(id: string, err: unknown, spillPath: string | null): void {\n const count = (this.failCounts.get(id) ?? 0) + 1;\n this.failCounts.set(id, count);\n const alertAt = count === 1 || (count >= 2 && (count & (count - 1)) === 0);\n if (!alertAt) return;\n const where = spillPath\n ? `; data spilled to ${rel(spillPath, this.dir)}`\n : \"; SPILLOVER ALSO FAILED — data at risk\";\n this.log(count === 1 ? \"error\" : \"warn\", `[persist] write failed for ${id} (total ${count}x): ${errText(err)}${where}`);\n }\n\n private clearFailure(id: string): void {\n this.failCounts.delete(id);\n }\n\n /** Parse + validate one file. Corrupt or invalid records return null\n * (logged) instead of throwing — load paths must never block boot. */\n private readEnvelope(file: string): PersistedEnvelope<T> | null {\n let parsed: unknown;\n try {\n const buf = fs.readFileSync(file);\n const text = this.codec ? this.codec.decode(buf) : buf.toString(\"utf8\");\n parsed = JSON.parse(text);\n } catch (e) {\n // A missing candidate path is an expected miss (loadSync probes);\n // only genuinely unreadable/corrupt files get logged.\n if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") {\n this.log(\"warn\", `[persist] skipping corrupt file ${rel(file, this.dir)}: ${errText(e)}`);\n }\n return null;\n }\n if (isEnvelopeLike(parsed)) {\n if (!this.validateFn(parsed as PersistedEnvelope<T>)) {\n this.log(\"warn\", `[persist] skipping invalid record ${rel(file, this.dir)}`);\n return null;\n }\n return parsed as PersistedEnvelope<T>;\n }\n const adopted = this.adoptLegacy(parsed);\n if (adopted) return adopted;\n this.log(\"warn\", `[persist] skipping invalid record ${rel(file, this.dir)}`);\n return null;\n }\n\n /** Wrap a legacy (pre-envelope) record as an envelope via the `legacy`\n * hook, then validate the adopted payload like any other. */\n private adoptLegacy(parsed: unknown): PersistedEnvelope<T> | null {\n const adoption = this.legacyFn ? this.legacyFn(parsed) : null;\n if (!adoption || typeof adoption.id !== \"string\" || adoption.id.length === 0 || adoption.payload == null) {\n return null;\n }\n const source = parsed as Partial<PersistedEnvelope<T>>;\n const envelope: PersistedEnvelope<T> = {\n version: adoption.version ?? source.version ?? this.version,\n savedAt: adoption.savedAt ?? source.savedAt ?? 0,\n id: adoption.id,\n payload: adoption.payload,\n };\n if (!this.validateFn(envelope)) return null;\n return envelope;\n }\n\n /** Iterative recursive walk (no readdir-recursive dependency), skipping\n * `.tmp-*` names and non-.json files. */\n private async walkJsonFiles(root: string): Promise<string[]> {\n const out: string[] = [];\n const queue: string[] = [root];\n while (queue.length > 0) {\n const dir = queue.pop()!;\n let dirents: fs.Dirent[];\n try {\n dirents = await fsp.readdir(dir, { withFileTypes: true });\n } catch {\n continue; // missing dir → no records yet\n }\n for (const d of dirents) {\n if (d.name.startsWith(\".tmp-\")) continue;\n const full = path.join(dir, d.name);\n if (d.isDirectory()) queue.push(full);\n else if (d.isFile() && d.name.endsWith(\".json\")) out.push(full);\n }\n }\n return out;\n }\n}\n\nfunction defaultValidate<T>(envelope: PersistedEnvelope<T>): boolean {\n return typeof envelope.id === \"string\" && envelope.id.length > 0 && envelope.payload != null;\n}\n\nfunction isEnvelopeLike(value: unknown): value is PersistedEnvelope<unknown> {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Partial<PersistedEnvelope<unknown>>;\n return typeof v.id === \"string\" && \"payload\" in v;\n}\n\n/** Deterministic flat filename: `<sha256(id)[:24]>.json`. Truncated hash —\n * 96 bits keeps collisions unreachable for realistic id counts, and short\n * names stay greppable. */\nexport function flatFileNameFor(id: string): string {\n return createHash(\"sha256\").update(id, \"utf8\").digest(\"hex\").slice(0, 24) + \".json\";\n}\n\n/** Transient fs failure codes worth retrying the whole write cycle for:\n * - EPERM/EBUSY/EACCES — a lock held on the file/dir (AV scan, search\n * indexer, SMB); releases within the backoff window.\n * - ENOENT/ENOTDIR — the temp file or its directory VANISHED between\n * create and rename (a sweeper deleting fresh files under %TEMP%, seen\n * on CI Windows runners), or a delete-pending tombstone from the same\n * sweep. Recreating the dir + rewriting a fresh temp heals it. */\nfunction isTransientFsError(e: unknown): boolean {\n const code = (e as NodeJS.ErrnoException).code;\n return code === \"EPERM\" || code === \"EBUSY\" || code === \"EACCES\" || code === \"ENOENT\" || code === \"ENOTDIR\";\n}\n\nfunction errText(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n\nfunction rel(p: string, base: string): string {\n const r = path.relative(base, p);\n return r && !r.startsWith(\"..\") ? r : p;\n}\n","import type { CompressionState } from \"../types.js\";\nimport { createInitialState } from \"../state.js\";\n\n/**\n * Forward-compat: merge a parsed state with a fresh one so fields added in\n * later kernel versions get sane defaults instead of `undefined`. Nested\n * groups (nudge, stats) are shallow-merged per field so older snapshots\n * keep their values while newer counters default in.\n *\n * Lifted verbatim in behavior from billion-context's proxy mergeState.\n */\nexport function mergeCompressionState(parsed: CompressionState): CompressionState {\n const fresh = createInitialState();\n return {\n blocks: parsed.blocks ?? fresh.blocks,\n messageRefs: parsed.messageRefs ?? fresh.messageRefs,\n nudge: { ...fresh.nudge, ...(parsed.nudge ?? {}) },\n stats: { ...fresh.stats, ...(parsed.stats ?? {}) },\n nextBlockId: parsed.nextBlockId ?? fresh.nextBlockId,\n nextRunId: parsed.nextRunId ?? fresh.nextRunId,\n tokenSnapshot: parsed.tokenSnapshot ?? fresh.tokenSnapshot,\n absorbed: parsed.absorbed ?? fresh.absorbed,\n };\n}\n"],"mappings":"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","names":["rel"]}
|
|
1
|
+
{"version":3,"sources":["../../src/persist/store.ts","../../src/persist/state-merge.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/** Logger callback. Downstream routes it wherever it wants (file, stderr). */\nexport type PersistLogger = (level: \"info\" | \"warn\" | \"error\", msg: string) => void;\n\n/**\n * On-disk record shape. The store owns the envelope (version stamp, save\n * time, id) so atomicity and discovery can rely on it; the payload schema\n * is entirely downstream-owned. `payload` must be JSON-serializable.\n */\nexport interface PersistedEnvelope<T> {\n version: number;\n savedAt: number;\n id: string;\n payload: T;\n}\n\n/** A legacy (pre-envelope) record adopted on load. `version`/`savedAt`\n * preserve the source record's own stamps when present. */\nexport interface LegacyAdoption<T> {\n id: string;\n payload: T;\n version?: number;\n savedAt?: number;\n}\n\nexport interface StateStoreOptions<T> {\n /**\n * Storage root. The kernel deliberately has NO default location: the\n * downstream decides where state lives (CLI dir, XDG data dir, plugin\n * dir, temp dir in tests). All records live under this directory.\n */\n dir: string;\n /**\n * Schema version stamped into every envelope. Owned by the downstream;\n * bump it when the payload shape changes. The store itself never\n * rejects a record over its version — migration policy belongs to the\n * reader.\n */\n version: number;\n /** Debounce window for scheduleSave, ms. Default 500. */\n debounceMs?: number;\n /** Default true. When false, all writes are silent no-ops and loads\n * return empty results. */\n enabled?: boolean;\n log?: PersistLogger;\n /**\n * Relative path (under `dir`) for a record, possibly namespaced into\n * subdirectories (e.g. `openai/host-hash.json`). Default: flat\n * `<sha256(id)[:24]>.json`.\n *\n * Because the path may depend on payload fields the store only learns\n * at write time, single-record loads resolve namespaced files only\n * after loadAll() has discovered them (or the store itself wrote\n * them). The flat default name is always checked as a fallback, so\n * downstreams using a custom relPath should call loadAll() at boot.\n */\n relPath?: (id: string, payload: T) => string;\n /**\n * Adopt records written by an older, pre-envelope schema. Receives the\n * parsed JSON of any file that failed the envelope-shape check; return\n * an adoption to load it as an envelope, or null to skip it. Adopted\n * records are re-persisted in the current envelope format on the next\n * dirty write — files migrate organically, and old files keep loading\n * (same policy billion-context's proxy used for its v1→v3 migration).\n */\n legacy?: (parsed: unknown) => LegacyAdoption<T> | null;\n /**\n * Payload validation on load. Return false to skip a record (foreign\n * schema, corrupt content). Default: envelope-shape check only\n * (string id, non-null payload).\n */\n validate?: (envelope: PersistedEnvelope<T>) => boolean;\n /**\n * Transient-write retry policy (Windows + hostile temp environments).\n * The whole write cycle — mkdir, temp write, rename — is retried with\n * exponential backoff when it fails with a TRANSIENT code: EPERM/EBUSY/\n * EACCES (a lock held by AV scan, search indexer, SMB) or ENOENT/ENOTDIR\n * (the temp file or its directory vanished between create and rename —\n * observed on CI Windows runners where a sweeper deletes freshly\n * created files under %TEMP%). The delay after attempt i is\n * min(retryBaseMs * 2^i, retryMaxMs). Defaults (6 attempts, 50ms base,\n * 1600ms cap) give a ~1.5s window per write path — long enough for most\n * locks to release, short enough not to stall a sync flush. When the\n * window is exhausted the write spills to a side file (see spillPath)\n * instead of dropping the data.\n */\n retryAttempts?: number;\n retryBaseMs?: number;\n retryMaxMs?: number;\n}\n\n/**\n * Crash-safe, debounce-coalescing JSON state store. Mechanism only — lifted\n * from billion-context's proxy SessionStore and generalized:\n *\n * - atomic writes: temp file + rename, so a crash mid-write never leaves a\n * truncated record (readers see either the old or the new file)\n * - whole-cycle retries with exponential backoff on transient fs failures:\n * Windows locks (EPERM/EBUSY/EACCES from AV/indexer/SMB) and vanishing\n * files/dirs (ENOENT/ENOTDIR — a sweeper deleting fresh temp files, seen\n * on CI Windows runners); when the window is exhausted the record spills\n * to a side file (`<name>.fb.json`) instead of being dropped, so a lock\n * held by AV/indexer/SMB never silently loses session data\n * - per-id serialization so concurrent writeNow calls never interleave\n * temp-file names or reorders writes\n * - debounced scheduleSave coalesces bursts into one write; the record is\n * built at WRITE time from a builder, so late mutations are picked up\n * - loadAll skips `.tmp-*` orphans, corrupt JSON, and records whose\n * filename does not match their id — one bad file never blocks boot; it\n * reconciles a canonical record against its spill by savedAt (freshest wins)\n *\n * The store never deletes a record's data. On a successful canonical write it\n * removes a now-stale spill of the SAME id (a duplicate, not distinct data).\n * Session cleanup is a downstream policy decision (kernel position: persisted\n * state should not be deleted opportunistically).\n */\nexport class StateStore<T> {\n readonly enabled: boolean;\n private readonly dir: string;\n private readonly version: number;\n private readonly debounceMs: number;\n private readonly log: PersistLogger;\n private readonly legacyFn?: (parsed: unknown) => LegacyAdoption<T> | null;\n private readonly relPathFn?: (id: string, payload: T) => string;\n private readonly validateFn: (envelope: PersistedEnvelope<T>) => boolean;\n private readonly retryAttempts: number;\n private readonly retryBaseMs: number;\n private readonly retryMaxMs: number;\n private readonly timers = new Map<string, NodeJS.Timeout>();\n private readonly pending = new Map<string, () => T>();\n private readonly writeChains = new Map<string, Promise<void>>();\n /** id → absolute path, populated by writes and loadAll. */\n private readonly discovered = new Map<string, string>();\n /** id → cumulative write-failure count, for rate-limited alerting. */\n private readonly failCounts = new Map<string, number>();\n /** Monotonic counter for unique temp filenames within a process. */\n private tmpSeq = 0;\n\n constructor(opts: StateStoreOptions<T>) {\n this.dir = opts.dir;\n this.version = opts.version;\n this.debounceMs = opts.debounceMs ?? 500;\n this.enabled = opts.enabled ?? true;\n this.log = opts.log ?? ((_level, _msg) => {});\n this.relPathFn = opts.relPath;\n this.legacyFn = opts.legacy;\n this.validateFn = opts.validate ?? defaultValidate;\n this.retryAttempts = Math.max(1, opts.retryAttempts ?? 6);\n this.retryBaseMs = Math.max(1, opts.retryBaseMs ?? 50);\n this.retryMaxMs = Math.max(this.retryBaseMs, opts.retryMaxMs ?? 1600);\n }\n\n /** Debounced save. Coalesces bursts; the builder runs at write time, so\n * the freshest state is always persisted. Never throws. */\n scheduleSave(id: string, build: () => T): void {\n if (!this.enabled) return;\n this.pending.set(id, build);\n const existing = this.timers.get(id);\n if (existing) return;\n const timer = setTimeout(() => {\n this.timers.delete(id);\n const builder = this.pending.get(id);\n this.pending.delete(id);\n if (!builder) return;\n // Errors are logged inside writeInner; swallowing here keeps a\n // failed timer write from becoming an unhandledRejection.\n void this.writeNow(id, builder).catch(() => {});\n }, this.debounceMs);\n timer.unref?.();\n this.timers.set(id, timer);\n }\n\n /** Immediate save, serialized per id. Rejects on write failure; a\n * failing write never breaks the chain for the next one. */\n async writeNow(id: string, build: () => T): Promise<void> {\n if (!this.enabled) return;\n // The previous promise may reject (disk full, EPERM) — catch so our\n // chain doesn't break, then run our own write.\n const prev = this.writeChains.get(id) ?? Promise.resolve();\n const next = prev.catch(() => {}).then(() => this.writeInner(id, build));\n this.writeChains.set(id, next);\n // Clean up the chain entry once settled so the Map doesn't grow. The\n // .catch() is load-bearing: .finally() returns a derived promise that\n // nobody else holds — when the chain rejects it would surface as an\n // unhandledRejection and crash the host on default Node settings.\n next\n .finally(() => {\n if (this.writeChains.get(id) === next) this.writeChains.delete(id);\n })\n .catch(() => {});\n return next;\n }\n\n /** Synchronous flush for one id. Used where the caller cannot await\n * (memory eviction, sync shutdown paths). Cancels any pending debounce\n * timer. Returns true on success, false on failure — callers that use\n * the result to drop in-memory state must NOT drop it on failure. */\n flushSync(id: string, build: () => T): boolean {\n if (!this.enabled) return true;\n const timer = this.timers.get(id);\n if (timer) {\n clearTimeout(timer);\n this.timers.delete(id);\n this.pending.delete(id);\n }\n let payload: T;\n try {\n payload = build();\n } catch (e) {\n this.log(\"error\", `[persist] builder failed for ${id}: ${errText(e)}`);\n return false;\n }\n const file = this.resolvePath(id, payload);\n const data = JSON.stringify(this.envelope(id, payload));\n let lastErr: unknown;\n for (let attempt = 0; attempt < this.retryAttempts; attempt++) {\n // Fresh temp name + mkdir per attempt — same reasoning as the\n // async writeInner loop (delete-pending tombstones, swept dirs).\n const tmp = this.tempPath(file);\n try {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(tmp, data, \"utf8\");\n fs.renameSync(tmp, file);\n lastErr = undefined;\n break;\n } catch (e) {\n lastErr = e;\n try {\n fs.unlinkSync(tmp);\n } catch {\n // temp never created or already gone\n }\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n // sync sleep (Atomics.wait) — backoff grows per attempt\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, this.backoffMs(attempt));\n }\n }\n if (!lastErr) {\n this.discovered.set(id, file);\n this.clearFailure(id);\n this.removeSpillSync(file);\n return true;\n }\n // The canonical write outlived the retry window (Windows lock held by\n // AV/indexer/SMB). Spill to a side file so the data still lands on\n // disk instead of being dropped — loadAll picks the freshest of the\n // canonical and the spill, with its own transient retry window.\n const spill = this.spillPathFor(file);\n let spillPath: string | null = null;\n for (let attempt = 0; attempt < this.retryAttempts && spillPath === null; attempt++) {\n try {\n fs.mkdirSync(path.dirname(spill), { recursive: true });\n fs.writeFileSync(spill, data, \"utf8\");\n spillPath = spill;\n } catch (e) {\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, this.backoffMs(attempt));\n }\n }\n if (spillPath) this.discovered.set(id, spillPath);\n this.recordFailure(id, lastErr, spillPath);\n return spillPath !== null;\n }\n\n /** Load one record. Checks the discovered path (from a prior\n * write/loadAll), an optional relative-path hint, and the flat default\n * name. Returns null when absent, disabled, corrupt, or rejected by\n * validate. The hint covers namespaced records the store has not\n * discovered (e.g. an evicted session re-requested with its meta,\n * where the path depends on data the store cannot reconstruct from\n * the id alone). */\n loadSync(id: string, hint?: string): PersistedEnvelope<T> | null {\n if (!this.enabled) return null;\n const candidates = [\n this.discovered.get(id),\n hint ? path.join(this.dir, hint) : undefined,\n path.join(this.dir, flatFileNameFor(id)),\n ];\n for (const file of candidates) {\n if (!file) continue;\n const envelope = this.readEnvelope(file);\n if (envelope && envelope.id === id) return envelope;\n }\n return null;\n }\n\n /** Load every record under dir. Populates the discovery map (enables\n * loadSync for namespaced relPaths). Skips corrupt files, `.tmp-*`\n * orphans, and records whose filename does not match their id — one\n * bad file never blocks boot. Never throws. */\n async loadAll(): Promise<Map<string, PersistedEnvelope<T>>> {\n const out = new Map<string, PersistedEnvelope<T>>();\n if (!this.enabled) return out;\n const files = await this.walkJsonFiles(this.dir);\n for (const file of files) {\n const envelope = this.readEnvelope(file);\n if (!envelope) continue;\n const base = path.basename(file);\n const relBase = path.basename(this.relPathOf(envelope.id, envelope.payload));\n const flatBase = flatFileNameFor(envelope.id);\n // A file is a valid record for its envelope id when its name is the\n // canonical name OR a spill (`.fb`) of the canonical name. This lets\n // a spilled record (written while the canonical was locked) be\n // discovered and reconciled against the canonical by savedAt.\n let owner: string | null = null;\n if (base === relBase || base === flatBase) {\n owner = envelope.id;\n } else if (base.endsWith(\".fb.json\")) {\n const canonicalBase = `${base.slice(0, -\".fb.json\".length)}.json`;\n if (canonicalBase === relBase || canonicalBase === flatBase) {\n owner = envelope.id;\n }\n }\n if (owner === null) {\n this.log(\"warn\", `[persist] skipping ${rel(file, this.dir)}: filename does not match record id`);\n continue;\n }\n const existing = out.get(owner);\n if (!existing || envelope.savedAt >= existing.savedAt) {\n out.set(owner, envelope);\n this.discovered.set(owner, file);\n }\n }\n return out;\n }\n\n /** Whether a debounced write is pending for an id. */\n hasPending(id: string): boolean {\n return this.timers.has(id);\n }\n\n /** Ids with a pending debounced write. */\n pendingIds(): string[] {\n return [...this.timers.keys()];\n }\n\n /** Flush every pending debounced write immediately, then drain in-flight\n * writes. For graceful shutdown (SIGTERM/SIGINT). Never rejects. */\n async flushAll(): Promise<void> {\n const ids = this.pendingIds();\n for (const timer of this.timers.values()) clearTimeout(timer);\n this.timers.clear();\n const builders = new Map<string, () => T>();\n for (const id of ids) {\n const build = this.pending.get(id);\n if (build) builders.set(id, build);\n this.pending.delete(id);\n }\n await Promise.all(\n [...builders.entries()].map(([id, build]) =>\n this.writeNow(id, build).catch((e) => {\n this.log(\"error\", `[persist] shutdown flush failed for ${id}: ${errText(e)}`);\n }),\n ),\n );\n // Drain writes whose timer fired earlier but are still mid-flight.\n await Promise.allSettled([...this.writeChains.values()]);\n }\n\n /** Cancel all pending debounced writes without flushing (tests). */\n cancelAll(): void {\n for (const timer of this.timers.values()) clearTimeout(timer);\n this.timers.clear();\n this.pending.clear();\n }\n\n private async writeInner(id: string, build: () => T): Promise<void> {\n let payload: T;\n try {\n payload = build();\n } catch (e) {\n this.log(\"error\", `[persist] builder failed for ${id}: ${errText(e)}`);\n throw e;\n }\n const file = this.resolvePath(id, payload);\n const data = JSON.stringify(this.envelope(id, payload));\n let lastErr: unknown;\n for (let attempt = 0; attempt < this.retryAttempts; attempt++) {\n // Fresh temp name per attempt: on Windows a failed cleanup can\n // leave the previous name delete-pending, and creating over a\n // tombstoned name fails immediately — reusing it would burn every\n // remaining attempt before the write even happens. mkdir runs per\n // attempt too: a sweeper may have deleted the directory after the\n // previous cycle created it.\n const tmp = this.tempPath(file);\n try {\n await fsp.mkdir(path.dirname(file), { recursive: true });\n await fsp.writeFile(tmp, data, \"utf8\");\n await fsp.rename(tmp, file);\n this.discovered.set(id, file);\n this.clearFailure(id);\n await this.removeSpill(file);\n return;\n } catch (e) {\n lastErr = e;\n // Clean up the .tmp orphan so the next write doesn't collide\n // with it.\n await fsp.unlink(tmp).catch(() => {});\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n const { promise, resolve } = Promise.withResolvers<void>();\n setTimeout(resolve, this.backoffMs(attempt));\n await promise;\n }\n }\n // The canonical write outlived the retry window (Windows lock held\n // by AV/indexer/SMB, or a sweeper keeps deleting the fresh temp).\n // Spill to a side file so the data still lands on disk instead of\n // being dropped — loadAll picks the freshest of the canonical and\n // the spill. The spill write gets its own transient retry window for\n // the same reason (observed failing with ENOENT alongside the\n // canonical on CI Windows runners).\n const spill = this.spillPathFor(file);\n let spillPath: string | null = null;\n for (let attempt = 0; attempt < this.retryAttempts && spillPath === null; attempt++) {\n try {\n await fsp.mkdir(path.dirname(spill), { recursive: true });\n await fsp.writeFile(spill, data, \"utf8\");\n spillPath = spill;\n } catch (e) {\n if (!isTransientFsError(e) || attempt === this.retryAttempts - 1) break;\n const { promise, resolve } = Promise.withResolvers<void>();\n setTimeout(resolve, this.backoffMs(attempt));\n await promise;\n }\n }\n if (spillPath) this.discovered.set(id, spillPath);\n this.recordFailure(id, lastErr, spillPath);\n throw lastErr;\n }\n\n private envelope(id: string, payload: T): PersistedEnvelope<T> {\n return { version: this.version, savedAt: Date.now(), id, payload };\n }\n\n /** Absolute path for a record: custom relPath (guarded against path\n * escape) or the flat hash default. */\n private resolvePath(id: string, payload: T): string {\n return path.join(this.dir, this.relPathOf(id, payload));\n }\n\n private relPathOf(id: string, payload: T): string {\n const custom = this.relPathFn?.(id, payload);\n if (!custom) return flatFileNameFor(id);\n const rel = path.normalize(custom);\n if (path.isAbsolute(rel) || rel.split(/[\\\\/]+/).includes(\"..\")) {\n this.log(\"warn\", `[persist] relPath for ${id} escapes dir; using flat name`);\n return flatFileNameFor(id);\n }\n return rel;\n }\n\n /** Unique temp file next to the destination (same dir ⇒ same volume ⇒\n * rename is atomic). Prefixed `.tmp-` so loadAll skips orphans. */\n private tempPath(dest: string): string {\n const seq = this.tmpSeq++;\n return path.join(path.dirname(dest), `.tmp-${path.basename(dest, \".json\")}-${process.pid}-${seq}`);\n }\n\n private backoffMs(attempt: number): number {\n return Math.min(this.retryBaseMs * 2 ** attempt, this.retryMaxMs);\n }\n\n /** Side file for a record whose canonical write keeps failing: the\n * canonical name with a `.fb` (fallback) infix, e.g. `a.json` →\n * `a.fb.json`. One slot per id, overwritten on each spill, so a stuck\n * lock never accumulates files. Ends in `.json` so loadAll discovers it. */\n private spillPathFor(file: string): string {\n const base = path.basename(file);\n const dot = base.lastIndexOf(\".\");\n const stem = dot > 0 ? base.slice(0, dot) : base;\n const ext = dot > 0 ? base.slice(dot) : \".json\";\n return path.join(path.dirname(file), `${stem}.fb${ext}`);\n }\n\n /** Remove a stale spill after a successful canonical write (best-effort). */\n private async removeSpill(file: string): Promise<void> {\n await fsp.unlink(this.spillPathFor(file)).catch(() => {});\n }\n\n private removeSpillSync(file: string): void {\n try {\n fs.unlinkSync(this.spillPathFor(file));\n } catch {\n // no spill or still locked — loadAll reconciles by savedAt\n }\n }\n\n /** Rate-limited failure alerting: log on the first failure and at each\n * power-of-two count (1,2,4,8,…), so a long lock yields ~log2(N) lines\n * instead of one per write. Includes the spill path so the operator can\n * see where the data landed. */\n private recordFailure(id: string, err: unknown, spillPath: string | null): void {\n const count = (this.failCounts.get(id) ?? 0) + 1;\n this.failCounts.set(id, count);\n const alertAt = count === 1 || (count >= 2 && (count & (count - 1)) === 0);\n if (!alertAt) return;\n const where = spillPath\n ? `; data spilled to ${rel(spillPath, this.dir)}`\n : \"; SPILLOVER ALSO FAILED — data at risk\";\n this.log(count === 1 ? \"error\" : \"warn\", `[persist] write failed for ${id} (total ${count}x): ${errText(err)}${where}`);\n }\n\n private clearFailure(id: string): void {\n this.failCounts.delete(id);\n }\n\n /** Parse + validate one file. Corrupt or invalid records return null\n * (logged) instead of throwing — load paths must never block boot. */\n private readEnvelope(file: string): PersistedEnvelope<T> | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch (e) {\n // A missing candidate path is an expected miss (loadSync probes);\n // only genuinely unreadable/corrupt files get logged.\n if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") {\n this.log(\"warn\", `[persist] skipping corrupt file ${rel(file, this.dir)}: ${errText(e)}`);\n }\n return null;\n }\n if (isEnvelopeLike(parsed)) {\n if (!this.validateFn(parsed as PersistedEnvelope<T>)) {\n this.log(\"warn\", `[persist] skipping invalid record ${rel(file, this.dir)}`);\n return null;\n }\n return parsed as PersistedEnvelope<T>;\n }\n const adopted = this.adoptLegacy(parsed);\n if (adopted) return adopted;\n this.log(\"warn\", `[persist] skipping invalid record ${rel(file, this.dir)}`);\n return null;\n }\n\n /** Wrap a legacy (pre-envelope) record as an envelope via the `legacy`\n * hook, then validate the adopted payload like any other. */\n private adoptLegacy(parsed: unknown): PersistedEnvelope<T> | null {\n const adoption = this.legacyFn ? this.legacyFn(parsed) : null;\n if (!adoption || typeof adoption.id !== \"string\" || adoption.id.length === 0 || adoption.payload == null) {\n return null;\n }\n const source = parsed as Partial<PersistedEnvelope<T>>;\n const envelope: PersistedEnvelope<T> = {\n version: adoption.version ?? source.version ?? this.version,\n savedAt: adoption.savedAt ?? source.savedAt ?? 0,\n id: adoption.id,\n payload: adoption.payload,\n };\n if (!this.validateFn(envelope)) return null;\n return envelope;\n }\n\n /** Iterative recursive walk (no readdir-recursive dependency), skipping\n * `.tmp-*` names and non-.json files. */\n private async walkJsonFiles(root: string): Promise<string[]> {\n const out: string[] = [];\n const queue: string[] = [root];\n while (queue.length > 0) {\n const dir = queue.pop()!;\n let dirents: fs.Dirent[];\n try {\n dirents = await fsp.readdir(dir, { withFileTypes: true });\n } catch {\n continue; // missing dir → no records yet\n }\n for (const d of dirents) {\n if (d.name.startsWith(\".tmp-\")) continue;\n const full = path.join(dir, d.name);\n if (d.isDirectory()) queue.push(full);\n else if (d.isFile() && d.name.endsWith(\".json\")) out.push(full);\n }\n }\n return out;\n }\n}\n\nfunction defaultValidate<T>(envelope: PersistedEnvelope<T>): boolean {\n return typeof envelope.id === \"string\" && envelope.id.length > 0 && envelope.payload != null;\n}\n\nfunction isEnvelopeLike(value: unknown): value is PersistedEnvelope<unknown> {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Partial<PersistedEnvelope<unknown>>;\n return typeof v.id === \"string\" && \"payload\" in v;\n}\n\n/** Deterministic flat filename: `<sha256(id)[:24]>.json`. Truncated hash —\n * 96 bits keeps collisions unreachable for realistic id counts, and short\n * names stay greppable. */\nexport function flatFileNameFor(id: string): string {\n return createHash(\"sha256\").update(id, \"utf8\").digest(\"hex\").slice(0, 24) + \".json\";\n}\n\n/** Transient fs failure codes worth retrying the whole write cycle for:\n * - EPERM/EBUSY/EACCES — a lock held on the file/dir (AV scan, search\n * indexer, SMB); releases within the backoff window.\n * - ENOENT/ENOTDIR — the temp file or its directory VANISHED between\n * create and rename (a sweeper deleting fresh files under %TEMP%, seen\n * on CI Windows runners), or a delete-pending tombstone from the same\n * sweep. Recreating the dir + rewriting a fresh temp heals it. */\nfunction isTransientFsError(e: unknown): boolean {\n const code = (e as NodeJS.ErrnoException).code;\n return code === \"EPERM\" || code === \"EBUSY\" || code === \"EACCES\" || code === \"ENOENT\" || code === \"ENOTDIR\";\n}\n\nfunction errText(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n\nfunction rel(p: string, base: string): string {\n const r = path.relative(base, p);\n return r && !r.startsWith(\"..\") ? r : p;\n}\n","import type { CompressionState } from \"../types.js\";\nimport { createInitialState } from \"../state.js\";\n\n/**\n * Forward-compat: merge a parsed state with a fresh one so fields added in\n * later kernel versions get sane defaults instead of `undefined`. Nested\n * groups (nudge, stats) are shallow-merged per field so older snapshots\n * keep their values while newer counters default in.\n *\n * Lifted verbatim in behavior from billion-context's proxy mergeState.\n */\nexport function mergeCompressionState(parsed: CompressionState): CompressionState {\n const fresh = createInitialState();\n return {\n blocks: parsed.blocks ?? fresh.blocks,\n messageRefs: parsed.messageRefs ?? fresh.messageRefs,\n nudge: { ...fresh.nudge, ...(parsed.nudge ?? {}) },\n stats: { ...fresh.stats, ...(parsed.stats ?? {}) },\n nextBlockId: parsed.nextBlockId ?? fresh.nextBlockId,\n nextRunId: parsed.nextRunId ?? fresh.nextRunId,\n tokenSnapshot: parsed.tokenSnapshot ?? fresh.tokenSnapshot,\n absorbed: parsed.absorbed ?? fresh.absorbed,\n };\n}\n"],"mappings":"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","names":["rel"]}
|
package/dist/persist/store.d.ts
CHANGED
|
@@ -19,20 +19,6 @@ export interface LegacyAdoption<T> {
|
|
|
19
19
|
version?: number;
|
|
20
20
|
savedAt?: number;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Optional record codec: transforms serialized envelopes between the store
|
|
24
|
-
* and disk (e.g. compress and/or encrypt at rest). The store is
|
|
25
|
-
* codec-agnostic — it hands `encode` the exact JSON string it would have
|
|
26
|
-
* written and passes raw file bytes to `decode`, which must return that same
|
|
27
|
-
* JSON string or throw, in which case the file is treated as corrupt
|
|
28
|
-
* (skipped + logged) like any unreadable record. Format detection (magic
|
|
29
|
-
* bytes, plaintext fallback for unencoded legacy files) is the codec's job,
|
|
30
|
-
* so mixed trees of legacy plaintext and encoded files load fine.
|
|
31
|
-
*/
|
|
32
|
-
export interface StateStoreCodec {
|
|
33
|
-
encode(data: string): Buffer | string;
|
|
34
|
-
decode(buf: Buffer): string;
|
|
35
|
-
}
|
|
36
22
|
export interface StateStoreOptions<T> {
|
|
37
23
|
/**
|
|
38
24
|
* Storage root. The kernel deliberately has NO default location: the
|
|
@@ -80,11 +66,6 @@ export interface StateStoreOptions<T> {
|
|
|
80
66
|
* (string id, non-null payload).
|
|
81
67
|
*/
|
|
82
68
|
validate?: (envelope: PersistedEnvelope<T>) => boolean;
|
|
83
|
-
/**
|
|
84
|
-
* Optional codec applied around every write (canonical + spill) and
|
|
85
|
-
* read. See StateStoreCodec. Default: none — files are plain UTF-8 JSON.
|
|
86
|
-
*/
|
|
87
|
-
codec?: StateStoreCodec;
|
|
88
69
|
/**
|
|
89
70
|
* Transient-write retry policy (Windows + hostile temp environments).
|
|
90
71
|
* The whole write cycle — mkdir, temp write, rename — is retried with
|
|
@@ -119,8 +100,6 @@ export interface StateStoreOptions<T> {
|
|
|
119
100
|
* temp-file names or reorders writes
|
|
120
101
|
* - debounced scheduleSave coalesces bursts into one write; the record is
|
|
121
102
|
* built at WRITE time from a builder, so late mutations are picked up
|
|
122
|
-
* - optional record codec (compress/encrypt at rest): encode on every write,
|
|
123
|
-
* decode on every read; a decode failure is corrupt-file semantics
|
|
124
103
|
* - loadAll skips `.tmp-*` orphans, corrupt JSON, and records whose
|
|
125
104
|
* filename does not match their id — one bad file never blocks boot; it
|
|
126
105
|
* reconciles a canonical record against its spill by savedAt (freshest wins)
|
|
@@ -139,7 +118,6 @@ export declare class StateStore<T> {
|
|
|
139
118
|
private readonly legacyFn?;
|
|
140
119
|
private readonly relPathFn?;
|
|
141
120
|
private readonly validateFn;
|
|
142
|
-
private readonly codec?;
|
|
143
121
|
private readonly retryAttempts;
|
|
144
122
|
private readonly retryBaseMs;
|
|
145
123
|
private readonly retryMaxMs;
|
|
@@ -188,10 +166,6 @@ export declare class StateStore<T> {
|
|
|
188
166
|
cancelAll(): void;
|
|
189
167
|
private writeInner;
|
|
190
168
|
private envelope;
|
|
191
|
-
/** Serialize an envelope for disk, applying the optional codec. Strings
|
|
192
|
-
* are written as UTF-8 (the fs default); Buffer results pass through as
|
|
193
|
-
* raw bytes. */
|
|
194
|
-
private serialize;
|
|
195
169
|
/** Absolute path for a record: custom relPath (guarded against path
|
|
196
170
|
* escape) or the flat hash default. */
|
|
197
171
|
private resolvePath;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/persist/store.ts"],"names":[],"mappings":"AAKA,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpF;;;;GAIG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,CAAC,CAAC;CACd;AAED;2DAC2D;AAC3D,MAAM,WAAW,cAAc,CAAC,CAAC;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/persist/store.ts"],"names":[],"mappings":"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"}
|
package/dist/prompts.d.ts
CHANGED
|
@@ -43,6 +43,11 @@ export interface ResolvePromptsOptions {
|
|
|
43
43
|
* malformed partial never degrades the canonical rules. Resolve once at host
|
|
44
44
|
* startup, then pass the resulting {@link Prompts} to {@link renderNudgeText}
|
|
45
45
|
* and to the adapter's system-prompt composition so both layers stay consistent.
|
|
46
|
+
*
|
|
47
|
+
* To keep guidance in the system prompt but drop it from every nudge (hosts that
|
|
48
|
+
* deliver these texts once at startup), do NOT override fields here — that would
|
|
49
|
+
* change both surfaces at once. Pass `{ includeGuidance: false }` to
|
|
50
|
+
* {@link renderNudgeText} instead; the system-prompt path is unaffected by it.
|
|
46
51
|
*/
|
|
47
52
|
export declare function resolvePrompts(overrides?: Partial<Prompts>, options?: ResolvePromptsOptions): Prompts;
|
|
48
53
|
//# sourceMappingURL=prompts.d.ts.map
|
package/dist/prompts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,OAAO;IACtB,gFAAgF;IAChF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,OAKhB,CAAC;AAEd,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,OAAO;IACtB,gFAAgF;IAChF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,OAKhB,CAAC;AAEd,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,SAAS,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAC5B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAkBT"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tokenize.ts","../src/compression-rules.ts","../src/prompts.ts","../src/nudge-text.ts","../src/viable.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\n\nexport function defaultCountTokens(text: string): number {\n if (!text) return 0;\n // CJK chars tokenize ~1:1 (chars/4 badly underestimates them). Count them\n // directly, then estimate the non-CJK remainder with chars/4 so digits,\n // punctuation, and symbols in code/JSON are not dropped to zero.\n const cjk = text.match(/[\\u4e00-\\u9fff\\u3040-\\u30ff\\uac00-\\ud7af]/g);\n const cjkCount = cjk?.length ?? 0;\n return cjkCount + Math.ceil((text.length - cjkCount) / 4);\n}\n\nexport function estimateMessageTokens(text: string | undefined): number {\n return defaultCountTokens(text ?? \"\");\n}\n\n/** Guarded contribution of a host-projected thinking payload to the metered\n * message size. Non-finite or non-positive values are treated as absent (0),\n * so a bad host value can never poison the accounting. */\nexport function thinkingTokenValue(thinking: number | undefined): number {\n return typeof thinking === \"number\" &&\n Number.isFinite(thinking) &&\n thinking > 0\n ? thinking\n : 0;\n}\n\n/** Total metered size of a core message: visible text plus any host-projected\n * thinking payload (CoreMessage.thinkingTokens). Every per-message counting\n * site (range recommendations, block compressedTokens, status reports, context\n * breakdown) goes through this so all surfaces share one caliber. */\nexport function countMessageTokens(\n message: { text?: string; thinkingTokens?: number },\n countTokens: TokenCountFn = defaultCountTokens,\n): number {\n return (\n countTokens(message.text ?? \"\") + thinkingTokenValue(message.thinkingTokens)\n );\n}\n\nexport function estimateTokensFast(text: string): number {\n if (!text) return 0;\n return Math.ceil(text.length / 4);\n}\n\nexport type TokenCountFn = (text: string) => number;\n\nconst BPE_SIZE_GUARD = 100_000;\n\nexport function createBpeTokenizer(): TokenCountFn {\n try {\n const mod = require(\"@anthropic-ai/tokenizer\");\n const bpeCount = mod.countTokens ?? mod.default?.countTokens;\n if (typeof bpeCount !== \"function\") return defaultCountTokens;\n return (text: string) => {\n if (text.length > BPE_SIZE_GUARD) return defaultCountTokens(text);\n try {\n return bpeCount(text);\n } catch {\n return defaultCountTokens(text);\n }\n };\n } catch {\n return defaultCountTokens;\n }\n}\n","/**\n * Compression rule texts — VERBATIM copy from context-compress-algorithms (MIT, ours).\n * These were tuned over months of production use.\n *\n * 2026-09-06 amendment (owner-approved; billion-context-pi#309 incident): write-side\n * quote-fidelity rules merged INTO the tuned text. Verbatim user quotes now require\n * a message ref; recorded task state is labeled as history; unicode written directly.\n * Motivation: session 01a071dc blocks b60/b61 stored a fabricated\n * `user verbatim '合并了 下一个'` as a live \"CURRENT TASK\", causing loop relapses.\n */\n\nexport 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 — the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;\n\nexport 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. The summary records the PAST as of this block's creation: label recorded task state as history (\"TASK AS OF THIS BLOCK: ...\") — never as a live instruction, so a later reader treats it as settled context, not something to re-execute. Write plain text with real unicode characters; never copy \\\\uXXXX escape sequences or JSON-escaped fragments out of tool output.\n\nKEEP VERBATIM — 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\\`) — 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 — 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 — you need the literal string to grep for it later).\n- Key details from reports and analyses — not just the conclusion. Keep the comparison numbers and the mechanism, not \"X is worse\" alone (write \"1.76× PPL gap because KV store is static\", not \"KVNet underperforms\").\n- Decisions and their rationale (\"chose X over Y because Z\" — 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 — quote short user messages verbatim ONLY WITH their message ref, e.g. \\`User said (m00132): \"ship it tonight\"\\`. Without a verifiable ref, paraphrase (\\`user previously asked (paraphrased): ...\\`) — this is the one exception to the verbatim rule above; never present a reconstructed or half-remembered phrase as a verbatim quote. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Quotes are historical records, never current directives. Losing these changes the task itself.\n- The user's overall goal and any changes to it — 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 → 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 — 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 — losing these changes what work appears to remain.\n- Message refs of key anchors (\\`m00420\\`, \\`m00510–m00520\\`) — they let you or a later reader jump back via decompress to the exact original.\n\nDROP — 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 — search hits, agent return values, successful tool outputs — 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 — 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 — 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 — 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 — not narrative prose. If the range spans distinct concerns (request → findings → 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.`;\n\nexport const TIER2_DISTILL_RULES = `TIER 2 COMPRESSION — 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 — these are the only things that survive distillation:\n- Decisions and their rationale (\"chose X over Y because Z\" — 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- User quotes and task state only as attributed history: keep the source ref with any user quote; never carry a tier-1 \"CURRENT TASK\" claim forward as a live directive — relabel it \"TASK AS OF THIS BLOCK\".\n- Whether content is OBSOLETE or SUPERSEDED — mark with one line: \"[SUPERSEDED by PR #NNN]\" or \"[OBSOLETE: deleted in vX.Y.Z]\". Do NOT keep the obsolete content's details — just the marker and reason.\n- Function/class/type names and module paths that are the SUBJECT of the work — e.g., \"fixed filterCompressedRanges in prune.ts\", \"added SessionStateRegistry in state.ts\". Not exact line numbers or full signatures — 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 — 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→YK tok, Zx). [original topic]\\`\n Example: \\`Source: b5+b7 (56K+44K→268 tok, 375x). [Tool-result recap + publish]\\`\n- 3-5 bullet points per source block, each a self-contained fact.\n- Dense, scannable — 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 — 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].\"`;\n\nexport const TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION — 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 — when a source block has more facts than the size target allows, keep in this order:\n1. Shipped outcomes (versions released, PRs merged) — these are permanent record.\n2. Open work (PRs/issues still pending) — 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→YK 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 — just the fact.\n- Format: \"[PR/Issue/Version] — [outcome in ≤8 words]\"\n- Merge related facts from different source blocks if they concern the same topic.\n\nEXAMPLES:\n- \"v1.13.0 shipped — quality gate + GC fix (7 PRs)\"\n- \"PR #196 merged — preserve-first-user (supersedes #169)\"\n- \"Bug 1214 fixed — compress consumed all user messages\"\n- \"Chose compress-as-anchor — prefix cache benefit over synthetic injection\"\n- \"Constraint: AGENTS.md forbids as any — 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\") — drop UNLESS the failure is likely to recur and the block is <30 days old.\n- Design rationale details — keep the decision, drop the \"because\" unless it's a critical constraint.\n- Anything marked [OBSOLETE] or [SUPERSEDED] — 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 ≈ N × 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;\n","import {\n COMPRESS_PHILOSOPHY,\n HOW_TO_COMPRESS_RULES,\n TIER2_DISTILL_RULES,\n TIER3_CONDENSE_RULES,\n} from \"./compression-rules.js\";\n\n/**\n * Overridable prompt text consumed by the kernel's nudge renderer and, via the\n * adapter, the system prompt. Every field here is LOAD-BEARING: these rules\n * were tuned over months of production use and are quality-critical. Overriding\n * them can degrade summary quality (loss of paths / signatures / decisions →\n * broken retrieval), so {@link resolvePrompts} requires `{ acknowledgeRisk: true }`.\n *\n * Surface-level text (summary section headers, status-report chrome, tool\n * descriptions) is intentionally NOT part of this interface — it is owned by\n * the adapter or a later \"prompt-set format\" layer and is safe to customize\n * freely. See DESIGN.md for the load-bearing vs surface classification.\n */\nexport interface Prompts {\n /** Core compression philosophy. Embedded in the system prompt + every nudge. */\n compressPhilosophy: string;\n /** Rules the model follows when writing a tier-1 summary. */\n howToCompressRules: string;\n /** Rules for tier-2 distillation of existing summaries. */\n tier2DistillRules: string;\n /** Rules for tier-3 ultra-condensation of distilled summaries. */\n tier3CondenseRules: string;\n}\n\n/**\n * The kernel's canonical prompt values (verbatim from compression-rules.ts).\n * Frozen so a buggy caller cannot mutate the shared singleton and corrupt\n * every other consumer of {@link defaultPrompts}.\n */\nexport const defaultPrompts: Prompts = Object.freeze({\n compressPhilosophy: COMPRESS_PHILOSOPHY,\n howToCompressRules: HOW_TO_COMPRESS_RULES,\n tier2DistillRules: TIER2_DISTILL_RULES,\n tier3CondenseRules: TIER3_CONDENSE_RULES,\n}) as Prompts;\n\nexport interface ResolvePromptsOptions {\n /**\n * Must be `true` to override any prompt field. Every {@link Prompts} field is\n * load-bearing; overriding without acknowledging the quality risk is a\n * programming error and throws.\n */\n acknowledgeRisk?: boolean;\n}\n\n/**\n * Merge prompt overrides onto the kernel defaults. All fields are load-bearing,\n * so ANY override requires `{ acknowledgeRisk: true }`.\n *\n * Only `string`-valued overrides take effect: an explicit `undefined`/`null` or\n * a wrong type is silently dropped (never clobbers a good default), so a\n * malformed partial never degrades the canonical rules. Resolve once at host\n * startup, then pass the resulting {@link Prompts} to {@link renderNudgeText}\n * and to the adapter's system-prompt composition so both layers stay consistent.\n */\nexport function resolvePrompts(\n overrides?: Partial<Prompts>,\n options: ResolvePromptsOptions = {},\n): Prompts {\n const clean: Partial<Prompts> = {};\n if (overrides) {\n for (const [key, value] of Object.entries(overrides)) {\n if (typeof value === \"string\") {\n (clean as Record<string, unknown>)[key] = value;\n }\n }\n }\n const keys = Object.keys(clean) as (keyof Prompts)[];\n if (keys.length > 0 && !options.acknowledgeRisk) {\n throw new Error(\n `resolvePrompts: overriding compression rules requires { acknowledgeRisk: true }. ` +\n `Overridden keys: ${keys.join(\", \")}. These rules are quality-critical (tuned over months of production use); ` +\n `changing them can degrade summary quality and break retrieval (summaries may lose paths, signatures, decisions).`,\n );\n }\n return { ...defaultPrompts, ...clean };\n}\n","import type { NudgeDecision, CompressibleRange, ProtectedRange, ContextBreakdown, CompressionBlock } from \"./types.js\";\nimport { defaultPrompts } from \"./prompts.js\";\nimport type { Prompts } from \"./prompts.js\";\n\nexport type NudgeVoice = \"gentle\" | \"emergency\";\n\nexport interface RenderedNudge {\n voice: NudgeVoice;\n text: string;\n}\n\nfunction efficiencyNote(prompts: Prompts): string {\n return `This is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full.\\n\\n${prompts.compressPhilosophy}`;\n}\n\nfunction emergencyHeader(prompts: Prompts): string {\n return `⚠️ Context limit reached — compress now. Prioritize consumed tool outputs.\\n\\n${prompts.compressPhilosophy}`;\n}\n\nfunction formatK(n: number): string {\n if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;\n return `${n}`;\n}\n\nfunction formatBreakdown(bd?: ContextBreakdown): string {\n if (!bd) return \"\";\n const parts: string[] = [];\n if (bd.system > 0) parts.push(`${formatK(bd.system)} system`);\n if (bd.tool > 0) parts.push(`${formatK(bd.tool)} tool`);\n if (bd.summaries > 0) parts.push(`${formatK(bd.summaries)} summaries`);\n if (bd.code > 0) parts.push(`${formatK(bd.code)} code`);\n if (bd.text > 0) parts.push(`${formatK(bd.text)} text`);\n const growth = bd.growth > 0 ? `\\n+${formatK(bd.growth)} since last nudge` : \"\";\n return `Context breakdown: ${parts.join(\" | \")}${growth}`;\n}\n\n\n\nfunction formatTierTargetBlocks(blocks: CompressionBlock[]): string {\n if (blocks.length === 0) {\n return \"Target blocks: (none — no tier blocks found)\";\n }\n const lines = blocks.map((b) => {\n const summaryTokens = Math.ceil((b.summary ?? \"\").length / 4);\n const topic = b.topic ? ` \"${b.topic}\"` : \"\";\n return ` ${b.blockId} ${b.effectiveMessageIds.length} msgs ${formatK(b.compressedTokens)}→${formatK(summaryTokens)}${topic}`;\n });\n return `Target ${blocks[0]!.tier === 1 ? \"tier-1\" : \"tier-2\"} blocks to distill (${blocks.length}):\\n${lines.join(\"\\n\")}`;\n}\n\nexport function formatRanges(compressible: CompressibleRange[], protectedRanges: ProtectedRange[]): string {\n if (compressible.length === 0 && protectedRanges.length === 0) {\n return \"[No specific ranges detected — compress any consumed content.]\";\n }\n\n // Merge compressible + protected into a single oldest-first list, mirroring\n // opencode-acp's formatCompressibleRanges. Splitting them into two sections\n // lost the time order and hid overlaps; a range can be partly compressible\n // and partly protected, which only the merged view shows correctly.\n interface Merged {\n startRef: string; endRef: string; startNum: number; endNum: number;\n count: number; tokens: number;\n compressibleTokens: number; compressibleCount: number;\n protectedTokens: number; protectedCount: number; protectedTools: string[];\n toolPct: number; textPct: number; dangerous: boolean;\n }\n const refNum = (ref: string): number => {\n const m = ref.match(/\\d+/);\n return m ? parseInt(m[0], 10) : 0;\n };\n const entries: Merged[] = [];\n for (const r of compressible) {\n entries.push({\n startRef: r.startRef, endRef: r.endRef, startNum: refNum(r.startRef), endNum: refNum(r.endRef),\n count: r.count, tokens: r.tokens, toolPct: r.toolPct, textPct: r.textPct,\n compressibleTokens: r.tokens, compressibleCount: r.count,\n protectedTokens: 0, protectedCount: 0, protectedTools: [], dangerous: r.dangerous ?? false,\n });\n }\n for (const r of protectedRanges) {\n entries.push({\n startRef: r.startRef, endRef: r.endRef, startNum: refNum(r.startRef), endNum: refNum(r.endRef),\n count: r.count, tokens: r.tokens, toolPct: 0, textPct: 0,\n compressibleTokens: 0, compressibleCount: 0,\n protectedTokens: r.tokens, protectedCount: r.count, protectedTools: [...r.tools], dangerous: false,\n });\n }\n entries.sort((a, b) => a.startNum - b.startNum);\n // Merge adjacent/overlapping ranges (gap ≤ 1 ref).\n const merged: Merged[] = [];\n for (const e of entries) {\n const last = merged[merged.length - 1];\n if (last && e.startNum <= last.endNum + 1) {\n last.endRef = e.endRef;\n last.endNum = Math.max(last.endNum, e.endNum);\n last.count += e.count;\n last.tokens += e.tokens;\n last.compressibleTokens += e.compressibleTokens;\n last.compressibleCount += e.compressibleCount;\n last.protectedTokens += e.protectedTokens;\n last.protectedCount += e.protectedCount;\n if (e.dangerous) last.dangerous = true;\n for (const t of e.protectedTools) {\n if (!last.protectedTools.includes(t)) last.protectedTools.push(t);\n }\n } else {\n merged.push({ ...e });\n }\n }\n const lines = merged.map((e) => {\n const suffix = e.dangerous && e.compressibleTokens > 0 ? \" ⚠️ NOT recommended unless you are certain.\" : \"\";\n if (e.protectedTokens > 0 && e.compressibleTokens === 0) {\n return ` ${e.startRef}–${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(\", \")} — not compressible]${suffix}`;\n }\n if (e.protectedTokens > 0 && e.compressibleTokens > 0) {\n return ` ${e.startRef}–${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(\", \")}]${suffix}`;\n }\n return ` ${e.startRef}–${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;\n });\n return `Compressible ranges (${merged.length}, oldest first):\\n${lines.join(\"\\n\")}`;\n}\n\nexport function renderNudgeText(decision: NudgeDecision, prompts: Prompts = defaultPrompts): RenderedNudge {\n const breakdownStr = formatBreakdown(decision.contextBreakdown);\n const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);\n const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;\n\n if (decision.tier !== null && decision.tier >= 2) {\n const isT2 = decision.tier === 2;\n const targets = decision.tierTargetBlocks ?? [];\n const blockList = formatTierTargetBlocks(targets);\n const startId = targets[0]?.blockId ?? \"b1\";\n const endId = targets[targets.length - 1]?.blockId ?? \"b5\";\n const voice: NudgeVoice = isEmergency ? \"emergency\" : \"gentle\";\n const triggerLine = isEmergency\n ? `[EMERGENCY — TIER ${decision.tier} ${isT2 ? \"DISTILLATION\" : \"CONDENSATION\"}] Context limit reached — distill NOW into a denser summary to reclaim tokens.`\n : `[TIER ${decision.tier} ${isT2 ? \"DISTILLATION\" : \"CONDENSATION\"} TRIGGER]`;\n return {\n voice,\n text: [\n efficiencyNote(prompts),\n \"\",\n breakdownStr,\n \"\",\n triggerLine,\n isT2\n ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well — apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.`\n : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well — apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,\n blockList,\n `Example: compress({ content: [{ startId: \"${startId}\", endId: \"${endId}\", summary: \"...\" }] })`,\n \"\",\n prompts.howToCompressRules,\n \"\",\n isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules,\n ].join(\"\\n\"),\n };\n }\n\n if (isEmergency) {\n return {\n voice: \"emergency\",\n text: [\n emergencyHeader(prompts),\n \"\",\n breakdownStr,\n \"\",\n prompts.howToCompressRules,\n \"\",\n `{ \"topic\": \"...\", \"content\": [{ \"startId\": \"<ID>\", \"endId\": \"<ID>\", \"summary\": \"...\" }] }`,\n \"Only use IDs from visible messages above. Compress older work first.\",\n \"\",\n rangesStr,\n ].join(\"\\n\"),\n };\n }\n\n return {\n voice: \"gentle\",\n text: [\n efficiencyNote(prompts),\n \"\",\n breakdownStr,\n \"\",\n prompts.howToCompressRules,\n \"\",\n rangesStr,\n \"\",\n `💡 Compress all ranges in one call (pass multiple content entries: \\`content: [{...}, {...}]\\`).`,\n ].join(\"\\n\"),\n };\n}\n","/** Minimum size for a compressible range to be worth recommending. Ranges\n * below this are fragmented leftovers (a 16-token ack, a one-line tool\n * result): the model cannot write a meaningful >=50-char summary for them,\n * and a batched compress call that includes one gets atomically rejected\n * (the kernel validates the whole batch). Observed in the wild: a 14-range\n * recommendation list containing a 16-token range → every batch attempt\n * failed with \"Summary too short\". Apply on every surface that recommends\n * ranges: the injected nudge, acp_status, and the /acp panel. */\nexport const VIABLE_RANGE_MIN_TOKENS = 200;\n\nexport function viableRanges<T extends { tokens: number }>(ranges: T[]): T[] {\n return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);\n}\n"],"mappings":"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","names":["require","require"]}
|