pi-smart-compact 7.18.2 → 7.20.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 +38 -0
- package/README.md +10 -2
- package/dist/app/run-context.d.ts +0 -6
- package/dist/app/run-context.d.ts.map +1 -1
- package/dist/app/run-smart-compact.d.ts +2 -0
- package/dist/app/run-smart-compact.d.ts.map +1 -1
- package/dist/app/steps/extract.d.ts.map +1 -1
- package/dist/app/steps/persist.d.ts +18 -11
- package/dist/app/steps/persist.d.ts.map +1 -1
- package/dist/app/steps/state.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +572 -444
- package/dist/infra/fs.d.ts +4 -0
- package/dist/infra/fs.d.ts.map +1 -1
- package/dist/infra/services.d.ts +1 -1
- package/dist/infra/services.d.ts.map +1 -1
- package/dist/phases/explore.d.ts +4 -7
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/dashboard-format.d.ts.map +1 -1
- package/dist/ui/metrics-report.d.ts +4 -0
- package/dist/ui/metrics-report.d.ts.map +1 -0
- package/dist/ui/overlays.d.ts +1 -1
- package/dist/ui/overlays.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +9 -14
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/damage.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/pruning.d.ts.map +1 -1
- package/dist/utils/session-log.d.ts.map +1 -1
- package/dist/utils/state.d.ts +1 -0
- package/dist/utils/state.d.ts.map +1 -1
- package/dist/utils/tokens.d.ts.map +1 -1
- package/docs/RELEASE.md +1 -0
- package/package.json +4 -6
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
var __require = import.meta.require;
|
|
18
18
|
|
|
19
19
|
// src/constants.ts
|
|
20
|
-
var VERSION = "7.
|
|
20
|
+
var VERSION = "7.20.0", CHARS_PER_TOKEN = 3.8, COMPACT_SYSTEM_PREFIX, PROFILES, DEFAULT_CONFIG, NO_OP_RE, SHIFT_RE, CHOICE_RE, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX = `
|
|
21
21
|
{PREV_CONTEXT}
|
|
22
22
|
|
|
23
23
|
{EXTRACTION_CONTEXT}
|
|
@@ -40,7 +40,7 @@ IMMUTABLE CONTEXT (verified deterministic data):
|
|
|
40
40
|
{EXPLORATION_CONTEXT}
|
|
41
41
|
{PREV_CONTEXT}
|
|
42
42
|
|
|
43
|
-
<summaries>{SUMMARIES}</summaries>`, SESSION_TYPE_INSTRUCTIONS, SECTION_GOAL = "## Goal", SECTION_CONSTRAINTS = "## Constraints & Preferences", SECTION_PROGRESS = "## Progress", SECTION_DECISIONS = "## Key Decisions", SECTION_FILES_MODIFIED = "## Files Modified", SECTION_FILES_READ = "## Files Read", SECTION_NEXT_STEPS = "## Next Steps", SECTION_CRITICAL_CONTEXT = "## Critical Context", SECTION_TOPICS = "## Topics Covered", SECTION_OPEN_LOOPS = "## Open Loops", SECTION_CHANGES = "## Changes Since Last Compaction", LOG_PREFIX = "[smart-compact]", MIN_TOKEN_THRESHOLD = 5000, MAX_EXPLORATION_ROUNDS = 8, BACKUP_MAX_FILES = 20, BACKUP_MAX_AGE_MS, FIVE_MINUTES_MS, ONE_HOUR_MS, SEVEN_DAYS_MS, THIRTY_DAYS_MS, EXTRACTION_CACHE_PREFIX = "compact-extraction-", ID_PREFIX, TUNING, TRUNC, MAX_TOOL_OUTPUT_CHARS = 800, LIKELY_ERROR_RE, ERROR_SCAN_MAX_LEN = 2000, ERROR_RETRY_WINDOW = 6, ERROR_RESOLVE_WINDOW = 10, METRICS_BUFFER_MAX = 200, CONFIG_KEY = "smartCompact", CONFIG_KEY_ALT = "semanticCompact", EXPLORER_SYSTEM_PROMPT;
|
|
43
|
+
<summaries>{SUMMARIES}</summaries>`, SESSION_TYPE_INSTRUCTIONS, SECTION_GOAL = "## Goal", SECTION_CONSTRAINTS = "## Constraints & Preferences", SECTION_PROGRESS = "## Progress", SECTION_DECISIONS = "## Key Decisions", SECTION_FILES_MODIFIED = "## Files Modified", SECTION_FILES_READ = "## Files Read", SECTION_NEXT_STEPS = "## Next Steps", SECTION_CRITICAL_CONTEXT = "## Critical Context", SECTION_TOPICS = "## Topics Covered", SECTION_OPEN_LOOPS = "## Open Loops", SECTION_CHANGES = "## Changes Since Last Compaction", LOG_PREFIX = "[smart-compact]", MIN_TOKEN_THRESHOLD = 5000, MAX_EXPLORATION_ROUNDS = 8, BACKUP_MAX_FILES = 20, BACKUP_MAX_AGE_MS, FIVE_MINUTES_MS, ONE_HOUR_MS, SEVEN_DAYS_MS, THIRTY_DAYS_MS, EXTRACTION_CACHE_PREFIX = "compact-extraction-", ID_PREFIX, TUNING, TRUNC, MAX_TOOL_OUTPUT_CHARS = 800, LIKELY_ERROR_RE, ERROR_SCAN_MAX_LEN = 2000, ERROR_RETRY_WINDOW = 6, ERROR_RESOLVE_WINDOW = 10, METRICS_BUFFER_MAX = 200, RUNTIME_LOG_MAX_BYTES, CONFIG_KEY = "smartCompact", CONFIG_KEY_ALT = "semanticCompact", EXPLORER_SYSTEM_PROMPT;
|
|
44
44
|
var init_constants = __esm(() => {
|
|
45
45
|
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.";
|
|
46
46
|
PROFILES = {
|
|
@@ -229,6 +229,7 @@ Rules for Accuracy:
|
|
|
229
229
|
FINGERPRINT_SEG: 2
|
|
230
230
|
};
|
|
231
231
|
LIKELY_ERROR_RE = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed|^FAIL\b|ERROR:)/i;
|
|
232
|
+
RUNTIME_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
232
233
|
EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
|
|
233
234
|
|
|
234
235
|
` + `Your job:
|
|
@@ -363,8 +364,17 @@ function acquireLockSync(target) {
|
|
|
363
364
|
try {
|
|
364
365
|
const stat = fs.statSync(lockDir);
|
|
365
366
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
367
|
+
const stolen = lockDir + ".stale." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
366
368
|
try {
|
|
367
|
-
fs.
|
|
369
|
+
fs.renameSync(lockDir, stolen);
|
|
370
|
+
const stolenStat = fs.statSync(stolen);
|
|
371
|
+
if (Date.now() - stolenStat.mtimeMs > LOCK_STALE_MS) {
|
|
372
|
+
fs.rmdirSync(stolen);
|
|
373
|
+
} else {
|
|
374
|
+
try {
|
|
375
|
+
fs.renameSync(stolen, lockDir);
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
368
378
|
} catch {}
|
|
369
379
|
continue;
|
|
370
380
|
}
|
|
@@ -393,6 +403,42 @@ function appendLineLocked(target, line) {
|
|
|
393
403
|
release();
|
|
394
404
|
}
|
|
395
405
|
}
|
|
406
|
+
function trimFileTailLocked(target, maxBytes) {
|
|
407
|
+
const release = acquireLockSync(target);
|
|
408
|
+
try {
|
|
409
|
+
const stat = fs.statSync(target);
|
|
410
|
+
if (stat.size <= maxBytes)
|
|
411
|
+
return;
|
|
412
|
+
const length = Math.min(stat.size, maxBytes);
|
|
413
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
414
|
+
const fd = fs.openSync(target, "r");
|
|
415
|
+
try {
|
|
416
|
+
fs.readSync(fd, buffer, 0, length, stat.size - length);
|
|
417
|
+
} finally {
|
|
418
|
+
fs.closeSync(fd);
|
|
419
|
+
}
|
|
420
|
+
const tail = buffer.toString("utf8");
|
|
421
|
+
const firstNewline = tail.indexOf(`
|
|
422
|
+
`);
|
|
423
|
+
atomicWriteFileSync(target, firstNewline >= 0 ? tail.slice(firstNewline + 1) : "");
|
|
424
|
+
} finally {
|
|
425
|
+
release();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function scheduleFileTailTrim(target, maxBytes) {
|
|
429
|
+
if (scheduledTailTrims.has(target))
|
|
430
|
+
return;
|
|
431
|
+
scheduledTailTrims.add(target);
|
|
432
|
+
setTimeout(() => {
|
|
433
|
+
try {
|
|
434
|
+
trimFileTailLocked(target, maxBytes);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
debug("tail trim failed for " + target, e);
|
|
437
|
+
} finally {
|
|
438
|
+
scheduledTailTrims.delete(target);
|
|
439
|
+
}
|
|
440
|
+
}, 0);
|
|
441
|
+
}
|
|
396
442
|
function readJsonSync(target) {
|
|
397
443
|
try {
|
|
398
444
|
if (!fs.existsSync(target))
|
|
@@ -407,9 +453,10 @@ function readJsonSync(target) {
|
|
|
407
453
|
function writeJsonSync(target, value, pretty = false) {
|
|
408
454
|
atomicWriteFileSync(target, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
|
|
409
455
|
}
|
|
410
|
-
var LOCK_STALE_MS = 5000, LOCK_RETRY_MS = 25, LOCK_MAX_RETRIES = 80;
|
|
456
|
+
var LOCK_STALE_MS = 5000, LOCK_RETRY_MS = 25, LOCK_MAX_RETRIES = 80, scheduledTailTrims;
|
|
411
457
|
var init_fs = __esm(() => {
|
|
412
458
|
init_logger();
|
|
459
|
+
scheduledTailTrims = new Set;
|
|
413
460
|
});
|
|
414
461
|
|
|
415
462
|
// src/utils/tokens.ts
|
|
@@ -466,7 +513,8 @@ function estimateTokens(text, provider, model, calibration = _fallbackCalibratio
|
|
|
466
513
|
if (structural / sample.length > JSON_DENSITY_THRESHOLD)
|
|
467
514
|
jsonPenalty = JSON_PENALTY;
|
|
468
515
|
}
|
|
469
|
-
const
|
|
516
|
+
const langSample = text.length > JSON_DENSITY_SCAN_CAP ? text.slice(0, JSON_DENSITY_SCAN_CAP) : text;
|
|
517
|
+
const langPenalty = /[\u00E7\u011F\u0131\u00F6\u015F\u00FC\u00C7\u011E\u0130\u00D6\u015E\u00DC]/.test(langSample) ? 0.9 : 1;
|
|
470
518
|
const factor = calibration.get(provider, model);
|
|
471
519
|
return Math.ceil(text.length / baseRatio * jsonPenalty * langPenalty * factor);
|
|
472
520
|
}
|
|
@@ -652,6 +700,59 @@ var init_tokens = __esm(() => {
|
|
|
652
700
|
_fallbackCalibration = new TokenCalibrationStore;
|
|
653
701
|
});
|
|
654
702
|
|
|
703
|
+
// src/utils/id-fingerprint.ts
|
|
704
|
+
import crypto3 from "crypto";
|
|
705
|
+
function buildEntryIdFingerprint(ids) {
|
|
706
|
+
const count = ids.length;
|
|
707
|
+
const tail = ids.slice(Math.max(0, count - FINGERPRINT_TAIL_LEN));
|
|
708
|
+
const prefixHash = hashIds(ids, count);
|
|
709
|
+
return { count, prefixHash, tail };
|
|
710
|
+
}
|
|
711
|
+
function hashIds(ids, count) {
|
|
712
|
+
const h = crypto3.createHash("sha256");
|
|
713
|
+
const limit = Math.min(count, ids.length);
|
|
714
|
+
for (let i = 0;i < limit; i++) {
|
|
715
|
+
if (i > 0)
|
|
716
|
+
h.update(`
|
|
717
|
+
`);
|
|
718
|
+
h.update(ids[i]);
|
|
719
|
+
}
|
|
720
|
+
return h.digest("hex");
|
|
721
|
+
}
|
|
722
|
+
function isPrefixOf(cached, currentIds) {
|
|
723
|
+
if (!cached)
|
|
724
|
+
return false;
|
|
725
|
+
if (cached.count > currentIds.length)
|
|
726
|
+
return false;
|
|
727
|
+
const tailStart = cached.count - cached.tail.length;
|
|
728
|
+
for (let i = 0;i < cached.tail.length; i++) {
|
|
729
|
+
if (currentIds[tailStart + i] !== cached.tail[i])
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
return hashIds(currentIds, cached.count) === cached.prefixHash;
|
|
733
|
+
}
|
|
734
|
+
function legacyPrefixMatch(legacy, currentIds) {
|
|
735
|
+
if (!legacy || legacy.length === 0)
|
|
736
|
+
return false;
|
|
737
|
+
if (legacy.length > currentIds.length)
|
|
738
|
+
return false;
|
|
739
|
+
for (let i = 0;i < legacy.length; i++) {
|
|
740
|
+
if (legacy[i] !== currentIds[i])
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
var FINGERPRINT_TAIL_LEN = 16;
|
|
746
|
+
var init_id_fingerprint = () => {};
|
|
747
|
+
|
|
748
|
+
// src/infra/clock.ts
|
|
749
|
+
var systemClock;
|
|
750
|
+
var init_clock = __esm(() => {
|
|
751
|
+
systemClock = {
|
|
752
|
+
now: () => Date.now()
|
|
753
|
+
};
|
|
754
|
+
});
|
|
755
|
+
|
|
655
756
|
// src/infra/llm-retry.ts
|
|
656
757
|
function classifyError(err) {
|
|
657
758
|
if (!err || typeof err !== "object")
|
|
@@ -761,59 +862,6 @@ var init_llm_client = __esm(() => {
|
|
|
761
862
|
_client = defaultLlmClient;
|
|
762
863
|
});
|
|
763
864
|
|
|
764
|
-
// src/utils/id-fingerprint.ts
|
|
765
|
-
import crypto3 from "crypto";
|
|
766
|
-
function buildEntryIdFingerprint(ids) {
|
|
767
|
-
const count = ids.length;
|
|
768
|
-
const tail = ids.slice(Math.max(0, count - FINGERPRINT_TAIL_LEN));
|
|
769
|
-
const prefixHash = hashIds(ids, count);
|
|
770
|
-
return { count, prefixHash, tail };
|
|
771
|
-
}
|
|
772
|
-
function hashIds(ids, count) {
|
|
773
|
-
const h = crypto3.createHash("sha256");
|
|
774
|
-
const limit = Math.min(count, ids.length);
|
|
775
|
-
for (let i = 0;i < limit; i++) {
|
|
776
|
-
if (i > 0)
|
|
777
|
-
h.update(`
|
|
778
|
-
`);
|
|
779
|
-
h.update(ids[i]);
|
|
780
|
-
}
|
|
781
|
-
return h.digest("hex");
|
|
782
|
-
}
|
|
783
|
-
function isPrefixOf(cached, currentIds) {
|
|
784
|
-
if (!cached)
|
|
785
|
-
return false;
|
|
786
|
-
if (cached.count > currentIds.length)
|
|
787
|
-
return false;
|
|
788
|
-
const tailStart = cached.count - cached.tail.length;
|
|
789
|
-
for (let i = 0;i < cached.tail.length; i++) {
|
|
790
|
-
if (currentIds[tailStart + i] !== cached.tail[i])
|
|
791
|
-
return false;
|
|
792
|
-
}
|
|
793
|
-
return hashIds(currentIds, cached.count) === cached.prefixHash;
|
|
794
|
-
}
|
|
795
|
-
function legacyPrefixMatch(legacy, currentIds) {
|
|
796
|
-
if (!legacy || legacy.length === 0)
|
|
797
|
-
return false;
|
|
798
|
-
if (legacy.length > currentIds.length)
|
|
799
|
-
return false;
|
|
800
|
-
for (let i = 0;i < legacy.length; i++) {
|
|
801
|
-
if (legacy[i] !== currentIds[i])
|
|
802
|
-
return false;
|
|
803
|
-
}
|
|
804
|
-
return true;
|
|
805
|
-
}
|
|
806
|
-
var FINGERPRINT_TAIL_LEN = 16;
|
|
807
|
-
var init_id_fingerprint = () => {};
|
|
808
|
-
|
|
809
|
-
// src/infra/clock.ts
|
|
810
|
-
var systemClock;
|
|
811
|
-
var init_clock = __esm(() => {
|
|
812
|
-
systemClock = {
|
|
813
|
-
now: () => Date.now()
|
|
814
|
-
};
|
|
815
|
-
});
|
|
816
|
-
|
|
817
865
|
// src/infra/services.ts
|
|
818
866
|
import crypto4 from "crypto";
|
|
819
867
|
|
|
@@ -904,7 +952,7 @@ function makeCompactSessionId() {
|
|
|
904
952
|
function createServices(overrides = {}) {
|
|
905
953
|
return {
|
|
906
954
|
clock: overrides.clock ?? systemClock,
|
|
907
|
-
llm: overrides.llm ?? getLlmClient(),
|
|
955
|
+
llm: overrides.llm ?? { complete: (...args) => getLlmClient().complete(...args) },
|
|
908
956
|
toolSupport: overrides.toolSupport ?? new ToolSupportCache,
|
|
909
957
|
metrics: overrides.metrics ?? new MetricsSink,
|
|
910
958
|
extractionCacheStats: overrides.extractionCacheStats ?? new ExtractionCacheStats,
|
|
@@ -936,9 +984,9 @@ function cacheOpts(opts, provider, phase, services) {
|
|
|
936
984
|
if (retention === "none") {
|
|
937
985
|
return { ...opts, cacheRetention: "none" };
|
|
938
986
|
}
|
|
939
|
-
return { ...opts, sessionId: services
|
|
987
|
+
return { ...opts, sessionId: services.compactSessionId, cacheRetention: retention };
|
|
940
988
|
}
|
|
941
|
-
function recordMetric(m, services
|
|
989
|
+
function recordMetric(m, services) {
|
|
942
990
|
services.metrics.record(m);
|
|
943
991
|
}
|
|
944
992
|
function effectivePromptInputTokens(inputTokens, cacheHitTokens) {
|
|
@@ -946,7 +994,7 @@ function effectivePromptInputTokens(inputTokens, cacheHitTokens) {
|
|
|
946
994
|
return Math.max(0, inputTokens);
|
|
947
995
|
return cacheHitTokens > inputTokens ? inputTokens + cacheHitTokens : inputTokens;
|
|
948
996
|
}
|
|
949
|
-
function getMetricsSummary(services
|
|
997
|
+
function getMetricsSummary(services) {
|
|
950
998
|
const sum = services.metrics.summary();
|
|
951
999
|
const cacheDenominator = effectivePromptInputTokens(sum.totalInput, sum.totalCacheHit);
|
|
952
1000
|
return {
|
|
@@ -955,10 +1003,11 @@ function getMetricsSummary(services = getDefaultServices()) {
|
|
|
955
1003
|
};
|
|
956
1004
|
}
|
|
957
1005
|
async function trackedComplete(phase, model, reqBody, opts, services) {
|
|
1006
|
+
const svc = services ?? getDefaultServices();
|
|
958
1007
|
const start = Date.now();
|
|
959
1008
|
try {
|
|
960
|
-
const resolvedOpts = cacheOpts(opts, model.provider, phase,
|
|
961
|
-
const resp = await
|
|
1009
|
+
const resolvedOpts = cacheOpts(opts, model.provider, phase, svc);
|
|
1010
|
+
const resp = await svc.llm.complete(model, reqBody, resolvedOpts);
|
|
962
1011
|
const latency = Date.now() - start;
|
|
963
1012
|
const usage = resp.usage;
|
|
964
1013
|
const inputT = usage?.input ?? 0;
|
|
@@ -973,11 +1022,11 @@ async function trackedComplete(phase, model, reqBody, opts, services) {
|
|
|
973
1022
|
cacheHitTokens: cacheT,
|
|
974
1023
|
latencyMs: latency,
|
|
975
1024
|
success: true
|
|
976
|
-
},
|
|
1025
|
+
}, svc);
|
|
977
1026
|
try {
|
|
978
1027
|
if (inputT > 0 && "messages" in reqBody) {
|
|
979
1028
|
const rawText = JSON.stringify(reqBody.messages);
|
|
980
|
-
const calibration =
|
|
1029
|
+
const calibration = svc.tokenCalibration;
|
|
981
1030
|
calibrateFromResponse(estimateTokens(rawText, model.provider, model.id, calibration), inputT, model.provider, model.id, calibration);
|
|
982
1031
|
}
|
|
983
1032
|
} catch (e) {
|
|
@@ -994,20 +1043,20 @@ async function trackedComplete(phase, model, reqBody, opts, services) {
|
|
|
994
1043
|
cacheHitTokens: 0,
|
|
995
1044
|
latencyMs: Date.now() - start,
|
|
996
1045
|
success: false
|
|
997
|
-
},
|
|
1046
|
+
}, svc);
|
|
998
1047
|
throw err;
|
|
999
1048
|
}
|
|
1000
1049
|
}
|
|
1001
1050
|
function getCachePath(sessionId) {
|
|
1002
1051
|
return extractionCacheFile(sessionId);
|
|
1003
1052
|
}
|
|
1004
|
-
function getExtractionCacheStats(services
|
|
1053
|
+
function getExtractionCacheStats(services) {
|
|
1005
1054
|
return services.extractionCacheStats.snapshot();
|
|
1006
1055
|
}
|
|
1007
|
-
function recordExtractionCacheHit(services
|
|
1056
|
+
function recordExtractionCacheHit(services) {
|
|
1008
1057
|
services.extractionCacheStats.recordHit();
|
|
1009
1058
|
}
|
|
1010
|
-
function recordExtractionCacheMiss(services
|
|
1059
|
+
function recordExtractionCacheMiss(services) {
|
|
1011
1060
|
services.extractionCacheStats.recordMiss();
|
|
1012
1061
|
}
|
|
1013
1062
|
function saveCachedExtraction(sessionId, extraction, msgCount, firstEntryId, lastEntryId, entryIds, keptEntryIds) {
|
|
@@ -1040,7 +1089,7 @@ function scheduleExtractionCacheCleanup() {
|
|
|
1040
1089
|
if (_extractionPruneInFlight)
|
|
1041
1090
|
return;
|
|
1042
1091
|
_extractionPruneInFlight = true;
|
|
1043
|
-
|
|
1092
|
+
setTimeout(() => {
|
|
1044
1093
|
try {
|
|
1045
1094
|
const dir = path5.dirname(getCachePath("_"));
|
|
1046
1095
|
if (!fs3.existsSync(dir))
|
|
@@ -1110,7 +1159,9 @@ function appendMetricsLog(sessionId, extra, services) {
|
|
|
1110
1159
|
...summary,
|
|
1111
1160
|
...extra
|
|
1112
1161
|
};
|
|
1113
|
-
|
|
1162
|
+
const logPath = metricsLogFile();
|
|
1163
|
+
appendLineLocked(logPath, JSON.stringify(entry));
|
|
1164
|
+
scheduleFileTailTrim(logPath, RUNTIME_LOG_MAX_BYTES);
|
|
1114
1165
|
} catch (e) {
|
|
1115
1166
|
warn("appendMetricsLog failed", e);
|
|
1116
1167
|
}
|
|
@@ -1154,243 +1205,10 @@ function readMetricsLog(limit = 100) {
|
|
|
1154
1205
|
return [];
|
|
1155
1206
|
}
|
|
1156
1207
|
}
|
|
1157
|
-
function escapeHtml(value) {
|
|
1158
|
-
return String(value ?? "").replace(/[&<>\"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
1159
|
-
}
|
|
1160
|
-
function durationOf(entry) {
|
|
1161
|
-
return entry.durationMs ?? entry.phaseTimings?.reduce((sum, phase) => sum + phase.durationMs, 0) ?? 0;
|
|
1162
|
-
}
|
|
1163
|
-
function percentile(values, p) {
|
|
1164
|
-
if (!values.length)
|
|
1165
|
-
return 0;
|
|
1166
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
1167
|
-
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor(p / 100 * sorted.length)))];
|
|
1168
|
-
}
|
|
1169
|
-
function average(values) {
|
|
1170
|
-
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
1171
|
-
}
|
|
1172
|
-
function compactNumber(value) {
|
|
1173
|
-
return new Intl.NumberFormat("en", { notation: Math.abs(value) >= 1e4 ? "compact" : "standard", maximumFractionDigits: 1 }).format(value);
|
|
1174
|
-
}
|
|
1175
|
-
function formatNumber(value) {
|
|
1176
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1177
|
-
return escapeHtml(value);
|
|
1178
|
-
return value.toLocaleString();
|
|
1179
|
-
}
|
|
1180
|
-
function formatMs(value) {
|
|
1181
|
-
if (!value)
|
|
1182
|
-
return "0ms";
|
|
1183
|
-
if (value >= 60000)
|
|
1184
|
-
return (value / 60000).toFixed(value >= 600000 ? 0 : 1) + "m";
|
|
1185
|
-
if (value >= 1000)
|
|
1186
|
-
return (value / 1000).toFixed(value >= 1e4 ? 0 : 1) + "s";
|
|
1187
|
-
return Math.round(value) + "ms";
|
|
1188
|
-
}
|
|
1189
|
-
function formatPercent(value) {
|
|
1190
|
-
return Math.round(value * 100) + "%";
|
|
1191
|
-
}
|
|
1192
|
-
function statusClass(status) {
|
|
1193
|
-
if (status === "timeout" || status === "error")
|
|
1194
|
-
return "bad";
|
|
1195
|
-
if (status === "dry-run")
|
|
1196
|
-
return "warn";
|
|
1197
|
-
return "good";
|
|
1198
|
-
}
|
|
1199
|
-
function statusLabel(status) {
|
|
1200
|
-
return status ?? "success";
|
|
1201
|
-
}
|
|
1202
|
-
function badge(status) {
|
|
1203
|
-
const label = statusLabel(status);
|
|
1204
|
-
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
1205
|
-
}
|
|
1206
|
-
function summarizeDashboard(entries) {
|
|
1207
|
-
const durations = entries.map(durationOf).filter(Boolean);
|
|
1208
|
-
const success = entries.filter((e) => statusLabel(e.status) === "success").length;
|
|
1209
|
-
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
1210
|
-
const error = entries.filter((e) => e.status === "error").length;
|
|
1211
|
-
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
1212
|
-
const scored = entries.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
1213
|
-
return {
|
|
1214
|
-
runs: entries.length,
|
|
1215
|
-
success,
|
|
1216
|
-
timeout,
|
|
1217
|
-
error,
|
|
1218
|
-
dryRun,
|
|
1219
|
-
successRate: entries.length ? success / entries.length : 0,
|
|
1220
|
-
avgDuration: Math.round(average(durations)),
|
|
1221
|
-
p95Duration: percentile(durations, 95),
|
|
1222
|
-
totalCalls: entries.reduce((sum, e) => sum + (e.totalCalls ?? 0), 0),
|
|
1223
|
-
totalInput: entries.reduce((sum, e) => sum + (e.totalInput ?? 0), 0),
|
|
1224
|
-
totalOutput: entries.reduce((sum, e) => sum + (e.totalOutput ?? 0), 0),
|
|
1225
|
-
totalSaved: entries.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1226
|
-
avgScore: Math.round(average(scored))
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
function groupMetrics(entries, keyFn) {
|
|
1230
|
-
const groups = new Map;
|
|
1231
|
-
for (const entry of entries) {
|
|
1232
|
-
const key = keyFn(entry);
|
|
1233
|
-
if (!key)
|
|
1234
|
-
continue;
|
|
1235
|
-
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
1236
|
-
}
|
|
1237
|
-
return [...groups.entries()].map(([name, group]) => {
|
|
1238
|
-
const durations = group.map(durationOf).filter(Boolean);
|
|
1239
|
-
const scores = group.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
1240
|
-
const failures = group.filter((e) => e.status === "timeout" || e.status === "error").length;
|
|
1241
|
-
return {
|
|
1242
|
-
name,
|
|
1243
|
-
runs: group.length,
|
|
1244
|
-
avgDuration: Math.round(average(durations)),
|
|
1245
|
-
p95Duration: percentile(durations, 95),
|
|
1246
|
-
avgScore: Math.round(average(scores)),
|
|
1247
|
-
totalSaved: group.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
1248
|
-
totalCalls: group.reduce((sum, e) => sum + (e.totalCalls ?? 0), 0),
|
|
1249
|
-
errorRate: group.length ? failures / group.length : 0
|
|
1250
|
-
};
|
|
1251
|
-
}).sort((a, b) => b.runs - a.runs || a.name.localeCompare(b.name));
|
|
1252
|
-
}
|
|
1253
|
-
function progressBar(value, label = formatPercent(value)) {
|
|
1254
|
-
const pct = Math.max(0, Math.min(100, Math.round(value * 100)));
|
|
1255
|
-
return `<div class="meter" title="${escapeHtml(label)}"><span style="width:${pct}%"></span></div>`;
|
|
1256
|
-
}
|
|
1257
|
-
function sparkline(values) {
|
|
1258
|
-
const nums = values.filter((v) => Number.isFinite(v));
|
|
1259
|
-
if (nums.length < 2)
|
|
1260
|
-
return `<div class="empty">Need at least two runs for trend</div>`;
|
|
1261
|
-
const width = 520;
|
|
1262
|
-
const height = 120;
|
|
1263
|
-
const min = Math.min(...nums);
|
|
1264
|
-
const max = Math.max(...nums);
|
|
1265
|
-
const span = Math.max(1, max - min);
|
|
1266
|
-
const points = nums.map((value, i) => {
|
|
1267
|
-
const x = i / Math.max(1, nums.length - 1) * width;
|
|
1268
|
-
const y = height - (value - min) / span * (height - 18) - 9;
|
|
1269
|
-
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
1270
|
-
}).join(" ");
|
|
1271
|
-
const last = nums[nums.length - 1];
|
|
1272
|
-
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>`;
|
|
1273
|
-
}
|
|
1274
|
-
function metricCard(label, value, detail, tone = "neutral") {
|
|
1275
|
-
return `<article class="card ${tone}"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(value)}</div><div class="detail">${escapeHtml(detail)}</div></article>`;
|
|
1276
|
-
}
|
|
1277
|
-
function comparisonRows(groups) {
|
|
1278
|
-
if (!groups.length)
|
|
1279
|
-
return `<tr><td colspan="8" class="empty">No data yet</td></tr>`;
|
|
1280
|
-
return groups.map((group) => `<tr>
|
|
1281
|
-
<td><strong>${escapeHtml(group.name)}</strong></td>
|
|
1282
|
-
<td class="num">${formatNumber(group.runs)}</td>
|
|
1283
|
-
<td class="num">${escapeHtml(formatMs(group.avgDuration))}</td>
|
|
1284
|
-
<td class="num">${escapeHtml(formatMs(group.p95Duration))}</td>
|
|
1285
|
-
<td class="num">${group.avgScore ? formatNumber(group.avgScore) : "\u2014"}</td>
|
|
1286
|
-
<td class="num">${formatNumber(group.totalCalls)}</td>
|
|
1287
|
-
<td class="num">${formatNumber(group.totalSaved)}</td>
|
|
1288
|
-
<td>${progressBar(1 - group.errorRate, formatPercent(1 - group.errorRate) + " reliable")}</td>
|
|
1289
|
-
</tr>`).join(`
|
|
1290
|
-
`);
|
|
1291
|
-
}
|
|
1292
|
-
function phaseRows(entry) {
|
|
1293
|
-
const timings = entry?.phaseTimings ?? [];
|
|
1294
|
-
if (!timings.length)
|
|
1295
|
-
return `<tr><td colspan="3" class="empty">No phase timings yet</td></tr>`;
|
|
1296
|
-
const total = timings.reduce((sum, phase) => sum + phase.durationMs, 0) || 1;
|
|
1297
|
-
return timings.map((phase) => `<tr>
|
|
1298
|
-
<td>${escapeHtml(phase.phase)}</td>
|
|
1299
|
-
<td class="num">${escapeHtml(formatMs(phase.durationMs))}</td>
|
|
1300
|
-
<td>${progressBar(phase.durationMs / total, formatPercent(phase.durationMs / total))}</td>
|
|
1301
|
-
</tr>`).join(`
|
|
1302
|
-
`);
|
|
1303
|
-
}
|
|
1304
|
-
function recentRunRows(entries) {
|
|
1305
|
-
if (!entries.length)
|
|
1306
|
-
return `<tr><td colspan="12" class="empty">No runs recorded yet</td></tr>`;
|
|
1307
|
-
return entries.slice(-80).reverse().map((entry) => `<tr>
|
|
1308
|
-
<td class="mono small">${escapeHtml(entry.ts)}</td>
|
|
1309
|
-
<td>${escapeHtml(entry.profile)}</td>
|
|
1310
|
-
<td>${escapeHtml(entry.provider ?? entry.model?.split("/")[0])}</td>
|
|
1311
|
-
<td>${escapeHtml(entry.method)}</td>
|
|
1312
|
-
<td>${escapeHtml(entry.runType)}</td>
|
|
1313
|
-
<td>${badge(entry.status)}</td>
|
|
1314
|
-
<td class="num">${escapeHtml(formatMs(durationOf(entry)))}</td>
|
|
1315
|
-
<td class="num">${typeof entry.verificationScore === "number" ? formatNumber(entry.verificationScore) : "\u2014"}</td>
|
|
1316
|
-
<td class="num">${typeof entry.tokensSaved === "number" ? formatNumber(entry.tokensSaved) : "\u2014"}</td>
|
|
1317
|
-
<td class="num">${formatNumber(entry.totalCalls ?? 0)}</td>
|
|
1318
|
-
<td class="num">${typeof entry.extractionCacheHitRate === "number" ? formatPercent(entry.extractionCacheHitRate) : "\u2014"}</td>
|
|
1319
|
-
<td class="mono small reason">${escapeHtml(entry.fallbackReason ?? entry.extractionCacheMissReason ?? "")}</td>
|
|
1320
|
-
</tr>`).join(`
|
|
1321
|
-
`);
|
|
1322
|
-
}
|
|
1323
|
-
function dashboardCss() {
|
|
1324
|
-
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}}`;
|
|
1325
|
-
}
|
|
1326
|
-
function buildMetricsReport(entries = readMetricsLog(100)) {
|
|
1327
|
-
if (!entries.length)
|
|
1328
|
-
return "No smart-compact metrics recorded yet.";
|
|
1329
|
-
const summary = summarizeDashboard(entries);
|
|
1330
|
-
const byProfile = groupMetrics(entries, (e) => e.profile);
|
|
1331
|
-
const byProvider = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]);
|
|
1332
|
-
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);
|
|
1333
|
-
const extractionCacheRuns = entries.filter((e) => typeof e.extractionCacheHitRate === "number");
|
|
1334
|
-
const extractionCacheAvg = average(extractionCacheRuns.map((e) => e.extractionCacheHitRate ?? 0));
|
|
1335
|
-
return [
|
|
1336
|
-
"# Smart Compact Metrics",
|
|
1337
|
-
"",
|
|
1338
|
-
"Runs: " + summary.runs + " (success " + summary.success + ", dry-run " + summary.dryRun + ", timeout " + summary.timeout + ", error " + summary.error + ")",
|
|
1339
|
-
"Reliability: " + formatPercent(summary.successRate),
|
|
1340
|
-
"Latency: avg " + summary.avgDuration + "ms, p95 " + summary.p95Duration + "ms",
|
|
1341
|
-
"LLM calls: " + summary.totalCalls + ", input " + summary.totalInput + "t, output " + summary.totalOutput + "t",
|
|
1342
|
-
"Extraction cache: avg " + (extractionCacheRuns.length ? formatPercent(extractionCacheAvg) : "\u2014") + " across " + extractionCacheRuns.length + " measured run(s)",
|
|
1343
|
-
"Tokens saved: " + summary.totalSaved + "t, average verification score: " + summary.avgScore,
|
|
1344
|
-
"",
|
|
1345
|
-
"## Profile comparison",
|
|
1346
|
-
...byProfile.map(summarizeGroup),
|
|
1347
|
-
"",
|
|
1348
|
-
"## Provider comparison",
|
|
1349
|
-
...byProvider.map(summarizeGroup)
|
|
1350
|
-
].join(`
|
|
1351
|
-
`);
|
|
1352
|
-
}
|
|
1353
|
-
function writeMetricsDashboard(entries = readMetricsLog(200)) {
|
|
1354
|
-
try {
|
|
1355
|
-
ensureDir(cacheDir());
|
|
1356
|
-
const summary = summarizeDashboard(entries);
|
|
1357
|
-
const latest = entries[entries.length - 1];
|
|
1358
|
-
const report = buildMetricsReport(entries);
|
|
1359
|
-
const profileGroups = groupMetrics(entries, (e) => e.profile);
|
|
1360
|
-
const providerGroups = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]);
|
|
1361
|
-
const healthTone = summary.error + summary.timeout > 0 ? "warn" : "good";
|
|
1362
|
-
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>
|
|
1363
|
-
<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>
|
|
1364
|
-
<section class="cards">
|
|
1365
|
-
${metricCard("Reliability", formatPercent(summary.successRate), `${summary.success} success \xB7 ${summary.timeout} timeout \xB7 ${summary.error} error`, healthTone)}
|
|
1366
|
-
${metricCard("Avg duration", formatMs(summary.avgDuration), `p95 ${formatMs(summary.p95Duration)}`)}
|
|
1367
|
-
${metricCard("LLM calls", compactNumber(summary.totalCalls), `${compactNumber(summary.totalInput)} input \xB7 ${compactNumber(summary.totalOutput)} output`)}
|
|
1368
|
-
${metricCard("Tokens saved", compactNumber(summary.totalSaved), `avg score ${summary.avgScore || "\u2014"}`)}
|
|
1369
|
-
</section>
|
|
1370
|
-
<section class="layout">
|
|
1371
|
-
<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>
|
|
1372
|
-
<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>
|
|
1373
|
-
</section>
|
|
1374
|
-
<section class="two section">
|
|
1375
|
-
<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>
|
|
1376
|
-
<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>
|
|
1377
|
-
</section>
|
|
1378
|
-
<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>
|
|
1379
|
-
<section class="section"><h2>Raw text report</h2><pre>${escapeHtml(report)}</pre></section>
|
|
1380
|
-
</main></body></html>`;
|
|
1381
|
-
const fp = metricsDashboardFile();
|
|
1382
|
-
atomicWriteFileSync(fp, html);
|
|
1383
|
-
return fp;
|
|
1384
|
-
} catch (e) {
|
|
1385
|
-
warn("writeMetricsDashboard failed", e);
|
|
1386
|
-
return null;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
1208
|
var INTERNAL_PHASES, EXTRACTION_CACHE_TTL_MS, EXTRACTION_CACHE_PRUNE_MAX_AGE_MS, _extractionPruneInFlight = false;
|
|
1390
1209
|
var init_cache = __esm(() => {
|
|
1391
1210
|
init_tokens();
|
|
1392
1211
|
init_logger();
|
|
1393
|
-
init_llm_client();
|
|
1394
1212
|
init_paths();
|
|
1395
1213
|
init_fs();
|
|
1396
1214
|
init_constants();
|
|
@@ -1417,6 +1235,8 @@ function metricDuration(entry) {
|
|
|
1417
1235
|
function metricMs(ms) {
|
|
1418
1236
|
if (!Number.isFinite(ms) || ms <= 0)
|
|
1419
1237
|
return "0ms";
|
|
1238
|
+
if (ms >= 60000)
|
|
1239
|
+
return (ms / 60000).toFixed(ms >= 600000 ? 0 : 1) + "m";
|
|
1420
1240
|
return ms >= 1000 ? (ms / 1000).toFixed(ms >= 1e4 ? 0 : 1) + "s" : Math.round(ms) + "ms";
|
|
1421
1241
|
}
|
|
1422
1242
|
function clampRatio(value) {
|
|
@@ -2463,7 +2283,10 @@ function buildTimeline(msgs, errors) {
|
|
|
2463
2283
|
if (errorIndices.has(i))
|
|
2464
2284
|
timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, TRUNC.TIMELINE_ERROR) ?? "error" });
|
|
2465
2285
|
}
|
|
2466
|
-
return timeline.length > 30 ? [
|
|
2286
|
+
return timeline.length > 30 ? [
|
|
2287
|
+
...timeline.filter((t) => t.event === "user_request").slice(-TRUNC.TIMELINE_DISPLAY),
|
|
2288
|
+
...timeline.filter((t) => t.event === "error")
|
|
2289
|
+
].sort((a, b) => a.index - b.index) : timeline;
|
|
2467
2290
|
}
|
|
2468
2291
|
function extractMainGoal(msgs) {
|
|
2469
2292
|
for (const m of msgs) {
|
|
@@ -2518,12 +2341,14 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
2518
2341
|
}
|
|
2519
2342
|
}
|
|
2520
2343
|
}
|
|
2521
|
-
const BLOCKED_RE =
|
|
2344
|
+
const BLOCKED_RE = /\bblocked\b|waiting for|\bdepends?\s+on\b|ba[\u011Fg]l[i\u0131]|bekliyor|engell/i;
|
|
2522
2345
|
for (let idx = 0;idx < msgs.length; idx++) {
|
|
2523
2346
|
const msg = msgs[idx];
|
|
2524
2347
|
if (msg.role !== "user")
|
|
2525
2348
|
continue;
|
|
2526
2349
|
const txt = extractText(msg.content);
|
|
2350
|
+
if (txt.length < 10 || txt.startsWith("/"))
|
|
2351
|
+
continue;
|
|
2527
2352
|
if (BLOCKED_RE.test(txt)) {
|
|
2528
2353
|
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
2529
2354
|
if (!isDup) {
|
|
@@ -2726,13 +2551,13 @@ function schedulePruneBackups(dir) {
|
|
|
2726
2551
|
if (_pruneInFlight.has(dir))
|
|
2727
2552
|
return;
|
|
2728
2553
|
_pruneInFlight.add(dir);
|
|
2729
|
-
|
|
2554
|
+
setTimeout(() => {
|
|
2730
2555
|
try {
|
|
2731
2556
|
prunePass(dir);
|
|
2732
2557
|
} finally {
|
|
2733
2558
|
_pruneInFlight.delete(dir);
|
|
2734
2559
|
}
|
|
2735
|
-
});
|
|
2560
|
+
}, 0);
|
|
2736
2561
|
}
|
|
2737
2562
|
function backupConversation(convText, sessionId) {
|
|
2738
2563
|
try {
|
|
@@ -3060,52 +2885,269 @@ function computeToolCharPercentage(branchEntries) {
|
|
|
3060
2885
|
}
|
|
3061
2886
|
return totalChars > 0 ? Math.round(toolChars / totalChars * 100) : 0;
|
|
3062
2887
|
}
|
|
3063
|
-
function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
|
|
3064
|
-
if (totalTokens < minThreshold)
|
|
3065
|
-
return "none";
|
|
3066
|
-
if (contextPercent < minContextPercent)
|
|
3067
|
-
return "none";
|
|
3068
|
-
if (contextPercent < 80)
|
|
3069
|
-
return "light";
|
|
3070
|
-
return "full";
|
|
2888
|
+
function selectCompactionTier(contextPercent, toolPercent, totalTokens, minThreshold, minContextPercent = 60) {
|
|
2889
|
+
if (totalTokens < minThreshold)
|
|
2890
|
+
return "none";
|
|
2891
|
+
if (contextPercent < minContextPercent)
|
|
2892
|
+
return "none";
|
|
2893
|
+
if (contextPercent < 80)
|
|
2894
|
+
return "light";
|
|
2895
|
+
return "full";
|
|
2896
|
+
}
|
|
2897
|
+
function inferSessionType(extraction, report) {
|
|
2898
|
+
if (report?.sessionType)
|
|
2899
|
+
return report.sessionType;
|
|
2900
|
+
const hasModifications = extraction.modifiedFiles.length > 0;
|
|
2901
|
+
const hasUnresolvedErrors = extraction.errors.some((e) => !e.resolved);
|
|
2902
|
+
const hasResolvedErrors = extraction.errors.some((e) => e.resolved);
|
|
2903
|
+
const hasReadsOnly = extraction.readFiles.length > 2 && !hasModifications;
|
|
2904
|
+
const hasDecisions = extraction.decisions.length > 0;
|
|
2905
|
+
if (hasUnresolvedErrors && (hasModifications || hasResolvedErrors))
|
|
2906
|
+
return "debugging";
|
|
2907
|
+
if (hasReadsOnly && !hasDecisions)
|
|
2908
|
+
return "review";
|
|
2909
|
+
if (hasDecisions && !hasModifications && !hasUnresolvedErrors)
|
|
2910
|
+
return "discussion";
|
|
2911
|
+
if (hasModifications)
|
|
2912
|
+
return "implementation";
|
|
2913
|
+
return "implementation";
|
|
2914
|
+
}
|
|
2915
|
+
function buildExplorationContext(report) {
|
|
2916
|
+
if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
|
|
2917
|
+
return "";
|
|
2918
|
+
return [
|
|
2919
|
+
"## Exploration Report",
|
|
2920
|
+
"Main goal: " + report.mainGoal,
|
|
2921
|
+
"Session type: " + report.sessionType,
|
|
2922
|
+
report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
|
|
2923
|
+
report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
|
|
2924
|
+
report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
|
|
2925
|
+
report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
|
|
2926
|
+
report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
|
|
2927
|
+
].filter(Boolean).join(`
|
|
2928
|
+
`);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// src/index.ts
|
|
2932
|
+
init_tokens();
|
|
2933
|
+
init_cache();
|
|
2934
|
+
|
|
2935
|
+
// src/ui/metrics-report.ts
|
|
2936
|
+
init_paths();
|
|
2937
|
+
init_fs();
|
|
2938
|
+
init_logger();
|
|
2939
|
+
init_cache();
|
|
2940
|
+
function escapeHtml(value) {
|
|
2941
|
+
return String(value ?? "").replace(/[&<>\"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
2942
|
+
}
|
|
2943
|
+
function percentile(values, p) {
|
|
2944
|
+
if (!values.length)
|
|
2945
|
+
return 0;
|
|
2946
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2947
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor(p / 100 * sorted.length)))];
|
|
2948
|
+
}
|
|
2949
|
+
function average(values) {
|
|
2950
|
+
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
2951
|
+
}
|
|
2952
|
+
function compactNumber(value) {
|
|
2953
|
+
return new Intl.NumberFormat("en", { notation: Math.abs(value) >= 1e4 ? "compact" : "standard", maximumFractionDigits: 1 }).format(value);
|
|
2954
|
+
}
|
|
2955
|
+
function statusClass(status) {
|
|
2956
|
+
if (status === "timeout" || status === "error")
|
|
2957
|
+
return "bad";
|
|
2958
|
+
if (status === "dry-run")
|
|
2959
|
+
return "warn";
|
|
2960
|
+
return "good";
|
|
2961
|
+
}
|
|
2962
|
+
function statusLabel(status) {
|
|
2963
|
+
return status ?? "success";
|
|
2964
|
+
}
|
|
2965
|
+
function badge(status) {
|
|
2966
|
+
const label = statusLabel(status);
|
|
2967
|
+
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
2968
|
+
}
|
|
2969
|
+
function summarizeDashboard(entries) {
|
|
2970
|
+
const durations = entries.map(metricDuration).filter(Boolean);
|
|
2971
|
+
const success = entries.filter((e) => statusLabel(e.status) === "success").length;
|
|
2972
|
+
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
2973
|
+
const error = entries.filter((e) => e.status === "error").length;
|
|
2974
|
+
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
2975
|
+
const scored = entries.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
2976
|
+
return {
|
|
2977
|
+
runs: entries.length,
|
|
2978
|
+
success,
|
|
2979
|
+
timeout,
|
|
2980
|
+
error,
|
|
2981
|
+
dryRun,
|
|
2982
|
+
successRate: entries.length ? success / entries.length : 0,
|
|
2983
|
+
avgDuration: Math.round(average(durations)),
|
|
2984
|
+
p95Duration: percentile(durations, 95),
|
|
2985
|
+
totalCalls: entries.reduce((sum, e) => sum + (e.totalCalls ?? 0), 0),
|
|
2986
|
+
totalInput: entries.reduce((sum, e) => sum + (e.totalInput ?? 0), 0),
|
|
2987
|
+
totalOutput: entries.reduce((sum, e) => sum + (e.totalOutput ?? 0), 0),
|
|
2988
|
+
totalSaved: entries.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
2989
|
+
avgScore: Math.round(average(scored))
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function groupMetrics(entries, keyFn) {
|
|
2993
|
+
const groups = new Map;
|
|
2994
|
+
for (const entry of entries) {
|
|
2995
|
+
const key = keyFn(entry);
|
|
2996
|
+
if (!key)
|
|
2997
|
+
continue;
|
|
2998
|
+
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
2999
|
+
}
|
|
3000
|
+
return [...groups.entries()].map(([name, group]) => {
|
|
3001
|
+
const durations = group.map(metricDuration).filter(Boolean);
|
|
3002
|
+
const scores = group.map((e) => e.verificationScore).filter((v) => typeof v === "number");
|
|
3003
|
+
const failures = group.filter((e) => e.status === "timeout" || e.status === "error").length;
|
|
3004
|
+
return {
|
|
3005
|
+
name,
|
|
3006
|
+
runs: group.length,
|
|
3007
|
+
avgDuration: Math.round(average(durations)),
|
|
3008
|
+
p95Duration: percentile(durations, 95),
|
|
3009
|
+
avgScore: Math.round(average(scores)),
|
|
3010
|
+
totalSaved: group.reduce((sum, e) => sum + (e.tokensSaved ?? 0), 0),
|
|
3011
|
+
totalCalls: group.reduce((sum, e) => sum + (e.totalCalls ?? 0), 0),
|
|
3012
|
+
errorRate: group.length ? failures / group.length : 0
|
|
3013
|
+
};
|
|
3014
|
+
}).sort((a, b) => b.runs - a.runs || a.name.localeCompare(b.name));
|
|
3015
|
+
}
|
|
3016
|
+
function progressBar(value, label = metricPct(value)) {
|
|
3017
|
+
const pct = Math.max(0, Math.min(100, Math.round(value * 100)));
|
|
3018
|
+
return `<div class="meter" title="${escapeHtml(label)}"><span style="width:${pct}%"></span></div>`;
|
|
3019
|
+
}
|
|
3020
|
+
function sparkline(values) {
|
|
3021
|
+
const nums = values.filter((v) => Number.isFinite(v));
|
|
3022
|
+
if (nums.length < 2)
|
|
3023
|
+
return `<div class="empty">Need at least two runs for trend</div>`;
|
|
3024
|
+
const width = 520;
|
|
3025
|
+
const height = 120;
|
|
3026
|
+
const min = Math.min(...nums);
|
|
3027
|
+
const max = Math.max(...nums);
|
|
3028
|
+
const span = Math.max(1, max - min);
|
|
3029
|
+
const points = nums.map((value, i) => {
|
|
3030
|
+
const x = i / Math.max(1, nums.length - 1) * width;
|
|
3031
|
+
const y = height - (value - min) / span * (height - 18) - 9;
|
|
3032
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
3033
|
+
}).join(" ");
|
|
3034
|
+
const last = nums[nums.length - 1];
|
|
3035
|
+
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(metricMs(max))}</text><text x="0" y="${height - 4}">${escapeHtml(metricMs(min))}</text></svg>`;
|
|
3036
|
+
}
|
|
3037
|
+
function metricCard(label, value, detail, tone = "neutral") {
|
|
3038
|
+
return `<article class="card ${tone}"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(value)}</div><div class="detail">${escapeHtml(detail)}</div></article>`;
|
|
3039
|
+
}
|
|
3040
|
+
function comparisonRows(groups) {
|
|
3041
|
+
if (!groups.length)
|
|
3042
|
+
return `<tr><td colspan="8" class="empty">No data yet</td></tr>`;
|
|
3043
|
+
return groups.map((group) => `<tr>
|
|
3044
|
+
<td><strong>${escapeHtml(group.name)}</strong></td>
|
|
3045
|
+
<td class="num">${metricNum(group.runs)}</td>
|
|
3046
|
+
<td class="num">${escapeHtml(metricMs(group.avgDuration))}</td>
|
|
3047
|
+
<td class="num">${escapeHtml(metricMs(group.p95Duration))}</td>
|
|
3048
|
+
<td class="num">${group.avgScore ? metricNum(group.avgScore) : "\u2014"}</td>
|
|
3049
|
+
<td class="num">${metricNum(group.totalCalls)}</td>
|
|
3050
|
+
<td class="num">${metricNum(group.totalSaved)}</td>
|
|
3051
|
+
<td>${progressBar(1 - group.errorRate, metricPct(1 - group.errorRate) + " reliable")}</td>
|
|
3052
|
+
</tr>`).join(`
|
|
3053
|
+
`);
|
|
3054
|
+
}
|
|
3055
|
+
function phaseRows(entry) {
|
|
3056
|
+
const timings = entry?.phaseTimings ?? [];
|
|
3057
|
+
if (!timings.length)
|
|
3058
|
+
return `<tr><td colspan="3" class="empty">No phase timings yet</td></tr>`;
|
|
3059
|
+
const total = timings.reduce((sum, phase) => sum + phase.durationMs, 0) || 1;
|
|
3060
|
+
return timings.map((phase) => `<tr>
|
|
3061
|
+
<td>${escapeHtml(phase.phase)}</td>
|
|
3062
|
+
<td class="num">${escapeHtml(metricMs(phase.durationMs))}</td>
|
|
3063
|
+
<td>${progressBar(phase.durationMs / total, metricPct(phase.durationMs / total))}</td>
|
|
3064
|
+
</tr>`).join(`
|
|
3065
|
+
`);
|
|
3066
|
+
}
|
|
3067
|
+
function recentRunRows(entries) {
|
|
3068
|
+
if (!entries.length)
|
|
3069
|
+
return `<tr><td colspan="12" class="empty">No runs recorded yet</td></tr>`;
|
|
3070
|
+
return entries.slice(-80).reverse().map((entry) => `<tr>
|
|
3071
|
+
<td class="mono small">${escapeHtml(entry.ts)}</td>
|
|
3072
|
+
<td>${escapeHtml(entry.profile)}</td>
|
|
3073
|
+
<td>${escapeHtml(entry.provider ?? entry.model?.split("/")[0])}</td>
|
|
3074
|
+
<td>${escapeHtml(entry.method)}</td>
|
|
3075
|
+
<td>${escapeHtml(entry.runType)}</td>
|
|
3076
|
+
<td>${badge(entry.status)}</td>
|
|
3077
|
+
<td class="num">${escapeHtml(metricMs(metricDuration(entry)))}</td>
|
|
3078
|
+
<td class="num">${typeof entry.verificationScore === "number" ? metricNum(entry.verificationScore) : "\u2014"}</td>
|
|
3079
|
+
<td class="num">${typeof entry.tokensSaved === "number" ? metricNum(entry.tokensSaved) : "\u2014"}</td>
|
|
3080
|
+
<td class="num">${metricNum(entry.totalCalls ?? 0)}</td>
|
|
3081
|
+
<td class="num">${typeof entry.extractionCacheHitRate === "number" ? metricPct(entry.extractionCacheHitRate) : "\u2014"}</td>
|
|
3082
|
+
<td class="mono small reason">${escapeHtml(entry.fallbackReason ?? entry.extractionCacheMissReason ?? "")}</td>
|
|
3083
|
+
</tr>`).join(`
|
|
3084
|
+
`);
|
|
3071
3085
|
}
|
|
3072
|
-
function
|
|
3073
|
-
|
|
3074
|
-
return report.sessionType;
|
|
3075
|
-
const hasModifications = extraction.modifiedFiles.length > 0;
|
|
3076
|
-
const hasUnresolvedErrors = extraction.errors.some((e) => !e.resolved);
|
|
3077
|
-
const hasResolvedErrors = extraction.errors.some((e) => e.resolved);
|
|
3078
|
-
const hasReadsOnly = extraction.readFiles.length > 2 && !hasModifications;
|
|
3079
|
-
const hasDecisions = extraction.decisions.length > 0;
|
|
3080
|
-
if (hasUnresolvedErrors && (hasModifications || hasResolvedErrors))
|
|
3081
|
-
return "debugging";
|
|
3082
|
-
if (hasReadsOnly && !hasDecisions)
|
|
3083
|
-
return "review";
|
|
3084
|
-
if (hasDecisions && !hasModifications && !hasUnresolvedErrors)
|
|
3085
|
-
return "discussion";
|
|
3086
|
-
if (hasModifications)
|
|
3087
|
-
return "implementation";
|
|
3088
|
-
return "implementation";
|
|
3086
|
+
function dashboardCss() {
|
|
3087
|
+
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}}`;
|
|
3089
3088
|
}
|
|
3090
|
-
function
|
|
3091
|
-
if (!
|
|
3092
|
-
return "";
|
|
3089
|
+
function buildMetricsReport(entries = readMetricsLog(100)) {
|
|
3090
|
+
if (!entries.length)
|
|
3091
|
+
return "No smart-compact metrics recorded yet.";
|
|
3092
|
+
const summary = summarizeDashboard(entries);
|
|
3093
|
+
const byProfile = groupMetrics(entries, (e) => e.profile);
|
|
3094
|
+
const byProvider = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]);
|
|
3095
|
+
const summarizeGroup = (group) => "- " + group.name + ": n=" + group.runs + ", avg=" + group.avgDuration + "ms, p95=" + group.p95Duration + "ms, score=" + group.avgScore + ", saved=" + group.totalSaved + "t, reliability=" + metricPct(1 - group.errorRate);
|
|
3096
|
+
const extractionCacheRuns = entries.filter((e) => typeof e.extractionCacheHitRate === "number");
|
|
3097
|
+
const extractionCacheAvg = average(extractionCacheRuns.map((e) => e.extractionCacheHitRate ?? 0));
|
|
3093
3098
|
return [
|
|
3094
|
-
"
|
|
3095
|
-
"
|
|
3096
|
-
"
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3099
|
+
"# Smart Compact Metrics",
|
|
3100
|
+
"",
|
|
3101
|
+
"Runs: " + summary.runs + " (success " + summary.success + ", dry-run " + summary.dryRun + ", timeout " + summary.timeout + ", error " + summary.error + ")",
|
|
3102
|
+
"Reliability: " + metricPct(summary.successRate),
|
|
3103
|
+
"Latency: avg " + summary.avgDuration + "ms, p95 " + summary.p95Duration + "ms",
|
|
3104
|
+
"LLM calls: " + summary.totalCalls + ", input " + summary.totalInput + "t, output " + summary.totalOutput + "t",
|
|
3105
|
+
"Extraction cache: avg " + (extractionCacheRuns.length ? metricPct(extractionCacheAvg) : "\u2014") + " across " + extractionCacheRuns.length + " measured run(s)",
|
|
3106
|
+
"Tokens saved: " + summary.totalSaved + "t, average verification score: " + summary.avgScore,
|
|
3107
|
+
"",
|
|
3108
|
+
"## Profile comparison",
|
|
3109
|
+
...byProfile.map(summarizeGroup),
|
|
3110
|
+
"",
|
|
3111
|
+
"## Provider comparison",
|
|
3112
|
+
...byProvider.map(summarizeGroup)
|
|
3113
|
+
].join(`
|
|
3103
3114
|
`);
|
|
3104
3115
|
}
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3116
|
+
function writeMetricsDashboard(entries = readMetricsLog(200)) {
|
|
3117
|
+
try {
|
|
3118
|
+
const summary = summarizeDashboard(entries);
|
|
3119
|
+
const latest = entries[entries.length - 1];
|
|
3120
|
+
const report = buildMetricsReport(entries);
|
|
3121
|
+
const profileGroups = groupMetrics(entries, (e) => e.profile);
|
|
3122
|
+
const providerGroups = groupMetrics(entries, (e) => e.provider ?? e.model?.split("/")[0]);
|
|
3123
|
+
const healthTone = summary.error + summary.timeout > 0 ? "warn" : "good";
|
|
3124
|
+
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>
|
|
3125
|
+
<header><div><div class="eyebrow">pi-smart-compact</div><h1>Operational Metrics</h1><div class="muted">Generated ${escapeHtml(new Date().toISOString())} \xB7 ${metricNum(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>
|
|
3126
|
+
<section class="cards">
|
|
3127
|
+
${metricCard("Reliability", metricPct(summary.successRate), `${summary.success} success \xB7 ${summary.timeout} timeout \xB7 ${summary.error} error`, healthTone)}
|
|
3128
|
+
${metricCard("Avg duration", metricMs(summary.avgDuration), `p95 ${metricMs(summary.p95Duration)}`)}
|
|
3129
|
+
${metricCard("LLM calls", compactNumber(summary.totalCalls), `${compactNumber(summary.totalInput)} input \xB7 ${compactNumber(summary.totalOutput)} output`)}
|
|
3130
|
+
${metricCard("Tokens saved", compactNumber(summary.totalSaved), `avg score ${summary.avgScore || "\u2014"}`)}
|
|
3131
|
+
</section>
|
|
3132
|
+
<section class="layout">
|
|
3133
|
+
<div class="panel"><h2>Duration trend <span class="muted">last ${Math.min(entries.length, 80)} runs</span></h2>${sparkline(entries.slice(-80).map(metricDuration))}</div>
|
|
3134
|
+
<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>
|
|
3135
|
+
</section>
|
|
3136
|
+
<section class="two section">
|
|
3137
|
+
<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>
|
|
3138
|
+
<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>
|
|
3139
|
+
</section>
|
|
3140
|
+
<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>
|
|
3141
|
+
<section class="section"><h2>Raw text report</h2><pre>${escapeHtml(report)}</pre></section>
|
|
3142
|
+
</main></body></html>`;
|
|
3143
|
+
const fp = metricsDashboardFile();
|
|
3144
|
+
atomicWriteFileSync(fp, html);
|
|
3145
|
+
return fp;
|
|
3146
|
+
} catch (e) {
|
|
3147
|
+
warn("writeMetricsDashboard failed", e);
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3109
3151
|
|
|
3110
3152
|
// src/app/run-smart-compact.ts
|
|
3111
3153
|
init_overlays();
|
|
@@ -3234,6 +3276,7 @@ function asSerializableMessages(msgs) {
|
|
|
3234
3276
|
// src/utils/session-log.ts
|
|
3235
3277
|
import * as fs4 from "fs";
|
|
3236
3278
|
import * as path7 from "path";
|
|
3279
|
+
import { StringDecoder } from "string_decoder";
|
|
3237
3280
|
init_logger();
|
|
3238
3281
|
init_paths();
|
|
3239
3282
|
|
|
@@ -3268,6 +3311,7 @@ function* streamJsonlLines(fp, chunkSize = 64 * 1024) {
|
|
|
3268
3311
|
try {
|
|
3269
3312
|
fd = fs4.openSync(fp, "r");
|
|
3270
3313
|
const buf = Buffer.allocUnsafe(chunkSize);
|
|
3314
|
+
const decoder = new StringDecoder("utf8");
|
|
3271
3315
|
let leftover = "";
|
|
3272
3316
|
let totalRead = 0;
|
|
3273
3317
|
for (;; ) {
|
|
@@ -3279,7 +3323,7 @@ function* streamJsonlLines(fp, chunkSize = 64 * 1024) {
|
|
|
3279
3323
|
warn("streamJsonlLines: log file exceeded " + MAX_LOG_BYTES + " bytes, truncating read");
|
|
3280
3324
|
break;
|
|
3281
3325
|
}
|
|
3282
|
-
const data = leftover + buf.subarray(0, bytes)
|
|
3326
|
+
const data = leftover + decoder.write(buf.subarray(0, bytes));
|
|
3283
3327
|
const lines = data.split(`
|
|
3284
3328
|
`);
|
|
3285
3329
|
leftover = lines.pop() ?? "";
|
|
@@ -3288,6 +3332,7 @@ function* streamJsonlLines(fp, chunkSize = 64 * 1024) {
|
|
|
3288
3332
|
yield line;
|
|
3289
3333
|
}
|
|
3290
3334
|
}
|
|
3335
|
+
leftover += decoder.end();
|
|
3291
3336
|
if (leftover.length > 0)
|
|
3292
3337
|
yield leftover;
|
|
3293
3338
|
} finally {
|
|
@@ -3483,6 +3528,7 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3483
3528
|
}
|
|
3484
3529
|
const tcIdx = ensuredIndex;
|
|
3485
3530
|
const keep = new Set(msgs.map((_, i2) => i2));
|
|
3531
|
+
const removedToolCallIds = new Set;
|
|
3486
3532
|
const reasonMap = new Map;
|
|
3487
3533
|
const readIndices = new Map;
|
|
3488
3534
|
for (let i2 = 0;i2 < msgs.length; i2++) {
|
|
@@ -3500,10 +3546,10 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3500
3546
|
}
|
|
3501
3547
|
for (const [fp, indices] of readIndices) {
|
|
3502
3548
|
for (let j = 0;j < indices.length - 1; j++) {
|
|
3549
|
+
const toolCallId = msgs[indices[j]].toolCallId ?? "";
|
|
3503
3550
|
keep.delete(indices[j]);
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
keep.delete(tc.msgIndex);
|
|
3551
|
+
if (tcIdx.has(toolCallId))
|
|
3552
|
+
removedToolCallIds.add(toolCallId);
|
|
3507
3553
|
}
|
|
3508
3554
|
if (indices.length > 1) {
|
|
3509
3555
|
reasonMap.set("Duplicate file reads", (reasonMap.get("Duplicate file reads") ?? 0) + indices.length - 1);
|
|
@@ -3513,8 +3559,9 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3513
3559
|
for (let i2 = 0;i2 < msgs.length; i2++) {
|
|
3514
3560
|
if (msgs[i2].role !== "toolResult" || !msgs[i2].isError)
|
|
3515
3561
|
continue;
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3562
|
+
const toolCallId = msgs[i2].toolCallId ?? "";
|
|
3563
|
+
const tc = tcIdx.get(toolCallId);
|
|
3564
|
+
failedToolResults.push({ index: i2, tool: tc?.name ?? "unknown", toolCallId });
|
|
3518
3565
|
}
|
|
3519
3566
|
let i = 0;
|
|
3520
3567
|
while (i < failedToolResults.length) {
|
|
@@ -3526,8 +3573,8 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3526
3573
|
if (j - i >= 3) {
|
|
3527
3574
|
for (let k = i + 1;k < j - 1; k++) {
|
|
3528
3575
|
keep.delete(failedToolResults[k].index);
|
|
3529
|
-
if (failedToolResults[k].
|
|
3530
|
-
|
|
3576
|
+
if (tcIdx.has(failedToolResults[k].toolCallId))
|
|
3577
|
+
removedToolCallIds.add(failedToolResults[k].toolCallId);
|
|
3531
3578
|
}
|
|
3532
3579
|
reasonMap.set("Collapsed error chains", (reasonMap.get("Collapsed error chains") ?? 0) + (j - i - 2));
|
|
3533
3580
|
}
|
|
@@ -3565,22 +3612,57 @@ function pruneRedundant(msgs, precomputedTcIdx) {
|
|
|
3565
3612
|
const half = Math.floor(MAX_TOOL_OUTPUT_CHARS / 2);
|
|
3566
3613
|
for (let idx = 0;idx < msgs.length; idx++) {
|
|
3567
3614
|
const m = msgs[idx];
|
|
3568
|
-
const
|
|
3569
|
-
if (
|
|
3570
|
-
originalTokens += estimateTokens(
|
|
3615
|
+
const originalText = extractText(m.content);
|
|
3616
|
+
if (originalText.length > 0)
|
|
3617
|
+
originalTokens += estimateTokens(originalText);
|
|
3571
3618
|
if (!keep.has(idx))
|
|
3572
3619
|
continue;
|
|
3573
|
-
|
|
3574
|
-
if (m.role === "
|
|
3620
|
+
let keptMessage = m;
|
|
3621
|
+
if (m.role === "assistant" && removedToolCallIds.size > 0 && Array.isArray(m.content)) {
|
|
3622
|
+
let changed = false;
|
|
3623
|
+
const content = [];
|
|
3624
|
+
for (const block of m.content) {
|
|
3625
|
+
if (!isToolCallBlock(block)) {
|
|
3626
|
+
content.push(block);
|
|
3627
|
+
continue;
|
|
3628
|
+
}
|
|
3629
|
+
if (block.name === "multi_tool_use.parallel" && Array.isArray(block.arguments?.tool_uses)) {
|
|
3630
|
+
const tools = block.arguments.tool_uses;
|
|
3631
|
+
const retained = tools.filter((tool, toolIndex) => {
|
|
3632
|
+
const id = typeof tool.id === "string" ? tool.id : block.id ? block.id + "_" + toolIndex : "mtu_" + idx + "_" + toolIndex;
|
|
3633
|
+
return !removedToolCallIds.has(id);
|
|
3634
|
+
});
|
|
3635
|
+
if (retained.length !== tools.length)
|
|
3636
|
+
changed = true;
|
|
3637
|
+
if (retained.length > 0) {
|
|
3638
|
+
content.push(retained.length === tools.length ? block : { ...block, arguments: { ...block.arguments, tool_uses: retained } });
|
|
3639
|
+
}
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
if (block.id && removedToolCallIds.has(block.id)) {
|
|
3643
|
+
changed = true;
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
content.push(block);
|
|
3647
|
+
}
|
|
3648
|
+
if (changed) {
|
|
3649
|
+
if (content.length === 0)
|
|
3650
|
+
continue;
|
|
3651
|
+
keptMessage = { ...m, content };
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
const text = extractText(keptMessage.content);
|
|
3655
|
+
const protectFromTruncation = keptMessage.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS && text.length < ERROR_SCAN_MAX_LEN && LIKELY_ERROR_RE.test(text);
|
|
3656
|
+
if (keptMessage.role === "toolResult" && text.length > MAX_TOOL_OUTPUT_CHARS && !protectFromTruncation) {
|
|
3575
3657
|
const head = text.slice(0, half);
|
|
3576
3658
|
const tail = text.slice(-half);
|
|
3577
3659
|
const truncated = head + `
|
|
3578
3660
|
... [truncated ` + (text.length - MAX_TOOL_OUTPUT_CHARS) + ` chars] ...
|
|
3579
3661
|
` + tail;
|
|
3580
|
-
finalMsgs.push({ ...
|
|
3662
|
+
finalMsgs.push({ ...keptMessage, content: [{ type: "text", text: truncated }] });
|
|
3581
3663
|
prunedTokens += estimateTokens(truncated);
|
|
3582
3664
|
} else {
|
|
3583
|
-
finalMsgs.push(
|
|
3665
|
+
finalMsgs.push(keptMessage);
|
|
3584
3666
|
if (text.length > 0)
|
|
3585
3667
|
prunedTokens += estimateTokens(text);
|
|
3586
3668
|
}
|
|
@@ -3866,7 +3948,6 @@ function extractWithCache(rc) {
|
|
|
3866
3948
|
rc.vlog("Extraction cache ignored: legacy entry lacks keptEntryIds/keptEntryIdsFp");
|
|
3867
3949
|
}
|
|
3868
3950
|
}
|
|
3869
|
-
const prunedTcIdx = buildToolCallIndex(rc.llmMessages);
|
|
3870
3951
|
if (cacheUsable && cachedExt) {
|
|
3871
3952
|
const newMsgs = rc.llmMessages.slice(cachedExt.messageCount);
|
|
3872
3953
|
const deltaTcIdx = buildToolCallIndex(newMsgs);
|
|
@@ -3877,6 +3958,7 @@ function extractWithCache(rc) {
|
|
|
3877
3958
|
missReason = undefined;
|
|
3878
3959
|
recordExtractionCacheHit(rc.services);
|
|
3879
3960
|
} else {
|
|
3961
|
+
const prunedTcIdx = buildToolCallIndex(rc.llmMessages);
|
|
3880
3962
|
extraction = extractStructured(rc.llmMessages, rc.profileCfg, prunedTcIdx);
|
|
3881
3963
|
rc.notify("Phase 1 Full: " + extraction.modifiedFiles.length + " files, " + extraction.errors.length + " errors", "info");
|
|
3882
3964
|
rc.vlog("Full extraction \u2014 " + rc.llmMessages.length + " messages, tier=" + rc.tier);
|
|
@@ -3893,7 +3975,6 @@ function extractWithCache(rc) {
|
|
|
3893
3975
|
out.pruning = pruning;
|
|
3894
3976
|
out.currentEntryIds = currentEntryIds;
|
|
3895
3977
|
out.currentKeptEntryIds = currentKeptEntryIds;
|
|
3896
|
-
out.prunedToolCallIndex = prunedTcIdx;
|
|
3897
3978
|
out.extraction = extraction;
|
|
3898
3979
|
out.extractionCacheMissReason = missReason;
|
|
3899
3980
|
out.prevContext = prevContext;
|
|
@@ -5004,13 +5085,13 @@ function verifySummary(summary, extraction) {
|
|
|
5004
5085
|
}
|
|
5005
5086
|
}
|
|
5006
5087
|
const summaryFileRefs = extractFileRefs(summary);
|
|
5007
|
-
const knownFiles =
|
|
5088
|
+
const knownFiles = [
|
|
5008
5089
|
...extraction.modifiedFiles.map((f) => f.path.toLowerCase()),
|
|
5009
5090
|
...extraction.readFiles.map((f) => f.toLowerCase())
|
|
5010
|
-
]
|
|
5091
|
+
];
|
|
5011
5092
|
for (const ref of summaryFileRefs) {
|
|
5012
5093
|
const refLower = ref.toLowerCase();
|
|
5013
|
-
const isKnown =
|
|
5094
|
+
const isKnown = knownFiles.some((kf) => kf.endsWith("/" + refLower) || kf === refLower || kf.endsWith(refLower) && refLower.length > 3);
|
|
5014
5095
|
if (!isKnown) {
|
|
5015
5096
|
gaps.push("Potentially fabricated file: " + ref);
|
|
5016
5097
|
score -= 4;
|
|
@@ -5325,6 +5406,9 @@ function computeDelta(prev, current) {
|
|
|
5325
5406
|
previousGoal: goalChanged ? prev.goal : null
|
|
5326
5407
|
};
|
|
5327
5408
|
}
|
|
5409
|
+
function hasDeltaChanges(delta) {
|
|
5410
|
+
return delta.goalChanged || delta.removedDecisions.length > 0 || delta.resolvedLoops.length > 0 || delta.newLoops.length > 0 || delta.newDecisions.length > 0 || delta.resolvedErrors.length > 0 || delta.newErrors.length > 0 || delta.newModifiedFiles.length > 0;
|
|
5411
|
+
}
|
|
5328
5412
|
function formatDeltaSection(delta) {
|
|
5329
5413
|
const lines = ["## Changes Since Last Compaction", ""];
|
|
5330
5414
|
if (delta.goalChanged) {
|
|
@@ -5342,6 +5426,9 @@ function formatDeltaSection(delta) {
|
|
|
5342
5426
|
if (delta.newDecisions.length) {
|
|
5343
5427
|
lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0, TRUNC.SNIPPET)).join("; "));
|
|
5344
5428
|
}
|
|
5429
|
+
if (delta.removedDecisions.length) {
|
|
5430
|
+
lines.push("- **Removed decisions**: " + delta.removedDecisions.map((s) => "~~" + s.slice(0, TRUNC.SNIPPET) + "~~").join("; "));
|
|
5431
|
+
}
|
|
5345
5432
|
if (delta.resolvedErrors.length) {
|
|
5346
5433
|
lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0, TRUNC.DECISION_DETAIL)).join("; "));
|
|
5347
5434
|
}
|
|
@@ -5356,8 +5443,7 @@ function formatDeltaSection(delta) {
|
|
|
5356
5443
|
`);
|
|
5357
5444
|
}
|
|
5358
5445
|
function injectDeltaSection(summary, delta) {
|
|
5359
|
-
|
|
5360
|
-
if (!hasChanges)
|
|
5446
|
+
if (!hasDeltaChanges(delta))
|
|
5361
5447
|
return summary;
|
|
5362
5448
|
const body = formatDeltaSection(delta).replace(/^## Changes Since Last Compaction\s*\n?/i, "").trim();
|
|
5363
5449
|
if (!body)
|
|
@@ -5495,7 +5581,9 @@ function logDamageReport(sessionId, report, details) {
|
|
|
5495
5581
|
signals: report.signals.length,
|
|
5496
5582
|
summary: report.summary
|
|
5497
5583
|
};
|
|
5498
|
-
|
|
5584
|
+
const logPath = damageReportsFile();
|
|
5585
|
+
appendLineLocked(logPath, JSON.stringify(entry));
|
|
5586
|
+
scheduleFileTailTrim(logPath, RUNTIME_LOG_MAX_BYTES);
|
|
5499
5587
|
} catch (e) {
|
|
5500
5588
|
warn("logDamageReport failed", e);
|
|
5501
5589
|
}
|
|
@@ -5547,7 +5635,7 @@ function buildState(rc) {
|
|
|
5547
5635
|
const prevState = loadCompactionState(rc.projectId);
|
|
5548
5636
|
if (prevState) {
|
|
5549
5637
|
const delta = computeDelta(prevState, compactionState);
|
|
5550
|
-
if (delta
|
|
5638
|
+
if (hasDeltaChanges(delta)) {
|
|
5551
5639
|
summary = injectDeltaSection(summary, delta);
|
|
5552
5640
|
rc.notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
5553
5641
|
}
|
|
@@ -5587,71 +5675,6 @@ function buildState(rc) {
|
|
|
5587
5675
|
return advance(out, "_stated");
|
|
5588
5676
|
}
|
|
5589
5677
|
|
|
5590
|
-
// src/app/steps/persist.ts
|
|
5591
|
-
import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
|
|
5592
|
-
init_logger();
|
|
5593
|
-
function persistDurableState(rc) {
|
|
5594
|
-
saveProjectFingerprint(rc.projectId, rc.extraction);
|
|
5595
|
-
saveCompactionState(rc.projectId, rc.compactionState);
|
|
5596
|
-
}
|
|
5597
|
-
function runDamageDetection(rc) {
|
|
5598
|
-
try {
|
|
5599
|
-
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([asBranchMessage(e.message)])).flat();
|
|
5600
|
-
if (postCompactMsgs.length <= 2)
|
|
5601
|
-
return;
|
|
5602
|
-
const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
|
|
5603
|
-
if (!lastCompaction?.details)
|
|
5604
|
-
return;
|
|
5605
|
-
const safeDetails = sanitizeSmartCompactDetails(lastCompaction.details);
|
|
5606
|
-
if (!safeDetails) {
|
|
5607
|
-
rc.vlog("Damage detection skipped: previous compaction details have an unrecognized shape");
|
|
5608
|
-
return;
|
|
5609
|
-
}
|
|
5610
|
-
const damage = detectDamage(postCompactMsgs.slice(0, Math.min(15, postCompactMsgs.length)), safeDetails);
|
|
5611
|
-
if (damage.damageScore > 0) {
|
|
5612
|
-
rc.notify("Previous compaction damage: " + damage.summary, "warning");
|
|
5613
|
-
}
|
|
5614
|
-
logDamageReport(rc.sessionId, damage, safeDetails);
|
|
5615
|
-
if (damage.reReadFiles.length > 0) {
|
|
5616
|
-
writeRemediationHints(rc.projectId, damage.reReadFiles);
|
|
5617
|
-
}
|
|
5618
|
-
} catch (err) {
|
|
5619
|
-
warn("Damage detection error", err);
|
|
5620
|
-
}
|
|
5621
|
-
}
|
|
5622
|
-
function stagePendingCompaction(rc) {
|
|
5623
|
-
const pending = {
|
|
5624
|
-
summary: rc.finalSummary,
|
|
5625
|
-
firstKeptEntryId: rc.firstKeptId,
|
|
5626
|
-
tokensBefore: rc.totalTokens,
|
|
5627
|
-
details: rc.details,
|
|
5628
|
-
compactionState: rc.compactionState,
|
|
5629
|
-
sessionId: rc.sessionId
|
|
5630
|
-
};
|
|
5631
|
-
rc.pendingRef.set(pending);
|
|
5632
|
-
return pending;
|
|
5633
|
-
}
|
|
5634
|
-
function applyCompaction(rc) {
|
|
5635
|
-
if (rc.flags.skipCompact || rc.flags.autoTriggered)
|
|
5636
|
-
return;
|
|
5637
|
-
rc.ctx.compact({
|
|
5638
|
-
customInstructions: "Use pre-computed smart summary from /smart-compact",
|
|
5639
|
-
onComplete: () => {
|
|
5640
|
-
try {
|
|
5641
|
-
persistDurableState(rc);
|
|
5642
|
-
} catch (e) {
|
|
5643
|
-
warn("persistDurableState failed", e);
|
|
5644
|
-
}
|
|
5645
|
-
markPhase(rc, "persist");
|
|
5646
|
-
rc.ctx.ui.notify("Applied \u2713", "info");
|
|
5647
|
-
},
|
|
5648
|
-
onError: (e) => {
|
|
5649
|
-
rc.pendingRef.clear();
|
|
5650
|
-
rc.ctx.ui.notify("Failed: " + e.message, "error");
|
|
5651
|
-
}
|
|
5652
|
-
});
|
|
5653
|
-
}
|
|
5654
|
-
|
|
5655
5678
|
// src/app/steps/metrics.ts
|
|
5656
5679
|
init_cache();
|
|
5657
5680
|
function runType(rc) {
|
|
@@ -5709,6 +5732,86 @@ function recordFailureMetrics(rc, err, fields) {
|
|
|
5709
5732
|
}, rc.services);
|
|
5710
5733
|
}
|
|
5711
5734
|
|
|
5735
|
+
// src/app/steps/persist.ts
|
|
5736
|
+
import { convertToLlm as convertToLlm2 } from "@earendil-works/pi-coding-agent";
|
|
5737
|
+
init_logger();
|
|
5738
|
+
function persistConsumedState(pending) {
|
|
5739
|
+
if (!pending.projectId)
|
|
5740
|
+
return;
|
|
5741
|
+
try {
|
|
5742
|
+
if (pending.extraction)
|
|
5743
|
+
saveProjectFingerprint(pending.projectId, pending.extraction);
|
|
5744
|
+
if (pending.compactionState)
|
|
5745
|
+
saveCompactionState(pending.projectId, pending.compactionState);
|
|
5746
|
+
} catch (e) {
|
|
5747
|
+
warn("persistConsumedState failed", e);
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
function runDamageDetection(rc) {
|
|
5751
|
+
try {
|
|
5752
|
+
const postCompactMsgs = rc.msgs.slice(rc.keepFrom).map((e) => convertToLlm2([asBranchMessage(e.message)])).flat();
|
|
5753
|
+
if (postCompactMsgs.length <= 2)
|
|
5754
|
+
return;
|
|
5755
|
+
const lastCompaction = rc.branch.filter((e) => e?.type === "compaction").slice(-1)[0];
|
|
5756
|
+
if (!lastCompaction?.details)
|
|
5757
|
+
return;
|
|
5758
|
+
const safeDetails = sanitizeSmartCompactDetails(lastCompaction.details);
|
|
5759
|
+
if (!safeDetails) {
|
|
5760
|
+
rc.vlog("Damage detection skipped: previous compaction details have an unrecognized shape");
|
|
5761
|
+
return;
|
|
5762
|
+
}
|
|
5763
|
+
const damage = detectDamage(postCompactMsgs.slice(0, Math.min(15, postCompactMsgs.length)), safeDetails);
|
|
5764
|
+
if (damage.damageScore > 0) {
|
|
5765
|
+
rc.notify("Previous compaction damage: " + damage.summary, "warning");
|
|
5766
|
+
}
|
|
5767
|
+
logDamageReport(rc.sessionId, damage, safeDetails);
|
|
5768
|
+
if (damage.reReadFiles.length > 0) {
|
|
5769
|
+
writeRemediationHints(rc.projectId, damage.reReadFiles);
|
|
5770
|
+
}
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
warn("Damage detection error", err);
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
function stagePendingCompaction(rc) {
|
|
5776
|
+
const pending = {
|
|
5777
|
+
summary: rc.finalSummary,
|
|
5778
|
+
firstKeptEntryId: rc.firstKeptId,
|
|
5779
|
+
tokensBefore: rc.totalTokens,
|
|
5780
|
+
details: rc.details,
|
|
5781
|
+
compactionState: rc.compactionState,
|
|
5782
|
+
projectId: rc.projectId,
|
|
5783
|
+
extraction: rc.extraction,
|
|
5784
|
+
sessionId: rc.sessionId
|
|
5785
|
+
};
|
|
5786
|
+
rc.pendingRef.set(pending);
|
|
5787
|
+
return pending;
|
|
5788
|
+
}
|
|
5789
|
+
function applyCompaction(rc) {
|
|
5790
|
+
if (rc.flags.skipCompact || rc.flags.autoTriggered)
|
|
5791
|
+
return;
|
|
5792
|
+
rc.ctx.compact({
|
|
5793
|
+
customInstructions: "Use pre-computed smart summary from /smart-compact",
|
|
5794
|
+
onComplete: () => {
|
|
5795
|
+
markPhase(rc, "persist");
|
|
5796
|
+
recordSuccessMetrics(rc, "success");
|
|
5797
|
+
rc.ctx.ui.notify("Applied \u2713", "info");
|
|
5798
|
+
},
|
|
5799
|
+
onError: (e) => {
|
|
5800
|
+
rc.pendingRef.clear();
|
|
5801
|
+
recordFailureMetrics(rc, e, {
|
|
5802
|
+
sessionId: rc.sessionId,
|
|
5803
|
+
tier: rc.tier,
|
|
5804
|
+
contextPercent: rc.contextPercent,
|
|
5805
|
+
toolPercent: rc.toolPercent,
|
|
5806
|
+
totalTokens: rc.totalTokens,
|
|
5807
|
+
methodForMetrics: rc.method,
|
|
5808
|
+
profile: rc.profile
|
|
5809
|
+
});
|
|
5810
|
+
rc.ctx.ui.notify("Failed: " + e.message, "error");
|
|
5811
|
+
}
|
|
5812
|
+
});
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5712
5815
|
// src/app/run-smart-compact.ts
|
|
5713
5816
|
function makeBase(opts) {
|
|
5714
5817
|
const ctrl = new AbortController;
|
|
@@ -5756,6 +5859,16 @@ async function runSmartCompact(opts) {
|
|
|
5756
5859
|
}
|
|
5757
5860
|
opts.isRunning.value = true;
|
|
5758
5861
|
const base = makeBase(opts);
|
|
5862
|
+
const abortFromHost = () => {
|
|
5863
|
+
base.cancellation.timedOut = true;
|
|
5864
|
+
base.cancellation.controller.abort();
|
|
5865
|
+
};
|
|
5866
|
+
if (opts.abortSignal) {
|
|
5867
|
+
if (opts.abortSignal.aborted)
|
|
5868
|
+
abortFromHost();
|
|
5869
|
+
else
|
|
5870
|
+
opts.abortSignal.addEventListener("abort", abortFromHost, { once: true });
|
|
5871
|
+
}
|
|
5759
5872
|
let finalRc = null;
|
|
5760
5873
|
let failureSummaryFields = { profile: opts.profile };
|
|
5761
5874
|
if (opts.cancellationOut) {
|
|
@@ -5773,6 +5886,8 @@ async function runSmartCompact(opts) {
|
|
|
5773
5886
|
};
|
|
5774
5887
|
}
|
|
5775
5888
|
try {
|
|
5889
|
+
if (base.cancellation.signal.aborted)
|
|
5890
|
+
return;
|
|
5776
5891
|
const prepared = await prepareRun(base);
|
|
5777
5892
|
if (!prepared)
|
|
5778
5893
|
return;
|
|
@@ -5821,7 +5936,9 @@ async function runSmartCompact(opts) {
|
|
|
5821
5936
|
}
|
|
5822
5937
|
runDamageDetection(stated);
|
|
5823
5938
|
markPhase(stated, "damage");
|
|
5824
|
-
|
|
5939
|
+
const willApply = !stated.flags.skipCompact && !stated.flags.autoTriggered;
|
|
5940
|
+
if (!willApply)
|
|
5941
|
+
recordSuccessMetrics(stated, "success");
|
|
5825
5942
|
if (!stated.flags.autoTriggered) {
|
|
5826
5943
|
try {
|
|
5827
5944
|
let resultTimer;
|
|
@@ -5848,6 +5965,7 @@ async function runSmartCompact(opts) {
|
|
|
5848
5965
|
recordFailureMetrics(finalRc ?? base, err, failureSummaryFields);
|
|
5849
5966
|
throw err;
|
|
5850
5967
|
} finally {
|
|
5968
|
+
opts.abortSignal?.removeEventListener("abort", abortFromHost);
|
|
5851
5969
|
if (base.cancellation.timeoutId)
|
|
5852
5970
|
clearTimeout(base.cancellation.timeoutId);
|
|
5853
5971
|
opts.isRunning.value = false;
|
|
@@ -5930,6 +6048,7 @@ init_logger();
|
|
|
5930
6048
|
function unwrapConsumed(result, ctx) {
|
|
5931
6049
|
switch (result.kind) {
|
|
5932
6050
|
case "ok":
|
|
6051
|
+
persistConsumedState(result.pending);
|
|
5933
6052
|
return result.pending;
|
|
5934
6053
|
case "empty":
|
|
5935
6054
|
return null;
|
|
@@ -6041,7 +6160,8 @@ function smartCompactExtension(pi) {
|
|
|
6041
6160
|
}
|
|
6042
6161
|
return;
|
|
6043
6162
|
}
|
|
6044
|
-
const
|
|
6163
|
+
const knownProviders = new Set(ctx.modelRegistry.getAvailable().map((m) => m.provider));
|
|
6164
|
+
const modelArg = tokens.find((t) => /^[a-z0-9_.-]+\/[a-z0-9_.:-]+$/i.test(t) && (findModelById(ctx, t) || knownProviders.has(t.split("/")[0])));
|
|
6045
6165
|
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
6046
6166
|
const profile = profileArg ?? loadConfig().profile;
|
|
6047
6167
|
if (!tokens.length) {
|
|
@@ -6069,7 +6189,12 @@ function smartCompactExtension(pi) {
|
|
|
6069
6189
|
await runSmartCompact({ ctx, summaryModel: sumModel2, segModel: segModel2 ?? sumModel2, profile: selected.profile, pendingRef, isRunning, force: true });
|
|
6070
6190
|
return;
|
|
6071
6191
|
}
|
|
6072
|
-
const
|
|
6192
|
+
const explicitModel = modelArg ? findModelById(ctx, modelArg) : undefined;
|
|
6193
|
+
if (modelArg && !explicitModel) {
|
|
6194
|
+
ctx.ui.notify("Unknown model: " + modelArg + " \u2014 check available models", "error");
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
const { segModel, sumModel } = resolveModels(ctx, explicitModel ?? ctx.model, loadConfig(), Boolean(modelArg));
|
|
6073
6198
|
if (!sumModel) {
|
|
6074
6199
|
ctx.ui.notify("Could not resolve model", "error");
|
|
6075
6200
|
return;
|
|
@@ -6161,7 +6286,7 @@ function smartCompactExtension(pi) {
|
|
|
6161
6286
|
dashboard: { type: "boolean", description: "Write a local HTML metrics dashboard and return its path." }
|
|
6162
6287
|
}
|
|
6163
6288
|
},
|
|
6164
|
-
async execute(_id, params,
|
|
6289
|
+
async execute(_id, params, signal, _onUp, ctx) {
|
|
6165
6290
|
const profile = params.profile === "light" || params.profile === "balanced" || params.profile === "aggressive" ? params.profile : undefined;
|
|
6166
6291
|
const verbose = !!params.verbose;
|
|
6167
6292
|
const dryRun = !!params.dry_run;
|
|
@@ -6191,12 +6316,15 @@ Dashboard: ` + fp : "") }], details: undefined };
|
|
|
6191
6316
|
}
|
|
6192
6317
|
try {
|
|
6193
6318
|
const toolStart = Date.now();
|
|
6194
|
-
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true });
|
|
6319
|
+
await runSmartCompact({ ctx, summaryModel: sumModel, segModel: segModel ?? sumModel, profile: resolvedProfile, verbose, dryRun, pendingRef, isRunning, autoTriggered: true, skipCompact: true, abortSignal: signal });
|
|
6195
6320
|
const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
|
|
6196
6321
|
const staged = pendingRef.peek();
|
|
6197
6322
|
if (staged) {
|
|
6198
6323
|
return { content: [{ type: "text", text: "Smart summary prepared (" + resolvedProfile + "). Tokens: " + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 summary cached for " + Math.round(PENDING_TTL_MS / 60000) + " min. The next /compact will use this summary automatically." }], details: undefined };
|
|
6199
6324
|
}
|
|
6325
|
+
if (dryRun) {
|
|
6326
|
+
return { content: [{ type: "text", text: "Dry run finished (" + resolvedProfile + ", " + toolSecs + "s). Pipeline ran successfully; no summary was staged (dry-run skips staging by design)." }], details: undefined };
|
|
6327
|
+
}
|
|
6200
6328
|
return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }], details: undefined };
|
|
6201
6329
|
} catch (error) {
|
|
6202
6330
|
const msg = error instanceof Error ? error.message : String(error);
|