shariq-pi-extensions 0.3.1 → 0.3.2

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.
@@ -50,6 +50,7 @@ export interface SmartCompactionDetails {
50
50
  serializedCharacters: number;
51
51
  summaryCharacters: number;
52
52
  attemptCount: number;
53
+ retainedIdentifiersAppended?: number;
53
54
  durationMs: number;
54
55
  timestamp: number;
55
56
  }
@@ -328,7 +329,9 @@ const REQUIRED_SECTION_PATTERNS = [
328
329
  /## 6\.\s+Resume Anchor/i,
329
330
  ];
330
331
 
331
- export function validateSummaryOutput(response: AssistantMessage, protectedFacts: readonly string[] = []): string {
332
+ export const RETAINED_IDENTIFIERS_HEADING = "### Retained Identifiers";
333
+
334
+ export function extractSummaryText(response: AssistantMessage): string {
332
335
  if (response.stopReason !== "stop") {
333
336
  const errorDetails = response.errorMessage ? `: ${response.errorMessage}` : "";
334
337
  throw new Error(`Compaction model did not complete successfully (stopReason="${response.stopReason}"${errorDetails}).`);
@@ -350,13 +353,38 @@ export function validateSummaryOutput(response: AssistantMessage, protectedFacts
350
353
  throw new Error("Compaction model returned an empty summary.");
351
354
  }
352
355
 
356
+ return rawSummaryText;
357
+ }
358
+
359
+ export function getDroppedProtectedFacts(summaryText: string, protectedFacts: readonly string[] = []): string[] {
360
+ return protectedFacts.filter((fact) => fact && !summaryText.includes(fact));
361
+ }
362
+
363
+ export function withRetainedIdentifiers(summaryText: string, droppedFacts: readonly string[]): string {
364
+ // Protected facts are single-line opaque identifiers (hashes, UUIDs, clean
365
+ // URLs, IPs), so reproducing them on their own bullet lines is verbatim-safe.
366
+ const lines = [summaryText.trimEnd(), "", RETAINED_IDENTIFIERS_HEADING];
367
+ for (const fact of droppedFacts) {
368
+ const clean = String(fact).trim();
369
+ if (clean) lines.push(`- ${clean}`);
370
+ }
371
+ return lines.join("\n") + "\n";
372
+ }
373
+
374
+ export function validateSummaryText(summaryText: string, protectedFacts: readonly string[] = []): string {
375
+ const rawSummaryText = summaryText.trim();
376
+
377
+ if (!rawSummaryText) {
378
+ throw new Error("Compaction model returned an empty summary.");
379
+ }
380
+
353
381
  // Verify all 6 required sections exist
354
382
  for (const pattern of REQUIRED_SECTION_PATTERNS) {
355
383
  if (!pattern.test(rawSummaryText)) {
356
384
  throw new Error(`Compaction summary is incomplete: missing required section matching ${pattern.source}`);
357
385
  }
358
386
  }
359
- const missingFacts = protectedFacts.filter((fact) => !rawSummaryText.includes(fact));
387
+ const missingFacts = getDroppedProtectedFacts(rawSummaryText, protectedFacts);
360
388
  if (missingFacts.length > 0) {
361
389
  throw new Error(`Compaction summary dropped protected facts: ${missingFacts.slice(0, 3).join(" | ")}`);
362
390
  }
@@ -364,6 +392,29 @@ export function validateSummaryOutput(response: AssistantMessage, protectedFacts
364
392
  return rawSummaryText;
365
393
  }
366
394
 
395
+ export function validateSummaryOutput(response: AssistantMessage, protectedFacts: readonly string[] = []): string {
396
+ return validateSummaryText(extractSummaryText(response), protectedFacts);
397
+ }
398
+
399
+ /**
400
+ * Return the summary text when it is structurally sound (extractable with all
401
+ * required sections) regardless of dropped protected facts. Identifier-only
402
+ * defects can be repaired deterministically with {@link withRetainedIdentifiers};
403
+ * structural defects cannot, so those return undefined.
404
+ */
405
+ export function extractRepairableSummary(response: AssistantMessage): string | undefined {
406
+ let text: string;
407
+ try {
408
+ text = extractSummaryText(response);
409
+ } catch {
410
+ return undefined;
411
+ }
412
+ for (const pattern of REQUIRED_SECTION_PATTERNS) {
413
+ if (!pattern.test(text)) return undefined;
414
+ }
415
+ return text;
416
+ }
417
+
367
418
  export function computeCompactionTokenCeiling(
368
419
  model: Model<Api>,
369
420
  config: SmartCompactionConfig,
@@ -529,6 +580,8 @@ export async function runSmartCompaction(
529
580
 
530
581
  let lastError: Error | undefined;
531
582
  let finalSummaryText = "";
583
+ let repairCandidate: string | undefined;
584
+ let retainedIdentifiersAppended = 0;
532
585
  let accumulatedUsage: Usage | undefined;
533
586
  let attemptCount = 0;
534
587
  let activeModel = primaryModel;
@@ -572,7 +625,16 @@ export async function runSmartCompaction(
572
625
  if (response.usage) {
573
626
  accumulatedUsage = combineCompactionUsage(accumulatedUsage, response.usage);
574
627
  }
575
- finalSummaryText = validateSummaryOutput(response, protectedFacts);
628
+ try {
629
+ finalSummaryText = validateSummaryOutput(response, protectedFacts);
630
+ } catch (validationError) {
631
+ // Structurally sound summaries that only drop protected facts are
632
+ // kept as deterministic-repair candidates; structural defects cannot
633
+ // be repaired, so those leave no candidate behind.
634
+ const repairable = extractRepairableSummary(response);
635
+ if (repairable !== undefined) repairCandidate = repairable;
636
+ throw validationError;
637
+ }
576
638
  lastError = undefined;
577
639
  break; // Success!
578
640
  } catch (err) {
@@ -586,6 +648,23 @@ export async function runSmartCompaction(
586
648
  }
587
649
  }
588
650
 
651
+ if ((lastError || !finalSummaryText) && repairCandidate !== undefined) {
652
+ // Every stage produced a structurally sound summary but the model
653
+ // deterministically refused to repeat low-signal identifiers. Restore
654
+ // them verbatim instead of failing the whole compaction.
655
+ const stillMissing = getDroppedProtectedFacts(repairCandidate, protectedFacts);
656
+ try {
657
+ finalSummaryText = validateSummaryText(
658
+ withRetainedIdentifiers(repairCandidate, stillMissing),
659
+ protectedFacts,
660
+ );
661
+ lastError = undefined;
662
+ retainedIdentifiersAppended = stillMissing.length;
663
+ } catch {
664
+ // Repair rejected; fall through to the original failure below.
665
+ }
666
+ }
667
+
589
668
  if (lastError || !finalSummaryText) {
590
669
  throw lastError ?? new Error("All smart compaction retry stages failed.");
591
670
  }
@@ -643,6 +722,7 @@ export async function runSmartCompaction(
643
722
  serializedCharacters: conversationText.length,
644
723
  summaryCharacters: finalSummary.length,
645
724
  attemptCount,
725
+ retainedIdentifiersAppended: retainedIdentifiersAppended > 0 ? retainedIdentifiersAppended : undefined,
646
726
  durationMs: Date.now() - startedAt,
647
727
  timestamp: Date.now(),
648
728
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shariq-pi-extensions",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",