pi-smart-compact 7.11.0 → 7.12.4
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/CHANGELOG.md +49 -0
- package/README.md +18 -3
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +613 -82
- package/dist/phases/synthesize.d.ts +1 -1
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +5 -13
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts +3 -1
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +1 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/dist/utils/tokens.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/constants.ts
|
|
3
|
-
var VERSION = "7.
|
|
3
|
+
var VERSION = "7.12.4";
|
|
4
4
|
var CHARS_PER_TOKEN = 3.8;
|
|
5
5
|
var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
|
|
6
6
|
var PROFILES = {
|
|
@@ -35,9 +35,10 @@ var DEFAULT_CONFIG = {
|
|
|
35
35
|
summaryModel: null,
|
|
36
36
|
segmentationModel: null,
|
|
37
37
|
autoTrigger: true,
|
|
38
|
-
autoTriggerTimeoutMs:
|
|
38
|
+
autoTriggerTimeoutMs: 120000,
|
|
39
39
|
backupEnabled: true,
|
|
40
|
-
backupDir: ""
|
|
40
|
+
backupDir: "",
|
|
41
|
+
minContextPercent: 60
|
|
41
42
|
};
|
|
42
43
|
var NO_OP_RE = /applied:\s*0|no changes applied|nothing to (?:do|change)|0 edits? applied/i;
|
|
43
44
|
var SHIFT_RE = /simdi|peki|bide|bi de|gecelim|bakalim|yapalim|baska|sonra|tamam simdi|now let|also|next|let's|moving on|switch to/i;
|
|
@@ -201,8 +202,9 @@ function debug(msg, ...args) {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
// src/utils/helpers.ts
|
|
205
|
+
var VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
206
|
+
var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
|
|
204
207
|
function validateSmartCompactConfig(sc) {
|
|
205
|
-
const VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
206
208
|
if ("profile" in sc && !VALID_PROFILES.includes(sc.profile)) {
|
|
207
209
|
warn("smart-compact config: invalid profile '" + sc.profile + "', expected light|balanced|aggressive. Using default 'balanced'.");
|
|
208
210
|
delete sc.profile;
|
|
@@ -223,9 +225,37 @@ function validateSmartCompactConfig(sc) {
|
|
|
223
225
|
warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
|
|
224
226
|
delete sc.segmentationModel;
|
|
225
227
|
}
|
|
226
|
-
if ("profiles" in sc
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if ("profiles" in sc) {
|
|
229
|
+
if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
|
|
230
|
+
warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
|
|
231
|
+
delete sc.profiles;
|
|
232
|
+
} else {
|
|
233
|
+
const profiles = sc.profiles;
|
|
234
|
+
for (const [profileName, value] of Object.entries(profiles)) {
|
|
235
|
+
if (!VALID_PROFILES.includes(profileName)) {
|
|
236
|
+
warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
|
|
237
|
+
delete profiles[profileName];
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
241
|
+
warn("smart-compact config: profile '" + profileName + "' must be an object.");
|
|
242
|
+
delete profiles[profileName];
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const profileCfg = value;
|
|
246
|
+
for (const [key, raw] of Object.entries(profileCfg)) {
|
|
247
|
+
if (!PROFILE_NUMERIC_KEYS.includes(key)) {
|
|
248
|
+
warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
|
|
249
|
+
delete profileCfg[key];
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
|
|
253
|
+
warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
|
|
254
|
+
delete profileCfg[key];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
229
259
|
}
|
|
230
260
|
if ("autoTriggerTimeoutMs" in sc) {
|
|
231
261
|
const v = sc.autoTriggerTimeoutMs;
|
|
@@ -234,6 +264,13 @@ function validateSmartCompactConfig(sc) {
|
|
|
234
264
|
delete sc.autoTriggerTimeoutMs;
|
|
235
265
|
}
|
|
236
266
|
}
|
|
267
|
+
if ("minContextPercent" in sc) {
|
|
268
|
+
const v = sc.minContextPercent;
|
|
269
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) {
|
|
270
|
+
warn("smart-compact config: minContextPercent must be 0\u2013100, got " + v + ". Using default " + DEFAULT_CONFIG.minContextPercent + ".");
|
|
271
|
+
delete sc.minContextPercent;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
237
274
|
}
|
|
238
275
|
var _cfg = null;
|
|
239
276
|
var _cfgMtime = 0;
|
|
@@ -474,12 +511,14 @@ Read: ` + (cs.filesRead.join(", ") || "None");
|
|
|
474
511
|
function buildExtractionContext(extraction, forRange) {
|
|
475
512
|
const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
|
|
476
513
|
const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
|
|
514
|
+
const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
|
|
477
515
|
return [
|
|
478
516
|
"## Deterministic Extraction (verified facts)",
|
|
479
517
|
"Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
|
|
480
518
|
"Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
|
|
481
519
|
"Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 60)).join("; ") || "none"),
|
|
482
|
-
"Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none")
|
|
520
|
+
"Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none"),
|
|
521
|
+
"Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
|
|
483
522
|
].join(`
|
|
484
523
|
`);
|
|
485
524
|
}
|
|
@@ -507,9 +546,11 @@ function computeToolCharPercentage(branchEntries) {
|
|
|
507
546
|
}
|
|
508
547
|
return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
|
|
509
548
|
}
|
|
510
|
-
function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold) {
|
|
549
|
+
function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
|
|
511
550
|
if (totalTokens < minThreshold)
|
|
512
551
|
return "none";
|
|
552
|
+
if (contextPercent < minContextPercent)
|
|
553
|
+
return "none";
|
|
513
554
|
if (contextPercent < 45 && toolPercent < 60)
|
|
514
555
|
return "none";
|
|
515
556
|
if (contextPercent < 80)
|
|
@@ -550,9 +591,6 @@ function buildExplorationContext(report) {
|
|
|
550
591
|
`);
|
|
551
592
|
}
|
|
552
593
|
|
|
553
|
-
// src/core.ts
|
|
554
|
-
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
555
|
-
|
|
556
594
|
// src/utils/tokens.ts
|
|
557
595
|
var PROVIDER_MAP = {
|
|
558
596
|
"zai-anthropic": {
|
|
@@ -562,7 +600,22 @@ var PROVIDER_MAP = {
|
|
|
562
600
|
instructionFollowing: "high",
|
|
563
601
|
tokenRatioEstimate: 3.5,
|
|
564
602
|
concurrencyLimit: 3,
|
|
565
|
-
cacheStrategy: "anthropic"
|
|
603
|
+
cacheStrategy: "anthropic",
|
|
604
|
+
timeoutMultiplier: 1.2,
|
|
605
|
+
singlePassTokenMultiplier: 1,
|
|
606
|
+
multimodal: "metadata-only"
|
|
607
|
+
},
|
|
608
|
+
"kimi-coding": {
|
|
609
|
+
maxOutputTokens: 8192,
|
|
610
|
+
supportsTools: "probe",
|
|
611
|
+
jsonReliability: "high",
|
|
612
|
+
instructionFollowing: "high",
|
|
613
|
+
tokenRatioEstimate: 3.5,
|
|
614
|
+
concurrencyLimit: 2,
|
|
615
|
+
cacheStrategy: "anthropic",
|
|
616
|
+
timeoutMultiplier: 1.5,
|
|
617
|
+
singlePassTokenMultiplier: 0.95,
|
|
618
|
+
multimodal: "metadata-only"
|
|
566
619
|
},
|
|
567
620
|
anthropic: {
|
|
568
621
|
maxOutputTokens: 8192,
|
|
@@ -571,7 +624,10 @@ var PROVIDER_MAP = {
|
|
|
571
624
|
instructionFollowing: "high",
|
|
572
625
|
tokenRatioEstimate: 3.5,
|
|
573
626
|
concurrencyLimit: 3,
|
|
574
|
-
cacheStrategy: "anthropic"
|
|
627
|
+
cacheStrategy: "anthropic",
|
|
628
|
+
timeoutMultiplier: 1.2,
|
|
629
|
+
singlePassTokenMultiplier: 1,
|
|
630
|
+
multimodal: "native"
|
|
575
631
|
},
|
|
576
632
|
openai: {
|
|
577
633
|
maxOutputTokens: 16384,
|
|
@@ -580,7 +636,10 @@ var PROVIDER_MAP = {
|
|
|
580
636
|
instructionFollowing: "high",
|
|
581
637
|
tokenRatioEstimate: 4,
|
|
582
638
|
concurrencyLimit: 5,
|
|
583
|
-
cacheStrategy: "openai"
|
|
639
|
+
cacheStrategy: "openai",
|
|
640
|
+
timeoutMultiplier: 1,
|
|
641
|
+
singlePassTokenMultiplier: 1.15,
|
|
642
|
+
multimodal: "native"
|
|
584
643
|
},
|
|
585
644
|
google: {
|
|
586
645
|
maxOutputTokens: 8192,
|
|
@@ -589,7 +648,10 @@ var PROVIDER_MAP = {
|
|
|
589
648
|
instructionFollowing: "high",
|
|
590
649
|
tokenRatioEstimate: 3.8,
|
|
591
650
|
concurrencyLimit: 3,
|
|
592
|
-
cacheStrategy: "openai"
|
|
651
|
+
cacheStrategy: "openai",
|
|
652
|
+
timeoutMultiplier: 1.15,
|
|
653
|
+
singlePassTokenMultiplier: 1.1,
|
|
654
|
+
multimodal: "native"
|
|
593
655
|
},
|
|
594
656
|
deepseek: {
|
|
595
657
|
maxOutputTokens: 8192,
|
|
@@ -598,7 +660,10 @@ var PROVIDER_MAP = {
|
|
|
598
660
|
instructionFollowing: "medium",
|
|
599
661
|
tokenRatioEstimate: 3.6,
|
|
600
662
|
concurrencyLimit: 2,
|
|
601
|
-
cacheStrategy: "none"
|
|
663
|
+
cacheStrategy: "none",
|
|
664
|
+
timeoutMultiplier: 1.5,
|
|
665
|
+
singlePassTokenMultiplier: 0.85,
|
|
666
|
+
multimodal: "metadata-only"
|
|
602
667
|
},
|
|
603
668
|
minimax: {
|
|
604
669
|
maxOutputTokens: 4096,
|
|
@@ -607,7 +672,10 @@ var PROVIDER_MAP = {
|
|
|
607
672
|
instructionFollowing: "medium",
|
|
608
673
|
tokenRatioEstimate: 3.8,
|
|
609
674
|
concurrencyLimit: 2,
|
|
610
|
-
cacheStrategy: "anthropic"
|
|
675
|
+
cacheStrategy: "anthropic",
|
|
676
|
+
timeoutMultiplier: 1.6,
|
|
677
|
+
singlePassTokenMultiplier: 0.8,
|
|
678
|
+
multimodal: "metadata-only"
|
|
611
679
|
},
|
|
612
680
|
"xiaomi-token-plan": {
|
|
613
681
|
maxOutputTokens: 8192,
|
|
@@ -616,7 +684,34 @@ var PROVIDER_MAP = {
|
|
|
616
684
|
instructionFollowing: "medium",
|
|
617
685
|
tokenRatioEstimate: 3.3,
|
|
618
686
|
concurrencyLimit: 2,
|
|
619
|
-
cacheStrategy: "openai"
|
|
687
|
+
cacheStrategy: "openai",
|
|
688
|
+
timeoutMultiplier: 1.35,
|
|
689
|
+
singlePassTokenMultiplier: 0.9,
|
|
690
|
+
multimodal: "metadata-only"
|
|
691
|
+
},
|
|
692
|
+
"xiaomi-mimo": {
|
|
693
|
+
maxOutputTokens: 8192,
|
|
694
|
+
supportsTools: "probe",
|
|
695
|
+
jsonReliability: "medium",
|
|
696
|
+
instructionFollowing: "medium",
|
|
697
|
+
tokenRatioEstimate: 3.3,
|
|
698
|
+
concurrencyLimit: 2,
|
|
699
|
+
cacheStrategy: "anthropic",
|
|
700
|
+
timeoutMultiplier: 1.35,
|
|
701
|
+
singlePassTokenMultiplier: 0.9,
|
|
702
|
+
multimodal: "metadata-only"
|
|
703
|
+
},
|
|
704
|
+
crofai: {
|
|
705
|
+
maxOutputTokens: 8192,
|
|
706
|
+
supportsTools: "probe",
|
|
707
|
+
jsonReliability: "medium",
|
|
708
|
+
instructionFollowing: "medium",
|
|
709
|
+
tokenRatioEstimate: 3.8,
|
|
710
|
+
concurrencyLimit: 3,
|
|
711
|
+
cacheStrategy: "none",
|
|
712
|
+
timeoutMultiplier: 1.2,
|
|
713
|
+
singlePassTokenMultiplier: 0.95,
|
|
714
|
+
multimodal: "metadata-only"
|
|
620
715
|
},
|
|
621
716
|
mistral: {
|
|
622
717
|
maxOutputTokens: 8192,
|
|
@@ -625,7 +720,10 @@ var PROVIDER_MAP = {
|
|
|
625
720
|
instructionFollowing: "high",
|
|
626
721
|
tokenRatioEstimate: 3.5,
|
|
627
722
|
concurrencyLimit: 3,
|
|
628
|
-
cacheStrategy: "openai"
|
|
723
|
+
cacheStrategy: "openai",
|
|
724
|
+
timeoutMultiplier: 1.2,
|
|
725
|
+
singlePassTokenMultiplier: 1,
|
|
726
|
+
multimodal: "metadata-only"
|
|
629
727
|
},
|
|
630
728
|
xai: {
|
|
631
729
|
maxOutputTokens: 8192,
|
|
@@ -634,18 +732,24 @@ var PROVIDER_MAP = {
|
|
|
634
732
|
instructionFollowing: "high",
|
|
635
733
|
tokenRatioEstimate: 3.8,
|
|
636
734
|
concurrencyLimit: 3,
|
|
637
|
-
cacheStrategy: "openai"
|
|
735
|
+
cacheStrategy: "openai",
|
|
736
|
+
timeoutMultiplier: 1.2,
|
|
737
|
+
singlePassTokenMultiplier: 1,
|
|
738
|
+
multimodal: "native"
|
|
638
739
|
}
|
|
639
740
|
};
|
|
640
741
|
var PROVIDER_ALIASES = [
|
|
641
742
|
{ pattern: /anthropic/i, provider: "anthropic" },
|
|
743
|
+
{ pattern: /kimi/i, provider: "kimi-coding" },
|
|
642
744
|
{ pattern: /zai/i, provider: "zai-anthropic" },
|
|
643
745
|
{ pattern: /openai/i, provider: "openai" },
|
|
644
746
|
{ pattern: /gpt/i, provider: "openai" },
|
|
645
747
|
{ pattern: /google|gemini/i, provider: "google" },
|
|
646
748
|
{ pattern: /deepseek/i, provider: "deepseek" },
|
|
647
749
|
{ pattern: /minimax/i, provider: "minimax" },
|
|
750
|
+
{ pattern: /xiaomi-mimo/i, provider: "xiaomi-mimo" },
|
|
648
751
|
{ pattern: /xiaomi/i, provider: "xiaomi-token-plan" },
|
|
752
|
+
{ pattern: /crofai/i, provider: "crofai" },
|
|
649
753
|
{ pattern: /mistral/i, provider: "mistral" },
|
|
650
754
|
{ pattern: /xai|grok/i, provider: "xai" }
|
|
651
755
|
];
|
|
@@ -656,7 +760,10 @@ var DEFAULT_CAPS = {
|
|
|
656
760
|
instructionFollowing: "medium",
|
|
657
761
|
tokenRatioEstimate: 3.8,
|
|
658
762
|
concurrencyLimit: 2,
|
|
659
|
-
cacheStrategy: "none"
|
|
763
|
+
cacheStrategy: "none",
|
|
764
|
+
timeoutMultiplier: 1.35,
|
|
765
|
+
singlePassTokenMultiplier: 0.9,
|
|
766
|
+
multimodal: "metadata-only"
|
|
660
767
|
};
|
|
661
768
|
function getProviderCaps(provider) {
|
|
662
769
|
if (PROVIDER_MAP[provider])
|
|
@@ -764,6 +871,7 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
764
871
|
recordMetric({
|
|
765
872
|
phase,
|
|
766
873
|
model: model.id,
|
|
874
|
+
provider: model.provider,
|
|
767
875
|
inputTokens: inputT,
|
|
768
876
|
outputTokens: outputT,
|
|
769
877
|
cacheHitTokens: cacheT,
|
|
@@ -783,6 +891,7 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
783
891
|
recordMetric({
|
|
784
892
|
phase,
|
|
785
893
|
model: model.id,
|
|
894
|
+
provider: model.provider,
|
|
786
895
|
inputTokens: 0,
|
|
787
896
|
outputTokens: 0,
|
|
788
897
|
cacheHitTokens: 0,
|
|
@@ -840,10 +949,12 @@ function mergeExtractions(base, delta, baseMsgCount) {
|
|
|
840
949
|
...f,
|
|
841
950
|
lastModifiedIndex: f.lastModifiedIndex + baseMsgCount
|
|
842
951
|
}));
|
|
952
|
+
const offsetMedia = (delta.mediaAttachments ?? []).map((a) => ({ ...a, index: a.index + baseMsgCount }));
|
|
843
953
|
return {
|
|
844
954
|
modifiedFiles: [...new Map([...base.modifiedFiles, ...offsetModifiedFiles].map((f) => [f.path, f])).values()],
|
|
845
955
|
readFiles: [...new Set([...base.readFiles, ...delta.readFiles])],
|
|
846
956
|
deletedFiles: [...new Set([...base.deletedFiles, ...delta.deletedFiles])],
|
|
957
|
+
mediaAttachments: [...base.mediaAttachments ?? [], ...offsetMedia],
|
|
847
958
|
errors: [...base.errors, ...offsetErrors],
|
|
848
959
|
decisions: [...base.decisions, ...offsetDecisions],
|
|
849
960
|
constraints: [...base.constraints, ...offsetConstraints],
|
|
@@ -873,6 +984,256 @@ function appendMetricsLog(sessionId, extra) {
|
|
|
873
984
|
warn("appendMetricsLog failed", e);
|
|
874
985
|
}
|
|
875
986
|
}
|
|
987
|
+
function readMetricsLog(limit = 100) {
|
|
988
|
+
try {
|
|
989
|
+
const logPath = path2.join(CACHE_DIR, "compact-metrics.jsonl");
|
|
990
|
+
if (!fs2.existsSync(logPath))
|
|
991
|
+
return [];
|
|
992
|
+
const entries = [];
|
|
993
|
+
for (const line of fs2.readFileSync(logPath, "utf8").trim().split(`
|
|
994
|
+
`).filter(Boolean).slice(-limit * 2)) {
|
|
995
|
+
try {
|
|
996
|
+
entries.push(JSON.parse(line));
|
|
997
|
+
} catch {
|
|
998
|
+
warn("Skipping corrupt compact metrics line");
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return entries.slice(-limit);
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
warn("readMetricsLog failed", e);
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function escapeHtml(value) {
|
|
1008
|
+
return String(value ?? "").replace(/[&<>\"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
1009
|
+
}
|
|
1010
|
+
function durationOf(entry) {
|
|
1011
|
+
return entry.durationMs ?? entry.phaseTimings?.reduce((sum, phase) => sum + phase.durationMs, 0) ?? 0;
|
|
1012
|
+
}
|
|
1013
|
+
function percentile(values, p) {
|
|
1014
|
+
if (!values.length)
|
|
1015
|
+
return 0;
|
|
1016
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1017
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor(p / 100 * sorted.length)))];
|
|
1018
|
+
}
|
|
1019
|
+
function average(values) {
|
|
1020
|
+
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
1021
|
+
}
|
|
1022
|
+
function compactNumber(value) {
|
|
1023
|
+
return new Intl.NumberFormat("en", { notation: Math.abs(value) >= 1e4 ? "compact" : "standard", maximumFractionDigits: 1 }).format(value);
|
|
1024
|
+
}
|
|
1025
|
+
function formatNumber(value) {
|
|
1026
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1027
|
+
return escapeHtml(value);
|
|
1028
|
+
return value.toLocaleString();
|
|
1029
|
+
}
|
|
1030
|
+
function formatMs(value) {
|
|
1031
|
+
if (!value)
|
|
1032
|
+
return "0ms";
|
|
1033
|
+
if (value >= 60000)
|
|
1034
|
+
return (value / 60000).toFixed(value >= 600000 ? 0 : 1) + "m";
|
|
1035
|
+
if (value >= 1000)
|
|
1036
|
+
return (value / 1000).toFixed(value >= 1e4 ? 0 : 1) + "s";
|
|
1037
|
+
return Math.round(value) + "ms";
|
|
1038
|
+
}
|
|
1039
|
+
function formatPercent(value) {
|
|
1040
|
+
return Math.round(value * 100) + "%";
|
|
1041
|
+
}
|
|
1042
|
+
function statusClass(status) {
|
|
1043
|
+
if (status === "timeout" || status === "error")
|
|
1044
|
+
return "bad";
|
|
1045
|
+
if (status === "dry-run")
|
|
1046
|
+
return "warn";
|
|
1047
|
+
return "good";
|
|
1048
|
+
}
|
|
1049
|
+
function statusLabel(status) {
|
|
1050
|
+
return status ?? "success";
|
|
1051
|
+
}
|
|
1052
|
+
function badge(status) {
|
|
1053
|
+
const label = statusLabel(status);
|
|
1054
|
+
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
1055
|
+
}
|
|
1056
|
+
function summarizeDashboard(entries) {
|
|
1057
|
+
const durations = entries.map(durationOf).filter(Boolean);
|
|
1058
|
+
const success = entries.filter((e) => statusLabel(e.status) === "success").length;
|
|
1059
|
+
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
1060
|
+
const error = entries.filter((e) => e.status === "error").length;
|
|
1061
|
+
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
1062
|
+
const scored = entries.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
1063
|
+
return {
|
|
1064
|
+
runs: entries.length,
|
|
1065
|
+
success,
|
|
1066
|
+
timeout,
|
|
1067
|
+
error,
|
|
1068
|
+
dryRun,
|
|
1069
|
+
successRate: entries.length ? success / entries.length : 0,
|
|
1070
|
+
avgDuration: Math.round(average(durations)),
|
|
1071
|
+
p95Duration: percentile(durations, 95),
|
|
1072
|
+
totalCalls: entries.reduce((sum, e) => sum + e.totalCalls, 0),
|
|
1073
|
+
totalInput: entries.reduce((sum, e) => sum + e.totalInput, 0),
|
|
1074
|
+
totalOutput: entries.reduce((sum, e) => sum + e.totalOutput, 0),
|
|
1075
|
+
totalSaved: entries.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1076
|
+
avgScore: Math.round(average(scored))
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function groupMetrics(entries, keyFn) {
|
|
1080
|
+
const groups = new Map;
|
|
1081
|
+
for (const entry of entries) {
|
|
1082
|
+
const key = keyFn(entry) || "unknown";
|
|
1083
|
+
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
1084
|
+
}
|
|
1085
|
+
return [...groups.entries()].map(([name, group]) => {
|
|
1086
|
+
const durations = group.map(durationOf).filter(Boolean);
|
|
1087
|
+
const scores = group.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
1088
|
+
const failures = group.filter((e) => e.status === "timeout" || e.status === "error").length;
|
|
1089
|
+
return {
|
|
1090
|
+
name,
|
|
1091
|
+
runs: group.length,
|
|
1092
|
+
avgDuration: Math.round(average(durations)),
|
|
1093
|
+
p95Duration: percentile(durations, 95),
|
|
1094
|
+
avgScore: Math.round(average(scores)),
|
|
1095
|
+
totalSaved: group.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1096
|
+
totalCalls: group.reduce((sum, e) => sum + e.totalCalls, 0),
|
|
1097
|
+
errorRate: group.length ? failures / group.length : 0
|
|
1098
|
+
};
|
|
1099
|
+
}).sort((a, b) => b.runs - a.runs || a.name.localeCompare(b.name));
|
|
1100
|
+
}
|
|
1101
|
+
function progressBar(value, label = formatPercent(value)) {
|
|
1102
|
+
const pct = Math.max(0, Math.min(100, Math.round(value * 100)));
|
|
1103
|
+
return `<div class="meter" title="${escapeHtml(label)}"><span style="width:${pct}%"></span></div>`;
|
|
1104
|
+
}
|
|
1105
|
+
function sparkline(values) {
|
|
1106
|
+
const nums = values.filter((v) => Number.isFinite(v));
|
|
1107
|
+
if (nums.length < 2)
|
|
1108
|
+
return `<div class="empty">Need at least two runs for trend</div>`;
|
|
1109
|
+
const width = 520;
|
|
1110
|
+
const height = 120;
|
|
1111
|
+
const min = Math.min(...nums);
|
|
1112
|
+
const max = Math.max(...nums);
|
|
1113
|
+
const span = Math.max(1, max - min);
|
|
1114
|
+
const points = nums.map((value, i) => {
|
|
1115
|
+
const x = i / Math.max(1, nums.length - 1) * width;
|
|
1116
|
+
const y = height - (value - min) / span * (height - 18) - 9;
|
|
1117
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
1118
|
+
}).join(" ");
|
|
1119
|
+
const last = nums[nums.length - 1];
|
|
1120
|
+
return `<svg class="spark" viewBox="0 0 ${width} ${height}" role="img" aria-label="Duration trend"><polyline points="${points}" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><circle cx="${width}" cy="${(height - (last - min) / span * (height - 18) - 9).toFixed(1)}" r="4" fill="currentColor"/><text x="0" y="14">${escapeHtml(formatMs(max))}</text><text x="0" y="${height - 4}">${escapeHtml(formatMs(min))}</text></svg>`;
|
|
1121
|
+
}
|
|
1122
|
+
function metricCard(label, value, detail, tone = "neutral") {
|
|
1123
|
+
return `<article class="card ${tone}"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(value)}</div><div class="detail">${escapeHtml(detail)}</div></article>`;
|
|
1124
|
+
}
|
|
1125
|
+
function comparisonRows(groups) {
|
|
1126
|
+
if (!groups.length)
|
|
1127
|
+
return `<tr><td colspan="8" class="empty">No data yet</td></tr>`;
|
|
1128
|
+
return groups.map((group) => `<tr>
|
|
1129
|
+
<td><strong>${escapeHtml(group.name)}</strong></td>
|
|
1130
|
+
<td class="num">${formatNumber(group.runs)}</td>
|
|
1131
|
+
<td class="num">${escapeHtml(formatMs(group.avgDuration))}</td>
|
|
1132
|
+
<td class="num">${escapeHtml(formatMs(group.p95Duration))}</td>
|
|
1133
|
+
<td class="num">${group.avgScore ? formatNumber(group.avgScore) : "\u2014"}</td>
|
|
1134
|
+
<td class="num">${formatNumber(group.totalCalls)}</td>
|
|
1135
|
+
<td class="num">${formatNumber(group.totalSaved)}</td>
|
|
1136
|
+
<td>${progressBar(1 - group.errorRate, formatPercent(1 - group.errorRate) + " reliable")}</td>
|
|
1137
|
+
</tr>`).join(`
|
|
1138
|
+
`);
|
|
1139
|
+
}
|
|
1140
|
+
function phaseRows(entry) {
|
|
1141
|
+
const timings = entry?.phaseTimings ?? [];
|
|
1142
|
+
if (!timings.length)
|
|
1143
|
+
return `<tr><td colspan="3" class="empty">No phase timings yet</td></tr>`;
|
|
1144
|
+
const total = timings.reduce((sum, phase) => sum + phase.durationMs, 0) || 1;
|
|
1145
|
+
return timings.map((phase) => `<tr>
|
|
1146
|
+
<td>${escapeHtml(phase.phase)}</td>
|
|
1147
|
+
<td class="num">${escapeHtml(formatMs(phase.durationMs))}</td>
|
|
1148
|
+
<td>${progressBar(phase.durationMs / total, formatPercent(phase.durationMs / total))}</td>
|
|
1149
|
+
</tr>`).join(`
|
|
1150
|
+
`);
|
|
1151
|
+
}
|
|
1152
|
+
function recentRunRows(entries) {
|
|
1153
|
+
if (!entries.length)
|
|
1154
|
+
return `<tr><td colspan="11" class="empty">No runs recorded yet</td></tr>`;
|
|
1155
|
+
return entries.slice(-80).reverse().map((entry) => `<tr>
|
|
1156
|
+
<td class="mono small">${escapeHtml(entry.ts)}</td>
|
|
1157
|
+
<td>${escapeHtml(entry.profile)}</td>
|
|
1158
|
+
<td>${escapeHtml(entry.provider ?? entry.model?.split("/")[0])}</td>
|
|
1159
|
+
<td>${escapeHtml(entry.method)}</td>
|
|
1160
|
+
<td>${escapeHtml(entry.runType)}</td>
|
|
1161
|
+
<td>${badge(entry.status)}</td>
|
|
1162
|
+
<td class="num">${escapeHtml(formatMs(durationOf(entry)))}</td>
|
|
1163
|
+
<td class="num">${typeof entry.verificationScore === "number" ? formatNumber(entry.verificationScore) : "\u2014"}</td>
|
|
1164
|
+
<td class="num">${typeof entry.tokensSaved === "number" ? formatNumber(entry.tokensSaved) : "\u2014"}</td>
|
|
1165
|
+
<td class="num">${formatNumber(entry.totalCalls)}</td>
|
|
1166
|
+
<td class="mono small reason">${escapeHtml(entry.fallbackReason ?? "")}</td>
|
|
1167
|
+
</tr>`).join(`
|
|
1168
|
+
`);
|
|
1169
|
+
}
|
|
1170
|
+
function dashboardCss() {
|
|
1171
|
+
return `:root{color-scheme:dark;--bg:#08111f;--surface:#0f172a;--surface2:#111c33;--card:#111827;--text:#e5edf8;--muted:#8fa3bf;--line:#24324a;--accent:#60a5fa;--good:#22c55e;--bad:#fb7185;--warn:#fbbf24;--shadow:0 18px 50px rgba(0,0,0,.28)}@media(prefers-color-scheme:light){:root{color-scheme:light;--bg:#f4f7fb;--surface:#ffffff;--surface2:#f8fafc;--card:#ffffff;--text:#0f172a;--muted:#64748b;--line:#e2e8f0;--shadow:0 18px 50px rgba(15,23,42,.08)}}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top left,rgba(96,165,250,.20),transparent 34rem),var(--bg);color:var(--text);font:14px/1.5 Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}main{max-width:1280px;margin:0 auto;padding:32px}header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:var(--accent);font-weight:700;text-transform:uppercase;letter-spacing:.08em;font-size:12px}h1{font-size:32px;line-height:1.1;margin:6px 0 6px}.muted,.detail{color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin:20px 0 22px}.card{background:linear-gradient(180deg,rgba(255,255,255,.035),transparent),var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:var(--shadow)}.card.good{border-color:rgba(34,197,94,.45)}.card.warn{border-color:rgba(251,191,36,.45)}.card.bad{border-color:rgba(251,113,133,.5)}.label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:700}.value{font-size:28px;font-weight:800;margin-top:6px}.layout{display:grid;grid-template-columns:1.15fr .85fr;gap:18px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow);overflow:hidden}.panel h2{display:flex;align-items:center;justify-content:space-between;margin:0;padding:15px 18px;background:linear-gradient(180deg,rgba(255,255,255,.035),transparent),var(--surface2);font-size:15px}.table-wrap{overflow:auto;max-height:560px}table{border-collapse:separate;border-spacing:0;width:100%}th,td{border-bottom:1px solid var(--line);padding:9px 11px;text-align:left;vertical-align:middle;white-space:nowrap}th{position:sticky;top:0;z-index:1;background:var(--surface2);color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.06em}tr:hover td{background:rgba(96,165,250,.06)}.num{text-align:right}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.small{font-size:12px}.reason{max-width:280px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.badge{display:inline-flex;align-items:center;border-radius:999px;padding:3px 9px;font-size:12px;font-weight:800}.badge.good{background:rgba(34,197,94,.14);color:var(--good)}.badge.bad{background:rgba(251,113,133,.16);color:var(--bad)}.badge.warn{background:rgba(251,191,36,.16);color:var(--warn)}.meter{height:8px;background:rgba(148,163,184,.22);border-radius:99px;min-width:96px;overflow:hidden}.meter span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--accent),var(--good))}.spark{width:100%;height:160px;color:var(--accent);padding:18px}.spark text{fill:var(--muted);font-size:12px}.empty{padding:18px;color:var(--muted);text-align:center}pre{white-space:pre-wrap;background:var(--surface);border:1px solid var(--line);border-radius:18px;padding:16px;overflow:auto}.section{margin-top:18px}.two{display:grid;grid-template-columns:1fr 1fr;gap:18px}@media(max-width:960px){main{padding:20px}.cards,.layout,.two{grid-template-columns:1fr}header{display:block}th,td{padding:8px}.value{font-size:24px}}`;
|
|
1172
|
+
}
|
|
1173
|
+
function buildMetricsReport(entries = readMetricsLog(100)) {
|
|
1174
|
+
if (!entries.length)
|
|
1175
|
+
return "No smart-compact metrics recorded yet.";
|
|
1176
|
+
const summary = summarizeDashboard(entries);
|
|
1177
|
+
const byProfile = groupMetrics(entries, (e) => e.profile ?? "unknown");
|
|
1178
|
+
const byProvider = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0] ?? "unknown");
|
|
1179
|
+
const summarizeGroup = (group) => "- " + group.name + ": n=" + group.runs + ", avg=" + group.avgDuration + "ms, p95=" + group.p95Duration + "ms, score=" + group.avgScore + ", saved=" + group.totalSaved + "t, reliability=" + formatPercent(1 - group.errorRate);
|
|
1180
|
+
return [
|
|
1181
|
+
"# Smart Compact Metrics",
|
|
1182
|
+
"",
|
|
1183
|
+
"Runs: " + summary.runs + " (success " + summary.success + ", dry-run " + summary.dryRun + ", timeout " + summary.timeout + ", error " + summary.error + ")",
|
|
1184
|
+
"Reliability: " + formatPercent(summary.successRate),
|
|
1185
|
+
"Latency: avg " + summary.avgDuration + "ms, p95 " + summary.p95Duration + "ms",
|
|
1186
|
+
"LLM calls: " + summary.totalCalls + ", input " + summary.totalInput + "t, output " + summary.totalOutput + "t",
|
|
1187
|
+
"Tokens saved: " + summary.totalSaved + "t, average verification score: " + summary.avgScore,
|
|
1188
|
+
"",
|
|
1189
|
+
"## Profile comparison",
|
|
1190
|
+
...byProfile.map(summarizeGroup),
|
|
1191
|
+
"",
|
|
1192
|
+
"## Provider comparison",
|
|
1193
|
+
...byProvider.map(summarizeGroup)
|
|
1194
|
+
].join(`
|
|
1195
|
+
`);
|
|
1196
|
+
}
|
|
1197
|
+
function writeMetricsDashboard(entries = readMetricsLog(200)) {
|
|
1198
|
+
try {
|
|
1199
|
+
if (!fs2.existsSync(CACHE_DIR))
|
|
1200
|
+
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
1201
|
+
const summary = summarizeDashboard(entries);
|
|
1202
|
+
const latest = entries[entries.length - 1];
|
|
1203
|
+
const report = buildMetricsReport(entries);
|
|
1204
|
+
const profileGroups = groupMetrics(entries, (e) => e.profile ?? "unknown");
|
|
1205
|
+
const providerGroups = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0] ?? "unknown");
|
|
1206
|
+
const healthTone = summary.error + summary.timeout > 0 ? "warn" : "good";
|
|
1207
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Smart Compact Metrics</title><style>${dashboardCss()}</style></head><body><main>
|
|
1208
|
+
<header><div><div class="eyebrow">pi-smart-compact</div><h1>Operational Metrics</h1><div class="muted">Generated ${escapeHtml(new Date().toISOString())} \xB7 ${formatNumber(entries.length)} recent runs \xB7 local file dashboard</div></div><div>${badge(latest?.status)} ${latest ? `<span class="muted">latest ${escapeHtml(latest.profile ?? "unknown")}</span>` : ""}</div></header>
|
|
1209
|
+
<section class="cards">
|
|
1210
|
+
${metricCard("Reliability", formatPercent(summary.successRate), `${summary.success} success \xB7 ${summary.timeout} timeout \xB7 ${summary.error} error`, healthTone)}
|
|
1211
|
+
${metricCard("Avg duration", formatMs(summary.avgDuration), `p95 ${formatMs(summary.p95Duration)}`)}
|
|
1212
|
+
${metricCard("LLM calls", compactNumber(summary.totalCalls), `${compactNumber(summary.totalInput)} input \xB7 ${compactNumber(summary.totalOutput)} output`)}
|
|
1213
|
+
${metricCard("Tokens saved", compactNumber(summary.totalSaved), `avg score ${summary.avgScore || "\u2014"}`)}
|
|
1214
|
+
</section>
|
|
1215
|
+
<section class="layout">
|
|
1216
|
+
<div class="panel"><h2>Duration trend <span class="muted">last ${Math.min(entries.length, 80)} runs</span></h2>${sparkline(entries.slice(-80).map(durationOf))}</div>
|
|
1217
|
+
<div class="panel"><h2>Latest phase timings</h2><div class="table-wrap"><table><thead><tr><th>Phase</th><th class="num">Duration</th><th>Share</th></tr></thead><tbody>${phaseRows(latest)}</tbody></table></div></div>
|
|
1218
|
+
</section>
|
|
1219
|
+
<section class="two section">
|
|
1220
|
+
<div class="panel"><h2>Profile comparison</h2><div class="table-wrap"><table><thead><tr><th>Profile</th><th class="num">Runs</th><th class="num">Avg</th><th class="num">p95</th><th class="num">Score</th><th class="num">Calls</th><th class="num">Saved</th><th>Reliability</th></tr></thead><tbody>${comparisonRows(profileGroups)}</tbody></table></div></div>
|
|
1221
|
+
<div class="panel"><h2>Provider comparison</h2><div class="table-wrap"><table><thead><tr><th>Provider</th><th class="num">Runs</th><th class="num">Avg</th><th class="num">p95</th><th class="num">Score</th><th class="num">Calls</th><th class="num">Saved</th><th>Reliability</th></tr></thead><tbody>${comparisonRows(providerGroups)}</tbody></table></div></div>
|
|
1222
|
+
</section>
|
|
1223
|
+
<section class="panel section"><h2>Recent runs</h2><div class="table-wrap"><table><thead><tr><th>Time</th><th>Profile</th><th>Provider</th><th>Method</th><th>Run</th><th>Status</th><th class="num">Duration</th><th class="num">Score</th><th class="num">Saved</th><th class="num">Calls</th><th>Reason</th></tr></thead><tbody>${recentRunRows(entries)}</tbody></table></div></section>
|
|
1224
|
+
<section class="section"><h2>Raw text report</h2><pre>${escapeHtml(report)}</pre></section>
|
|
1225
|
+
</main></body></html>`;
|
|
1226
|
+
const fp = path2.join(CACHE_DIR, "smart-compact-report.html");
|
|
1227
|
+
fs2.writeFileSync(fp, html);
|
|
1228
|
+
return fp;
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
warn("writeMetricsDashboard failed", e);
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/core.ts
|
|
1236
|
+
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
876
1237
|
|
|
877
1238
|
// src/utils/extraction.ts
|
|
878
1239
|
import path3 from "path";
|
|
@@ -910,6 +1271,41 @@ function extractText(content) {
|
|
|
910
1271
|
}).join("");
|
|
911
1272
|
return "";
|
|
912
1273
|
}
|
|
1274
|
+
function mediaKind(type, mime) {
|
|
1275
|
+
const s = (type + " " + (mime ?? "")).toLowerCase();
|
|
1276
|
+
if (/image|input_image|image_url/.test(s))
|
|
1277
|
+
return "image";
|
|
1278
|
+
if (/audio/.test(s))
|
|
1279
|
+
return "audio";
|
|
1280
|
+
if (/video/.test(s))
|
|
1281
|
+
return "video";
|
|
1282
|
+
if (/file|document|pdf|attachment/.test(s))
|
|
1283
|
+
return "file";
|
|
1284
|
+
return "unknown";
|
|
1285
|
+
}
|
|
1286
|
+
function extractMediaAttachments(msgs) {
|
|
1287
|
+
const out = [];
|
|
1288
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
1289
|
+
const blocks = Array.isArray(msgs[i].content) ? msgs[i].content : [];
|
|
1290
|
+
for (const b of blocks) {
|
|
1291
|
+
if (!b || typeof b !== "object")
|
|
1292
|
+
continue;
|
|
1293
|
+
const rec = b;
|
|
1294
|
+
const type = String(rec.type ?? "");
|
|
1295
|
+
if (type === "text" || type === "toolCall" || type === "tool_use")
|
|
1296
|
+
continue;
|
|
1297
|
+
const mimeType = rec.mimeType ?? rec.mime_type ?? rec.mediaType ?? rec.media_type;
|
|
1298
|
+
const name = rec.name ?? rec.filename ?? rec.fileName ?? rec.title;
|
|
1299
|
+
const sizeBytes = rec.sizeBytes ?? rec.size_bytes ?? rec.size;
|
|
1300
|
+
const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
|
|
1301
|
+
const kind = mediaKind(type, mimeType);
|
|
1302
|
+
if (kind !== "unknown" || source || mimeType || name) {
|
|
1303
|
+
out.push({ index: i, kind, mimeType, name, sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined, source });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
913
1309
|
function buildToolCallIndex(msgs) {
|
|
914
1310
|
const idx = new Map;
|
|
915
1311
|
for (let i = 0;i < msgs.length; i++) {
|
|
@@ -1212,14 +1608,14 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
1212
1608
|
});
|
|
1213
1609
|
}
|
|
1214
1610
|
const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|gotta|yapalim|yapmamiz|gerekiyor|eklenecek|d\u00FCzeltilecek|bitmedi|kaldi)/i;
|
|
1215
|
-
for (
|
|
1611
|
+
for (let idx = 0;idx < msgs.length; idx++) {
|
|
1612
|
+
const msg = msgs[idx];
|
|
1216
1613
|
if (msg.role !== "user")
|
|
1217
1614
|
continue;
|
|
1218
1615
|
const txt = extractText(msg.content);
|
|
1219
1616
|
if (txt.length < 10 || txt.startsWith("/"))
|
|
1220
1617
|
continue;
|
|
1221
1618
|
if (FOLLOWUP_RE.test(txt)) {
|
|
1222
|
-
const idx = msgs.indexOf(msg);
|
|
1223
1619
|
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
1224
1620
|
if (!isDup) {
|
|
1225
1621
|
loops.push({
|
|
@@ -1235,12 +1631,12 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
1235
1631
|
}
|
|
1236
1632
|
}
|
|
1237
1633
|
const BLOCKED_RE = /blocked|waiting for|depend|ba[\u011Fg]li|bekliyor|engell/i;
|
|
1238
|
-
for (
|
|
1634
|
+
for (let idx = 0;idx < msgs.length; idx++) {
|
|
1635
|
+
const msg = msgs[idx];
|
|
1239
1636
|
if (msg.role !== "user")
|
|
1240
1637
|
continue;
|
|
1241
1638
|
const txt = extractText(msg.content);
|
|
1242
1639
|
if (BLOCKED_RE.test(txt)) {
|
|
1243
|
-
const idx = msgs.indexOf(msg);
|
|
1244
1640
|
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
1245
1641
|
if (!isDup) {
|
|
1246
1642
|
loops.push({
|
|
@@ -1279,6 +1675,7 @@ function extractStructured(msgs, pc) {
|
|
|
1279
1675
|
const constraints = mineConstraints(msgs);
|
|
1280
1676
|
const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
|
|
1281
1677
|
const timeline = buildTimeline(msgs, errors);
|
|
1678
|
+
const mediaAttachments = extractMediaAttachments(msgs);
|
|
1282
1679
|
const mainGoal = extractMainGoal(msgs);
|
|
1283
1680
|
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
1284
1681
|
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
@@ -1291,6 +1688,7 @@ function extractStructured(msgs, pc) {
|
|
|
1291
1688
|
constraints,
|
|
1292
1689
|
topics,
|
|
1293
1690
|
timeline,
|
|
1691
|
+
mediaAttachments,
|
|
1294
1692
|
mainGoal,
|
|
1295
1693
|
lastUserMessages,
|
|
1296
1694
|
lastErrors,
|
|
@@ -1304,27 +1702,42 @@ import * as path4 from "path";
|
|
|
1304
1702
|
function getSessionsDir() {
|
|
1305
1703
|
return path4.join(process.env.HOME ?? "/tmp", ".pi", "agent", "sessions");
|
|
1306
1704
|
}
|
|
1705
|
+
var LOG_PATH_CACHE_TTL_MS = 30000;
|
|
1706
|
+
var logPathCache = new Map;
|
|
1707
|
+
var messageMapCache = new Map;
|
|
1307
1708
|
function findSessionLogFile(sessionId) {
|
|
1308
1709
|
try {
|
|
1710
|
+
const home = process.env.HOME ?? "/tmp";
|
|
1711
|
+
const cached = logPathCache.get(sessionId);
|
|
1712
|
+
if (cached && cached.home === home && cached.expiresAt > Date.now())
|
|
1713
|
+
return cached.path;
|
|
1309
1714
|
const sessionsDir = getSessionsDir();
|
|
1310
|
-
if (!fs3.existsSync(sessionsDir))
|
|
1715
|
+
if (!fs3.existsSync(sessionsDir)) {
|
|
1716
|
+
logPathCache.set(sessionId, { path: null, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home });
|
|
1311
1717
|
return null;
|
|
1718
|
+
}
|
|
1312
1719
|
for (const subdir of fs3.readdirSync(sessionsDir)) {
|
|
1313
1720
|
const subdirPath = path4.join(sessionsDir, subdir);
|
|
1314
1721
|
const stat = fs3.statSync(subdirPath);
|
|
1315
1722
|
if (!stat.isDirectory())
|
|
1316
1723
|
continue;
|
|
1317
1724
|
const exact = path4.join(subdirPath, sessionId + ".jsonl");
|
|
1318
|
-
if (fs3.existsSync(exact))
|
|
1725
|
+
if (fs3.existsSync(exact)) {
|
|
1726
|
+
logPathCache.set(sessionId, { path: exact, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home });
|
|
1319
1727
|
return exact;
|
|
1728
|
+
}
|
|
1320
1729
|
const files = fs3.readdirSync(subdirPath);
|
|
1321
1730
|
const match = files.find((f) => f.endsWith("_" + sessionId + ".jsonl"));
|
|
1322
|
-
if (match)
|
|
1323
|
-
|
|
1731
|
+
if (match) {
|
|
1732
|
+
const found = path4.join(subdirPath, match);
|
|
1733
|
+
logPathCache.set(sessionId, { path: found, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home });
|
|
1734
|
+
return found;
|
|
1735
|
+
}
|
|
1324
1736
|
}
|
|
1325
1737
|
} catch (e) {
|
|
1326
1738
|
debug("findSessionLogFile failed", e);
|
|
1327
1739
|
}
|
|
1740
|
+
logPathCache.set(sessionId, { path: null, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home: process.env.HOME ?? "/tmp" });
|
|
1328
1741
|
return null;
|
|
1329
1742
|
}
|
|
1330
1743
|
function normalizeLogMessage(msg) {
|
|
@@ -1352,6 +1765,11 @@ function readOriginalMessageMap(sessionId) {
|
|
|
1352
1765
|
return null;
|
|
1353
1766
|
}
|
|
1354
1767
|
try {
|
|
1768
|
+
const stat = fs3.statSync(logPath);
|
|
1769
|
+
const cached = messageMapCache.get(sessionId);
|
|
1770
|
+
if (cached && cached.logPath === logPath && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
1771
|
+
return cached.map;
|
|
1772
|
+
}
|
|
1355
1773
|
const raw = fs3.readFileSync(logPath, "utf-8");
|
|
1356
1774
|
const map = new Map;
|
|
1357
1775
|
for (const line of raw.split(`
|
|
@@ -1371,7 +1789,11 @@ function readOriginalMessageMap(sessionId) {
|
|
|
1371
1789
|
}
|
|
1372
1790
|
}
|
|
1373
1791
|
debug("readOriginalMessageMap: " + map.size + " msgs from " + logPath);
|
|
1374
|
-
|
|
1792
|
+
if (map.size > 0) {
|
|
1793
|
+
messageMapCache.set(sessionId, { logPath, mtimeMs: stat.mtimeMs, size: stat.size, map });
|
|
1794
|
+
return map;
|
|
1795
|
+
}
|
|
1796
|
+
return null;
|
|
1375
1797
|
} catch (e) {
|
|
1376
1798
|
debug("readOriginalMessageMap failed", e);
|
|
1377
1799
|
return null;
|
|
@@ -2486,7 +2908,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
2486
2908
|
}
|
|
2487
2909
|
return merged;
|
|
2488
2910
|
}
|
|
2489
|
-
async function singlePassCompact(convText, extraction, report, prevContext, model, auth, signal) {
|
|
2911
|
+
async function singlePassCompact(convText, extraction, report, prevContext, model, auth, budgetTokens, signal) {
|
|
2490
2912
|
const extractionCtx = buildExtractionContext(extraction);
|
|
2491
2913
|
const explorationCtx = report ? buildExplorationContext(report) : "";
|
|
2492
2914
|
const sessionType = inferSessionType(extraction, report);
|
|
@@ -2501,7 +2923,7 @@ Session-specific instructions:
|
|
|
2501
2923
|
{ role: "user", content: [{ type: "text", text: adaptedPrefix }], timestamp: Date.now() },
|
|
2502
2924
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
2503
2925
|
]
|
|
2504
|
-
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal });
|
|
2926
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budgetTokens, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2505
2927
|
const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2506
2928
|
`).trim();
|
|
2507
2929
|
if (!summary.startsWith("##"))
|
|
@@ -2547,13 +2969,16 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
|
|
|
2547
2969
|
return !v || v === "None" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2548
2970
|
};
|
|
2549
2971
|
const prio = f("Priority").toLowerCase();
|
|
2972
|
+
const sectionFallback = sec.split(`
|
|
2973
|
+
`).slice(1).join(`
|
|
2974
|
+
`).trim().slice(0, 500);
|
|
2975
|
+
const chunkFallback = ch.messages.map((m) => "[" + (m?.role ?? "unknown") + "] " + extractText(m?.content).slice(0, 180)).join(`
|
|
2976
|
+
`).slice(0, 500);
|
|
2550
2977
|
return {
|
|
2551
2978
|
topic: ch.topic,
|
|
2552
2979
|
startIndex: ch.startIndex,
|
|
2553
2980
|
endIndex: ch.endIndex,
|
|
2554
|
-
summary: f("Summary") ||
|
|
2555
|
-
`).slice(1).join(`
|
|
2556
|
-
`).trim().slice(0, 500),
|
|
2981
|
+
summary: f("Summary") || sectionFallback || chunkFallback || "No summary generated for this segment.",
|
|
2557
2982
|
keyDecisions: l("Decisions"),
|
|
2558
2983
|
filesModified: l("Modified"),
|
|
2559
2984
|
filesRead: l("Read"),
|
|
@@ -3037,13 +3462,27 @@ async function showCompactUI(ctx, opts) {
|
|
|
3037
3462
|
|
|
3038
3463
|
// src/core.ts
|
|
3039
3464
|
async function runSmartCompact(opts) {
|
|
3040
|
-
const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact } = opts;
|
|
3465
|
+
const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact, force = false, timeoutMs = 0 } = opts;
|
|
3041
3466
|
if (isRunning.value)
|
|
3042
3467
|
return;
|
|
3043
3468
|
isRunning.value = true;
|
|
3044
3469
|
const pipelineStart = Date.now();
|
|
3470
|
+
const phaseTimings = [];
|
|
3471
|
+
let phaseStart = pipelineStart;
|
|
3472
|
+
const markPhase = (phase) => {
|
|
3473
|
+
const now = Date.now();
|
|
3474
|
+
phaseTimings.push({ phase, durationMs: now - phaseStart });
|
|
3475
|
+
phaseStart = now;
|
|
3476
|
+
};
|
|
3045
3477
|
resetCompactSessionId();
|
|
3046
3478
|
resetMetrics();
|
|
3479
|
+
let sessionId = "unknown";
|
|
3480
|
+
let totalTokens = 0;
|
|
3481
|
+
let contextPercent = 0;
|
|
3482
|
+
let toolPercent = 0;
|
|
3483
|
+
let tier;
|
|
3484
|
+
let methodForMetrics;
|
|
3485
|
+
const modelLabel = summaryModel ? summaryModel.provider + "/" + summaryModel.id : "unknown";
|
|
3047
3486
|
if (!summaryModel || !segModel) {
|
|
3048
3487
|
isRunning.value = false;
|
|
3049
3488
|
if (!autoTriggered)
|
|
@@ -3066,7 +3505,7 @@ async function runSmartCompact(opts) {
|
|
|
3066
3505
|
const apiKey = auth.apiKey;
|
|
3067
3506
|
const apiHeaders = auth.headers;
|
|
3068
3507
|
const usage = ctx.getContextUsage();
|
|
3069
|
-
|
|
3508
|
+
totalTokens = usage?.tokens ?? 0;
|
|
3070
3509
|
const notify = (msg, type = "info") => {
|
|
3071
3510
|
ctx.ui.notify(msg, type === "success" ? "info" : type);
|
|
3072
3511
|
};
|
|
@@ -3076,14 +3515,13 @@ async function runSmartCompact(opts) {
|
|
|
3076
3515
|
};
|
|
3077
3516
|
const ctrl = new AbortController;
|
|
3078
3517
|
const signal = ctrl.signal;
|
|
3079
|
-
if (
|
|
3518
|
+
if (timeoutMs > 0) {
|
|
3080
3519
|
timeoutId = setTimeout(() => {
|
|
3081
3520
|
timedOut = true;
|
|
3082
3521
|
ctrl.abort();
|
|
3083
|
-
notify("Smart compact auto-trigger
|
|
3084
|
-
},
|
|
3522
|
+
notify("Smart compact auto-trigger exceeded " + timeoutMs + "ms; Pi will use native compact for this run", "warning");
|
|
3523
|
+
}, timeoutMs);
|
|
3085
3524
|
}
|
|
3086
|
-
const modelLabel = summaryModel.provider + "/" + summaryModel.id;
|
|
3087
3525
|
notify("Smart compact: " + modelLabel + ", " + profile + ", tokens=" + totalTokens, "info");
|
|
3088
3526
|
notify("EESV Compact (" + modelLabel + ", " + profile + ") \u2014 " + (totalTokens ?? 0).toLocaleString() + "t", "info");
|
|
3089
3527
|
const branch = ctx.sessionManager.getBranch();
|
|
@@ -3111,6 +3549,7 @@ async function runSmartCompact(opts) {
|
|
|
3111
3549
|
return;
|
|
3112
3550
|
}
|
|
3113
3551
|
const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id;
|
|
3552
|
+
markPhase("prepare");
|
|
3114
3553
|
if (!autoTriggered) {
|
|
3115
3554
|
showProgressOverlay(ctx, { phase: 1, phaseName: "Extract", detail: "Preparing...", model: modelLabel, profile });
|
|
3116
3555
|
}
|
|
@@ -3123,9 +3562,10 @@ async function runSmartCompact(opts) {
|
|
|
3123
3562
|
notify("Using untruncated session log (" + llmMessages.length + " msgs)", "info");
|
|
3124
3563
|
}
|
|
3125
3564
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3565
|
+
markPhase("recover");
|
|
3566
|
+
contextPercent = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
|
|
3567
|
+
toolPercent = computeToolCharPercentage(branch);
|
|
3568
|
+
tier = force ? contextPercent >= 80 ? "full" : "light" : selectCompactionTier(contextPercent, toolPercent, totalTokens, MIN_TOKEN_THRESHOLD, config.minContextPercent);
|
|
3129
3569
|
if (tier === "none") {
|
|
3130
3570
|
isRunning.value = false;
|
|
3131
3571
|
if (!autoTriggered)
|
|
@@ -3138,9 +3578,10 @@ async function runSmartCompact(opts) {
|
|
|
3138
3578
|
notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
3139
3579
|
}
|
|
3140
3580
|
llmMessages = pruning.messages;
|
|
3581
|
+
markPhase("prune");
|
|
3141
3582
|
const convText = serializeConversation(llmMessages);
|
|
3142
3583
|
const convTokens = estimateTokens(convText);
|
|
3143
|
-
|
|
3584
|
+
sessionId = ctx.sessionManager.getSessionId?.() ?? "unknown";
|
|
3144
3585
|
const backupPath = backupConversation(convText, sessionId);
|
|
3145
3586
|
const prevContext = getPreviousCompactionContext(branch);
|
|
3146
3587
|
const cachedExt = loadCachedExtraction(sessionId);
|
|
@@ -3162,6 +3603,7 @@ async function runSmartCompact(opts) {
|
|
|
3162
3603
|
vlog("Full extraction \u2014 " + llmMessages.length + " messages, tier=" + tier);
|
|
3163
3604
|
}
|
|
3164
3605
|
saveCachedExtraction(sessionId, extraction, llmMessages.length, currentFirstId, currentLastId);
|
|
3606
|
+
markPhase("extract");
|
|
3165
3607
|
const projectId = deriveProjectId(findGitRoot(ctx.cwd) ?? ctx.cwd, extraction, sessionId);
|
|
3166
3608
|
const fingerprint = loadProjectFingerprint(projectId);
|
|
3167
3609
|
if (fingerprint) {
|
|
@@ -3175,12 +3617,14 @@ async function runSmartCompact(opts) {
|
|
|
3175
3617
|
let explorationReport = null;
|
|
3176
3618
|
let explorationRounds = 0;
|
|
3177
3619
|
let chunkCount = 0;
|
|
3178
|
-
|
|
3179
|
-
|
|
3620
|
+
const providerCaps = getProviderCaps(summaryModel.provider);
|
|
3621
|
+
const singlePassMaxTokens = Math.round(pc.singlePassMaxTokens * providerCaps.singlePassTokenMultiplier);
|
|
3622
|
+
vlog("Tier=" + tier + " | convTokens=" + convTokens + " | singlePassMax=" + singlePassMaxTokens);
|
|
3623
|
+
if (convTokens < singlePassMaxTokens) {
|
|
3180
3624
|
if (!autoTriggered)
|
|
3181
3625
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Single-pass (" + convTokens.toLocaleString() + "t)", model: modelLabel, profile, extraction });
|
|
3182
3626
|
try {
|
|
3183
|
-
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey, headers: apiHeaders }, signal);
|
|
3627
|
+
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey, headers: apiHeaders }, pc.summaryBudgetTokens, signal);
|
|
3184
3628
|
finalSummary = r.summary;
|
|
3185
3629
|
method = "single-pass";
|
|
3186
3630
|
llmCalls = r.llmCalls;
|
|
@@ -3207,6 +3651,7 @@ async function runSmartCompact(opts) {
|
|
|
3207
3651
|
} else {
|
|
3208
3652
|
notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
|
|
3209
3653
|
}
|
|
3654
|
+
markPhase("explore");
|
|
3210
3655
|
let boundaries;
|
|
3211
3656
|
if (explorationReport?.boundaries.length) {
|
|
3212
3657
|
const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
|
|
@@ -3243,8 +3688,7 @@ async function runSmartCompact(opts) {
|
|
|
3243
3688
|
const totalBatches = batches.length;
|
|
3244
3689
|
if (!autoTriggered)
|
|
3245
3690
|
showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: "0/" + totalBatches + " batches", model: modelLabel, profile, extraction, totalBatches });
|
|
3246
|
-
const
|
|
3247
|
-
const concurrency = caps.concurrencyLimit;
|
|
3691
|
+
const concurrency = providerCaps.concurrencyLimit;
|
|
3248
3692
|
if (totalBatches <= 1) {
|
|
3249
3693
|
try {
|
|
3250
3694
|
summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey, headers: apiHeaders }, signal));
|
|
@@ -3315,6 +3759,10 @@ async function runSmartCompact(opts) {
|
|
|
3315
3759
|
method = "eesv";
|
|
3316
3760
|
llmCalls = explorationRounds + batches.length + assemblyCalls;
|
|
3317
3761
|
}
|
|
3762
|
+
methodForMetrics = method;
|
|
3763
|
+
if (method === "single-pass" || method === "heuristic")
|
|
3764
|
+
markPhase("explore");
|
|
3765
|
+
markPhase("synthesize");
|
|
3318
3766
|
if (!autoTriggered)
|
|
3319
3767
|
showProgressOverlay(ctx, { phase: 4, phaseName: "Verify", detail: "Checking...", model: modelLabel, profile, extraction, explorationRounds });
|
|
3320
3768
|
const verification = verifySummary(finalSummary, extraction);
|
|
@@ -3337,6 +3785,7 @@ async function runSmartCompact(opts) {
|
|
|
3337
3785
|
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + " \u2265 85 \u2014 skipping patch", "info");
|
|
3338
3786
|
}
|
|
3339
3787
|
}
|
|
3788
|
+
markPhase("verify");
|
|
3340
3789
|
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
3341
3790
|
const detRead = extraction.readFiles;
|
|
3342
3791
|
const estimatedAfter = estimateTokens(finalSummary) + accTokens;
|
|
@@ -3362,6 +3811,7 @@ async function runSmartCompact(opts) {
|
|
|
3362
3811
|
notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
3363
3812
|
}
|
|
3364
3813
|
}
|
|
3814
|
+
markPhase("state");
|
|
3365
3815
|
const details = {
|
|
3366
3816
|
method,
|
|
3367
3817
|
chunkCount: chunkCount || 1,
|
|
@@ -3385,6 +3835,25 @@ async function runSmartCompact(opts) {
|
|
|
3385
3835
|
openLoops
|
|
3386
3836
|
};
|
|
3387
3837
|
if (dryRun) {
|
|
3838
|
+
appendMetricsLog(sessionId, {
|
|
3839
|
+
profile,
|
|
3840
|
+
tier,
|
|
3841
|
+
contextPercent: Math.round(contextPercent),
|
|
3842
|
+
toolPercent,
|
|
3843
|
+
tokensBefore: totalTokens,
|
|
3844
|
+
tokensSaved,
|
|
3845
|
+
pruneSavedTokens: pruning.prunedTokenSaving,
|
|
3846
|
+
chunkCount: chunkCount || 1,
|
|
3847
|
+
verificationScore: verification.score,
|
|
3848
|
+
verificationGaps: verification.gaps.length,
|
|
3849
|
+
method,
|
|
3850
|
+
model: modelLabel,
|
|
3851
|
+
provider: summaryModel.provider,
|
|
3852
|
+
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3853
|
+
status: "dry-run",
|
|
3854
|
+
phaseTimings,
|
|
3855
|
+
durationMs: Date.now() - pipelineStart
|
|
3856
|
+
});
|
|
3388
3857
|
notify("DRY RUN (" + method + ", " + profile + ") \u2014 " + toCompact.length + " msgs, " + llmCalls + " calls", "info");
|
|
3389
3858
|
return;
|
|
3390
3859
|
}
|
|
@@ -3395,17 +3864,7 @@ async function runSmartCompact(opts) {
|
|
|
3395
3864
|
pendingRef.createdAt = Date.now();
|
|
3396
3865
|
saveProjectFingerprint(projectId, extraction);
|
|
3397
3866
|
saveCompactionState(projectId, compactionState);
|
|
3398
|
-
|
|
3399
|
-
profile,
|
|
3400
|
-
tier,
|
|
3401
|
-
contextPercent: Math.round(contextPercent),
|
|
3402
|
-
toolPercent,
|
|
3403
|
-
tokensBefore: totalTokens,
|
|
3404
|
-
tokensSaved,
|
|
3405
|
-
pruneSavedTokens: pruning.prunedTokenSaving,
|
|
3406
|
-
chunkCount: chunkCount || 1,
|
|
3407
|
-
verificationScore: verification.score
|
|
3408
|
-
});
|
|
3867
|
+
markPhase("persist");
|
|
3409
3868
|
try {
|
|
3410
3869
|
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
|
|
3411
3870
|
if (postCompactMsgs.length > 2) {
|
|
@@ -3422,6 +3881,26 @@ async function runSmartCompact(opts) {
|
|
|
3422
3881
|
} catch (err) {
|
|
3423
3882
|
warn("Damage detection error", err);
|
|
3424
3883
|
}
|
|
3884
|
+
markPhase("damage");
|
|
3885
|
+
appendMetricsLog(sessionId, {
|
|
3886
|
+
profile,
|
|
3887
|
+
tier,
|
|
3888
|
+
contextPercent: Math.round(contextPercent),
|
|
3889
|
+
toolPercent,
|
|
3890
|
+
tokensBefore: totalTokens,
|
|
3891
|
+
tokensSaved,
|
|
3892
|
+
pruneSavedTokens: pruning.prunedTokenSaving,
|
|
3893
|
+
chunkCount: chunkCount || 1,
|
|
3894
|
+
verificationScore: verification.score,
|
|
3895
|
+
verificationGaps: verification.gaps.length,
|
|
3896
|
+
method,
|
|
3897
|
+
model: modelLabel,
|
|
3898
|
+
provider: summaryModel.provider,
|
|
3899
|
+
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3900
|
+
status: "success",
|
|
3901
|
+
phaseTimings,
|
|
3902
|
+
durationMs: Date.now() - pipelineStart
|
|
3903
|
+
});
|
|
3425
3904
|
const ms = getMetricsSummary();
|
|
3426
3905
|
if (ms.totalCalls > 0) {
|
|
3427
3906
|
notify("Metrics: " + ms.totalCalls + " calls, " + ms.totalInput + "t in, " + ms.totalOutput + "t out, cache " + Math.round(ms.cacheHitRate * 100) + "%, " + ms.avgLatency + "ms avg", "info");
|
|
@@ -3446,6 +3925,23 @@ async function runSmartCompact(opts) {
|
|
|
3446
3925
|
}
|
|
3447
3926
|
});
|
|
3448
3927
|
}
|
|
3928
|
+
} catch (err) {
|
|
3929
|
+
appendMetricsLog(sessionId, {
|
|
3930
|
+
profile,
|
|
3931
|
+
tier,
|
|
3932
|
+
contextPercent: Math.round(contextPercent),
|
|
3933
|
+
toolPercent,
|
|
3934
|
+
tokensBefore: totalTokens,
|
|
3935
|
+
method: methodForMetrics,
|
|
3936
|
+
model: modelLabel,
|
|
3937
|
+
provider: summaryModel.provider,
|
|
3938
|
+
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3939
|
+
status: timedOut ? "timeout" : "error",
|
|
3940
|
+
fallbackReason: err instanceof Error ? err.message : String(err),
|
|
3941
|
+
phaseTimings,
|
|
3942
|
+
durationMs: Date.now() - pipelineStart
|
|
3943
|
+
});
|
|
3944
|
+
throw err;
|
|
3449
3945
|
} finally {
|
|
3450
3946
|
if (timeoutId)
|
|
3451
3947
|
clearTimeout(timeoutId);
|
|
@@ -3495,7 +3991,7 @@ function smartCompactExtension(pi) {
|
|
|
3495
3991
|
pi.registerCommand("smart-compact", {
|
|
3496
3992
|
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
|
|
3497
3993
|
getArgumentCompletions: (prefix) => {
|
|
3498
|
-
const m = ["verbose", "debug", "dry-run", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
3994
|
+
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
3499
3995
|
return m.length ? m : null;
|
|
3500
3996
|
},
|
|
3501
3997
|
handler: async (args, ctx) => {
|
|
@@ -3505,6 +4001,15 @@ function smartCompactExtension(pi) {
|
|
|
3505
4001
|
const flags = tokens.map((t) => t.toLowerCase());
|
|
3506
4002
|
const verbose = flags.includes("verbose") || flags.includes("debug");
|
|
3507
4003
|
const dryRun = flags.includes("dry-run");
|
|
4004
|
+
if (flags.includes("metrics") || flags.includes("dashboard")) {
|
|
4005
|
+
const dashboard = flags.includes("dashboard");
|
|
4006
|
+
const report = buildMetricsReport();
|
|
4007
|
+
const fp = dashboard ? writeMetricsDashboard() : null;
|
|
4008
|
+
ctx.ui.notify(report + (fp ? `
|
|
4009
|
+
|
|
4010
|
+
Dashboard: ` + fp : ""), "info");
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
3508
4013
|
const modelArg = tokens.find((t) => t.includes("/"));
|
|
3509
4014
|
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
3510
4015
|
const profile = profileArg ?? loadConfig().profile;
|
|
@@ -3530,7 +4035,7 @@ function smartCompactExtension(pi) {
|
|
|
3530
4035
|
ctx.ui.notify("Could not resolve model", "error");
|
|
3531
4036
|
return;
|
|
3532
4037
|
}
|
|
3533
|
-
await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning });
|
|
4038
|
+
await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning, force: true });
|
|
3534
4039
|
return;
|
|
3535
4040
|
}
|
|
3536
4041
|
const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
|
|
@@ -3539,7 +4044,7 @@ function smartCompactExtension(pi) {
|
|
|
3539
4044
|
return;
|
|
3540
4045
|
}
|
|
3541
4046
|
const note = extractUserNote(args);
|
|
3542
|
-
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, userNote: note });
|
|
4047
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, userNote: note, force: true });
|
|
3543
4048
|
} catch (error) {
|
|
3544
4049
|
const msg = error instanceof Error ? error.message + `
|
|
3545
4050
|
` + error.stack : String(error);
|
|
@@ -3551,6 +4056,7 @@ function smartCompactExtension(pi) {
|
|
|
3551
4056
|
if (pendingRef.value) {
|
|
3552
4057
|
const age = Date.now() - pendingRef.createdAt;
|
|
3553
4058
|
if (age > PENDING_TTL_MS) {
|
|
4059
|
+
warn("Discarding expired pending smart compaction after " + Math.round(age / 1000) + "s");
|
|
3554
4060
|
pendingRef.value = null;
|
|
3555
4061
|
pendingRef.createdAt = 0;
|
|
3556
4062
|
} else {
|
|
@@ -3568,6 +4074,9 @@ function smartCompactExtension(pi) {
|
|
|
3568
4074
|
const totalTokens = usage?.tokens ?? 0;
|
|
3569
4075
|
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD)
|
|
3570
4076
|
return;
|
|
4077
|
+
const pct = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
|
|
4078
|
+
if (pct < config.minContextPercent)
|
|
4079
|
+
return;
|
|
3571
4080
|
const cur = ctx.model;
|
|
3572
4081
|
if (!cur)
|
|
3573
4082
|
return;
|
|
@@ -3575,21 +4084,26 @@ function smartCompactExtension(pi) {
|
|
|
3575
4084
|
if (!sumModel)
|
|
3576
4085
|
return;
|
|
3577
4086
|
if (!isRunning.value) {
|
|
3578
|
-
const
|
|
3579
|
-
const
|
|
3580
|
-
|
|
4087
|
+
const caps = getProviderCaps(sumModel.provider);
|
|
4088
|
+
const effectiveTimeoutMs = Math.round(config.autoTriggerTimeoutMs * caps.timeoutMultiplier);
|
|
4089
|
+
const compactPromise = runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true, timeoutMs: effectiveTimeoutMs });
|
|
4090
|
+
let timeoutId = null;
|
|
4091
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
4092
|
+
timeoutId = setTimeout(() => resolve("timeout"), effectiveTimeoutMs + 100);
|
|
3581
4093
|
});
|
|
4094
|
+
let result;
|
|
3582
4095
|
try {
|
|
3583
|
-
await Promise.race([compactPromise, timeoutPromise]);
|
|
3584
|
-
}
|
|
3585
|
-
if (
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4096
|
+
result = await Promise.race([compactPromise.then(() => "done"), timeoutPromise]);
|
|
4097
|
+
} finally {
|
|
4098
|
+
if (timeoutId)
|
|
4099
|
+
clearTimeout(timeoutId);
|
|
4100
|
+
}
|
|
4101
|
+
if (result === "timeout") {
|
|
4102
|
+
warn("Smart compact auto-trigger hard timeout after " + effectiveTimeoutMs + "ms");
|
|
4103
|
+
isRunning.value = false;
|
|
4104
|
+
pendingRef.value = null;
|
|
4105
|
+
pendingRef.createdAt = 0;
|
|
4106
|
+
return;
|
|
3593
4107
|
}
|
|
3594
4108
|
const pending = pendingRef.value;
|
|
3595
4109
|
if (pending) {
|
|
@@ -3605,30 +4119,47 @@ function smartCompactExtension(pi) {
|
|
|
3605
4119
|
pi.registerTool({
|
|
3606
4120
|
name: "smart_compact",
|
|
3607
4121
|
label: "Smart Compact",
|
|
3608
|
-
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Compacts the conversation into a structured summary preserving goals, decisions, open loops, modified files, and critical context. Call
|
|
4122
|
+
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification. Compacts the conversation into a structured summary preserving goals, decisions, open loops, modified files, and critical context. Call only when actual context usage is high; ignore pi-auto-context tool=XX% because that is tool-output ratio, not context fullness. The tool internally checks context usage and skips if not needed.",
|
|
3609
4123
|
promptSnippet: "Smart compaction",
|
|
3610
|
-
promptGuidelines: [
|
|
4124
|
+
promptGuidelines: [
|
|
4125
|
+
"Use only when actual context usage is high (for example pi-auto-context context>=60%).",
|
|
4126
|
+
"Do NOT call just because pi-auto-context shows tool=XX%; tool% is tool-output ratio, not context fullness.",
|
|
4127
|
+
"Prefer this over default compact only when compaction is actually needed."
|
|
4128
|
+
],
|
|
3611
4129
|
parameters: {
|
|
3612
4130
|
type: "object",
|
|
3613
4131
|
properties: {
|
|
3614
4132
|
profile: { type: "string", description: "light, balanced, or aggressive. Default: balanced." },
|
|
3615
4133
|
verbose: { type: "boolean", description: "Show detailed pipeline output." },
|
|
3616
|
-
dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." }
|
|
4134
|
+
dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." },
|
|
4135
|
+
report: { type: "boolean", description: "Return recent performance metrics instead of compacting." },
|
|
4136
|
+
dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." }
|
|
3617
4137
|
}
|
|
3618
4138
|
},
|
|
3619
4139
|
async execute(_id, params, _sig, _onUp, ctx) {
|
|
3620
4140
|
const profile = params.profile === "light" || params.profile === "balanced" || params.profile === "aggressive" ? params.profile : undefined;
|
|
3621
4141
|
const verbose = !!params.verbose;
|
|
3622
4142
|
const dryRun = !!params.dry_run;
|
|
4143
|
+
if (params.report || params.dashboard) {
|
|
4144
|
+
const report = buildMetricsReport();
|
|
4145
|
+
const fp = params.dashboard ? writeMetricsDashboard() : null;
|
|
4146
|
+
return { content: [{ type: "text", text: report + (fp ? `
|
|
4147
|
+
|
|
4148
|
+
Dashboard: ` + fp : "") }], details: undefined };
|
|
4149
|
+
}
|
|
3623
4150
|
const config = loadConfig();
|
|
3624
4151
|
const resolvedProfile = profile ?? config.profile;
|
|
3625
4152
|
const cmdCtx = ctx;
|
|
3626
4153
|
const usage = ctx.getContextUsage?.();
|
|
3627
4154
|
const totalTokens = usage?.tokens ?? 0;
|
|
4155
|
+
const rawPct = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
|
|
4156
|
+
const pct = Math.round(rawPct);
|
|
3628
4157
|
if (!totalTokens || totalTokens < MIN_TOKEN_THRESHOLD) {
|
|
3629
|
-
const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
|
|
3630
4158
|
return { content: [{ type: "text", text: "Context is not large enough for compaction (" + totalTokens.toLocaleString() + " tokens, " + pct + "%). No action needed." }], details: undefined };
|
|
3631
4159
|
}
|
|
4160
|
+
if (rawPct < config.minContextPercent) {
|
|
4161
|
+
return { content: [{ type: "text", text: "Context is only " + pct + "% full (" + totalTokens.toLocaleString() + " tokens). Compaction is not needed yet. The tool=97% in status means tool output ratio, NOT context usage." }], details: undefined };
|
|
4162
|
+
}
|
|
3632
4163
|
const cur = "model" in ctx ? ctx.model : undefined;
|
|
3633
4164
|
const { segModel, sumModel } = resolveModels(cmdCtx, cur, config);
|
|
3634
4165
|
if (!sumModel) {
|