pi-smart-compact 7.12.4 → 7.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +465 -96
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/dashboard-format.d.ts +14 -0
- package/dist/ui/dashboard-format.d.ts.map +1 -0
- package/dist/ui/overlays.d.ts +8 -1
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +18 -3
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/fingerprint.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +3 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/pruning.d.ts +2 -0
- package/dist/utils/pruning.d.ts.map +1 -1
- package/package.json +1 -1
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.13.0";
|
|
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 = {
|
|
@@ -95,8 +95,8 @@ Rules for Accuracy:
|
|
|
95
95
|
` + `2. Status: only mark "done" if there's clear evidence (successful test run, user confirmation)
|
|
96
96
|
` + `3. Quote specific values, don't paraphrase code
|
|
97
97
|
|
|
98
|
-
` + `For EACH segment produce:
|
|
99
|
-
` + `### {TOPIC_NAME}
|
|
98
|
+
` + `For EACH segment produce EXACTLY:
|
|
99
|
+
` + `### CHUNK {NUMBER}: {TOPIC_NAME}
|
|
100
100
|
` + `**Priority**: [critical|high|normal|low]
|
|
101
101
|
` + `**Summary**: [2-4 sentences: what happened, errors, code changes with paths]
|
|
102
102
|
` + `**Decisions**: [comma-separated, or "None"]
|
|
@@ -398,6 +398,25 @@ function smartKeepBoundary(msgs, keepFromIndex, branchEntries) {
|
|
|
398
398
|
}
|
|
399
399
|
return adjusted;
|
|
400
400
|
}
|
|
401
|
+
function collectToolCallIds(blocks, msgIndex, out) {
|
|
402
|
+
for (const b of blocks) {
|
|
403
|
+
const block = b;
|
|
404
|
+
if (block?.type === "toolCall") {
|
|
405
|
+
if (typeof block.id === "string") {
|
|
406
|
+
out.set(block.id, msgIndex);
|
|
407
|
+
}
|
|
408
|
+
const args = block.arguments;
|
|
409
|
+
if (block.name === "multi_tool_use.parallel" && args && Array.isArray(args.tool_uses)) {
|
|
410
|
+
for (const nested of args.tool_uses) {
|
|
411
|
+
const n = nested;
|
|
412
|
+
if (typeof n.id === "string") {
|
|
413
|
+
out.set(n.id, msgIndex);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
401
420
|
function guardToolCallBoundary(msgs, keepFrom) {
|
|
402
421
|
if (keepFrom <= 0 || keepFrom >= msgs.length)
|
|
403
422
|
return keepFrom;
|
|
@@ -407,11 +426,7 @@ function guardToolCallBoundary(msgs, keepFrom) {
|
|
|
407
426
|
if (m?.role !== "assistant")
|
|
408
427
|
continue;
|
|
409
428
|
const blocks = Array.isArray(m?.content) ? m.content : [];
|
|
410
|
-
|
|
411
|
-
if (b?.type === "toolCall" && b.id) {
|
|
412
|
-
tcMap.set(b.id, i);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
429
|
+
collectToolCallIds(blocks, i, tcMap);
|
|
415
430
|
}
|
|
416
431
|
let adjusted = keepFrom;
|
|
417
432
|
let changed = true;
|
|
@@ -551,8 +566,6 @@ function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThres
|
|
|
551
566
|
return "none";
|
|
552
567
|
if (contextPercent < minContextPercent)
|
|
553
568
|
return "none";
|
|
554
|
-
if (contextPercent < 45 && toolPercent < 60)
|
|
555
|
-
return "none";
|
|
556
569
|
if (contextPercent < 80)
|
|
557
570
|
return "light";
|
|
558
571
|
return "full";
|
|
@@ -841,6 +854,11 @@ function recordMetric(m) {
|
|
|
841
854
|
if (_metrics.length > 200)
|
|
842
855
|
_metrics.splice(0, _metrics.length - 100);
|
|
843
856
|
}
|
|
857
|
+
function effectivePromptInputTokens(inputTokens, cacheHitTokens) {
|
|
858
|
+
if (cacheHitTokens <= 0)
|
|
859
|
+
return Math.max(0, inputTokens);
|
|
860
|
+
return cacheHitTokens > inputTokens ? inputTokens + cacheHitTokens : inputTokens;
|
|
861
|
+
}
|
|
844
862
|
function getMetricsSummary() {
|
|
845
863
|
const n = _metrics.length;
|
|
846
864
|
if (!n)
|
|
@@ -849,13 +867,14 @@ function getMetricsSummary() {
|
|
|
849
867
|
const totalOutput = _metrics.reduce((s, m) => s + m.outputTokens, 0);
|
|
850
868
|
const totalCacheHit = _metrics.reduce((s, m) => s + m.cacheHitTokens, 0);
|
|
851
869
|
const avgLatency = _metrics.reduce((s, m) => s + m.latencyMs, 0) / n;
|
|
870
|
+
const cacheDenominator = effectivePromptInputTokens(totalInput, totalCacheHit);
|
|
852
871
|
return {
|
|
853
872
|
totalCalls: n,
|
|
854
873
|
totalInput,
|
|
855
874
|
totalOutput,
|
|
856
875
|
totalCacheHit,
|
|
857
876
|
avgLatency: Math.round(avgLatency),
|
|
858
|
-
cacheHitRate:
|
|
877
|
+
cacheHitRate: cacheDenominator > 0 ? Math.min(1, totalCacheHit / cacheDenominator) : 0
|
|
859
878
|
};
|
|
860
879
|
}
|
|
861
880
|
async function trackedComplete(phase, model, reqBody, opts) {
|
|
@@ -904,7 +923,27 @@ async function trackedComplete(phase, model, reqBody, opts) {
|
|
|
904
923
|
function getCachePath(sessionId) {
|
|
905
924
|
return path2.join(CACHE_DIR, "compact-extraction-" + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
|
|
906
925
|
}
|
|
907
|
-
|
|
926
|
+
var _extractionCacheHits = 0;
|
|
927
|
+
var _extractionCacheMisses = 0;
|
|
928
|
+
function resetExtractionCacheStats() {
|
|
929
|
+
_extractionCacheHits = 0;
|
|
930
|
+
_extractionCacheMisses = 0;
|
|
931
|
+
}
|
|
932
|
+
function getExtractionCacheStats() {
|
|
933
|
+
const total = _extractionCacheHits + _extractionCacheMisses;
|
|
934
|
+
return {
|
|
935
|
+
hits: _extractionCacheHits,
|
|
936
|
+
misses: _extractionCacheMisses,
|
|
937
|
+
hitRate: total > 0 ? _extractionCacheHits / total : 0
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function recordExtractionCacheHit() {
|
|
941
|
+
_extractionCacheHits++;
|
|
942
|
+
}
|
|
943
|
+
function recordExtractionCacheMiss() {
|
|
944
|
+
_extractionCacheMisses++;
|
|
945
|
+
}
|
|
946
|
+
function saveCachedExtraction(sessionId, extraction, msgCount, firstEntryId, lastEntryId, entryIds, keptEntryIds) {
|
|
908
947
|
try {
|
|
909
948
|
if (!fs2.existsSync(CACHE_DIR))
|
|
910
949
|
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
@@ -914,7 +953,9 @@ function saveCachedExtraction(sessionId, extraction, msgCount, firstEntryId, las
|
|
|
914
953
|
messageCount: msgCount,
|
|
915
954
|
timestamp: Date.now(),
|
|
916
955
|
firstEntryId,
|
|
917
|
-
lastEntryId
|
|
956
|
+
lastEntryId,
|
|
957
|
+
entryIds,
|
|
958
|
+
keptEntryIds
|
|
918
959
|
};
|
|
919
960
|
fs2.writeFileSync(getCachePath(sessionId), JSON.stringify(cached));
|
|
920
961
|
} catch (e) {
|
|
@@ -1069,9 +1110,9 @@ function summarizeDashboard(entries) {
|
|
|
1069
1110
|
successRate: entries.length ? success / entries.length : 0,
|
|
1070
1111
|
avgDuration: Math.round(average(durations)),
|
|
1071
1112
|
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),
|
|
1113
|
+
totalCalls: entries.reduce((sum, e) => sum + (e.totalCalls ?? 0), 0),
|
|
1114
|
+
totalInput: entries.reduce((sum, e) => sum + (e.totalInput ?? 0), 0),
|
|
1115
|
+
totalOutput: entries.reduce((sum, e) => sum + (e.totalOutput ?? 0), 0),
|
|
1075
1116
|
totalSaved: entries.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1076
1117
|
avgScore: Math.round(average(scored))
|
|
1077
1118
|
};
|
|
@@ -1079,7 +1120,9 @@ function summarizeDashboard(entries) {
|
|
|
1079
1120
|
function groupMetrics(entries, keyFn) {
|
|
1080
1121
|
const groups = new Map;
|
|
1081
1122
|
for (const entry of entries) {
|
|
1082
|
-
const key = keyFn(entry)
|
|
1123
|
+
const key = keyFn(entry);
|
|
1124
|
+
if (!key)
|
|
1125
|
+
continue;
|
|
1083
1126
|
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
1084
1127
|
}
|
|
1085
1128
|
return [...groups.entries()].map(([name, group]) => {
|
|
@@ -1093,7 +1136,7 @@ function groupMetrics(entries, keyFn) {
|
|
|
1093
1136
|
p95Duration: percentile(durations, 95),
|
|
1094
1137
|
avgScore: Math.round(average(scores)),
|
|
1095
1138
|
totalSaved: group.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1096
|
-
totalCalls: group.reduce((sum, e) => sum + e.totalCalls, 0),
|
|
1139
|
+
totalCalls: group.reduce((sum, e) => sum + (e.totalCalls ?? 0), 0),
|
|
1097
1140
|
errorRate: group.length ? failures / group.length : 0
|
|
1098
1141
|
};
|
|
1099
1142
|
}).sort((a, b) => b.runs - a.runs || a.name.localeCompare(b.name));
|
|
@@ -1151,7 +1194,7 @@ function phaseRows(entry) {
|
|
|
1151
1194
|
}
|
|
1152
1195
|
function recentRunRows(entries) {
|
|
1153
1196
|
if (!entries.length)
|
|
1154
|
-
return `<tr><td colspan="
|
|
1197
|
+
return `<tr><td colspan="12" class="empty">No runs recorded yet</td></tr>`;
|
|
1155
1198
|
return entries.slice(-80).reverse().map((entry) => `<tr>
|
|
1156
1199
|
<td class="mono small">${escapeHtml(entry.ts)}</td>
|
|
1157
1200
|
<td>${escapeHtml(entry.profile)}</td>
|
|
@@ -1162,8 +1205,9 @@ function recentRunRows(entries) {
|
|
|
1162
1205
|
<td class="num">${escapeHtml(formatMs(durationOf(entry)))}</td>
|
|
1163
1206
|
<td class="num">${typeof entry.verificationScore === "number" ? formatNumber(entry.verificationScore) : "\u2014"}</td>
|
|
1164
1207
|
<td class="num">${typeof entry.tokensSaved === "number" ? formatNumber(entry.tokensSaved) : "\u2014"}</td>
|
|
1165
|
-
<td class="num">${formatNumber(entry.totalCalls)}</td>
|
|
1166
|
-
<td class="
|
|
1208
|
+
<td class="num">${formatNumber(entry.totalCalls ?? 0)}</td>
|
|
1209
|
+
<td class="num">${typeof entry.extractionCacheHitRate === "number" ? formatPercent(entry.extractionCacheHitRate) : "\u2014"}</td>
|
|
1210
|
+
<td class="mono small reason">${escapeHtml(entry.fallbackReason ?? entry.extractionCacheMissReason ?? "")}</td>
|
|
1167
1211
|
</tr>`).join(`
|
|
1168
1212
|
`);
|
|
1169
1213
|
}
|
|
@@ -1174,9 +1218,11 @@ function buildMetricsReport(entries = readMetricsLog(100)) {
|
|
|
1174
1218
|
if (!entries.length)
|
|
1175
1219
|
return "No smart-compact metrics recorded yet.";
|
|
1176
1220
|
const summary = summarizeDashboard(entries);
|
|
1177
|
-
const byProfile = groupMetrics(entries, (e) => e.profile
|
|
1178
|
-
const byProvider = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]
|
|
1221
|
+
const byProfile = groupMetrics(entries, (e) => e.profile);
|
|
1222
|
+
const byProvider = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]);
|
|
1179
1223
|
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);
|
|
1224
|
+
const extractionCacheRuns = entries.filter((e) => typeof e.extractionCacheHitRate === "number");
|
|
1225
|
+
const extractionCacheAvg = average(extractionCacheRuns.map((e) => e.extractionCacheHitRate ?? 0));
|
|
1180
1226
|
return [
|
|
1181
1227
|
"# Smart Compact Metrics",
|
|
1182
1228
|
"",
|
|
@@ -1184,6 +1230,7 @@ function buildMetricsReport(entries = readMetricsLog(100)) {
|
|
|
1184
1230
|
"Reliability: " + formatPercent(summary.successRate),
|
|
1185
1231
|
"Latency: avg " + summary.avgDuration + "ms, p95 " + summary.p95Duration + "ms",
|
|
1186
1232
|
"LLM calls: " + summary.totalCalls + ", input " + summary.totalInput + "t, output " + summary.totalOutput + "t",
|
|
1233
|
+
"Extraction cache: avg " + (extractionCacheRuns.length ? formatPercent(extractionCacheAvg) : "\u2014") + " across " + extractionCacheRuns.length + " measured run(s)",
|
|
1187
1234
|
"Tokens saved: " + summary.totalSaved + "t, average verification score: " + summary.avgScore,
|
|
1188
1235
|
"",
|
|
1189
1236
|
"## Profile comparison",
|
|
@@ -1201,8 +1248,8 @@ function writeMetricsDashboard(entries = readMetricsLog(200)) {
|
|
|
1201
1248
|
const summary = summarizeDashboard(entries);
|
|
1202
1249
|
const latest = entries[entries.length - 1];
|
|
1203
1250
|
const report = buildMetricsReport(entries);
|
|
1204
|
-
const profileGroups = groupMetrics(entries, (e) => e.profile
|
|
1205
|
-
const providerGroups = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]
|
|
1251
|
+
const profileGroups = groupMetrics(entries, (e) => e.profile);
|
|
1252
|
+
const providerGroups = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]);
|
|
1206
1253
|
const healthTone = summary.error + summary.timeout > 0 ? "warn" : "good";
|
|
1207
1254
|
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
1255
|
<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>
|
|
@@ -1220,7 +1267,7 @@ function writeMetricsDashboard(entries = readMetricsLog(200)) {
|
|
|
1220
1267
|
<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
1268
|
<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
1269
|
</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>
|
|
1270
|
+
<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 class="num">Ext cache</th><th>Reason</th></tr></thead><tbody>${recentRunRows(entries)}</tbody></table></div></section>
|
|
1224
1271
|
<section class="section"><h2>Raw text report</h2><pre>${escapeHtml(report)}</pre></section>
|
|
1225
1272
|
</main></body></html>`;
|
|
1226
1273
|
const fp = path2.join(CACHE_DIR, "smart-compact-report.html");
|
|
@@ -2047,7 +2094,7 @@ var PI_STATUS_RE = /^\[pi-auto-context\]/;
|
|
|
2047
2094
|
var MAX_TOOL_OUTPUT_CHARS = 800;
|
|
2048
2095
|
function pruneRedundant(msgs) {
|
|
2049
2096
|
if (msgs.length < 5)
|
|
2050
|
-
return { messages: msgs, prunedCount: 0, prunedTokenSaving: 0, reasons: [] };
|
|
2097
|
+
return { messages: msgs, keptIndices: msgs.map((_, i2) => i2), prunedCount: 0, prunedTokenSaving: 0, reasons: [] };
|
|
2051
2098
|
const tcIdx = buildToolCallIndex(msgs);
|
|
2052
2099
|
const keep = new Set(msgs.map((_, i2) => i2));
|
|
2053
2100
|
const reasonMap = new Map;
|
|
@@ -2141,13 +2188,22 @@ function pruneRedundant(msgs) {
|
|
|
2141
2188
|
}
|
|
2142
2189
|
return m;
|
|
2143
2190
|
});
|
|
2144
|
-
const
|
|
2191
|
+
const keptIndices = [];
|
|
2192
|
+
const finalMsgs = [];
|
|
2193
|
+
for (let idx = 0;idx < kept.length; idx++) {
|
|
2194
|
+
const m = kept[idx];
|
|
2195
|
+
if (m === null)
|
|
2196
|
+
continue;
|
|
2197
|
+
keptIndices.push(idx);
|
|
2198
|
+
finalMsgs.push(m);
|
|
2199
|
+
}
|
|
2145
2200
|
const prunedCount = msgs.length - finalMsgs.length;
|
|
2146
2201
|
const originalTokens = estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
|
|
2147
2202
|
const prunedTokens = estimateTokens(finalMsgs.map((m) => extractText(m.content)).join(""));
|
|
2148
2203
|
const reasons = [...reasonMap.entries()].map(([reason, count]) => ({ count, reason }));
|
|
2149
2204
|
return {
|
|
2150
2205
|
messages: finalMsgs,
|
|
2206
|
+
keptIndices,
|
|
2151
2207
|
prunedCount,
|
|
2152
2208
|
prunedTokenSaving: Math.max(0, originalTokens - prunedTokens),
|
|
2153
2209
|
reasons
|
|
@@ -2337,10 +2393,12 @@ function saveProjectFingerprint(projectId, extraction) {
|
|
|
2337
2393
|
...extraction.modifiedFiles.map((f) => f.path),
|
|
2338
2394
|
...extraction.readFiles
|
|
2339
2395
|
])].slice(-50);
|
|
2396
|
+
const detectedLanguage = detectLanguage(extraction);
|
|
2397
|
+
const detectedFramework = detectFramework(extraction);
|
|
2340
2398
|
const fingerprint = {
|
|
2341
2399
|
id: projectId,
|
|
2342
|
-
language: existing?.language
|
|
2343
|
-
framework: existing?.framework ??
|
|
2400
|
+
language: existing?.language && existing.language !== "unknown" ? existing.language : detectedLanguage,
|
|
2401
|
+
framework: existing?.framework ?? detectedFramework,
|
|
2344
2402
|
keyDirectories: extractKeyDirs(extraction),
|
|
2345
2403
|
knownFiles: newKnownFiles,
|
|
2346
2404
|
sessionCount: (existing?.sessionCount ?? 0) + 1,
|
|
@@ -2531,14 +2589,21 @@ function executeExplorationTool(call, llmMessages) {
|
|
|
2531
2589
|
}
|
|
2532
2590
|
case "search_conversation": {
|
|
2533
2591
|
const q = (args.query ?? "").toLowerCase();
|
|
2534
|
-
|
|
2592
|
+
const matches = [];
|
|
2593
|
+
for (let i = 0;i < llmMessages.length && matches.length < 10; i++) {
|
|
2594
|
+
const m = llmMessages[i];
|
|
2535
2595
|
const text = extractText(m?.content).toLowerCase();
|
|
2536
|
-
if (text.includes(q))
|
|
2537
|
-
|
|
2596
|
+
if (text.includes(q)) {
|
|
2597
|
+
matches.push({ idx: i, m });
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2538
2600
|
const tcs = filterToolCalls(m?.content);
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2601
|
+
if (tcs.some((tc) => JSON.stringify(tc.arguments).toLowerCase().includes(q))) {
|
|
2602
|
+
matches.push({ idx: i, m });
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return JSON.stringify(matches.map(({ idx, m }) => ({
|
|
2606
|
+
idx,
|
|
2542
2607
|
role: m?.role,
|
|
2543
2608
|
preview: extractText(m?.content).slice(0, 150)
|
|
2544
2609
|
})));
|
|
@@ -2938,28 +3003,43 @@ async function summarizeBatch(batch, extraction, model, auth, signal) {
|
|
|
2938
3003
|
## Active Decisions from previous segments (honour these):
|
|
2939
3004
|
` + activeDecisions.join(`
|
|
2940
3005
|
`) : "";
|
|
2941
|
-
const text = batch.map((ch) =>
|
|
3006
|
+
const text = batch.map((ch, i) => {
|
|
3007
|
+
const id = i + 1;
|
|
3008
|
+
return "--- CHUNK " + id + ": " + ch.topic + " (" + ch.priority + `) ---
|
|
2942
3009
|
` + ch.messages.map((m) => {
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3010
|
+
const role = m?.role ?? "unknown";
|
|
3011
|
+
const content = extractText(m?.content).slice(0, 500);
|
|
3012
|
+
const toolCalls = filterToolCalls(m?.content).map((tc) => tc.name + " " + JSON.stringify(tc.arguments).slice(0, 300)).join("; ");
|
|
3013
|
+
const toolSuffix = toolCalls ? `
|
|
3014
|
+
[tool_calls] ` + toolCalls : "";
|
|
3015
|
+
return "[" + role + "] " + content + toolSuffix;
|
|
3016
|
+
}).join(`
|
|
3017
|
+
`);
|
|
2946
3018
|
}).join(`
|
|
2947
|
-
`)).join(`
|
|
2948
3019
|
|
|
2949
3020
|
`);
|
|
3021
|
+
const promptPrefix = BATCH_PROMPT_PREFIX;
|
|
2950
3022
|
const dynamicSuffix = BATCH_PROMPT_SUFFIX.replace("{EXTRACTION_CONTEXT}", extractionCtx + decisionCtx).replace("{TEXT}", text);
|
|
2951
3023
|
const resp = await trackedComplete("batch", model, {
|
|
2952
3024
|
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
2953
3025
|
messages: [
|
|
2954
|
-
{ role: "user", content: [{ type: "text", text:
|
|
3026
|
+
{ role: "user", content: [{ type: "text", text: promptPrefix }], timestamp: Date.now() },
|
|
2955
3027
|
{ role: "user", content: [{ type: "text", text: dynamicSuffix }], timestamp: Date.now() }
|
|
2956
3028
|
]
|
|
2957
|
-
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
3029
|
+
}, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(Math.max(4096, batch.length * 1500), getProviderCaps(model.provider).maxOutputTokens), signal });
|
|
2958
3030
|
const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2959
3031
|
`);
|
|
3032
|
+
const sectionMap = new Map;
|
|
2960
3033
|
const sections = output.split(/^### /m).filter((s) => s.trim());
|
|
3034
|
+
for (const sec of sections) {
|
|
3035
|
+
const m = sec.match(/^CHUNK\s+(\d+):\s*(.*?)\n/i);
|
|
3036
|
+
if (m) {
|
|
3037
|
+
sectionMap.set(parseInt(m[1], 10), sec);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
2961
3040
|
return batch.map((ch, i) => {
|
|
2962
|
-
const
|
|
3041
|
+
const id = i + 1;
|
|
3042
|
+
const sec = sectionMap.get(id) ?? "";
|
|
2963
3043
|
const f = (n) => {
|
|
2964
3044
|
const m = sec.match(new RegExp("\\*\\*" + n + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i"));
|
|
2965
3045
|
return m ? m[1].trim() : "";
|
|
@@ -3082,12 +3162,18 @@ function verifySummary(summary, extraction) {
|
|
|
3082
3162
|
score -= 10;
|
|
3083
3163
|
}
|
|
3084
3164
|
}
|
|
3085
|
-
if (!lower.includes("## goal"))
|
|
3165
|
+
if (!lower.includes("## goal")) {
|
|
3166
|
+
gaps.push("Missing section: ## Goal");
|
|
3086
3167
|
score -= 5;
|
|
3087
|
-
|
|
3168
|
+
}
|
|
3169
|
+
if (!lower.includes("## progress")) {
|
|
3170
|
+
gaps.push("Missing section: ## Progress");
|
|
3088
3171
|
score -= 5;
|
|
3089
|
-
|
|
3172
|
+
}
|
|
3173
|
+
if (!lower.includes("## critical context")) {
|
|
3174
|
+
gaps.push("Missing section: ## Critical Context");
|
|
3090
3175
|
score -= 3;
|
|
3176
|
+
}
|
|
3091
3177
|
const summaryFileRefs = (summary.match(/[\w.\/-]+\.[\w]+/g) ?? []).filter((p) => p.includes("/") || p.match(/\.(ts|tsx|js|jsx|rs|py|go|java|rb|css|html|json|yaml|yml|toml|md|sh|sql)$/i));
|
|
3092
3178
|
const knownFiles = new Set([
|
|
3093
3179
|
...extraction.modifiedFiles.map((f) => f.path.toLowerCase()),
|
|
@@ -3137,7 +3223,8 @@ function verifySummary(summary, extraction) {
|
|
|
3137
3223
|
score -= 5;
|
|
3138
3224
|
}
|
|
3139
3225
|
}
|
|
3140
|
-
|
|
3226
|
+
const finalScore = Math.max(0, score);
|
|
3227
|
+
return { ok: gaps.length === 0 && finalScore >= 85, gaps, score: finalScore };
|
|
3141
3228
|
}
|
|
3142
3229
|
function patchDeterministic(summary, gaps, extraction) {
|
|
3143
3230
|
let patched = summary;
|
|
@@ -3145,47 +3232,75 @@ function patchDeterministic(summary, gaps, extraction) {
|
|
|
3145
3232
|
const errorGaps = gaps.filter((g) => g.startsWith("Missing error:"));
|
|
3146
3233
|
const constraintGaps = gaps.filter((g) => g.startsWith("Missing constraint:"));
|
|
3147
3234
|
const decisionGaps = gaps.filter((g) => g.startsWith("Missing decision:"));
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3235
|
+
const sectionGaps = gaps.filter((g) => g.startsWith("Missing section:"));
|
|
3236
|
+
const otherGaps = gaps.filter((g) => !g.startsWith("Missing modified file:") && !g.startsWith("Missing error:") && !g.startsWith("Missing constraint:") && !g.startsWith("Missing decision:") && !g.startsWith("Missing section:") && !g.startsWith("Potentially fabricated") && !g.startsWith("Inconsistency"));
|
|
3237
|
+
const escapeRe = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3238
|
+
const findOrCreateSectionInsert = (header, defaultBody = "") => {
|
|
3239
|
+
const re = new RegExp(escapeRe(header) + "\\s*\\n", "i");
|
|
3151
3240
|
const m = patched.match(re);
|
|
3152
|
-
|
|
3241
|
+
if (m?.index != null)
|
|
3242
|
+
return m.index + m[0].length;
|
|
3243
|
+
const prefix = patched.endsWith(`
|
|
3244
|
+
`) ? patched.endsWith(`
|
|
3245
|
+
|
|
3246
|
+
`) ? "" : `
|
|
3247
|
+
` : `
|
|
3248
|
+
|
|
3249
|
+
`;
|
|
3250
|
+
patched += prefix + header + `
|
|
3251
|
+
` + (defaultBody ? defaultBody.replace(/\n?$/, `
|
|
3252
|
+
`) : "");
|
|
3253
|
+
return patched.length;
|
|
3254
|
+
};
|
|
3255
|
+
const ensureMissingSection = (header) => {
|
|
3256
|
+
if (header === "## Goal") {
|
|
3257
|
+
findOrCreateSectionInsert(header, (extraction.mainGoal ?? "Continue the current coding task.") + `
|
|
3258
|
+
`);
|
|
3259
|
+
} else if (header === "## Progress") {
|
|
3260
|
+
findOrCreateSectionInsert(header, `### Done
|
|
3261
|
+
- See preceding summary.
|
|
3262
|
+
### In Progress
|
|
3263
|
+
- Continue from the latest user request.
|
|
3264
|
+
### Blocked
|
|
3265
|
+
- None recorded.
|
|
3266
|
+
`);
|
|
3267
|
+
} else if (header === "## Critical Context") {
|
|
3268
|
+
findOrCreateSectionInsert(header, `- None recorded.
|
|
3269
|
+
`);
|
|
3270
|
+
} else {
|
|
3271
|
+
findOrCreateSectionInsert(header);
|
|
3272
|
+
}
|
|
3153
3273
|
};
|
|
3274
|
+
for (const gap of sectionGaps) {
|
|
3275
|
+
ensureMissingSection(gap.replace("Missing section: ", ""));
|
|
3276
|
+
}
|
|
3154
3277
|
if (fileGaps.length > 0) {
|
|
3155
|
-
const insertPos =
|
|
3156
|
-
|
|
3157
|
-
const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
|
|
3278
|
+
const insertPos = findOrCreateSectionInsert("## Files Modified");
|
|
3279
|
+
const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
|
|
3158
3280
|
`) + `
|
|
3159
3281
|
`;
|
|
3160
|
-
|
|
3161
|
-
}
|
|
3282
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
3162
3283
|
}
|
|
3163
3284
|
if (errorGaps.length > 0) {
|
|
3164
|
-
const insertPos =
|
|
3165
|
-
|
|
3166
|
-
const entries = errorGaps.map((g) => "- " + g).join(`
|
|
3285
|
+
const insertPos = findOrCreateSectionInsert("## Critical Context");
|
|
3286
|
+
const entries = errorGaps.map((g) => "- " + g).join(`
|
|
3167
3287
|
`) + `
|
|
3168
3288
|
`;
|
|
3169
|
-
|
|
3170
|
-
}
|
|
3289
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
3171
3290
|
}
|
|
3172
3291
|
if (constraintGaps.length > 0) {
|
|
3173
|
-
const insertPos =
|
|
3174
|
-
|
|
3175
|
-
const entries = constraintGaps.map((g) => "- " + g).join(`
|
|
3292
|
+
const insertPos = findOrCreateSectionInsert("## Constraints & Preferences");
|
|
3293
|
+
const entries = constraintGaps.map((g) => "- " + g).join(`
|
|
3176
3294
|
`) + `
|
|
3177
3295
|
`;
|
|
3178
|
-
|
|
3179
|
-
}
|
|
3296
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
3180
3297
|
}
|
|
3181
3298
|
if (decisionGaps.length > 0) {
|
|
3182
|
-
const insertPos =
|
|
3183
|
-
|
|
3184
|
-
const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
|
|
3299
|
+
const insertPos = findOrCreateSectionInsert("## Key Decisions");
|
|
3300
|
+
const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
|
|
3185
3301
|
`) + `
|
|
3186
3302
|
`;
|
|
3187
|
-
|
|
3188
|
-
}
|
|
3303
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
3189
3304
|
}
|
|
3190
3305
|
if (otherGaps.length > 0) {
|
|
3191
3306
|
patched += `
|
|
@@ -3222,7 +3337,106 @@ Return the COMPLETE updated summary with missing items integrated. Keep the same
|
|
|
3222
3337
|
|
|
3223
3338
|
// src/ui/overlays.ts
|
|
3224
3339
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
3225
|
-
import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
3340
|
+
import { Container, Key, matchesKey, SelectList, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3341
|
+
|
|
3342
|
+
// src/ui/dashboard-format.ts
|
|
3343
|
+
var DASHBOARD_PAGE_SIZE = 24;
|
|
3344
|
+
function metricDuration(entry) {
|
|
3345
|
+
return entry.durationMs ?? entry.phaseTimings?.reduce((sum, phase) => sum + phase.durationMs, 0) ?? 0;
|
|
3346
|
+
}
|
|
3347
|
+
function metricMs(ms) {
|
|
3348
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
3349
|
+
return "0ms";
|
|
3350
|
+
return ms >= 1000 ? (ms / 1000).toFixed(ms >= 1e4 ? 0 : 1) + "s" : Math.round(ms) + "ms";
|
|
3351
|
+
}
|
|
3352
|
+
function clampRatio(value) {
|
|
3353
|
+
return Math.max(0, Math.min(1, value));
|
|
3354
|
+
}
|
|
3355
|
+
function metricPct(value) {
|
|
3356
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.round(clampRatio(value) * 100) + "%" : "\u2014";
|
|
3357
|
+
}
|
|
3358
|
+
function metricNum(value) {
|
|
3359
|
+
return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString() : "\u2014";
|
|
3360
|
+
}
|
|
3361
|
+
function metricScore(entry) {
|
|
3362
|
+
return typeof entry?.verificationScore === "number" && Number.isFinite(entry.verificationScore) ? entry.verificationScore + "/100" : "\u2014";
|
|
3363
|
+
}
|
|
3364
|
+
function formatMetricRun(entry, index) {
|
|
3365
|
+
const prefix = typeof index === "number" ? String(index).padStart(2, " ") + ". " : "";
|
|
3366
|
+
const time = entry.ts ? new Date(entry.ts).toLocaleString() : "unknown time";
|
|
3367
|
+
return prefix + time + " | " + (entry.profile ?? "?") + " | " + (entry.provider ?? entry.model ?? "?") + " | " + (entry.method ?? "?") + " | " + (entry.status ?? "?") + " | score " + metricScore(entry) + " | saved " + metricNum(entry.tokensSaved) + "t";
|
|
3368
|
+
}
|
|
3369
|
+
function formatMetricRunCompact(entry) {
|
|
3370
|
+
return "score " + metricScore(entry) + " \u2022 saved " + metricNum(entry.tokensSaved) + "t \u2022 " + (entry.status ?? "?") + " \u2022 " + (entry.profile ?? "?") + " / " + (entry.provider ?? entry.model ?? "?");
|
|
3371
|
+
}
|
|
3372
|
+
function formatPhaseTiming(phase, total) {
|
|
3373
|
+
const share = total > 0 ? Math.round(phase.durationMs / total * 100) : 0;
|
|
3374
|
+
return "- " + phase.phase + ": " + metricMs(phase.durationMs) + " (" + share + "%)";
|
|
3375
|
+
}
|
|
3376
|
+
function formatRunDetails(entry, title) {
|
|
3377
|
+
if (!entry)
|
|
3378
|
+
return [title, "", "No run recorded yet."];
|
|
3379
|
+
const totalDuration = metricDuration(entry);
|
|
3380
|
+
const lines = [
|
|
3381
|
+
title,
|
|
3382
|
+
"",
|
|
3383
|
+
"Session: " + entry.sessionId,
|
|
3384
|
+
"Time: " + (entry.ts ? new Date(entry.ts).toLocaleString() : "unknown"),
|
|
3385
|
+
"Status: " + (entry.status ?? "unknown") + " | run: " + (entry.runType ?? "?") + " | profile: " + (entry.profile ?? "?"),
|
|
3386
|
+
"Provider/model: " + (entry.provider ?? "?") + " / " + (entry.model ?? "?"),
|
|
3387
|
+
"Method: " + (entry.method ?? "?") + " | duration: " + metricMs(totalDuration),
|
|
3388
|
+
"Quality: " + metricScore(entry) + " | gaps: " + metricNum(entry.verificationGaps),
|
|
3389
|
+
"Tokens: before " + metricNum(entry.tokensBefore) + "t | saved " + metricNum(entry.tokensSaved) + "t | prune saved " + metricNum(entry.pruneSavedTokens) + "t",
|
|
3390
|
+
"LLM: " + metricNum(entry.totalCalls) + " calls | input " + metricNum(entry.totalInput) + "t | output " + metricNum(entry.totalOutput) + "t | provider cache " + metricPct(entry.cacheHitRate),
|
|
3391
|
+
"Extraction cache: " + metricNum(entry.extractionCacheHits) + " hit / " + metricNum(entry.extractionCacheMisses) + " miss | rate " + metricPct(entry.extractionCacheHitRate),
|
|
3392
|
+
"Context: " + metricNum(entry.contextPercent) + "% | tool share: " + metricNum(entry.toolPercent) + "% | chunks: " + metricNum(entry.chunkCount)
|
|
3393
|
+
];
|
|
3394
|
+
if (entry.extractionCacheMissReason)
|
|
3395
|
+
lines.push("Extraction miss reason: " + entry.extractionCacheMissReason);
|
|
3396
|
+
if (entry.fallbackReason)
|
|
3397
|
+
lines.push("Reason: " + entry.fallbackReason);
|
|
3398
|
+
if (entry.phaseTimings?.length) {
|
|
3399
|
+
lines.push("", "Phase timings:");
|
|
3400
|
+
lines.push(...entry.phaseTimings.map((phase) => formatPhaseTiming(phase, totalDuration)));
|
|
3401
|
+
}
|
|
3402
|
+
return lines;
|
|
3403
|
+
}
|
|
3404
|
+
function formatCurrentSession(entries, currentSessionId) {
|
|
3405
|
+
if (!currentSessionId || currentSessionId === "unknown")
|
|
3406
|
+
return ["Current session", "", "Session id is not available from Pi context."];
|
|
3407
|
+
const runs = entries.filter((entry) => entry.sessionId === currentSessionId);
|
|
3408
|
+
if (!runs.length)
|
|
3409
|
+
return ["Current session", "", "Session: " + currentSessionId, "No smart-compact metrics recorded for this session yet."];
|
|
3410
|
+
const success = runs.filter((entry) => entry.status === "success").length;
|
|
3411
|
+
const latest = runs[runs.length - 1];
|
|
3412
|
+
const totalSaved = runs.reduce((sum, entry) => sum + (entry.tokensSaved ?? 0), 0);
|
|
3413
|
+
const avgScoreValues = runs.map((entry) => entry.verificationScore).filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
3414
|
+
const avgScore = avgScoreValues.length ? Math.round(avgScoreValues.reduce((sum, value) => sum + value, 0) / avgScoreValues.length) : 0;
|
|
3415
|
+
return [
|
|
3416
|
+
"Current session",
|
|
3417
|
+
"",
|
|
3418
|
+
"Session: " + currentSessionId,
|
|
3419
|
+
"Runs: " + runs.length + " | success " + success + " | total saved " + totalSaved.toLocaleString() + "t | avg score " + (avgScore || "\u2014"),
|
|
3420
|
+
"Latest: " + formatMetricRun(latest),
|
|
3421
|
+
"",
|
|
3422
|
+
"Runs in this session:",
|
|
3423
|
+
...runs.slice(-20).reverse().map((entry, i) => formatMetricRun(entry, i + 1))
|
|
3424
|
+
];
|
|
3425
|
+
}
|
|
3426
|
+
function formatRecentRuns(entries) {
|
|
3427
|
+
if (!entries.length)
|
|
3428
|
+
return ["Recent runs", "", "No smart-compact metrics recorded yet."];
|
|
3429
|
+
return [
|
|
3430
|
+
"Recent runs",
|
|
3431
|
+
"",
|
|
3432
|
+
...entries.slice(-30).reverse().map((entry, i) => formatMetricRun(entry, i + 1))
|
|
3433
|
+
];
|
|
3434
|
+
}
|
|
3435
|
+
function isDashboardTitleLine(line) {
|
|
3436
|
+
return line.startsWith("#") || line === "Latest run details" || line === "Current session" || line === "Recent runs" || line === "Phase timings:" || line === "Runs in this session:";
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// src/ui/overlays.ts
|
|
3226
3440
|
import path8 from "path";
|
|
3227
3441
|
function renderContextBar(theme, pct, tokens, barLen = 24) {
|
|
3228
3442
|
const clamped = Math.min(Math.max(pct, 0), 100);
|
|
@@ -3367,10 +3581,14 @@ async function showResultScreen(ctx, details, extraction) {
|
|
|
3367
3581
|
c.addChild(new Text("", 0, 0));
|
|
3368
3582
|
c.addChild(new Text(theme.fg("text", theme.bold(" \uD83D\uDCCB Extraction")), 0, 0));
|
|
3369
3583
|
const ms = getMetricsSummary();
|
|
3584
|
+
const ecs = getExtractionCacheStats();
|
|
3370
3585
|
if (ms.totalCalls > 0) {
|
|
3371
|
-
const
|
|
3372
|
-
const
|
|
3373
|
-
|
|
3586
|
+
const providerCachePct = Math.round(ms.cacheHitRate * 100);
|
|
3587
|
+
const extractionCachePct = Math.round(ecs.hitRate * 100);
|
|
3588
|
+
const promptInput = effectivePromptInputTokens(ms.totalInput, ms.totalCacheHit);
|
|
3589
|
+
const inputLabel = ms.totalCacheHit > 0 ? promptInput.toLocaleString() + "t prompt (" + ms.totalInput.toLocaleString() + "t new, " + ms.totalCacheHit.toLocaleString() + "t cached)" : ms.totalInput.toLocaleString() + "t in";
|
|
3590
|
+
const cacheColor = extractionCachePct >= 50 ? "success" : extractionCachePct >= 20 ? "warning" : "dim";
|
|
3591
|
+
c.addChild(new Text(theme.fg("dim", " LLM: ") + theme.fg("text", ms.totalCalls + " calls") + theme.fg("dim", " \u2022 ") + theme.fg("text", inputLabel) + theme.fg("dim", " \u2022 ") + theme.fg("dim", providerCachePct + "% provider cache") + theme.fg("dim", " \u2022 ") + theme.fg(cacheColor, extractionCachePct + "% extraction cache") + theme.fg("dim", " \u2022 ") + theme.fg("dim", ms.avgLatency + "ms avg"), 0, 0));
|
|
3374
3592
|
}
|
|
3375
3593
|
const modFiles = details.modifiedFiles;
|
|
3376
3594
|
const errCount = extraction.errors.length;
|
|
@@ -3450,6 +3668,116 @@ async function showResultScreen(ctx, details, extraction) {
|
|
|
3450
3668
|
};
|
|
3451
3669
|
}, { overlay: true, overlayOptions: { width: "70%", anchor: "center", maxHeight: "80%" } });
|
|
3452
3670
|
}
|
|
3671
|
+
async function showMetricsDashboardUI(ctx, opts) {
|
|
3672
|
+
const entries = opts.entries;
|
|
3673
|
+
const latest = entries[entries.length - 1];
|
|
3674
|
+
const currentRuns = opts.currentSessionId ? entries.filter((entry) => entry.sessionId === opts.currentSessionId) : [];
|
|
3675
|
+
const menuItems = [
|
|
3676
|
+
{ view: "overview", label: "Overview report", desc: entries.length + " recent run(s), profile/provider comparison" },
|
|
3677
|
+
{ view: "latest", label: "Latest run details", desc: latest ? formatMetricRunCompact(latest) : "No run recorded yet" },
|
|
3678
|
+
{ view: "session", label: "Current session", desc: (opts.currentSessionId ?? "unknown") + " \u2014 " + currentRuns.length + " run(s)" },
|
|
3679
|
+
{ view: "recent", label: "Recent runs", desc: "Last " + Math.min(entries.length, 30) + " run(s)" },
|
|
3680
|
+
{ action: "html", label: "Write HTML dashboard", desc: "Generate ~/.pi/agent/.cache/smart-compact-report.html" }
|
|
3681
|
+
];
|
|
3682
|
+
return await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
3683
|
+
let view = "menu";
|
|
3684
|
+
let selected = 0;
|
|
3685
|
+
let scroll = 0;
|
|
3686
|
+
const pageLines = () => {
|
|
3687
|
+
if (view === "overview")
|
|
3688
|
+
return opts.report.split(`
|
|
3689
|
+
`);
|
|
3690
|
+
if (view === "latest")
|
|
3691
|
+
return formatRunDetails(latest, "Latest run details");
|
|
3692
|
+
if (view === "session")
|
|
3693
|
+
return formatCurrentSession(entries, opts.currentSessionId);
|
|
3694
|
+
if (view === "recent")
|
|
3695
|
+
return formatRecentRuns(entries);
|
|
3696
|
+
return [];
|
|
3697
|
+
};
|
|
3698
|
+
const resetPage = (nextView) => {
|
|
3699
|
+
view = nextView;
|
|
3700
|
+
scroll = 0;
|
|
3701
|
+
};
|
|
3702
|
+
const renderHeader = (width) => [
|
|
3703
|
+
truncateToWidth(theme.fg("accent", theme.bold(" \uD83D\uDCCA Smart Compact Dashboard")) + theme.fg("dim", " " + entries.length + " recorded run(s)"), width),
|
|
3704
|
+
truncateToWidth(theme.fg("dim", " session: " + (opts.currentSessionId ?? "unknown")) + theme.fg("dim", latest ? " \u2022 latest score " + metricScore(latest) : ""), width),
|
|
3705
|
+
truncateToWidth(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, width))), width)
|
|
3706
|
+
];
|
|
3707
|
+
return {
|
|
3708
|
+
render: (width) => {
|
|
3709
|
+
const lines = renderHeader(width);
|
|
3710
|
+
if (view === "menu") {
|
|
3711
|
+
lines.push(truncateToWidth(theme.fg("text", " Choose what to inspect:"), width), "");
|
|
3712
|
+
for (let i = 0;i < menuItems.length; i++) {
|
|
3713
|
+
const item = menuItems[i];
|
|
3714
|
+
const active = i === selected;
|
|
3715
|
+
const prefix = active ? " \u203A " : " ";
|
|
3716
|
+
const label = active ? theme.fg("accent", theme.bold(item.label)) : theme.fg("text", item.label);
|
|
3717
|
+
lines.push(truncateToWidth(prefix + label, width));
|
|
3718
|
+
lines.push(truncateToWidth(" " + theme.fg(active ? "muted" : "dim", item.desc), width));
|
|
3719
|
+
}
|
|
3720
|
+
lines.push("", truncateToWidth(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter open \u2022 esc/q close"), width));
|
|
3721
|
+
return lines;
|
|
3722
|
+
}
|
|
3723
|
+
const content = pageLines();
|
|
3724
|
+
const available = DASHBOARD_PAGE_SIZE;
|
|
3725
|
+
const maxScroll = Math.max(0, content.length - available);
|
|
3726
|
+
if (scroll > maxScroll)
|
|
3727
|
+
scroll = maxScroll;
|
|
3728
|
+
for (const line of content.slice(scroll, scroll + available)) {
|
|
3729
|
+
const styled = isDashboardTitleLine(line) ? theme.fg("accent", theme.bold(line)) : line.startsWith("-") ? theme.fg("dim", line) : theme.fg("text", line);
|
|
3730
|
+
lines.push(truncateToWidth(" " + styled, width));
|
|
3731
|
+
}
|
|
3732
|
+
if (content.length > available) {
|
|
3733
|
+
lines.push(truncateToWidth(theme.fg("dim", " showing " + (scroll + 1) + "-" + Math.min(content.length, scroll + available) + " of " + content.length), width));
|
|
3734
|
+
}
|
|
3735
|
+
lines.push("", truncateToWidth(theme.fg("dim", " \u2191\u2193 scroll \u2022 pgup/pgdn page \u2022 home/end jump \u2022 b back \u2022 esc/q close"), width));
|
|
3736
|
+
return lines;
|
|
3737
|
+
},
|
|
3738
|
+
invalidate: () => {},
|
|
3739
|
+
handleInput: (data) => {
|
|
3740
|
+
if (keybindings.matches(data, "tui.select.cancel") || data === "q") {
|
|
3741
|
+
done(null);
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (view === "menu") {
|
|
3745
|
+
if (keybindings.matches(data, "tui.select.up"))
|
|
3746
|
+
selected = Math.max(0, selected - 1);
|
|
3747
|
+
else if (keybindings.matches(data, "tui.select.down"))
|
|
3748
|
+
selected = Math.min(menuItems.length - 1, selected + 1);
|
|
3749
|
+
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
3750
|
+
const item = menuItems[selected];
|
|
3751
|
+
if (item.action) {
|
|
3752
|
+
done(item.action);
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
if (item.view)
|
|
3756
|
+
resetPage(item.view);
|
|
3757
|
+
}
|
|
3758
|
+
} else {
|
|
3759
|
+
const content = pageLines();
|
|
3760
|
+
const maxScroll = Math.max(0, content.length - DASHBOARD_PAGE_SIZE);
|
|
3761
|
+
if (data === "b" || matchesKey(data, Key.left))
|
|
3762
|
+
resetPage("menu");
|
|
3763
|
+
else if (matchesKey(data, Key.home))
|
|
3764
|
+
scroll = 0;
|
|
3765
|
+
else if (matchesKey(data, Key.end))
|
|
3766
|
+
scroll = maxScroll;
|
|
3767
|
+
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
3768
|
+
scroll = Math.max(0, scroll - DASHBOARD_PAGE_SIZE);
|
|
3769
|
+
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
3770
|
+
scroll = Math.min(maxScroll, scroll + DASHBOARD_PAGE_SIZE);
|
|
3771
|
+
else if (keybindings.matches(data, "tui.select.up"))
|
|
3772
|
+
scroll = Math.max(0, scroll - 1);
|
|
3773
|
+
else if (keybindings.matches(data, "tui.select.down"))
|
|
3774
|
+
scroll = Math.min(maxScroll, scroll + 1);
|
|
3775
|
+
}
|
|
3776
|
+
tui.requestRender();
|
|
3777
|
+
}
|
|
3778
|
+
};
|
|
3779
|
+
}, { overlay: true, overlayOptions: { width: "80%", anchor: "center", maxHeight: "85%" } });
|
|
3780
|
+
}
|
|
3453
3781
|
async function showCompactUI(ctx, opts) {
|
|
3454
3782
|
const selectedModel = await selectModel(ctx, opts);
|
|
3455
3783
|
if (!selectedModel)
|
|
@@ -3476,6 +3804,7 @@ async function runSmartCompact(opts) {
|
|
|
3476
3804
|
};
|
|
3477
3805
|
resetCompactSessionId();
|
|
3478
3806
|
resetMetrics();
|
|
3807
|
+
resetExtractionCacheStats();
|
|
3479
3808
|
let sessionId = "unknown";
|
|
3480
3809
|
let totalTokens = 0;
|
|
3481
3810
|
let contextPercent = 0;
|
|
@@ -3573,7 +3902,9 @@ async function runSmartCompact(opts) {
|
|
|
3573
3902
|
return;
|
|
3574
3903
|
}
|
|
3575
3904
|
const shouldSkipExplore = tier === "light";
|
|
3905
|
+
const currentEntryIds = toCompact.map((e) => e.id);
|
|
3576
3906
|
const pruning = pruneRedundant(llmMessages);
|
|
3907
|
+
const currentKeptEntryIds = pruning.keptIndices.map((i) => currentEntryIds[i]).filter((id) => typeof id === "string");
|
|
3577
3908
|
if (pruning.prunedCount > 0) {
|
|
3578
3909
|
notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
3579
3910
|
}
|
|
@@ -3586,23 +3917,36 @@ async function runSmartCompact(opts) {
|
|
|
3586
3917
|
const prevContext = getPreviousCompactionContext(branch);
|
|
3587
3918
|
const cachedExt = loadCachedExtraction(sessionId);
|
|
3588
3919
|
let extraction;
|
|
3920
|
+
let extractionCacheMissReason = cachedExt ? "not-incremental" : "no-cache";
|
|
3589
3921
|
const currentFirstId = toCompact[0]?.id;
|
|
3590
3922
|
const currentLastId = toCompact[toCompact.length - 1]?.id;
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3923
|
+
let cacheUsable = false;
|
|
3924
|
+
if (cachedExt?.keptEntryIds && cachedExt.keptEntryIds.length > 0) {
|
|
3925
|
+
const branchPrefixMatch = cachedExt.entryIds?.every((id, i) => id === currentEntryIds[i]) ?? false;
|
|
3926
|
+
const prunedPrefixMatch = cachedExt.keptEntryIds.length <= currentKeptEntryIds.length && cachedExt.keptEntryIds.every((id, i) => id === currentKeptEntryIds[i]);
|
|
3927
|
+
cacheUsable = branchPrefixMatch && prunedPrefixMatch && cachedExt.messageCount === cachedExt.keptEntryIds.length && cachedExt.messageCount < llmMessages.length;
|
|
3928
|
+
if (!cacheUsable) {
|
|
3929
|
+
extractionCacheMissReason = !branchPrefixMatch ? "entry-prefix-mismatch" : !prunedPrefixMatch ? "pruned-prefix-changed" : cachedExt.messageCount !== cachedExt.keptEntryIds.length ? "cache-shape-mismatch" : "no-new-pruned-messages";
|
|
3930
|
+
}
|
|
3931
|
+
} else if (cachedExt) {
|
|
3932
|
+
extractionCacheMissReason = "legacy-no-kept-entryids";
|
|
3933
|
+
vlog("Extraction cache ignored: legacy entry lacks keptEntryIds");
|
|
3934
|
+
}
|
|
3935
|
+
if (cacheUsable && cachedExt) {
|
|
3936
|
+
const newMsgs = llmMessages.slice(cachedExt.messageCount);
|
|
3596
3937
|
const delta = extractStructured(newMsgs, pc);
|
|
3597
3938
|
extraction = mergeExtractions(cachedExt.extraction, delta, cachedExt.messageCount);
|
|
3598
|
-
notify("Phase 1 Incremental: " +
|
|
3599
|
-
vlog("Incremental extraction \u2014 cached messages: " + cachedExt.messageCount + ", current: " + llmMessages.length);
|
|
3939
|
+
notify("Phase 1 Incremental: " + cachedExt.messageCount + " cached + " + newMsgs.length + " new pruned messages", "info");
|
|
3940
|
+
vlog("Incremental extraction \u2014 cached pruned messages: " + cachedExt.messageCount + ", current pruned: " + llmMessages.length);
|
|
3941
|
+
extractionCacheMissReason = undefined;
|
|
3942
|
+
recordExtractionCacheHit();
|
|
3600
3943
|
} else {
|
|
3601
3944
|
extraction = extractStructured(llmMessages, pc);
|
|
3602
3945
|
notify("Phase 1 Full: " + extraction.modifiedFiles.length + " files, " + extraction.errors.length + " errors", "info");
|
|
3603
3946
|
vlog("Full extraction \u2014 " + llmMessages.length + " messages, tier=" + tier);
|
|
3947
|
+
recordExtractionCacheMiss();
|
|
3604
3948
|
}
|
|
3605
|
-
saveCachedExtraction(sessionId, extraction, llmMessages.length, currentFirstId, currentLastId);
|
|
3949
|
+
saveCachedExtraction(sessionId, extraction, llmMessages.length, currentFirstId, currentLastId, currentEntryIds, currentKeptEntryIds);
|
|
3606
3950
|
markPhase("extract");
|
|
3607
3951
|
const projectId = deriveProjectId(findGitRoot(ctx.cwd) ?? ctx.cwd, extraction, sessionId);
|
|
3608
3952
|
const fingerprint = loadProjectFingerprint(projectId);
|
|
@@ -3765,13 +4109,13 @@ async function runSmartCompact(opts) {
|
|
|
3765
4109
|
markPhase("synthesize");
|
|
3766
4110
|
if (!autoTriggered)
|
|
3767
4111
|
showProgressOverlay(ctx, { phase: 4, phaseName: "Verify", detail: "Checking...", model: modelLabel, profile, extraction, explorationRounds });
|
|
3768
|
-
|
|
4112
|
+
let verification = verifySummary(finalSummary, extraction);
|
|
3769
4113
|
vlog("Verification score=" + verification.score + " ok=" + verification.ok + " gaps=" + verification.gaps.length);
|
|
3770
4114
|
if (!verification.ok) {
|
|
3771
4115
|
if (verification.score < 85) {
|
|
3772
4116
|
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + ", applying deterministic patch", "warning");
|
|
3773
4117
|
finalSummary = patchDeterministic(finalSummary, verification.gaps, extraction);
|
|
3774
|
-
|
|
4118
|
+
let recheck = verifySummary(finalSummary, extraction);
|
|
3775
4119
|
if (!recheck.ok && recheck.score < 75) {
|
|
3776
4120
|
notify("Phase 4 Verify: deterministic patch insufficient (score=" + recheck.score + "), trying LLM patch", "warning");
|
|
3777
4121
|
try {
|
|
@@ -3780,7 +4124,9 @@ async function runSmartCompact(opts) {
|
|
|
3780
4124
|
} catch (err) {
|
|
3781
4125
|
warn("LLM patch failed", err);
|
|
3782
4126
|
}
|
|
4127
|
+
recheck = verifySummary(finalSummary, extraction);
|
|
3783
4128
|
}
|
|
4129
|
+
verification = recheck;
|
|
3784
4130
|
} else {
|
|
3785
4131
|
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + " \u2265 85 \u2014 skipping patch", "info");
|
|
3786
4132
|
}
|
|
@@ -3835,6 +4181,7 @@ async function runSmartCompact(opts) {
|
|
|
3835
4181
|
openLoops
|
|
3836
4182
|
};
|
|
3837
4183
|
if (dryRun) {
|
|
4184
|
+
const ecs2 = getExtractionCacheStats();
|
|
3838
4185
|
appendMetricsLog(sessionId, {
|
|
3839
4186
|
profile,
|
|
3840
4187
|
tier,
|
|
@@ -3852,7 +4199,11 @@ async function runSmartCompact(opts) {
|
|
|
3852
4199
|
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3853
4200
|
status: "dry-run",
|
|
3854
4201
|
phaseTimings,
|
|
3855
|
-
durationMs: Date.now() - pipelineStart
|
|
4202
|
+
durationMs: Date.now() - pipelineStart,
|
|
4203
|
+
extractionCacheHits: ecs2.hits,
|
|
4204
|
+
extractionCacheMisses: ecs2.misses,
|
|
4205
|
+
extractionCacheHitRate: ecs2.hitRate,
|
|
4206
|
+
extractionCacheMissReason
|
|
3856
4207
|
});
|
|
3857
4208
|
notify("DRY RUN (" + method + ", " + profile + ") \u2014 " + toCompact.length + " msgs, " + llmCalls + " calls", "info");
|
|
3858
4209
|
return;
|
|
@@ -3882,6 +4233,7 @@ async function runSmartCompact(opts) {
|
|
|
3882
4233
|
warn("Damage detection error", err);
|
|
3883
4234
|
}
|
|
3884
4235
|
markPhase("damage");
|
|
4236
|
+
const ecs = getExtractionCacheStats();
|
|
3885
4237
|
appendMetricsLog(sessionId, {
|
|
3886
4238
|
profile,
|
|
3887
4239
|
tier,
|
|
@@ -3899,11 +4251,19 @@ async function runSmartCompact(opts) {
|
|
|
3899
4251
|
runType: skipCompact ? "tool" : autoTriggered ? "auto" : "manual",
|
|
3900
4252
|
status: "success",
|
|
3901
4253
|
phaseTimings,
|
|
3902
|
-
durationMs: Date.now() - pipelineStart
|
|
4254
|
+
durationMs: Date.now() - pipelineStart,
|
|
4255
|
+
extractionCacheHits: ecs.hits,
|
|
4256
|
+
extractionCacheMisses: ecs.misses,
|
|
4257
|
+
extractionCacheHitRate: ecs.hitRate,
|
|
4258
|
+
extractionCacheMissReason
|
|
3903
4259
|
});
|
|
3904
4260
|
const ms = getMetricsSummary();
|
|
3905
4261
|
if (ms.totalCalls > 0) {
|
|
3906
|
-
|
|
4262
|
+
const providerCacheRate = Math.round(ms.cacheHitRate * 100);
|
|
4263
|
+
const extractionCacheRate = Math.round(ecs.hitRate * 100);
|
|
4264
|
+
const promptInput = effectivePromptInputTokens(ms.totalInput, ms.totalCacheHit);
|
|
4265
|
+
const inputLabel = ms.totalCacheHit > 0 ? promptInput + "t prompt (" + ms.totalInput + "t new, " + ms.totalCacheHit + "t cached)" : ms.totalInput + "t in";
|
|
4266
|
+
notify("Metrics: " + ms.totalCalls + " calls, " + inputLabel + ", " + ms.totalOutput + "t out, provider-cache " + providerCacheRate + "% (internal phases disabled), extraction-cache " + extractionCacheRate + "%, " + ms.avgLatency + "ms avg", "info");
|
|
3907
4267
|
}
|
|
3908
4268
|
if (!autoTriggered) {
|
|
3909
4269
|
try {
|
|
@@ -4002,12 +4362,21 @@ function smartCompactExtension(pi) {
|
|
|
4002
4362
|
const verbose = flags.includes("verbose") || flags.includes("debug");
|
|
4003
4363
|
const dryRun = flags.includes("dry-run");
|
|
4004
4364
|
if (flags.includes("metrics") || flags.includes("dashboard")) {
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4365
|
+
if (flags.includes("dashboard")) {
|
|
4366
|
+
const entries = readMetricsLog(200);
|
|
4367
|
+
const sessionId = ctx.sessionManager.getSessionId?.() ?? "unknown";
|
|
4368
|
+
const action = await showMetricsDashboardUI(ctx, {
|
|
4369
|
+
entries,
|
|
4370
|
+
currentSessionId: sessionId,
|
|
4371
|
+
report: buildMetricsReport(entries)
|
|
4372
|
+
});
|
|
4373
|
+
if (action === "html") {
|
|
4374
|
+
const fp = writeMetricsDashboard(entries);
|
|
4375
|
+
ctx.ui.notify(fp ? "Dashboard written: " + fp : "Dashboard could not be written", fp ? "info" : "error");
|
|
4376
|
+
}
|
|
4377
|
+
} else {
|
|
4378
|
+
ctx.ui.notify(buildMetricsReport(), "info");
|
|
4379
|
+
}
|
|
4011
4380
|
return;
|
|
4012
4381
|
}
|
|
4013
4382
|
const modelArg = tokens.find((t) => t.includes("/"));
|
|
@@ -4057,6 +4426,7 @@ Dashboard: ` + fp : ""), "info");
|
|
|
4057
4426
|
const age = Date.now() - pendingRef.createdAt;
|
|
4058
4427
|
if (age > PENDING_TTL_MS) {
|
|
4059
4428
|
warn("Discarding expired pending smart compaction after " + Math.round(age / 1000) + "s");
|
|
4429
|
+
ctx.ui?.notify?.("Expired pending smart compaction discarded", "warning");
|
|
4060
4430
|
pendingRef.value = null;
|
|
4061
4431
|
pendingRef.createdAt = 0;
|
|
4062
4432
|
} else {
|
|
@@ -4100,7 +4470,6 @@ Dashboard: ` + fp : ""), "info");
|
|
|
4100
4470
|
}
|
|
4101
4471
|
if (result === "timeout") {
|
|
4102
4472
|
warn("Smart compact auto-trigger hard timeout after " + effectiveTimeoutMs + "ms");
|
|
4103
|
-
isRunning.value = false;
|
|
4104
4473
|
pendingRef.value = null;
|
|
4105
4474
|
pendingRef.createdAt = 0;
|
|
4106
4475
|
return;
|