pi-cache-optimizer 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +85 -15
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -51,11 +51,12 @@ const NO_SKILL_COMPRESSION_ENV = "PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION";
51
51
  const DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY";
52
52
 
53
53
  // WORM-flag: if optimizeSystemPrompt ever detects that its blind-replace
54
- // logic has accidentally truncated the trellis `<workflow-state>` block
55
- // (or any structural marker from an upstream extension), we flip this.
56
- // publishStatus reads it once, appends a footer warning, then resets it.
57
- // The flag surface is kept separate from the regular cache-stats counter
58
- // so that a one-turn glitch doesn't poison the persisted metrics.
54
+ // logic has accidentally truncated a structural marker (any XML tag or
55
+ // HTML comment boundary marker present in the original prompt), we flip
56
+ // this. publishStatus reads it once, appends a footer warning, then
57
+ // resets it. The flag surface is kept separate from the regular
58
+ // cache-stats counter so that a one-turn glitch doesn't poison the
59
+ // persisted metrics.
59
60
  let promptTruncationDetected = false;
60
61
 
61
62
  // Minimum count of skills before compression is worth applying.
@@ -387,6 +388,62 @@ function stripSessionOverviewChurn(prompt: string): string {
387
388
  return before + cleaned + after;
388
389
  }
389
390
 
