pi-smart-compact 7.11.0 → 7.12.1
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 +25 -0
- package/README.md +13 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +535 -68
- package/dist/phases/synthesize.d.ts +1 -1
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/types.d.ts +48 -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.map +1 -1
- package/dist/utils/session-log.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.1";
|
|
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,7 +35,7 @@ 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
40
|
backupDir: ""
|
|
41
41
|
};
|
|
@@ -201,8 +201,9 @@ function debug(msg, ...args) {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
// src/utils/helpers.ts
|
|
204
|
+
var VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
205
|
+
var PROFILE_NUMERIC_KEYS = ["summaryBudgetTokens", "keepRecentTokens", "minChunkTokens", "maxChunkTokens", "singlePassMaxTokens", "batchMaxTokens"];
|
|
204
206
|
function validateSmartCompactConfig(sc) {
|
|
205
|
-
const VALID_PROFILES = ["light", "balanced", "aggressive"];
|
|
206
207
|
if ("profile" in sc && !VALID_PROFILES.includes(sc.profile)) {
|
|
207
208
|
warn("smart-compact config: invalid profile '" + sc.profile + "', expected light|balanced|aggressive. Using default 'balanced'.");
|
|
208
209
|
delete sc.profile;
|
|
@@ -223,9 +224,37 @@ function validateSmartCompactConfig(sc) {
|
|
|
223
224
|
warn("smart-compact config: segmentationModel must be string|null, got " + typeof sc.segmentationModel);
|
|
224
225
|
delete sc.segmentationModel;
|
|
225
226
|
}
|
|
226
|
-
if ("profiles" in sc
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
if ("profiles" in sc) {
|
|
228
|
+
if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
|
|
229
|
+
warn("smart-compact config: profiles must be an object, got " + typeof sc.profiles);
|
|
230
|
+
delete sc.profiles;
|
|
231
|
+
} else {
|
|
232
|
+
const profiles = sc.profiles;
|
|
233
|
+
for (const [profileName, value] of Object.entries(profiles)) {
|
|
234
|
+
if (!VALID_PROFILES.includes(profileName)) {
|
|
235
|
+
warn("smart-compact config: ignoring unknown profile override '" + profileName + "'.");
|
|
236
|
+
delete profiles[profileName];
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
240
|
+
warn("smart-compact config: profile '" + profileName + "' must be an object.");
|
|
241
|
+
delete profiles[profileName];
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const profileCfg = value;
|
|
245
|
+
for (const [key, raw] of Object.entries(profileCfg)) {
|
|
246
|
+
if (!PROFILE_NUMERIC_KEYS.includes(key)) {
|
|
247
|
+
warn("smart-compact config: ignoring unknown profile key '" + profileName + "." + key + "'.");
|
|
248
|
+
delete profileCfg[key];
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0 || raw > 1e6) {
|
|
252
|
+
warn("smart-compact config: profile '" + profileName + "." + key + "' must be a positive finite number.");
|
|
253
|
+
delete profileCfg[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
229
258
|
}
|
|
230
259
|
if ("autoTriggerTimeoutMs" in sc) {
|
|
231
260
|
const v = sc.autoTriggerTimeoutMs;
|
|
@@ -474,12 +503,14 @@ Read: ` + (cs.filesRead.join(", ") || "None");
|
|
|
474
503
|
function buildExtractionContext(extraction, forRange) {
|
|
475
504
|
const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
|
|
476
505
|
const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
|
|
506
|
+
const media = forRange ? (extraction.mediaAttachments ?? []).filter((a) => a.index >= forRange.start && a.index <= forRange.end) : extraction.mediaAttachments ?? [];
|
|
477
507
|
return [
|
|
478
508
|
"## Deterministic Extraction (verified facts)",
|
|
479
509
|
"Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
|
|
480
510
|
"Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
|
|
481
511
|
"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")
|
|
512
|
+
"Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none"),
|
|
513
|
+
"Media attachments: " + (media.map((a) => a.kind + (a.name ? ":" + a.name : "") + (a.mimeType ? " (" + a.mimeType + ")" : "") + " @msg" + a.index).join("; ") || "none")
|
|
483
514
|
].join(`
|
|
484
515
|
`);
|
|
485
516
|
}
|
|
@@ -550,9 +581,6 @@ function buildExplorationContext(report) {
|
|
|
550
581
|
`);
|
|
551
582
|
}
|
|
552
583
|
|
|
553
|
-
// src/core.ts
|
|
554
|
-
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
555
|
-
|
|
556
584
|
// src/utils/tokens.ts
|
|
557
585
|
var PROVIDER_MAP = {
|
|
558
586
|
"zai-anthropic": {
|
|
@@ -562,7 +590,10 @@ var PROVIDER_MAP = {
|
|
|
562
590
|
instructionFollowing: "high",
|
|
563
591
|
tokenRatioEstimate: 3.5,
|
|
564
592
|
concurrencyLimit: 3,
|
|
565
|
-
cacheStrategy: "anthropic"
|
|
593
|
+
cacheStrategy: "anthropic",
|
|
594
|
+
timeoutMultiplier: 1.2,
|
|
595
|
+
singlePassTokenMultiplier: 1,
|
|
596
|
+
multimodal: "metadata-only"
|
|
566
597
|
},
|
|
567
598
|
anthropic: {
|
|
568
599
|
maxOutputTokens: 8192,
|
|
@@ -571,7 +602,10 @@ var PROVIDER_MAP = {
|
|
|
571
602
|
instructionFollowing: "high",
|
|
572
603
|
tokenRatioEstimate: 3.5,
|
|
573
604
|
concurrencyLimit: 3,
|
|
574
|
-
cacheStrategy: "anthropic"
|
|
605
|
+
cacheStrategy: "anthropic",
|
|
606
|
+
timeoutMultiplier: 1.2,
|
|
607
|
+
singlePassTokenMultiplier: 1,
|
|
608
|
+
multimodal: "native"
|
|
575
609
|
},
|
|
576
610
|
openai: {
|
|
577
611
|
maxOutputTokens: 16384,
|
|
@@ -580,7 +614,10 @@ var PROVIDER_MAP = {
|
|
|
580
614
|
instructionFollowing: "high",
|
|
581
615
|
tokenRatioEstimate: 4,
|
|
582
616
|
concurrencyLimit: 5,
|
|
583
|
-
cacheStrategy: "openai"
|
|
617
|
+
cacheStrategy: "openai",
|
|
618
|
+
timeoutMultiplier: 1,
|
|
619
|
+
singlePassTokenMultiplier: 1.15,
|
|
620
|
+
multimodal: "native"
|
|
584
621
|
},
|
|
585
622
|
google: {
|
|
586
623
|
maxOutputTokens: 8192,
|
|
@@ -589,7 +626,10 @@ var PROVIDER_MAP = {
|
|
|
589
626
|
instructionFollowing: "high",
|
|
590
627
|
tokenRatioEstimate: 3.8,
|
|
591
628
|
concurrencyLimit: 3,
|
|
592
|
-
cacheStrategy: "openai"
|
|
629
|
+
cacheStrategy: "openai",
|
|
630
|
+
timeoutMultiplier: 1.15,
|
|
631
|
+
singlePassTokenMultiplier: 1.1,
|
|
632
|
+
multimodal: "native"
|
|
593
633
|
},
|
|
594
634
|
deepseek: {
|
|
595
635
|
maxOutputTokens: 8192,
|
|
@@ -598,7 +638,10 @@ var PROVIDER_MAP = {
|
|
|
598
638
|
instructionFollowing: "medium",
|
|
599
639
|
tokenRatioEstimate: 3.6,
|
|
600
640
|
concurrencyLimit: 2,
|
|
601
|
-
cacheStrategy: "none"
|
|
641
|
+
cacheStrategy: "none",
|
|
642
|
+
timeoutMultiplier: 1.5,
|
|
643
|
+
singlePassTokenMultiplier: 0.85,
|
|
644
|
+
multimodal: "metadata-only"
|
|
602
645
|
},
|
|
603
646
|
minimax: {
|
|
604
647
|
maxOutputTokens: 4096,
|
|
@@ -607,7 +650,10 @@ var PROVIDER_MAP = {
|
|
|
607
650
|
instructionFollowing: "medium",
|
|
608
651
|
tokenRatioEstimate: 3.8,
|
|
609
652
|
concurrencyLimit: 2,
|
|
610
|
-
cacheStrategy: "anthropic"
|
|
653
|
+
cacheStrategy: "anthropic",
|
|
654
|
+
timeoutMultiplier: 1.6,
|
|
655
|
+
singlePassTokenMultiplier: 0.8,
|
|
656
|
+
multimodal: "metadata-only"
|
|
611
657
|
},
|
|
612
658
|
"xiaomi-token-plan": {
|
|
613
659
|
maxOutputTokens: 8192,
|
|
@@ -616,7 +662,10 @@ var PROVIDER_MAP = {
|
|
|
616
662
|
instructionFollowing: "medium",
|
|
617
663
|
tokenRatioEstimate: 3.3,
|
|
618
664
|
concurrencyLimit: 2,
|
|
619
|
-
cacheStrategy: "openai"
|
|
665
|
+
cacheStrategy: "openai",
|
|
666
|
+
timeoutMultiplier: 1.35,
|
|
667
|
+
singlePassTokenMultiplier: 0.9,
|
|
668
|
+
multimodal: "metadata-only"
|
|
620
669
|
},
|
|
621
670
|
mistral: {
|
|
622
671
|
maxOutputTokens: 8192,
|
|
@@ -625,7 +674,10 @@ var PROVIDER_MAP = {
|
|
|
625
674
|
instructionFollowing: "high",
|
|
626
675
|
tokenRatioEstimate: 3.5,
|
|
627
676
|
concurrencyLimit: 3,
|
|
628
|
-
cacheStrategy: "openai"
|
|
677
|
+
cacheStrategy: "openai",
|
|
678
|
+
timeoutMultiplier: 1.2,
|
|
679
|
+
singlePassTokenMultiplier: 1,
|
|
680
|
+
multimodal: "metadata-only"
|
|
629
681
|
},
|
|
630
682
|
xai: {
|
|
631
683
|
maxOutputTokens: 8192,
|
|
@@ -634,7 +686,10 @@ var PROVIDER_MAP = {
|
|
|
634
686
|
instructionFollowing: "high",
|
|
635
687
|
tokenRatioEstimate: 3.8,
|
|
636
688
|
concurrencyLimit: 3,
|
|
637
|
-
cacheStrategy: "openai"
|
|
689
|
+
cacheStrategy: "openai",
|
|
690
|
+
timeoutMultiplier: 1.2,
|
|
691
|
+
singlePassTokenMultiplier: 1,
|
|
692
|
+
multimodal: "native"
|
|
638
693
|
}
|
|
639
694
|
};
|
|
640
695
|
var PROVIDER_ALIASES = [
|
|
@@ -656,7 +711,10 @@ var DEFAULT_CAPS = {
|
|
|
656
711
|
instructionFollowing: "medium",
|
|
657
712
|
tokenRatioEstimate: 3.8,
|
|
658
713
|
concurrencyLimit: 2,
|
|
659
|
-
cacheStrategy: "none"
|
|
714
|
+
cacheStrategy: "none",
|
|
715
|
+
timeoutMultiplier: 1.35,
|
|
716
|
+
singlePassTokenMultiplier: 0.9,
|
|
717
|
+
multimodal: "metadata-only"
|
|
660
718
|
};
|
|
661
719
|
function getProviderCaps(provider) {
|
|
662
720
|
if (PROVIDER_MAP[provider])
|
|
@@ -764,6 +822,7 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
764
822
|
recordMetric({
|
|
765
823
|
phase,
|
|
766
824
|
model: model.id,
|
|
825
|
+
provider: model.provider,
|
|
767
826
|
inputTokens: inputT,
|
|
768
827
|
outputTokens: outputT,
|
|
769
828
|
cacheHitTokens: cacheT,
|
|
@@ -783,6 +842,7 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
783
842
|
recordMetric({
|
|
784
843
|
phase,
|
|
785
844
|
model: model.id,
|
|
845
|
+
provider: model.provider,
|
|
786
846
|
inputTokens: 0,
|
|
787
847
|
outputTokens: 0,
|
|
788
848
|
cacheHitTokens: 0,
|
|
@@ -840,10 +900,12 @@ function mergeExtractions(base, delta, baseMsgCount) {
|
|
|
840
900
|
...f,
|
|
841
901
|
lastModifiedIndex: f.lastModifiedIndex + baseMsgCount
|
|
842
902
|
}));
|
|
903
|
+
const offsetMedia = (delta.mediaAttachments ?? []).map((a) => ({ ...a, index: a.index + baseMsgCount }));
|
|
843
904
|
return {
|
|
844
905
|
modifiedFiles: [...new Map([...base.modifiedFiles, ...offsetModifiedFiles].map((f) => [f.path, f])).values()],
|
|
845
906
|
readFiles: [...new Set([...base.readFiles, ...delta.readFiles])],
|
|
846
907
|
deletedFiles: [...new Set([...base.deletedFiles, ...delta.deletedFiles])],
|
|
908
|
+
mediaAttachments: [...base.mediaAttachments ?? [], ...offsetMedia],
|
|
847
909
|
errors: [...base.errors, ...offsetErrors],
|
|
848
910
|
decisions: [...base.decisions, ...offsetDecisions],
|
|
849
911
|
constraints: [...base.constraints, ...offsetConstraints],
|
|
@@ -873,6 +935,256 @@ function appendMetricsLog(sessionId, extra) {
|
|
|
873
935
|
warn("appendMetricsLog failed", e);
|
|
874
936
|
}
|
|
875
937
|
}
|
|
938
|
+
function readMetricsLog(limit = 100) {
|
|
939
|
+
try {
|
|
940
|
+
const logPath = path2.join(CACHE_DIR, "compact-metrics.jsonl");
|
|
941
|
+
if (!fs2.existsSync(logPath))
|
|
942
|
+
return [];
|
|
943
|
+
const entries = [];
|
|
944
|
+
for (const line of fs2.readFileSync(logPath, "utf8").trim().split(`
|
|
945
|
+
`).filter(Boolean).slice(-limit * 2)) {
|
|
946
|
+
try {
|
|
947
|
+
entries.push(JSON.parse(line));
|
|
948
|
+
} catch {
|
|
949
|
+
warn("Skipping corrupt compact metrics line");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return entries.slice(-limit);
|
|
953
|
+
} catch (e) {
|
|
954
|
+
warn("readMetricsLog failed", e);
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function escapeHtml(value) {
|
|
959
|
+
return String(value ?? "").replace(/[&<>\"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
960
|
+
}
|
|
961
|
+
function durationOf(entry) {
|
|
962
|
+
return entry.durationMs ?? entry.phaseTimings?.reduce((sum, phase) => sum + phase.durationMs, 0) ?? 0;
|
|
963
|
+
}
|
|
964
|
+
function percentile(values, p) {
|
|
965
|
+
if (!values.length)
|
|
966
|
+
return 0;
|
|
967
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
968
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor(p / 100 * sorted.length)))];
|
|
969
|
+
}
|
|
970
|
+
function average(values) {
|
|
971
|
+
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
972
|
+
}
|
|
973
|
+
function compactNumber(value) {
|
|
974
|
+
return new Intl.NumberFormat("en", { notation: Math.abs(value) >= 1e4 ? "compact" : "standard", maximumFractionDigits: 1 }).format(value);
|
|
975
|
+
}
|
|
976
|
+
function formatNumber(value) {
|
|
977
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
978
|
+
return escapeHtml(value);
|
|
979
|
+
return value.toLocaleString();
|
|
980
|
+
}
|
|
981
|
+
function formatMs(value) {
|
|
982
|
+
if (!value)
|
|
983
|
+
return "0ms";
|
|
984
|
+
if (value >= 60000)
|
|
985
|
+
return (value / 60000).toFixed(value >= 600000 ? 0 : 1) + "m";
|
|
986
|
+
if (value >= 1000)
|
|
987
|
+
return (value / 1000).toFixed(value >= 1e4 ? 0 : 1) + "s";
|
|
988
|
+
return Math.round(value) + "ms";
|
|
989
|
+
}
|
|
990
|
+
function formatPercent(value) {
|
|
991
|
+
return Math.round(value * 100) + "%";
|
|
992
|
+
}
|
|
993
|
+
function statusClass(status) {
|
|
994
|
+
if (status === "timeout" || status === "error")
|
|
995
|
+
return "bad";
|
|
996
|
+
if (status === "dry-run")
|
|
997
|
+
return "warn";
|
|
998
|
+
return "good";
|
|
999
|
+
}
|
|
1000
|
+
function statusLabel(status) {
|
|
1001
|
+
return status ?? "success";
|
|
1002
|
+
}
|
|
1003
|
+
function badge(status) {
|
|
1004
|
+
const label = statusLabel(status);
|
|
1005
|
+
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
1006
|
+
}
|
|
1007
|
+
function summarizeDashboard(entries) {
|
|
1008
|
+
const durations = entries.map(durationOf).filter(Boolean);
|
|
1009
|
+
const success = entries.filter((e) => statusLabel(e.status) === "success").length;
|
|
1010
|
+
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
1011
|
+
const error = entries.filter((e) => e.status === "error").length;
|
|
1012
|
+
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
1013
|
+
const scored = entries.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
1014
|
+
return {
|
|
1015
|
+
runs: entries.length,
|
|
1016
|
+
success,
|
|
1017
|
+
timeout,
|
|
1018
|
+
error,
|
|
1019
|
+
dryRun,
|
|
1020
|
+
successRate: entries.length ? success / entries.length : 0,
|
|
1021
|
+
avgDuration: Math.round(average(durations)),
|
|
1022
|
+
p95Duration: percentile(durations, 95),
|
|
1023
|
+
totalCalls: entries.reduce((sum, e) => sum + e.totalCalls, 0),
|
|
1024
|
+
totalInput: entries.reduce((sum, e) => sum + e.totalInput, 0),
|
|
1025
|
+
totalOutput: entries.reduce((sum, e) => sum + e.totalOutput, 0),
|
|
1026
|
+
totalSaved: entries.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1027
|
+
avgScore: Math.round(average(scored))
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function groupMetrics(entries, keyFn) {
|
|
1031
|
+
const groups = new Map;
|
|
1032
|
+
for (const entry of entries) {
|
|
1033
|
+
const key = keyFn(entry) || "unknown";
|
|
1034
|
+
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
1035
|
+
}
|
|
1036
|
+
return [...groups.entries()].map(([name, group]) => {
|
|
1037
|
+
const durations = group.map(durationOf).filter(Boolean);
|
|
1038
|
+
const scores = group.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
1039
|
+
const failures = group.filter((e) => e.status === "timeout" || e.status === "error").length;
|
|
1040
|
+
return {
|
|
1041
|
+
name,
|
|
1042
|
+
runs: group.length,
|
|
1043
|
+
avgDuration: Math.round(average(durations)),
|
|
1044
|
+
p95Duration: percentile(durations, 95),
|
|
1045
|
+
avgScore: Math.round(average(scores)),
|
|
1046
|
+
totalSaved: group.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1047
|
+
totalCalls: group.reduce((sum, e) => sum + e.totalCalls, 0),
|
|
1048
|
+
errorRate: group.length ? failures / group.length : 0
|
|
1049
|
+
};
|
|
1050
|
+
}).sort((a, b) => b.runs - a.runs || a.name.localeCompare(b.name));
|
|
1051
|
+
}
|
|
1052
|
+
function progressBar(value, label = formatPercent(value)) {
|
|
1053
|
+
const pct = Math.max(0, Math.min(100, Math.round(value * 100)));
|
|
1054
|
+
return `<div class="meter" title="${escapeHtml(label)}"><span style="width:${pct}%"></span></div>`;
|
|
1055
|
+
}
|
|
1056
|
+
function sparkline(values) {
|
|
1057
|
+
const nums = values.filter((v) => Number.isFinite(v));
|
|
1058
|
+
if (nums.length < 2)
|
|
1059
|
+
return `<div class="empty">Need at least two runs for trend</div>`;
|
|
1060
|
+
const width = 520;
|
|
1061
|
+
const height = 120;
|
|
1062
|
+
const min = Math.min(...nums);
|
|
1063
|
+
const max = Math.max(...nums);
|
|
1064
|
+
const span = Math.max(1, max - min);
|
|
1065
|
+
const points = nums.map((value, i) => {
|
|
1066
|
+
const x = i / Math.max(1, nums.length - 1) * width;
|
|
1067
|
+
const y = height - (value - min) / span * (height - 18) - 9;
|
|
1068
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
1069
|
+
}).join(" ");
|
|
1070
|
+
const last = nums[nums.length - 1];
|
|
1071
|
+
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>`;
|
|
1072
|
+
}
|
|
1073
|
+
function metricCard(label, value, detail, tone = "neutral") {
|
|
1074
|
+
return `<article class="card ${tone}"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(value)}</div><div class="detail">${escapeHtml(detail)}</div></article>`;
|
|
1075
|
+
}
|
|
1076
|
+
function comparisonRows(groups) {
|
|
1077
|
+
if (!groups.length)
|
|
1078
|
+
return `<tr><td colspan="8" class="empty">No data yet</td></tr>`;
|
|
1079
|
+
return groups.map((group) => `<tr>
|
|
1080
|
+
<td><strong>${escapeHtml(group.name)}</strong></td>
|
|
1081
|
+
<td class="num">${formatNumber(group.runs)}</td>
|
|
1082
|
+
<td class="num">${escapeHtml(formatMs(group.avgDuration))}</td>
|
|
1083
|
+
<td class="num">${escapeHtml(formatMs(group.p95Duration))}</td>
|
|
1084
|
+
<td class="num">${group.avgScore ? formatNumber(group.avgScore) : "\u2014"}</td>
|
|
1085
|
+
<td class="num">${formatNumber(group.totalCalls)}</td>
|
|
1086
|
+
<td class="num">${formatNumber(group.totalSaved)}</td>
|
|
1087
|
+
<td>${progressBar(1 - group.errorRate, formatPercent(1 - group.errorRate) + " reliable")}</td>
|
|
1088
|
+
</tr>`).join(`
|
|
1089
|
+
`);
|
|
1090
|
+
}
|
|
1091
|
+
function phaseRows(entry) {
|
|
1092
|
+
const timings = entry?.phaseTimings ?? [];
|
|
1093
|
+
if (!timings.length)
|
|
1094
|
+
return `<tr><td colspan="3" class="empty">No phase timings yet</td></tr>`;
|
|
1095
|
+
const total = timings.reduce((sum, phase) => sum + phase.durationMs, 0) || 1;
|
|
1096
|
+
return timings.map((phase) => `<tr>
|
|
1097
|
+
<td>${escapeHtml(phase.phase)}</td>
|
|
1098
|
+
<td class="num">${escapeHtml(formatMs(phase.durationMs))}</td>
|
|
1099
|
+
<td>${progressBar(phase.durationMs / total, formatPercent(phase.durationMs / total))}</td>
|
|
1100
|
+
</tr>`).join(`
|
|
1101
|
+
`);
|
|
1102
|
+
}
|
|
1103
|
+
function recentRunRows(entries) {
|
|
1104
|
+
if (!entries.length)
|
|
1105
|
+
return `<tr><td colspan="11" class="empty">No runs recorded yet</td></tr>`;
|
|
1106
|
+
return entries.slice(-80).reverse().map((entry) => `<tr>
|
|
1107
|
+
<td class="mono small">${escapeHtml(entry.ts)}</td>
|
|
1108
|
+
<td>${escapeHtml(entry.profile)}</td>
|
|
1109
|
+
<td>${escapeHtml(entry.provider ?? entry.model?.split("/")[0])}</td>
|
|
1110
|
+
<td>${escapeHtml(entry.method)}</td>
|
|
1111
|
+
<td>${escapeHtml(entry.runType)}</td>
|
|
1112
|
+
<td>${badge(entry.status)}</td>
|
|
1113
|
+
<td class="num">${escapeHtml(formatMs(durationOf(entry)))}</td>
|
|
1114
|
+
<td class="num">${typeof entry.verificationScore === "number" ? formatNumber(entry.verificationScore) : "\u2014"}</td>
|
|
1115
|
+
<td class="num">${typeof entry.tokensSaved === "number" ? formatNumber(entry.tokensSaved) : "\u2014"}</td>
|
|
1116
|
+
<td class="num">${formatNumber(entry.totalCalls)}</td>
|
|
1117
|
+
<td class="mono small reason">${escapeHtml(entry.fallbackReason ?? "")}</td>
|
|
1118
|
+
</tr>`).join(`
|
|
1119
|
+
`);
|
|
1120
|
+
}
|
|
1121
|
+
function dashboardCss() {
|
|
1122
|
+
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}}`;
|
|
1123
|
+
}
|
|
1124
|
+
function buildMetricsReport(entries = readMetricsLog(100)) {
|
|
1125
|
+
if (!entries.length)
|
|
1126
|
+
return "No smart-compact metrics recorded yet.";
|
|
1127
|
+
const summary = summarizeDashboard(entries);
|
|
1128
|
+
const byProfile = groupMetrics(entries, (e) => e.profile ?? "unknown");
|
|
1129
|
+
const byProvider = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0] ?? "unknown");
|
|
1130
|
+
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);
|
|
1131
|
+
return [
|
|
1132
|
+
"# Smart Compact Metrics",
|
|
1133
|
+
"",
|
|
1134
|
+
"Runs: " + summary.runs + " (success " + summary.success + ", dry-run " + summary.dryRun + ", timeout " + summary.timeout + ", error " + summary.error + ")",
|
|
1135
|
+
"Reliability: " + formatPercent(summary.successRate),
|
|
1136
|
+
"Latency: avg " + summary.avgDuration + "ms, p95 " + summary.p95Duration + "ms",
|
|
1137
|
+
"LLM calls: " + summary.totalCalls + ", input " + summary.totalInput + "t, output " + summary.totalOutput + "t",
|
|
1138
|
+
"Tokens saved: " + summary.totalSaved + "t, average verification score: " + summary.avgScore,
|
|
1139
|
+
"",
|
|
1140
|
+
"## Profile comparison",
|
|
1141
|
+
...byProfile.map(summarizeGroup),
|
|
1142
|
+
"",
|
|
1143
|
+
"## Provider comparison",
|
|
1144
|
+
...byProvider.map(summarizeGroup)
|
|
1145
|
+
].join(`
|
|
1146
|
+
`);
|
|
1147
|
+
}
|
|
1148
|
+
function writeMetricsDashboard(entries = readMetricsLog(200)) {
|
|
1149
|
+
try {
|
|
1150
|
+
if (!fs2.existsSync(CACHE_DIR))
|
|
1151
|
+
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
1152
|
+
const summary = summarizeDashboard(entries);
|
|
1153
|
+
const latest = entries[entries.length - 1];
|
|
1154
|
+
const report = buildMetricsReport(entries);
|
|
1155
|
+
const profileGroups = groupMetrics(entries, (e) => e.profile ?? "unknown");
|
|
1156
|
+
const providerGroups = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0] ?? "unknown");
|
|
1157
|
+
const healthTone = summary.error + summary.timeout > 0 ? "warn" : "good";
|
|
1158
|
+
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>
|
|
1159
|
+
<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>
|
|
1160
|
+
<section class="cards">
|
|
1161
|
+
${metricCard("Reliability", formatPercent(summary.successRate), `${summary.success} success \xB7 ${summary.timeout} timeout \xB7 ${summary.error} error`, healthTone)}
|
|
1162
|
+
${metricCard("Avg duration", formatMs(summary.avgDuration), `p95 ${formatMs(summary.p95Duration)}`)}
|
|
1163
|
+
${metricCard("LLM calls", compactNumber(summary.totalCalls), `${compactNumber(summary.totalInput)} input \xB7 ${compactNumber(summary.totalOutput)} output`)}
|
|
1164
|
+
${metricCard("Tokens saved", compactNumber(summary.totalSaved), `avg score ${summary.avgScore || "\u2014"}`)}
|
|
1165
|
+
</section>
|
|
1166
|
+
<section class="layout">
|
|
1167
|
+
<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>
|
|
1168
|
+
<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>
|
|
1169
|
+
</section>
|
|
1170
|
+
<section class="two section">
|
|
1171
|
+
<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>
|
|
1172
|
+
<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>
|
|
1173
|
+
</section>
|
|
1174
|
+
<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>
|
|
1175
|
+
<section class="section"><h2>Raw text report</h2><pre>${escapeHtml(report)}</pre></section>
|
|
1176
|
+
</main></body></html>`;
|
|
1177
|
+
const fp = path2.join(CACHE_DIR, "smart-compact-report.html");
|
|
1178
|
+
fs2.writeFileSync(fp, html);
|
|
1179
|
+
return fp;
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
warn("writeMetricsDashboard failed", e);
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/core.ts
|
|
1187
|
+
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
876
1188
|
|
|
877
1189
|
// src/utils/extraction.ts
|
|
878
1190
|
import path3 from "path";
|
|
@@ -910,6 +1222,41 @@ function extractText(content) {
|
|
|
910
1222
|
}).join("");
|
|
911
1223
|
return "";
|
|
912
1224
|
}
|
|
1225
|
+
function mediaKind(type, mime) {
|
|
1226
|
+
const s = (type + " " + (mime ?? "")).toLowerCase();
|
|
1227
|
+
if (/image|input_image|image_url/.test(s))
|
|
1228
|
+
return "image";
|
|
1229
|
+
if (/audio/.test(s))
|
|
1230
|
+
return "audio";
|
|
1231
|
+
if (/video/.test(s))
|
|
1232
|
+
return "video";
|
|
1233
|
+
if (/file|document|pdf|attachment/.test(s))
|
|
1234
|
+
return "file";
|
|
1235
|
+
return "unknown";
|
|
1236
|
+
}
|
|
1237
|
+
function extractMediaAttachments(msgs) {
|
|
1238
|
+
const out = [];
|
|
1239
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
1240
|
+
const blocks = Array.isArray(msgs[i].content) ? msgs[i].content : [];
|
|
1241
|
+
for (const b of blocks) {
|
|
1242
|
+
if (!b || typeof b !== "object")
|
|
1243
|
+
continue;
|
|
1244
|
+
const rec = b;
|
|
1245
|
+
const type = String(rec.type ?? "");
|
|
1246
|
+
if (type === "text" || type === "toolCall" || type === "tool_use")
|
|
1247
|
+
continue;
|
|
1248
|
+
const mimeType = rec.mimeType ?? rec.mime_type ?? rec.mediaType ?? rec.media_type;
|
|
1249
|
+
const name = rec.name ?? rec.filename ?? rec.fileName ?? rec.title;
|
|
1250
|
+
const sizeBytes = rec.sizeBytes ?? rec.size_bytes ?? rec.size;
|
|
1251
|
+
const source = typeof rec.url === "string" ? "url" : typeof rec.path === "string" ? "path" : typeof rec.data === "string" || typeof rec.base64 === "string" ? "inline" : undefined;
|
|
1252
|
+
const kind = mediaKind(type, mimeType);
|
|
1253
|
+
if (kind !== "unknown" || source || mimeType || name) {
|
|
1254
|
+
out.push({ index: i, kind, mimeType, name, sizeBytes: typeof sizeBytes === "number" ? sizeBytes : undefined, source });
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return out;
|
|
1259
|
+
}
|
|
913
1260
|
function buildToolCallIndex(msgs) {
|
|
914
1261
|
const idx = new Map;
|
|
915
1262
|
for (let i = 0;i < msgs.length; i++) {
|
|
@@ -1279,6 +1626,7 @@ function extractStructured(msgs, pc) {
|
|
|
1279
1626
|
const constraints = mineConstraints(msgs);
|
|
1280
1627
|
const topics = segmentTopicsHeuristic(msgs, pc, 20, tcIdx);
|
|
1281
1628
|
const timeline = buildTimeline(msgs, errors);
|
|
1629
|
+
const mediaAttachments = extractMediaAttachments(msgs);
|
|
1282
1630
|
const mainGoal = extractMainGoal(msgs);
|
|
1283
1631
|
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
1284
1632
|
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
@@ -1291,6 +1639,7 @@ function extractStructured(msgs, pc) {
|
|
|
1291
1639
|
constraints,
|
|
1292
1640
|
topics,
|
|
1293
1641
|
timeline,
|
|
1642
|
+
mediaAttachments,
|
|
1294
1643
|
mainGoal,
|
|
1295
1644
|
lastUserMessages,
|
|
1296
1645
|
lastErrors,
|
|
@@ -1304,27 +1653,42 @@ import * as path4 from "path";
|
|
|
1304
1653
|
function getSessionsDir() {
|
|
1305
1654
|
return path4.join(process.env.HOME ?? "/tmp", ".pi", "agent", "sessions");
|
|
1306
1655
|
}
|
|
1656
|
+
var LOG_PATH_CACHE_TTL_MS = 30000;
|
|
1657
|
+
var logPathCache = new Map;
|
|
1658
|
+
var messageMapCache = new Map;
|
|
1307
1659
|
function findSessionLogFile(sessionId) {
|
|
1308
1660
|
try {
|
|
1661
|
+
const home = process.env.HOME ?? "/tmp";
|
|
1662
|
+
const cached = logPathCache.get(sessionId);
|
|
1663
|
+
if (cached && cached.home === home && cached.expiresAt > Date.now())
|
|
1664
|
+
return cached.path;
|
|
1309
1665
|
const sessionsDir = getSessionsDir();
|
|
1310
|
-
if (!fs3.existsSync(sessionsDir))
|
|
1666
|
+
if (!fs3.existsSync(sessionsDir)) {
|
|
1667
|
+
logPathCache.set(sessionId, { path: null, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home });
|
|
1311
1668
|
return null;
|
|
1669
|
+
}
|
|
1312
1670
|
for (const subdir of fs3.readdirSync(sessionsDir)) {
|
|
1313
1671
|
const subdirPath = path4.join(sessionsDir, subdir);
|
|
1314
1672
|
const stat = fs3.statSync(subdirPath);
|
|
1315
1673
|
if (!stat.isDirectory())
|
|
1316
1674
|
continue;
|
|
1317
1675
|
const exact = path4.join(subdirPath, sessionId + ".jsonl");
|
|
1318
|
-
if (fs3.existsSync(exact))
|
|
1676
|
+
if (fs3.existsSync(exact)) {
|
|
1677
|
+
logPathCache.set(sessionId, { path: exact, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home });
|
|
1319
1678
|
return exact;
|
|
1679
|
+
}
|
|
1320
1680
|
const files = fs3.readdirSync(subdirPath);
|
|
1321
1681
|
const match = files.find((f) => f.endsWith("_" + sessionId + ".jsonl"));
|
|
1322
|
-
if (match)
|
|
1323
|
-
|
|
1682
|
+
if (match) {
|
|
1683
|
+
const found = path4.join(subdirPath, match);
|
|
1684
|
+
logPathCache.set(sessionId, { path: found, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home });
|
|
1685
|
+
return found;
|
|
1686
|
+
}
|
|
1324
1687
|
}
|
|
1325
1688
|
} catch (e) {
|
|
1326
1689
|
debug("findSessionLogFile failed", e);
|
|
1327
1690
|
}
|
|
1691
|
+
logPathCache.set(sessionId, { path: null, expiresAt: Date.now() + LOG_PATH_CACHE_TTL_MS, home: process.env.HOME ?? "/tmp" });
|
|
1328
1692
|
return null;
|
|
1329
1693
|
}
|
|
1330
1694
|
function normalizeLogMessage(msg) {
|
|
@@ -1352,6 +1716,11 @@ function readOriginalMessageMap(sessionId) {
|
|
|
1352
1716
|
return null;
|
|
1353
1717
|
}
|
|
1354
1718
|
try {
|
|
1719
|
+
const stat = fs3.statSync(logPath);
|
|
1720
|
+
const cached = messageMapCache.get(sessionId);
|
|
1721
|
+
if (cached && cached.logPath === logPath && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
1722
|
+
return cached.map;
|
|
1723
|
+
}
|
|
1355
1724
|
const raw = fs3.readFileSync(logPath, "utf-8");
|
|
1356
1725
|
const map = new Map;
|
|
1357
1726
|
for (const line of raw.split(`
|
|
@@ -1371,7 +1740,11 @@ function readOriginalMessageMap(sessionId) {
|
|
|
1371
1740
|
}
|
|
1372
1741
|
}
|
|
1373
1742
|
debug("readOriginalMessageMap: " + map.size + " msgs from " + logPath);
|
|
1374
|
-
|
|
1743
|
+
if (map.size > 0) {
|
|
1744
|
+
messageMapCache.set(sessionId, { logPath, mtimeMs: stat.mtimeMs, size: stat.size, map });
|
|
1745
|
+
return map;
|
|
1746
|
+
}
|
|
1747
|
+
return null;
|
|
1375
1748
|
} catch (e) {
|
|
1376
1749
|
debug("readOriginalMessageMap failed", e);
|
|
1377
1750
|
return null;
|
|
@@ -2486,7 +2859,7 @@ function chunkLlmMessages(msgs, boundaries, pc) {
|
|
|
2486
2859
|
}
|
|
2487
2860
|
return merged;
|
|
2488
2861
|
}
|
|
2489
|
-
async function singlePassCompact(convText, extraction, report, prevContext, model, auth, signal) {
|
|
2862
|
+
async function singlePassCompact(convText, extraction, report, prevContext, model, auth, budgetTokens, signal) {
|
|
2490
2863
|
const extractionCtx = buildExtractionContext(extraction);
|
|
2491
2864
|
const explorationCtx = report ? buildExplorationContext(report) : "";
|
|
2492
2865
|
const sessionType = inferSessionType(extraction, report);
|
|
@@ -2501,7 +2874,7 @@ Session-specific instructions:
|
|
|
2501
2874
|
{ role: "user", content: [{ type: "text", text: adaptedPrefix }], timestamp: Date.now() },
|
|
2502
2875
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
2503
2876
|
]
|
|
2504
|
-
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: getProviderCaps(model.provider).maxOutputTokens, signal });
|
|
2877
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budgetTokens, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2505
2878
|
const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2506
2879
|
`).trim();
|
|
2507
2880
|
if (!summary.startsWith("##"))
|
|
@@ -3037,13 +3410,27 @@ async function showCompactUI(ctx, opts) {
|
|
|
3037
3410
|
|
|
3038
3411
|
// src/core.ts
|
|
3039
3412
|
async function runSmartCompact(opts) {
|
|
3040
|
-
const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact } = opts;
|
|
3413
|
+
const { ctx, summaryModel, segModel, profile, verbose = false, dryRun = false, pendingRef, isRunning, autoTriggered = false, userNote, skipCompact, timeoutMs = 0 } = opts;
|
|
3041
3414
|
if (isRunning.value)
|
|
3042
3415
|
return;
|
|
3043
3416
|
isRunning.value = true;
|
|
3044
3417
|
const pipelineStart = Date.now();
|
|
3418
|
+
const phaseTimings = [];
|
|
3419
|
+
let phaseStart = pipelineStart;
|
|
3420
|
+
const markPhase = (phase) => {
|
|
3421
|
+
const now = Date.now();
|
|
3422
|
+
phaseTimings.push({ phase, durationMs: now - phaseStart });
|
|
3423
|
+
phaseStart = now;
|
|
3424
|
+
};
|
|
3045
3425
|
resetCompactSessionId();
|
|
3046
3426
|
resetMetrics();
|
|
3427
|
+
let sessionId = "unknown";
|
|
3428
|
+
let totalTokens = 0;
|
|
3429
|
+
let contextPercent = 0;
|
|
3430
|
+
let toolPercent = 0;
|
|
3431
|
+
let tier;
|
|
3432
|
+
let methodForMetrics;
|
|
3433
|
+
const modelLabel = summaryModel ? summaryModel.provider + "/" + summaryModel.id : "unknown";
|
|
3047
3434
|
if (!summaryModel || !segModel) {
|
|
3048
3435
|
isRunning.value = false;
|
|
3049
3436
|
if (!autoTriggered)
|
|
@@ -3066,7 +3453,7 @@ async function runSmartCompact(opts) {
|
|
|
3066
3453
|
const apiKey = auth.apiKey;
|
|
3067
3454
|
const apiHeaders = auth.headers;
|
|
3068
3455
|
const usage = ctx.getContextUsage();
|
|
3069
|
-
|
|
3456
|
+
totalTokens = usage?.tokens ?? 0;
|
|
3070
3457
|
const notify = (msg, type = "info") => {
|
|
3071
3458
|
ctx.ui.notify(msg, type === "success" ? "info" : type);
|
|
3072
3459
|
};
|
|
@@ -3076,14 +3463,13 @@ async function runSmartCompact(opts) {
|
|
|
3076
3463
|
};
|
|
3077
3464
|
const ctrl = new AbortController;
|
|
3078
3465
|
const signal = ctrl.signal;
|
|
3079
|
-
if (
|
|
3466
|
+
if (timeoutMs > 0) {
|
|
3080
3467
|
timeoutId = setTimeout(() => {
|
|
3081
3468
|
timedOut = true;
|
|
3082
3469
|
ctrl.abort();
|
|
3083
|
-
notify("Smart compact auto-trigger
|
|
3084
|
-
},
|
|
3470
|
+
notify("Smart compact auto-trigger exceeded " + timeoutMs + "ms; Pi will use native compact for this run", "warning");
|
|
3471
|
+
}, timeoutMs);
|
|
3085
3472
|
}
|
|
3086
|
-
const modelLabel = summaryModel.provider + "/" + summaryModel.id;
|
|
3087
3473
|
notify("Smart compact: " + modelLabel + ", " + profile + ", tokens=" + totalTokens, "info");
|
|
3088
3474
|
notify("EESV Compact (" + modelLabel + ", " + profile + ") \u2014 " + (totalTokens ?? 0).toLocaleString() + "t", "info");
|
|
3089
3475
|
const branch = ctx.sessionManager.getBranch();
|
|
@@ -3111,6 +3497,7 @@ async function runSmartCompact(opts) {
|
|
|
3111
3497
|
return;
|
|
3112
3498
|
}
|
|
3113
3499
|
const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id;
|
|
3500
|
+
markPhase("prepare");
|
|
3114
3501
|
if (!autoTriggered) {
|
|
3115
3502
|
showProgressOverlay(ctx, { phase: 1, phaseName: "Extract", detail: "Preparing...", model: modelLabel, profile });
|
|
3116
3503
|
}
|
|
@@ -3123,9 +3510,10 @@ async function runSmartCompact(opts) {
|
|
|
3123
3510
|
notify("Using untruncated session log (" + llmMessages.length + " msgs)", "info");
|
|
3124
3511
|
}
|
|
3125
3512
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3513
|
+
markPhase("recover");
|
|
3514
|
+
contextPercent = ctx.model && totalTokens ? totalTokens / ctx.model.contextWindow * 100 : 0;
|
|
3515
|
+
toolPercent = computeToolCharPercentage(branch);
|
|
3516
|
+
tier = selectCompactionTier(contextPercent, toolPercent, totalTokens, MIN_TOKEN_THRESHOLD);
|
|
3129
3517
|
if (tier === "none") {
|
|
3130
3518
|
isRunning.value = false;
|
|
3131
3519
|
if (!autoTriggered)
|
|
@@ -3138,9 +3526,10 @@ async function runSmartCompact(opts) {
|
|
|
3138
3526
|
notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
3139
3527
|
}
|
|
3140
3528
|
llmMessages = pruning.messages;
|
|
3529
|
+
markPhase("prune");
|
|
3141
3530
|
const convText = serializeConversation(llmMessages);
|
|
3142
3531
|
const convTokens = estimateTokens(convText);
|
|
3143
|
-
|
|
3532
|
+
sessionId = ctx.sessionManager.getSessionId?.() ?? "unknown";
|
|
3144
3533
|
const backupPath = backupConversation(convText, sessionId);
|
|
3145
3534
|
const prevContext = getPreviousCompactionContext(branch);
|
|
3146
3535
|
const cachedExt = loadCachedExtraction(sessionId);
|
|
@@ -3162,6 +3551,7 @@ async function runSmartCompact(opts) {
|
|
|
3162
3551
|
vlog("Full extraction \u2014 " + llmMessages.length + " messages, tier=" + tier);
|
|
3163
3552
|
}
|
|
3164
3553
|
saveCachedExtraction(sessionId, extraction, llmMessages.length, currentFirstId, currentLastId);
|
|
3554
|
+
markPhase("extract");
|
|
3165
3555
|
const projectId = deriveProjectId(findGitRoot(ctx.cwd) ?? ctx.cwd, extraction, sessionId);
|
|
3166
3556
|
const fingerprint = loadProjectFingerprint(projectId);
|
|
3167
3557
|
if (fingerprint) {
|
|
@@ -3175,12 +3565,14 @@ async function runSmartCompact(opts) {
|
|
|
3175
3565
|
let explorationReport = null;
|
|
3176
3566
|
let explorationRounds = 0;
|
|
3177
3567
|
let chunkCount = 0;
|
|
3178
|
-
|
|
3179
|
-
|
|
3568
|
+
const providerCaps = getProviderCaps(summaryModel.provider);
|
|
3569
|
+
const singlePassMaxTokens = Math.round(pc.singlePassMaxTokens * providerCaps.singlePassTokenMultiplier);
|
|
3570
|
+
vlog("Tier=" + tier + " | convTokens=" + convTokens + " | singlePassMax=" + singlePassMaxTokens);
|
|
3571
|
+
if (convTokens < singlePassMaxTokens) {
|
|
3180
3572
|
if (!autoTriggered)
|
|
3181
3573
|
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Single-pass (" + convTokens.toLocaleString() + "t)", model: modelLabel, profile, extraction });
|
|
3182
3574
|
try {
|
|
3183
|
-
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey, headers: apiHeaders }, signal);
|
|
3575
|
+
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey, headers: apiHeaders }, pc.summaryBudgetTokens, signal);
|
|
3184
3576
|
finalSummary = r.summary;
|
|
3185
3577
|
method = "single-pass";
|
|
3186
3578
|
llmCalls = r.llmCalls;
|
|
@@ -3207,6 +3599,7 @@ async function runSmartCompact(opts) {
|
|
|
3207
3599
|
} else {
|
|
3208
3600
|
notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
|
|
3209
3601
|
}
|
|
3602
|
+
markPhase("explore");
|
|
3210
3603
|
let boundaries;
|
|
3211
3604
|
if (explorationReport?.boundaries.length) {
|
|
3212
3605
|
const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
|
|
@@ -3243,8 +3636,7 @@ async function runSmartCompact(opts) {
|
|
|
3243
3636
|
const totalBatches = batches.length;
|
|
3244
3637
|
if (!autoTriggered)
|
|
3245
3638
|
showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: "0/" + totalBatches + " batches", model: modelLabel, profile, extraction, totalBatches });
|
|
3246
|
-
const
|
|
3247
|
-
const concurrency = caps.concurrencyLimit;
|
|
3639
|
+
const concurrency = providerCaps.concurrencyLimit;
|
|
3248
3640
|
if (totalBatches <= 1) {
|
|
3249
3641
|
try {
|
|
3250
3642
|
summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey, headers: apiHeaders }, signal));
|
|
@@ -3315,6 +3707,10 @@ async function runSmartCompact(opts) {
|
|
|
3315
3707
|
method = "eesv";
|
|
3316
3708
|
llmCalls = explorationRounds + batches.length + assemblyCalls;
|
|
3317
3709
|
}
|
|
3710
|
+
methodForMetrics = method;
|
|
3711
|
+
if (method === "single-pass" || method === "heuristic")
|
|
3712
|
+
markPhase("explore");
|
|
3713
|
+
markPhase("synthesize");
|
|
3318
3714
|
if (!autoTriggered)
|
|
3319
3715
|
showProgressOverlay(ctx, { phase: 4, phaseName: "Verify", detail: "Checking...", model: modelLabel, profile, extraction, explorationRounds });
|
|
3320
3716
|
const verification = verifySummary(finalSummary, extraction);
|
|
@@ -3337,6 +3733,7 @@ async function runSmartCompact(opts) {
|
|
|
3337
3733
|
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + " \u2265 85 \u2014 skipping patch", "info");
|
|
3338
3734
|
}
|
|
3339
3735
|
}
|
|
3736
|
+
markPhase("verify");
|
|
3340
3737
|
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
3341
3738
|
const detRead = extraction.readFiles;
|
|
3342
3739
|
const estimatedAfter = estimateTokens(finalSummary) + accTokens;
|
|
@@ -3362,6 +3759,7 @@ async function runSmartCompact(opts) {
|
|
|
3362
3759
|
notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
3363
3760
|
}
|
|
3364
3761
|
}
|
|
3762
|
+
markPhase("state");
|
|
3365
3763
|
const details = {
|
|
3366
3764
|
method,
|
|
3367
3765
|
chunkCount: chunkCount || 1,
|
|
@@ -3385,6 +3783,25 @@ async function runSmartCompact(opts) {
|
|
|
3385
3783
|
openLoops
|
|
3386
3784
|
};
|
|
3387
3785
|
if (dryRun) {
|
|
3786
|
+
appendMetricsLog(sessionId, {
|
|
3787
|
+
profile,
|
|
3788
|
+
tier,
|
|
3789
|
+
contextPercent: Math.round(contextPercent),
|
|
3790
|
+
toolPercent,
|
|
3791
|
+
tokensBefore: totalTokens,
|
|
3792
|
+
tokensSaved,
|
|
3793
|
+
pruneSavedTokens: pruning.prunedTokenSaving,
|
|
3794
|
+
chunkCount: chunkCount || 1,
|
|
3795
|
+
verificationScore: verification.score,
|
|
3796
|
+
verificationGaps: verification.gaps.length,
|
|
3797
|
+
method,
|
|
3798
|
+
model: modelLabel,
|
|
3799
|
+
provider: summaryModel.provider,
|
|
3800
|
+
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3801
|
+
status: "dry-run",
|
|
3802
|
+
phaseTimings,
|
|
3803
|
+
durationMs: Date.now() - pipelineStart
|
|
3804
|
+
});
|
|
3388
3805
|
notify("DRY RUN (" + method + ", " + profile + ") \u2014 " + toCompact.length + " msgs, " + llmCalls + " calls", "info");
|
|
3389
3806
|
return;
|
|
3390
3807
|
}
|
|
@@ -3395,17 +3812,7 @@ async function runSmartCompact(opts) {
|
|
|
3395
3812
|
pendingRef.createdAt = Date.now();
|
|
3396
3813
|
saveProjectFingerprint(projectId, extraction);
|
|
3397
3814
|
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
|
-
});
|
|
3815
|
+
markPhase("persist");
|
|
3409
3816
|
try {
|
|
3410
3817
|
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat();
|
|
3411
3818
|
if (postCompactMsgs.length > 2) {
|
|
@@ -3422,6 +3829,26 @@ async function runSmartCompact(opts) {
|
|
|
3422
3829
|
} catch (err) {
|
|
3423
3830
|
warn("Damage detection error", err);
|
|
3424
3831
|
}
|
|
3832
|
+
markPhase("damage");
|
|
3833
|
+
appendMetricsLog(sessionId, {
|
|
3834
|
+
profile,
|
|
3835
|
+
tier,
|
|
3836
|
+
contextPercent: Math.round(contextPercent),
|
|
3837
|
+
toolPercent,
|
|
3838
|
+
tokensBefore: totalTokens,
|
|
3839
|
+
tokensSaved,
|
|
3840
|
+
pruneSavedTokens: pruning.prunedTokenSaving,
|
|
3841
|
+
chunkCount: chunkCount || 1,
|
|
3842
|
+
verificationScore: verification.score,
|
|
3843
|
+
verificationGaps: verification.gaps.length,
|
|
3844
|
+
method,
|
|
3845
|
+
model: modelLabel,
|
|
3846
|
+
provider: summaryModel.provider,
|
|
3847
|
+
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3848
|
+
status: "success",
|
|
3849
|
+
phaseTimings,
|
|
3850
|
+
durationMs: Date.now() - pipelineStart
|
|
3851
|
+
});
|
|
3425
3852
|
const ms = getMetricsSummary();
|
|
3426
3853
|
if (ms.totalCalls > 0) {
|
|
3427
3854
|
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 +3873,23 @@ async function runSmartCompact(opts) {
|
|
|
3446
3873
|
}
|
|
3447
3874
|
});
|
|
3448
3875
|
}
|
|
3876
|
+
} catch (err) {
|
|
3877
|
+
appendMetricsLog(sessionId, {
|
|
3878
|
+
profile,
|
|
3879
|
+
tier,
|
|
3880
|
+
contextPercent: Math.round(contextPercent),
|
|
3881
|
+
toolPercent,
|
|
3882
|
+
tokensBefore: totalTokens,
|
|
3883
|
+
method: methodForMetrics,
|
|
3884
|
+
model: modelLabel,
|
|
3885
|
+
provider: summaryModel.provider,
|
|
3886
|
+
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3887
|
+
status: timedOut ? "timeout" : "error",
|
|
3888
|
+
fallbackReason: err instanceof Error ? err.message : String(err),
|
|
3889
|
+
phaseTimings,
|
|
3890
|
+
durationMs: Date.now() - pipelineStart
|
|
3891
|
+
});
|
|
3892
|
+
throw err;
|
|
3449
3893
|
} finally {
|
|
3450
3894
|
if (timeoutId)
|
|
3451
3895
|
clearTimeout(timeoutId);
|
|
@@ -3495,7 +3939,7 @@ function smartCompactExtension(pi) {
|
|
|
3495
3939
|
pi.registerCommand("smart-compact", {
|
|
3496
3940
|
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
|
|
3497
3941
|
getArgumentCompletions: (prefix) => {
|
|
3498
|
-
const m = ["verbose", "debug", "dry-run", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
3942
|
+
const m = ["verbose", "debug", "dry-run", "metrics", "dashboard", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
3499
3943
|
return m.length ? m : null;
|
|
3500
3944
|
},
|
|
3501
3945
|
handler: async (args, ctx) => {
|
|
@@ -3505,6 +3949,15 @@ function smartCompactExtension(pi) {
|
|
|
3505
3949
|
const flags = tokens.map((t) => t.toLowerCase());
|
|
3506
3950
|
const verbose = flags.includes("verbose") || flags.includes("debug");
|
|
3507
3951
|
const dryRun = flags.includes("dry-run");
|
|
3952
|
+
if (flags.includes("metrics") || flags.includes("dashboard")) {
|
|
3953
|
+
const dashboard = flags.includes("dashboard");
|
|
3954
|
+
const report = buildMetricsReport();
|
|
3955
|
+
const fp = dashboard ? writeMetricsDashboard() : null;
|
|
3956
|
+
ctx.ui.notify(report + (fp ? `
|
|
3957
|
+
|
|
3958
|
+
Dashboard: ` + fp : ""), "info");
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3508
3961
|
const modelArg = tokens.find((t) => t.includes("/"));
|
|
3509
3962
|
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
3510
3963
|
const profile = profileArg ?? loadConfig().profile;
|
|
@@ -3575,21 +4028,26 @@ function smartCompactExtension(pi) {
|
|
|
3575
4028
|
if (!sumModel)
|
|
3576
4029
|
return;
|
|
3577
4030
|
if (!isRunning.value) {
|
|
3578
|
-
const
|
|
3579
|
-
const
|
|
3580
|
-
|
|
4031
|
+
const caps = getProviderCaps(sumModel.provider);
|
|
4032
|
+
const effectiveTimeoutMs = Math.round(config.autoTriggerTimeoutMs * caps.timeoutMultiplier);
|
|
4033
|
+
const compactPromise = runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: config.profile, pendingRef, isRunning, autoTriggered: true, timeoutMs: effectiveTimeoutMs });
|
|
4034
|
+
let timeoutId = null;
|
|
4035
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
4036
|
+
timeoutId = setTimeout(() => resolve("timeout"), effectiveTimeoutMs + 100);
|
|
3581
4037
|
});
|
|
4038
|
+
let result;
|
|
3582
4039
|
try {
|
|
3583
|
-
await Promise.race([compactPromise, timeoutPromise]);
|
|
3584
|
-
}
|
|
3585
|
-
if (
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4040
|
+
result = await Promise.race([compactPromise.then(() => "done"), timeoutPromise]);
|
|
4041
|
+
} finally {
|
|
4042
|
+
if (timeoutId)
|
|
4043
|
+
clearTimeout(timeoutId);
|
|
4044
|
+
}
|
|
4045
|
+
if (result === "timeout") {
|
|
4046
|
+
warn("Smart compact auto-trigger hard timeout after " + effectiveTimeoutMs + "ms");
|
|
4047
|
+
isRunning.value = false;
|
|
4048
|
+
pendingRef.value = null;
|
|
4049
|
+
pendingRef.createdAt = 0;
|
|
4050
|
+
return;
|
|
3593
4051
|
}
|
|
3594
4052
|
const pending = pendingRef.value;
|
|
3595
4053
|
if (pending) {
|
|
@@ -3613,13 +4071,22 @@ function smartCompactExtension(pi) {
|
|
|
3613
4071
|
properties: {
|
|
3614
4072
|
profile: { type: "string", description: "light, balanced, or aggressive. Default: balanced." },
|
|
3615
4073
|
verbose: { type: "boolean", description: "Show detailed pipeline output." },
|
|
3616
|
-
dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." }
|
|
4074
|
+
dry_run: { type: "boolean", description: "Run the pipeline but skip applying the compaction." },
|
|
4075
|
+
report: { type: "boolean", description: "Return recent performance metrics instead of compacting." },
|
|
4076
|
+
dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." }
|
|
3617
4077
|
}
|
|
3618
4078
|
},
|
|
3619
4079
|
async execute(_id, params, _sig, _onUp, ctx) {
|
|
3620
4080
|
const profile = params.profile === "light" || params.profile === "balanced" || params.profile === "aggressive" ? params.profile : undefined;
|
|
3621
4081
|
const verbose = !!params.verbose;
|
|
3622
4082
|
const dryRun = !!params.dry_run;
|
|
4083
|
+
if (params.report || params.dashboard) {
|
|
4084
|
+
const report = buildMetricsReport();
|
|
4085
|
+
const fp = params.dashboard ? writeMetricsDashboard() : null;
|
|
4086
|
+
return { content: [{ type: "text", text: report + (fp ? `
|
|
4087
|
+
|
|
4088
|
+
Dashboard: ` + fp : "") }], details: undefined };
|
|
4089
|
+
}
|
|
3623
4090
|
const config = loadConfig();
|
|
3624
4091
|
const resolvedProfile = profile ?? config.profile;
|
|
3625
4092
|
const cmdCtx = ctx;
|