391
+ /**
392
+ * Extract structural markers from a prompt for the integrity guard.
393
+ *
394
+ * The guard runs in `optimizeSystemPrompt` to catch cases where the
395
+ * blind `rest.replace(part, "")` reorder accidentally eats text inside
396
+ * an extension-injected structural block (e.g., trellis
397
+ * `<workflow-state>`, a hypothetical `<task-tracker>`, or AGENTS.md
398
+ * `<!-- TRELLIS:START -->` markers). When the original prompt contains
399
+ * a marker that the result is missing, we fall back to the original
400
+ * prompt rather than ship a corrupted one.
401
+ *
402
+ * Three marker categories are recognized (covers ~99% of real-world
403
+ * extension injection patterns in the pi ecosystem):
404
+ *
405
+ * 1. XML-style opening tags `<tagname>` (lowercase, alpha-num + `_`/`-`)
406
+ * 2. XML-style closing tags `</tagname>`
407
+ * 3. HTML comment START/END `<!-- NAME:START -->` / `<!-- NAME:END -->`
408
+ *
409
+ * Tags with attributes (e.g., `<task id="42">`) are not currently emitted
410
+ * by any pi extension we know of and are skipped to keep the regex tight.
411
+ * Markdown headers, horizontal rules, and timestamp patterns are not
412
+ * usable as guards because they have no closing form to verify.
413
+ *
414
+ * The check is deliberately set-based (presence/absence) rather than
415
+ * count-based: a single occurrence per request is the universal
416
+ * convention, and a count drop with the same set of unique tags would
417
+ * be a different class of bug not catchable here.
418
+ */
419
+ function extractStructuralMarkers(prompt: string): {
420
+ openingTags: Set<string>;
421
+ closingTags: Set<string>;
422
+ commentMarkers: Set<string>;
423
+ } {
424
+ const openingTags = new Set<string>();
425
+ const closingTags = new Set<string>();
426
+ const commentMarkers = new Set<string>();
427
+
428
+ // Opening tags: <tagname> with no attributes and no leading slash.
429
+ // Tagname must start with a letter and contain only alpha-num, `-`, `_`.
430
+ for (const match of prompt.matchAll(/<([a-z][a-z0-9_-]*)>/gi)) {
431
+ openingTags.add(match[1].toLowerCase());
432
+ }
433
+ // Closing tags: </tagname>
434
+ for (const match of prompt.matchAll(/<\/([a-z][a-z0-9_-]*)>/gi)) {
435
+ closingTags.add(match[1].toLowerCase());
436
+ }
437
+ // HTML comments with NAME:START or NAME:END inside.
438
+ // Trellis emits `<!-- TRELLIS:START -->` / `<!-- TRELLIS:END -->` in
439
+ // the AGENTS.md managed block; other extensions follow this convention.
440
+ for (const match of prompt.matchAll(/<!--\s*([A-Z][A-Z0-9_-]*):(START|END)\s*-->/g)) {
441
+ commentMarkers.add(`${match[1]}:${match[2]}`);
442
+ }
443
+
444
+ return { openingTags, closingTags, commentMarkers };
445
+ }
446
+
390
447
  function optimizeSystemPrompt(
391
448
  original: string,
392
449
  opts: BuildSystemPromptOptions,
@@ -420,17 +477,29 @@ function optimizeSystemPrompt(
420
477
  stablePrefix +
421
478
  (dynamicRemainder.length > 0 ? "\n\n---\n\n" + dynamicRemainder : "");
422
479
 
423
- // Sanity check: if trellis (or another extension) injected structural
424
- // markers into the prompt that happen to share a substring with one of
425
- // our stable candidates, the blind `rest.replace(part, "")` could
426
- // silently eat part of the dynamic layer. We anchor on
427
- // `<workflow-state>` because it is the most stable structural marker
428
- // trellis emits and is never a stable candidate itself.
480
+ // Sanity check: scan ALL structural markers (XML tags + HTML comment
481
+ // boundary markers) in the original and verify each one survives the
482
+ // reorder. If any marker drops, the blind `rest.replace(part, "")`
483
+ // logic ate something it shouldn't have fall back to the original
484
+ // prompt and flag the footer warning. This is provider-agnostic and
485
+ // extension-agnostic: trellis `<workflow-state>`, a hypothetical
486
+ // `<task-tracker>`, AGENTS.md `<!-- TRELLIS:START -->`, etc., are all
487
+ // protected without code changes when new extensions ship.
429
488
  //
430
- // When the marker was present in the original but is missing in the
431
- // result, the reorder is unsafe fall back to the original prompt
432
- // so the model gets a complete prompt, and flag the footer warning.
433
- if (original.includes("<workflow-state>") && !systemPrompt.includes("<workflow-state>")) {
489
+ // Our skills compression runs BEFORE optimizeSystemPrompt and replaces
490
+ // pi's verbose `<available_skills>` block with a compressed text
491
+ // section that has no XML tag. So `original` here (post-compression)
492
+ // does not contain `<available_skills>` and the result doesn't either
493
+ // — no false positive.
494
+ const originalMarkers = extractStructuralMarkers(original);
495
+ const resultMarkers = extractStructuralMarkers(systemPrompt);
496
+
497
+ const missing =
498
+ [...originalMarkers.openingTags].some((tag) => !resultMarkers.openingTags.has(tag)) ||
499
+ [...originalMarkers.closingTags].some((tag) => !resultMarkers.closingTags.has(tag)) ||
500
+ [...originalMarkers.commentMarkers].some((m) => !resultMarkers.commentMarkers.has(m));
501
+
502
+ if (missing) {
434
503
  promptTruncationDetected = true;
435
504
  return { systemPrompt: original, stablePrefix: "", changed: false };
436
505
  }
@@ -1240,6 +1309,7 @@ export const __internals_for_tests = {
1240
1309
  buildStableCandidates,
1241
1310
  optimizeSystemPrompt,
1242
1311
  stripSessionOverviewChurn,
1312
+ extractStructuralMarkers,
1243
1313
  formatSkillsForPrompt,
1244
1314
  formatSkillsForPromptCompressed,
1245
1315
  compressSkillsInSystemPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Pi extension that improves provider-side KV/prompt cache hit rates (DeepSeek, OpenAI, Claude, Gemini) by reordering the system prompt, requesting long retention, and showing footer cache stats. Renamed from pi-deepseek-cache-optimizer.",
5
5
  "keywords": [
6
6
  "pi-package",